项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

118 行
3.4 KiB
TypeScript

import { createAppStoreWithActions, memoSelector } from '../../os/createAppStore';
import { BROWSER_CONFIG } from './data';
// --- Types ---
export interface Tab {
id: string;
url: string;
title: string;
}
interface BrowserState {
tabs: Tab[];
activeTabId: string;
visitedUrls: string[];
}
interface BrowserActions {
/** Set the active tab by ID */
setActiveTabId: (id: string) => void;
/** Navigate a tab to a URL (updates url + title) */
navigateTab: (tabId: string, url: string) => void;
/** Add a new empty tab and make it active; returns the new tab ID */
addTab: () => string;
/** Close a tab; if it's the last one, replaces with a fresh default tab.
* Returns { newActiveTabId, newActiveTabHasUrl } so the caller can do router navigation. */
closeTab: (id: string) => { newActiveTabId: string; newActiveTabHasUrl: boolean };
/** Reset a tab to the home state (empty URL) */
goHome: (tabId: string) => void;
/** Track a visited URL (no-op if already tracked) */
trackVisitedUrl: (url: string) => void;
}
// --- Initial state ---
const initialState: BrowserState = {
tabs: BROWSER_CONFIG.tabs as Tab[],
activeTabId: BROWSER_CONFIG.activeTabId,
visitedUrls: BROWSER_CONFIG.visitedUrls as string[],
};
// --- Store ---
export const useBrowserStore = createAppStoreWithActions<BrowserState, BrowserActions>(
'browser',
initialState,
(set, get) => ({
setActiveTabId(id: string) {
set({ activeTabId: id });
},
navigateTab(tabId: string, url: string) {
const { tabs } = get();
set({
tabs: tabs.map(t =>
t.id === tabId ? { ...t, url, title: url.split('/')[2] || url } : t,
),
});
},
addTab() {
const newId = Math.random().toString(36).substr(2, 9);
const { tabs } = get();
set({
tabs: [...tabs, { id: newId, url: '', title: '主页' }],
activeTabId: newId,
});
return newId;
},
closeTab(id: string) {
const { tabs, activeTabId } = get();
// Last tab: replace with fresh default
if (tabs.length === 1) {
const freshTab: Tab = { id: '1', url: '', title: '主页' };
set({ tabs: [freshTab], activeTabId: '1' });
return { newActiveTabId: '1', newActiveTabHasUrl: false };
}
const newTabs = tabs.filter(t => t.id !== id);
// If we're closing the active tab, switch to the first remaining one
if (id === activeTabId) {
const newActive = newTabs[0];
set({ tabs: newTabs, activeTabId: newActive.id });
return { newActiveTabId: newActive.id, newActiveTabHasUrl: !!newActive.url };
}
set({ tabs: newTabs });
return { newActiveTabId: activeTabId, newActiveTabHasUrl: !!tabs.find(t => t.id === activeTabId)?.url };
},
goHome(tabId: string) {
const { tabs } = get();
set({
tabs: tabs.map(t =>
t.id === tabId ? { ...t, url: '', title: '主页' } : t,
),
});
},
trackVisitedUrl(url: string) {
if (!url) return;
const { visitedUrls } = get();
if (visitedUrls.includes(url)) return;
set({ visitedUrls: [...visitedUrls, url] });
},
}),
);
// --- Memoized selectors ---
/** Derive the active tab from tabs + activeTabId */
export const selectActiveTab = memoSelector(
(state: BrowserState & BrowserActions) => ({ tabs: state.tabs, activeTabId: state.activeTabId }),
({ tabs, activeTabId }) => tabs.find(t => t.id === activeTabId) || tabs[0],
);