All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new store for managing active tab states, improving the responsiveness of tab interactions. - Updated DashboardTabPanes and DashboardTabPreview components to utilize the new active state management, enhancing user experience. - Enhanced CSS styles for dashboard tab previews and panes, ensuring better layout handling and visual consistency across themes. - Improved tab preview capture logic to support dynamic theme changes, refining the overall rendering process. - Adjusted various components to streamline the integration of tab previews and active state management.
705 lines
19 KiB
JavaScript
705 lines
19 KiB
JavaScript
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
useSyncExternalStore
|
|
} from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
import { ElectronContext } from './ElectronContext'
|
|
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
|
import { captureTabPaneImage } from './tabPreviewCapture'
|
|
import { useThemeContext } from './ThemeContext'
|
|
|
|
const NavigationTabsContext = createContext()
|
|
const NavigationTabMetaContext = createContext({
|
|
setTabPage: () => {},
|
|
isElectron: false
|
|
})
|
|
const NavigationTabPreviewsContext = createContext({
|
|
tabPreviews: {},
|
|
tabPreviewCapturing: {}
|
|
})
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const createNavigationTabActiveStore = (initial = true) => {
|
|
let isActive = Boolean(initial)
|
|
const listeners = new Set()
|
|
return {
|
|
getSnapshot: () => isActive,
|
|
setActive: (next) => {
|
|
const value = Boolean(next)
|
|
if (isActive === value) return
|
|
isActive = value
|
|
listeners.forEach((listener) => listener())
|
|
},
|
|
subscribe: (listener) => {
|
|
listeners.add(listener)
|
|
return () => listeners.delete(listener)
|
|
}
|
|
}
|
|
}
|
|
|
|
const defaultNavigationTabActiveStore = createNavigationTabActiveStore(true)
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const NavigationTabActiveContext = createContext(
|
|
defaultNavigationTabActiveStore
|
|
)
|
|
|
|
const createTabId = () =>
|
|
`tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
|
|
const locationToEntry = (location) => ({
|
|
pathname: location?.pathname || '/',
|
|
search: location?.search || '',
|
|
hash: location?.hash || ''
|
|
})
|
|
|
|
const entryToPath = (entry) => {
|
|
if (!entry) return '/'
|
|
if (typeof entry === 'string') return entry
|
|
return `${entry.pathname || '/'}${entry.search || ''}${entry.hash || ''}`
|
|
}
|
|
|
|
const entriesEqual = (left, right) => entryToPath(left) === entryToPath(right)
|
|
|
|
const createTabFromLocation = (location, extras = {}) => ({
|
|
id: createTabId(),
|
|
title: extras.title || 'Farm Control',
|
|
modelName: extras.modelName || null,
|
|
history: [locationToEntry(location)],
|
|
historyIndex: 0
|
|
})
|
|
|
|
const cloneCurrentPageTab = (tab, location) => ({
|
|
id: createTabId(),
|
|
title: tab?.title || 'Farm Control',
|
|
modelName: tab?.modelName || null,
|
|
history: [locationToEntry(location)],
|
|
historyIndex: 0
|
|
})
|
|
|
|
const getTabCurrentEntry = (tab) =>
|
|
tab?.history?.[tab.historyIndex] || tab?.history?.[tab.history.length - 1]
|
|
|
|
export const NavigationTabsProvider = ({ children }) => {
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const {
|
|
isElectron,
|
|
getWindowSession,
|
|
syncWindowTabs,
|
|
createAppWindow,
|
|
handleWindowControl,
|
|
registerTabHistoryHandler,
|
|
onNewTabRequest,
|
|
onNewWindowRequest,
|
|
onTabMovedAway,
|
|
beginTabDrag,
|
|
completeTabDrop,
|
|
cancelTabDrag
|
|
} = useContext(ElectronContext)
|
|
const { isDarkMode } = useThemeContext()
|
|
|
|
const [tabs, setTabs] = useState([])
|
|
const [activeTabId, setActiveTabId] = useState(null)
|
|
const [hydrated, setHydrated] = useState(!isElectron)
|
|
const [tabPreviews, setTabPreviews] = useState({})
|
|
const [tabPreviewCapturing, setTabPreviewCapturing] = useState({})
|
|
const tabsRef = useRef(tabs)
|
|
const activeTabIdRef = useRef(activeTabId)
|
|
const previousActiveTabIdRef = useRef(null)
|
|
const isRestoringRef = useRef(false)
|
|
const tabPaneElsRef = useRef(new Map())
|
|
const captureQueueRef = useRef(Promise.resolve())
|
|
const captureInFlightRef = useRef(new Map())
|
|
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
|
|
tabsRef.current = tabs
|
|
activeTabIdRef.current = activeTabId
|
|
previewThemeRef.current = isDarkMode ? 'dark' : 'light'
|
|
|
|
const restoreToEntry = useCallback(
|
|
(entry) => {
|
|
if (!entry) return
|
|
isRestoringRef.current = true
|
|
navigate(entryToPath(entry), { replace: true })
|
|
},
|
|
[navigate]
|
|
)
|
|
|
|
const selectTab = useCallback(
|
|
(tabId) => {
|
|
if (!tabId || tabId === activeTabIdRef.current) return
|
|
const nextTab = tabsRef.current.find((tab) => tab.id === tabId)
|
|
if (!nextTab) return
|
|
|
|
const nextEntry = getTabCurrentEntry(nextTab)
|
|
setActiveTabId(tabId)
|
|
|
|
if (entriesEqual(nextEntry, location)) {
|
|
return
|
|
}
|
|
|
|
restoreToEntry(nextEntry)
|
|
},
|
|
[location, restoreToEntry]
|
|
)
|
|
|
|
const addTab = useCallback(() => {
|
|
const currentTab = tabsRef.current.find(
|
|
(tab) => tab.id === activeTabIdRef.current
|
|
)
|
|
const nextTab = cloneCurrentPageTab(currentTab, location)
|
|
setTabs((current) => [...current, nextTab])
|
|
setActiveTabId(nextTab.id)
|
|
}, [location])
|
|
|
|
const removeTab = useCallback(
|
|
(tabId, { closeWindowIfLast = true } = {}) => {
|
|
const current = tabsRef.current
|
|
const index = current.findIndex((tab) => tab.id === tabId)
|
|
if (index === -1) return false
|
|
|
|
if (current.length <= 1) {
|
|
if (closeWindowIfLast) {
|
|
handleWindowControl?.('close')
|
|
}
|
|
return true
|
|
}
|
|
|
|
const remaining = current.filter((tab) => tab.id !== tabId)
|
|
const nextActive =
|
|
tabId === activeTabIdRef.current
|
|
? remaining[Math.max(0, index - 1)]
|
|
: remaining.find((tab) => tab.id === activeTabIdRef.current)
|
|
|
|
setTabs(remaining)
|
|
setTabPreviews((currentPreviews) => {
|
|
if (!(tabId in currentPreviews)) return currentPreviews
|
|
const nextPreviews = { ...currentPreviews }
|
|
delete nextPreviews[tabId]
|
|
return nextPreviews
|
|
})
|
|
setTabPreviewCapturing((currentCapturing) => {
|
|
if (!(tabId in currentCapturing)) return currentCapturing
|
|
const nextCapturing = { ...currentCapturing }
|
|
delete nextCapturing[tabId]
|
|
return nextCapturing
|
|
})
|
|
if (nextActive) {
|
|
setActiveTabId(nextActive.id)
|
|
const nextEntry = getTabCurrentEntry(nextActive)
|
|
if (!entriesEqual(nextEntry, location)) {
|
|
restoreToEntry(nextEntry)
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
[handleWindowControl, location, restoreToEntry]
|
|
)
|
|
|
|
const closeTab = useCallback(
|
|
(tabId) => {
|
|
removeTab(tabId, { closeWindowIfLast: true })
|
|
},
|
|
[removeTab]
|
|
)
|
|
|
|
const acceptIncomingTab = useCallback(
|
|
(incomingTab, targetId, insertBefore = false) => {
|
|
if (!incomingTab?.id) return
|
|
|
|
setTabs((current) => {
|
|
if (current.some((tab) => tab.id === incomingTab.id)) {
|
|
return current
|
|
}
|
|
|
|
const next = [...current]
|
|
const targetIndex = next.findIndex((tab) => tab.id === targetId)
|
|
if (targetIndex === -1) {
|
|
next.push(incomingTab)
|
|
return next
|
|
}
|
|
|
|
next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, incomingTab)
|
|
return next
|
|
})
|
|
setActiveTabId(incomingTab.id)
|
|
const nextEntry = getTabCurrentEntry(incomingTab)
|
|
if (!entriesEqual(nextEntry, location)) {
|
|
restoreToEntry(nextEntry)
|
|
}
|
|
},
|
|
[location, restoreToEntry]
|
|
)
|
|
|
|
const handleTabDragStart = useCallback(
|
|
(tabId) => {
|
|
const tab = tabsRef.current.find((item) => item.id === tabId)
|
|
if (!tab) return
|
|
void beginTabDrag?.({
|
|
windowId: getDesktopWindowId(),
|
|
tab
|
|
})
|
|
},
|
|
[beginTabDrag]
|
|
)
|
|
|
|
const handleTabDragEnd = useCallback(() => {
|
|
window.setTimeout(() => {
|
|
void cancelTabDrag?.({ windowId: getDesktopWindowId() })
|
|
}, 150)
|
|
}, [cancelTabDrag])
|
|
|
|
const handleExternalTabDrop = useCallback(
|
|
async (targetId, insertBefore) => {
|
|
const result = await completeTabDrop?.({
|
|
windowId: getDesktopWindowId(),
|
|
beforeTabId: targetId,
|
|
insertBefore
|
|
})
|
|
if (!result?.ok || result.sameWindow || !result.tab) {
|
|
return
|
|
}
|
|
acceptIncomingTab(result.tab, targetId, insertBefore)
|
|
},
|
|
[acceptIncomingTab, completeTabDrop]
|
|
)
|
|
|
|
const reorderTabs = useCallback((draggedId, targetId, insertBefore) => {
|
|
setTabs((current) => {
|
|
const fromIndex = current.findIndex(
|
|
(tab) => String(tab.id) === String(draggedId)
|
|
)
|
|
const toIndex = current.findIndex(
|
|
(tab) => String(tab.id) === String(targetId)
|
|
)
|
|
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) {
|
|
return current
|
|
}
|
|
|
|
const next = [...current]
|
|
const [moved] = next.splice(fromIndex, 1)
|
|
let insertIndex = next.findIndex(
|
|
(tab) => String(tab.id) === String(targetId)
|
|
)
|
|
if (!insertBefore) {
|
|
insertIndex += 1
|
|
}
|
|
next.splice(insertIndex, 0, moved)
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const registerTabPane = useCallback((tabId, element) => {
|
|
if (!tabId) return
|
|
if (!element) {
|
|
tabPaneElsRef.current.delete(tabId)
|
|
return
|
|
}
|
|
tabPaneElsRef.current.set(tabId, element)
|
|
}, [])
|
|
|
|
const captureTabPreview = useCallback((tabId) => {
|
|
if (!tabId || tabId === activeTabIdRef.current) {
|
|
return Promise.resolve(null)
|
|
}
|
|
|
|
const inFlight = captureInFlightRef.current.get(tabId)
|
|
if (inFlight) return inFlight
|
|
|
|
setTabPreviewCapturing((current) =>
|
|
current[tabId] ? current : { ...current, [tabId]: true }
|
|
)
|
|
|
|
const run = async () => {
|
|
if (tabId === activeTabIdRef.current) {
|
|
return null
|
|
}
|
|
|
|
const pane =
|
|
tabPaneElsRef.current.get(tabId) ||
|
|
document.querySelector(
|
|
`.dashboard-tab-pane[data-tab-id="${CSS.escape(tabId)}"]`
|
|
)
|
|
if (!pane) return null
|
|
|
|
try {
|
|
const dataUrl = await captureTabPaneImage(pane)
|
|
if (dataUrl) {
|
|
const theme = previewThemeRef.current
|
|
setTabPreviews((current) => {
|
|
const existing = current[tabId]
|
|
if (existing?.src === dataUrl && existing?.theme === theme) {
|
|
return current
|
|
}
|
|
return { ...current, [tabId]: { src: dataUrl, theme } }
|
|
})
|
|
}
|
|
return dataUrl
|
|
} catch (error) {
|
|
console.error('Failed to capture tab preview', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
const next = captureQueueRef.current.then(run, run)
|
|
captureInFlightRef.current.set(tabId, next)
|
|
captureQueueRef.current = next.then(
|
|
() => undefined,
|
|
() => undefined
|
|
)
|
|
next.finally(() => {
|
|
if (captureInFlightRef.current.get(tabId) === next) {
|
|
captureInFlightRef.current.delete(tabId)
|
|
}
|
|
setTabPreviewCapturing((current) => {
|
|
if (!current[tabId]) return current
|
|
const nextCapturing = { ...current }
|
|
delete nextCapturing[tabId]
|
|
return nextCapturing
|
|
})
|
|
})
|
|
return next
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const previousId = previousActiveTabIdRef.current
|
|
previousActiveTabIdRef.current = activeTabId
|
|
if (!hydrated || !previousId || !activeTabId || previousId === activeTabId) {
|
|
return
|
|
}
|
|
if (!tabsRef.current.some((tab) => tab.id === previousId)) return
|
|
void captureTabPreview(previousId)
|
|
}, [activeTabId, captureTabPreview, hydrated])
|
|
|
|
const setTabPage = useCallback(({ title, modelName } = {}) => {
|
|
const activeId = activeTabIdRef.current
|
|
if (!activeId) return
|
|
|
|
setTabs((current) =>
|
|
current.map((tab) => {
|
|
if (tab.id !== activeId) return tab
|
|
return {
|
|
...tab,
|
|
title: title || tab.title,
|
|
modelName:
|
|
modelName === undefined ? tab.modelName : modelName || null
|
|
}
|
|
})
|
|
)
|
|
}, [])
|
|
|
|
const goBack = useCallback(() => {
|
|
const currentTab = tabsRef.current.find(
|
|
(tab) => tab.id === activeTabIdRef.current
|
|
)
|
|
if (!currentTab || currentTab.historyIndex <= 0) return
|
|
|
|
const nextIndex = currentTab.historyIndex - 1
|
|
const nextEntry = currentTab.history[nextIndex]
|
|
setTabs((current) =>
|
|
current.map((tab) =>
|
|
tab.id === currentTab.id ? { ...tab, historyIndex: nextIndex } : tab
|
|
)
|
|
)
|
|
restoreToEntry(nextEntry)
|
|
}, [restoreToEntry])
|
|
|
|
const goForward = useCallback(() => {
|
|
const currentTab = tabsRef.current.find(
|
|
(tab) => tab.id === activeTabIdRef.current
|
|
)
|
|
if (!currentTab) return
|
|
if (currentTab.historyIndex >= currentTab.history.length - 1) return
|
|
|
|
const nextIndex = currentTab.historyIndex + 1
|
|
const nextEntry = currentTab.history[nextIndex]
|
|
setTabs((current) =>
|
|
current.map((tab) =>
|
|
tab.id === currentTab.id ? { ...tab, historyIndex: nextIndex } : tab
|
|
)
|
|
)
|
|
restoreToEntry(nextEntry)
|
|
}, [restoreToEntry])
|
|
|
|
const createNewWindow = useCallback(() => {
|
|
const currentTab = tabsRef.current.find(
|
|
(tab) => tab.id === activeTabIdRef.current
|
|
)
|
|
const nextTab = cloneCurrentPageTab(currentTab, location)
|
|
void createAppWindow?.({
|
|
tabs: [nextTab],
|
|
activeTabId: nextTab.id
|
|
})
|
|
}, [createAppWindow, location])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron) {
|
|
setHydrated(true)
|
|
return undefined
|
|
}
|
|
|
|
let cancelled = false
|
|
|
|
const hydrate = async () => {
|
|
const deadline = Date.now() + 2000
|
|
while (!getDesktopWindowId() && Date.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
}
|
|
|
|
const session = await getWindowSession?.()
|
|
if (cancelled) return
|
|
|
|
if (session?.tabs?.length) {
|
|
isRestoringRef.current = true
|
|
setTabs(session.tabs)
|
|
const nextActiveId =
|
|
session.activeTabId &&
|
|
session.tabs.some((tab) => tab.id === session.activeTabId)
|
|
? session.activeTabId
|
|
: session.tabs[0].id
|
|
setActiveTabId(nextActiveId)
|
|
const activeTab =
|
|
session.tabs.find((tab) => tab.id === nextActiveId) ||
|
|
session.tabs[0]
|
|
const entry = getTabCurrentEntry(activeTab)
|
|
if (!entriesEqual(entry, location)) {
|
|
restoreToEntry(entry)
|
|
} else {
|
|
isRestoringRef.current = false
|
|
}
|
|
} else {
|
|
const initialTab = createTabFromLocation(location)
|
|
setTabs([initialTab])
|
|
setActiveTabId(initialTab.id)
|
|
}
|
|
|
|
setHydrated(true)
|
|
}
|
|
|
|
void hydrate()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
// Hydrate once per window on mount.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isElectron])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !hydrated) return
|
|
if (isRestoringRef.current) {
|
|
isRestoringRef.current = false
|
|
return
|
|
}
|
|
|
|
const entry = locationToEntry(location)
|
|
setTabs((current) => {
|
|
if (current.length === 0) {
|
|
const initialTab = createTabFromLocation(location)
|
|
setActiveTabId(initialTab.id)
|
|
return [initialTab]
|
|
}
|
|
|
|
return current.map((tab) => {
|
|
if (tab.id !== activeTabIdRef.current) return tab
|
|
const currentEntry = getTabCurrentEntry(tab)
|
|
if (entriesEqual(currentEntry, entry)) return tab
|
|
|
|
const truncated = tab.history.slice(0, tab.historyIndex + 1)
|
|
return {
|
|
...tab,
|
|
history: [...truncated, entry],
|
|
historyIndex: truncated.length
|
|
}
|
|
})
|
|
})
|
|
}, [hydrated, isElectron, location])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !hydrated || !syncWindowTabs) return undefined
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
void syncWindowTabs({
|
|
windowId: getDesktopWindowId(),
|
|
activeTabId,
|
|
tabs
|
|
})
|
|
}, 200)
|
|
|
|
return () => clearTimeout(timeoutId)
|
|
}, [activeTabId, hydrated, isElectron, syncWindowTabs, tabs])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !registerTabHistoryHandler) return undefined
|
|
registerTabHistoryHandler((direction) => {
|
|
if (direction === 'back') goBack()
|
|
if (direction === 'forward') goForward()
|
|
})
|
|
return () => registerTabHistoryHandler(null)
|
|
}, [goBack, goForward, isElectron, registerTabHistoryHandler])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !onNewTabRequest) return undefined
|
|
return onNewTabRequest(() => addTab())
|
|
}, [addTab, isElectron, onNewTabRequest])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !onNewWindowRequest) return undefined
|
|
return onNewWindowRequest(() => createNewWindow())
|
|
}, [createNewWindow, isElectron, onNewWindowRequest])
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !onTabMovedAway) return undefined
|
|
return onTabMovedAway(({ tabId } = {}) => {
|
|
if (!tabId) return
|
|
removeTab(tabId, { closeWindowIfLast: true })
|
|
})
|
|
}, [isElectron, onTabMovedAway, removeTab])
|
|
|
|
const activeTab = useMemo(
|
|
() => tabs.find((tab) => tab.id === activeTabId) || null,
|
|
[activeTabId, tabs]
|
|
)
|
|
const canGoBack = (activeTab?.historyIndex || 0) > 0
|
|
const canGoForward =
|
|
(activeTab?.historyIndex || 0) < (activeTab?.history?.length || 1) - 1
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
tabs,
|
|
activeTabId,
|
|
activeTab,
|
|
hydrated,
|
|
isElectron,
|
|
selectTab,
|
|
addTab,
|
|
closeTab,
|
|
reorderTabs,
|
|
acceptIncomingTab,
|
|
handleTabDragStart,
|
|
handleTabDragEnd,
|
|
handleExternalTabDrop,
|
|
setTabPage,
|
|
goBack,
|
|
goForward,
|
|
canGoBack,
|
|
canGoForward,
|
|
createNewWindow,
|
|
registerTabPane,
|
|
captureTabPreview
|
|
}),
|
|
[
|
|
activeTab,
|
|
activeTabId,
|
|
addTab,
|
|
canGoBack,
|
|
canGoForward,
|
|
captureTabPreview,
|
|
closeTab,
|
|
createNewWindow,
|
|
goBack,
|
|
goForward,
|
|
handleExternalTabDrop,
|
|
handleTabDragEnd,
|
|
handleTabDragStart,
|
|
acceptIncomingTab,
|
|
hydrated,
|
|
isElectron,
|
|
registerTabPane,
|
|
reorderTabs,
|
|
selectTab,
|
|
setTabPage,
|
|
tabs
|
|
]
|
|
)
|
|
|
|
const previewsValue = useMemo(
|
|
() => ({
|
|
tabPreviews,
|
|
tabPreviewCapturing
|
|
}),
|
|
[tabPreviewCapturing, tabPreviews]
|
|
)
|
|
const metaValue = useMemo(
|
|
() => ({
|
|
setTabPage,
|
|
isElectron
|
|
}),
|
|
[isElectron, setTabPage]
|
|
)
|
|
|
|
return (
|
|
<NavigationTabsContext.Provider value={value}>
|
|
<NavigationTabMetaContext.Provider value={metaValue}>
|
|
<NavigationTabPreviewsContext.Provider value={previewsValue}>
|
|
{children}
|
|
</NavigationTabPreviewsContext.Provider>
|
|
</NavigationTabMetaContext.Provider>
|
|
</NavigationTabsContext.Provider>
|
|
)
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const useNavigationTabs = () => {
|
|
const context = useContext(NavigationTabsContext)
|
|
if (!context) {
|
|
throw new Error(
|
|
'useNavigationTabs must be used within a NavigationTabsProvider'
|
|
)
|
|
}
|
|
return context
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const useTabPreview = (tabId) => {
|
|
const { tabPreviews, tabPreviewCapturing } = useContext(
|
|
NavigationTabPreviewsContext
|
|
)
|
|
const preview = tabId ? tabPreviews[tabId] : null
|
|
return {
|
|
src: preview?.src || null,
|
|
theme: preview?.theme || null,
|
|
capturing: Boolean(tabId && tabPreviewCapturing[tabId])
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const useNavigationTabPage = ({ title, modelName } = {}) => {
|
|
const { setTabPage, isElectron } = useContext(NavigationTabMetaContext)
|
|
const store = useContext(NavigationTabActiveContext)
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !title) return undefined
|
|
|
|
const sync = () => {
|
|
if (!store.getSnapshot()) return
|
|
setTabPage({ title, modelName })
|
|
}
|
|
|
|
sync()
|
|
return store.subscribe(sync)
|
|
}, [isElectron, modelName, setTabPage, store, title])
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const useIsNavigationTabActive = () => {
|
|
const store = useContext(NavigationTabActiveContext)
|
|
return useSyncExternalStore(
|
|
store.subscribe,
|
|
store.getSnapshot,
|
|
store.getSnapshot
|
|
)
|
|
}
|
|
|
|
NavigationTabsProvider.propTypes = {
|
|
children: PropTypes.node.isRequired
|
|
}
|
|
|
|
export default NavigationTabsContext
|