import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { dimensToCssVars, themeToCssVars } from '../../os/utils/themeToCssVars';
import { applySkinToThemeColors } from '../../os/SkinService';
import { useDarkMode } from '../../os/hooks/useDarkMode';
import { MemoryRouter, useLocation, UNSAFE_NavigationContext, Routes, Route } from 'react-router-dom';
import { IcGlobe, IcBack, IcForward, IcRefresh, IcHome, IcSearch, IcShield, IcNavBack, IcNavForward, IcTabs, IcMoreHoriz, IcClose, IcAdd } from './res/icons';
import { manifest } from './manifest';
import { colors, colorsDark } from './res/colors';
import { colorStates, colorStatesDark } from './res/colors.states';
import { dimens } from './res/dimens';
import { anim } from './res/anim';
import { useBrowserStore } from './state';
import type { Tab } from './state';
import { useAppNavigationHandler } from '../../os/hooks/useAppNavigationHandler';
import { useBrowserGestures } from './hooks/useBrowserGestures';
// --- Navigation Handler ---
const BrowserNavigationHandler: React.FC = () => {
const location = useLocation();
const { back } = useBrowserGestures();
const { navigator } = React.useContext(UNSAFE_NavigationContext);
const historyIndexRef = useRef(0);
useEffect(() => {
const memoryNavigator = navigator as any;
if (typeof memoryNavigator.index === 'number') {
historyIndexRef.current = memoryNavigator.index;
}
}, [location, navigator]);
const handleBackPress = useCallback((): boolean => {
const memoryNavigator = navigator as any;
const currentIndex = typeof memoryNavigator.index === 'number'
? memoryNavigator.index
: historyIndexRef.current;
if (currentIndex > 0) {
back();
return true;
}
return false;
}, [back, navigator]);
useAppNavigationHandler('browser', { onBack: handleBackPress });
return null;
};
// --- Home Component ---
const BrowserHome: React.FC<{ onNavigate: (url: string) => void }> = ({ onNavigate }) => {
const [input, setInput] = useState('');
const recommendations = [
{ name: '维基百科', url: 'https://zh.m.wikipedia.org', icon: 'https://zh.wikipedia.org/favicon.ico', color: 'bg-white' },
{ name: 'GitHub', url: 'https://github.com', icon: 'https://github.com/favicon.ico', color: 'bg-black' },
{ name: '百度', url: 'https://www.baidu.com', icon: 'https://www.baidu.com/favicon.ico', color: 'bg-blue-500' },
{ name: 'Bilibili', url: 'https://m.bilibili.com', icon: 'https://www.bilibili.com/favicon.ico', color: 'bg-pink-400' },
{ name: '知乎', url: 'https://www.zhihu.com', icon: 'https://www.zhihu.com/favicon.ico', color: 'bg-blue-600' },
{ name: 'React', url: 'https://react.dev', icon: 'https://react.dev/favicon.ico', color: 'bg-sky-400' },
{ name: 'Google', url: 'https://www.google.com', icon: 'https://www.google.com/favicon.ico', color: 'bg-white' },
{ name: 'Bing', url: 'https://www.bing.com', icon: 'https://www.bing.com/favicon.ico', color: 'bg-white' },
];
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
onNavigate(input.trim());
};
return (
{recommendations.map((item) => (
))}
);
};
// --- WebView Component ---
const BrowserView: React.FC<{ url: string }> = ({ url }) => {
const [isLoading, setIsLoading] = useState(true);
return (
);
};
// --- Tabs Overlay ---
const TabsOverlay: React.FC<{
tabs: Tab[],
activeTabId: string,
onCloseTab: (id: string, e: React.MouseEvent) => void,
onSelectTab: (id: string) => void,
onNewTab: () => void,
onCloseOverlay: () => void
}> = ({ tabs, activeTabId, onCloseTab, onSelectTab, onNewTab, onCloseOverlay }) => {
return (
标签页
{tabs.map((tab) => (
onSelectTab(tab.id)}
className={`aspect-[3/4] rounded-2xl p-4 flex flex-col relative overflow-hidden transition-all ${tab.id === activeTabId ? 'ring-4 ring-blue-500 scale-100' : 'bg-white/10 scale-95'}`}
>
{tab.title || '新标签页'}
{tab.url}
))}
);
};
// --- Main App Logic ---
const BrowserContent: React.FC = () => {
const { go, back, bindBack } = useBrowserGestures();
const location = useLocation();
const { navigator } = React.useContext(UNSAFE_NavigationContext);
// Store state
const tabs = useBrowserStore(s => s.tabs);
const activeTabId = useBrowserStore(s => s.activeTabId);
const activeTab = useMemo(() => tabs.find(t => t.id === activeTabId) || tabs[0], [tabs, activeTabId]);
// Store actions (stable refs)
const navigateTab = useBrowserStore(s => s.navigateTab);
const storeAddTab = useBrowserStore(s => s.addTab);
const storeCloseTab = useBrowserStore(s => s.closeTab);
const storeGoHome = useBrowserStore(s => s.goHome);
const setActiveTabId = useBrowserStore(s => s.setActiveTabId);
const trackVisitedUrl = useBrowserStore(s => s.trackVisitedUrl);
const [isTabsOverlayOpen, setIsTabsOverlayOpen] = useState(false);
// Track visited URLs
useEffect(() => {
if (activeTab.url) {
trackVisitedUrl(activeTab.url);
}
}, [activeTab.url, trackVisitedUrl]);
const handleNavigate = (input: string) => {
let target = input.trim();
if (!target.startsWith('http')) {
if (target.includes('.') && !target.includes(' ')) {
target = 'https://' + target;
} else {
target = `https://www.bing.com/search?q=${encodeURIComponent(target)}`;
}
}
navigateTab(activeTabId, target);
go('view.open');
};
const handleNewTab = () => {
storeAddTab();
setIsTabsOverlayOpen(false);
go('home.open');
};
const handleCloseTab = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
const result = storeCloseTab(id);
if (id === activeTabId || tabs.length === 1) {
if (result.newActiveTabHasUrl) {
go('view.open');
} else {
go('home.open');
}
}
};
const currentHistoryPos = (navigator as any).index || 0;
const canGoBack = currentHistoryPos > 0;
const canGoForward = false; // MemoryRouter Forward is tricky to detect easily
return (
{/* Content */}
} />
} />
{/* Bottom Bar */}
{/* Tabs Overlay */}
{isTabsOverlayOpen && (
{
setActiveTabId(id);
setIsTabsOverlayOpen(false);
const target = tabs.find(t => t.id === id);
if (target?.url) {
go('view.open');
} else {
go('home.open');
}
}}
onNewTab={handleNewTab}
onCloseOverlay={() => setIsTabsOverlayOpen(false)}
/>
)}
);
};
export const BrowserApp: React.FC = () => {
const { isDark } = useDarkMode();
const themeColors = isDark
? { ...manifest.theme.colors, ...(manifest.theme.colorsDark ?? {}) }
: manifest.theme.colors;
const appColors = isDark ? { ...colors, ...colorsDark } : colors;
const appColorStates = isDark ? { ...colorStates, ...colorStatesDark } : colorStates;
const cssVars = {
...themeToCssVars(applySkinToThemeColors(themeColors)),
...dimensToCssVars(appColors, { prefix: '--app-c-' }),
...dimensToCssVars(appColorStates, { prefix: '--app-cs-' }),
...dimensToCssVars(dimens),
...dimensToCssVars(anim, { prefix: '--app-' }),
};
return (
);
};
export default BrowserApp;