359 lines
7.7 KiB
TypeScript
359 lines
7.7 KiB
TypeScript
import {makeAutoObservable} from 'mobx'
|
|
import {TABS_ENABLED} from '../../build-flags'
|
|
|
|
let __id = 0
|
|
function genId() {
|
|
return ++__id
|
|
}
|
|
|
|
// NOTE
|
|
// this model was originally built for a freeform "tabs" concept like a browser
|
|
// we've since decided to pause that idea and do something more traditional
|
|
// until we're fully sure what that is, the tabs are being repurposed into a fixed topology
|
|
// - Tab 0: The "Default" tab
|
|
// - Tab 1: The "Notifications" tab
|
|
// These tabs always retain the first item in their history.
|
|
// The default tab is used for basically everything except notifications.
|
|
// -prf
|
|
export enum TabPurpose {
|
|
Default = 0,
|
|
Notifs = 1,
|
|
}
|
|
|
|
interface HistoryItem {
|
|
url: string
|
|
ts: number
|
|
title?: string
|
|
id: number
|
|
}
|
|
|
|
export type HistoryPtr = [number, number]
|
|
|
|
export class NavigationTabModel {
|
|
id = genId()
|
|
history: HistoryItem[]
|
|
index = 0
|
|
isNewTab = false
|
|
|
|
constructor(public fixedTabPurpose: TabPurpose) {
|
|
if (fixedTabPurpose === TabPurpose.Notifs) {
|
|
this.history = [{url: '/notifications', ts: Date.now(), id: genId()}]
|
|
} else {
|
|
this.history = [{url: '/', ts: Date.now(), id: genId()}]
|
|
}
|
|
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 = this.index
|
|
return this.history.slice(start, end).map((item, i) => ({
|
|
url: item.url,
|
|
title: item.title,
|
|
index: start + i,
|
|
id: item.id,
|
|
}))
|
|
}
|
|
|
|
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 + 1, this.history.length)
|
|
return this.history.slice(start, end).map((item, i) => ({
|
|
url: item.url,
|
|
title: item.title,
|
|
index: start + i,
|
|
id: item.id,
|
|
}))
|
|
}
|
|
|
|
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
|
|
}
|
|
// TEMP ensure the tab has its purpose's main view -prf
|
|
if (this.history.length < 1) {
|
|
const fixedUrl =
|
|
this.fixedTabPurpose === TabPurpose.Notifs ? '/notifications' : '/'
|
|
this.history.push({url: fixedUrl, ts: Date.now(), id: genId()})
|
|
}
|
|
this.history.push({url, title, ts: Date.now(), id: genId()})
|
|
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(),
|
|
id: this.current.id,
|
|
},
|
|
...this.history.slice(this.index + 1),
|
|
]
|
|
}
|
|
|
|
goBack() {
|
|
if (this.canGoBack) {
|
|
this.index--
|
|
}
|
|
}
|
|
|
|
// TEMP
|
|
// a helper to bring the tab back to its base state
|
|
// -prf
|
|
fixedTabReset() {
|
|
this.index = 0
|
|
}
|
|
|
|
goForward() {
|
|
if (this.canGoForward) {
|
|
this.index++
|
|
}
|
|
}
|
|
|
|
goToIndex(index: number) {
|
|
if (index >= 0 && index <= this.history.length - 1) {
|
|
this.index = index
|
|
}
|
|
}
|
|
|
|
setTitle(id: number, title: string) {
|
|
this.history = this.history.map(h => {
|
|
if (h.id === id) {
|
|
return {...h, title}
|
|
}
|
|
return h
|
|
})
|
|
}
|
|
|
|
setIsNewTab(v: boolean) {
|
|
this.isNewTab = v
|
|
}
|
|
|
|
// persistence
|
|
// =
|
|
|
|
serialize(): unknown {
|
|
return {
|
|
history: this.history,
|
|
index: this.index,
|
|
}
|
|
}
|
|
|
|
hydrate(v: unknown) {
|
|
// TODO fixme
|
|
// 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(TabPurpose.Default),
|
|
new NavigationTabModel(TabPurpose.Notifs),
|
|
]
|
|
tabIndex = 0
|
|
|
|
constructor() {
|
|
makeAutoObservable(this, {
|
|
serialize: false,
|
|
hydrate: false,
|
|
})
|
|
}
|
|
|
|
clear() {
|
|
this.tabs = [
|
|
new NavigationTabModel(TabPurpose.Default),
|
|
new NavigationTabModel(TabPurpose.Notifs),
|
|
]
|
|
this.tabIndex = 0
|
|
}
|
|
|
|
// accessors
|
|
// =
|
|
|
|
get tab() {
|
|
return this.tabs[this.tabIndex]
|
|
}
|
|
|
|
get tabCount() {
|
|
return this.tabs.length
|
|
}
|
|
|
|
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(ptr: HistoryPtr, title: string) {
|
|
this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title)
|
|
}
|
|
|
|
handleLink(url: string) {
|
|
let path
|
|
if (url.startsWith('/')) {
|
|
path = url
|
|
} else if (url.startsWith('http')) {
|
|
try {
|
|
path = new URL(url).pathname
|
|
} catch (e) {
|
|
console.error('Invalid url', url, e)
|
|
return
|
|
}
|
|
} else {
|
|
console.error('Invalid url', url)
|
|
return
|
|
}
|
|
this.navigate(path)
|
|
}
|
|
|
|
// tab management
|
|
// =
|
|
|
|
// TEMP
|
|
// fixed tab helper function
|
|
// -prf
|
|
switchTo(purpose: TabPurpose, reset: boolean) {
|
|
if (purpose === TabPurpose.Notifs) {
|
|
this.tabIndex = 1
|
|
} else {
|
|
this.tabIndex = 0
|
|
}
|
|
if (reset) {
|
|
this.tab.fixedTabReset()
|
|
}
|
|
}
|
|
|
|
newTab(url: string, title?: string) {
|
|
if (!TABS_ENABLED) {
|
|
return this.navigate(url)
|
|
}
|
|
const tab = new NavigationTabModel(TabPurpose.Default)
|
|
tab.navigate(url, title)
|
|
tab.isNewTab = true
|
|
this.tabs.push(tab)
|
|
this.tabIndex = this.tabs.length - 1
|
|
}
|
|
|
|
setActiveTab(tabIndex: number) {
|
|
if (!TABS_ENABLED) {
|
|
return
|
|
}
|
|
this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0)
|
|
}
|
|
|
|
closeTab(tabIndex: number) {
|
|
if (!TABS_ENABLED) {
|
|
return
|
|
}
|
|
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) {
|
|
// TODO fixme
|
|
this.clear()
|
|
/*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
|
|
}
|
|
}*/
|
|
}
|
|
}
|