diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-08-31 14:36:50 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-31 14:36:50 -0500 |
commit | 97f52b6a03ab36dcbf21256cc0137b550b10f174 (patch) | |
tree | c632b69f038d33ea82c3378451f72177dc136cfe /src/state/models/navigation.ts | |
parent | d1470bad6628022eda66c658d228cc7646abc746 (diff) | |
download | voidsky-97f52b6a03ab36dcbf21256cc0137b550b10f174.tar.zst |
New navigation model (#1)
* Flatten all routing into a single stack * Replace router with custom implementation * Add shell header and titles * Add tab selector * Add back/forward history menus on longpress * Fix: don't modify state during render * Add refresh() to navigation and reroute navigations to the current location to refresh instead of add to history * Cache screens during navigation to maintain scroll position and improve load-time for renders
Diffstat (limited to 'src/state/models/navigation.ts')
-rw-r--r-- | src/state/models/navigation.ts | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts new file mode 100644 index 000000000..d5338ac05 --- /dev/null +++ b/src/state/models/navigation.ts @@ -0,0 +1,251 @@ +import {makeAutoObservable} from 'mobx' +import {isObj, hasProp} from '../lib/type-guards' + +let __tabId = 0 +function genTabId() { + return ++__tabId +} + +interface HistoryItem { + url: string + ts: number + title?: string +} + +export class NavigationTabModel { + id = genTabId() + history: HistoryItem[] = [{url: '/', ts: Date.now()}] + index = 0 + + constructor() { + makeAutoObservable(this, { + serialize: false, + hydrate: false, + }) + } + + // accessors + // = + + get current() { + return this.history[this.index] + } + + get canGoBack() { + return this.index > 0 + } + + get canGoForward() { + return this.index < this.history.length - 1 + } + + getBackList(n: number) { + const start = Math.max(this.index - n, 0) + const end = Math.min(this.index, n) + return this.history.slice(start, end).map((item, i) => ({ + url: item.url, + title: item.title, + index: start + i, + })) + } + + get backTen() { + return this.getBackList(10) + } + + getForwardList(n: number) { + const start = Math.min(this.index + 1, this.history.length) + const end = Math.min(this.index + n, this.history.length) + return this.history.slice(start, end).map((item, i) => ({ + url: item.url, + title: item.title, + index: start + i, + })) + } + + get forwardTen() { + return this.getForwardList(10) + } + + // navigation + // = + + navigate(url: string, title?: string) { + if (this.current?.url === url) { + this.refresh() + } else { + if (this.index < this.history.length - 1) { + this.history.length = this.index + 1 + } + this.history.push({url, title, ts: Date.now()}) + this.index = this.history.length - 1 + } + } + + refresh() { + this.history = [ + ...this.history.slice(0, this.index), + {url: this.current.url, title: this.current.title, ts: Date.now()}, + ...this.history.slice(this.index + 1), + ] + } + + goBack() { + if (this.canGoBack) { + this.index-- + } + } + + goForward() { + if (this.canGoForward) { + this.index++ + } + } + + goToIndex(index: number) { + if (index >= 0 && index <= this.history.length - 1) { + this.index = index + } + } + + setTitle(title: string) { + this.current.title = title + } + + // persistence + // = + + serialize(): unknown { + return { + history: this.history, + index: this.index, + } + } + + hydrate(v: unknown) { + this.history = [] + this.index = 0 + if (isObj(v)) { + if (hasProp(v, 'history') && Array.isArray(v.history)) { + for (const item of v.history) { + if ( + isObj(item) && + hasProp(item, 'url') && + typeof item.url === 'string' + ) { + let copy: HistoryItem = { + url: item.url, + ts: + hasProp(item, 'ts') && typeof item.ts === 'number' + ? item.ts + : Date.now(), + } + if (hasProp(item, 'title') && typeof item.title === 'string') { + copy.title = item.title + } + this.history.push(copy) + } + } + } + if (hasProp(v, 'index') && typeof v.index === 'number') { + this.index = v.index + } + if (this.index >= this.history.length - 1) { + this.index = this.history.length - 1 + } + } + } +} + +export class NavigationModel { + tabs: NavigationTabModel[] = [new NavigationTabModel()] + tabIndex = 0 + + constructor() { + makeAutoObservable(this, { + serialize: false, + hydrate: false, + }) + } + + // accessors + // = + + get tab() { + return this.tabs[this.tabIndex] + } + + isCurrentScreen(tabId: number, index: number) { + return this.tab.id === tabId && this.tab.index === index + } + + // navigation + // = + + navigate(url: string, title?: string) { + this.tab.navigate(url, title) + } + + refresh() { + this.tab.refresh() + } + + setTitle(title: string) { + this.tab.setTitle(title) + } + + // tab management + // = + + newTab(url: string, title?: string) { + const tab = new NavigationTabModel() + tab.navigate(url, title) + this.tabs.push(tab) + this.tabIndex = this.tabs.length - 1 + } + + setActiveTab(tabIndex: number) { + this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0) + } + + closeTab(tabIndex: number) { + this.tabs = [ + ...this.tabs.slice(0, tabIndex), + ...this.tabs.slice(tabIndex + 1), + ] + if (this.tabs.length === 0) { + this.newTab('/') + } else if (this.tabIndex >= this.tabs.length) { + this.tabIndex = this.tabs.length - 1 + } + } + + // persistence + // = + + serialize(): unknown { + return { + tabs: this.tabs.map(t => t.serialize()), + tabIndex: this.tabIndex, + } + } + + hydrate(v: unknown) { + this.tabs.length = 0 + this.tabIndex = 0 + if (isObj(v)) { + if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) { + for (const tab of v.tabs) { + const copy = new NavigationTabModel() + copy.hydrate(tab) + if (copy.history.length) { + this.tabs.push(copy) + } + } + } + if (hasProp(v, 'tabIndex') && typeof v.tabIndex === 'number') { + this.tabIndex = v.tabIndex + } + } + } +} |