import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { MemoryRouter, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { IcNavBack, IcLoading } from './res/icons'; import { useTheme } from '../../os/ThemeContext'; import type { ThemeMeta } from '../../os/ThemeService'; import { useAppNavigationHandler } from '../../os/hooks/useAppNavigationHandler'; import { SIMULATOR_CONFIG } from '@/os/data'; import { loadLauncherLayout, saveLauncherLayout } from '../../os/launcher/storage'; import type { LauncherWallpaper } from '../../os/launcher/types'; import { dimensToCssVars, themeToCssVars } from '../../os/utils/themeToCssVars'; import { applySkinToThemeColors } from '../../os/SkinService'; import { useDarkMode } from '../../os/hooks/useDarkMode'; import { useAppStrings } from '../../os/useAppStrings'; 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 { strings } from './res/strings'; import { stringsEn } from './res/strings.en'; import { useThemeStoreGestures } from './hooks/useThemeStoreGestures'; const { statusBarHeight } = SIMULATOR_CONFIG.framework; type StoreTab = 'themes' | 'fonts' | 'aod'; function cn(...parts: Array) { return parts.filter(Boolean).join(' '); } function pill(label: string, tone: 'blue' | 'green' | 'gray' = 'gray') { const cls = tone === 'blue' ? 'bg-blue-50 text-blue-700' : tone === 'green' ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'; return {label}; } function setLauncherWallpaper(wallpaper: LauncherWallpaper) { try { const layout = loadLauncherLayout(); saveLauncherLayout({ ...layout, wallpaper }); } catch (e) { console.warn('[ThemeStore] Failed to set launcher wallpaper', e); } } const ThemeStoreNavigationHandler: React.FC = () => { const location = useLocation(); const { back } = useThemeStoreGestures(); const handleBackPress = useCallback((): boolean => { if (location.pathname !== '/') { back(); return true; } return false; }, [location.pathname, back]); useAppNavigationHandler('theme_store', { onBack: handleBackPress }); return null; }; function ThemeStoreHomePage() { const s = useAppStrings(strings, stringsEn); const { themeService, version } = useTheme(); const { go } = useThemeStoreGestures(); const [tab, setTab] = useState('themes'); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const loadItems = useCallback((forceReload: boolean) => { setLoading(true); setError(null); const request = tab === 'themes' ? themeService.getInstalledThemes(forceReload) : tab === 'fonts' ? themeService.getFonts(forceReload) : themeService.getAod(forceReload); request .then((nextItems) => setItems(nextItems)) .catch((e) => setError(e instanceof Error ? e.message : String(e))) .finally(() => setLoading(false)); }, [tab, themeService]); useEffect(() => { let cancelled = false; setLoading(true); setError(null); const request = tab === 'themes' ? themeService.getInstalledThemes(true) : tab === 'fonts' ? themeService.getFonts(true) : themeService.getAod(true); request .then((nextItems) => { if (!cancelled) setItems(nextItems); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [tab, themeService, version]); const currentThemeId = themeService.getCurrentThemeId(); return (
{s.app_name}
{loading ? (
{s.loading}
) : error ? (
{s.load_failed}{error}
) : items.length === 0 ? (
{s.no_resources_found}
python3 scripts/dev/prepare-themes.py -> public/themes
) : (
{items.map((themeMeta) => { const previewUrl = themeService.getThemePreviewUrls(themeMeta.id)[0] || ''; const active = tab === 'themes' ? currentThemeId === themeMeta.id : false; const supportsIcons = (themeMeta.iconPackageNames?.length || 0) > 0; const supportsWallpaper = !!themeMeta.extracted?.wallpaper?.default; const supportsStatusbar = (themeMeta.statusbarIcons?.length || 0) > 0; const supportsShade = !!themeMeta.extracted?.shade; return ( ); })}
)}
); } function StoreItemDetailPage() { const s = useAppStrings(strings, stringsEn); const { themeService, version } = useTheme(); const { bindBack } = useThemeStoreGestures(); const params = useParams<{ kind: 'theme' | 'font' | 'aod'; id: string }>(); const kind = params.kind || 'theme'; const id = params.id || ''; const [themeMeta, setThemeMeta] = useState(null); const [loading, setLoading] = useState(true); const [applying, setApplying] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); setError(null); const request = kind === 'theme' ? themeService.getInstalledThemes(false) : kind === 'font' ? themeService.getFonts(false) : themeService.getAod(false); request .then((nextItems) => { if (!cancelled) setThemeMeta(nextItems.find((item) => item.id === id) || null); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [id, kind, themeService, version]); const currentThemeId = themeService.getCurrentThemeId(); const active = kind === 'theme' ? currentThemeId === id : false; const previewPaths = useMemo(() => { if (!themeMeta) return []; return themeService.getThemePreviewUrls(themeMeta.id); }, [themeMeta, themeService]); const supportsIcons = (themeMeta?.iconPackageNames?.length || 0) > 0; const supportsWallpaper = !!themeMeta?.extracted?.wallpaper?.default; const supportsStatusbar = (themeMeta?.statusbarIcons?.length || 0) > 0; const supportsShade = !!themeMeta?.extracted?.shade; const apply = async (applyKind: 'theme' | 'icons' | 'wallpaper') => { if (!themeMeta) return; setApplying(applyKind); setError(null); try { if (applyKind === 'theme') { await themeService.applyTheme(themeMeta.id); const wallpaperUrl = themeService.getThemeWallpaperUrl(themeMeta.id); if (wallpaperUrl) setLauncherWallpaper({ kind: 'image', imageUrl: wallpaperUrl }); } if (applyKind === 'icons') { await themeService.applyComponent(themeMeta.id, 'icons'); } if (applyKind === 'wallpaper') { const wallpaperUrl = themeService.getThemeWallpaperUrl(themeMeta.id); if (!wallpaperUrl) throw new Error(s.no_available_wallpaper); setLauncherWallpaper({ kind: 'image', imageUrl: wallpaperUrl }); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setApplying(null); } }; return (
{themeMeta?.title || s.theme_detail}
{themeMeta?.author || ''}
{active ? pill(s.enabled, 'green') : null}
{loading ? (
{s.loading}
) : !themeMeta ? (
{s.resource_not_found}{id}
) : (
{previewPaths.slice(0, 4).map((previewPath) => (
{themeMeta.title}
))}
{kind === 'theme' ? ( <>
{s.apply}
{supportsIcons ? pill(s.pill_icons, 'blue') : pill(s.pill_icons, 'gray')} {supportsWallpaper ? pill(s.pill_wallpaper, 'blue') : pill(s.pill_wallpaper, 'gray')} {supportsStatusbar ? pill(s.pill_statusbar, 'blue') : pill(s.pill_statusbar, 'gray')} {supportsShade ? pill(s.pill_control_center, 'blue') : pill(s.pill_control_center, 'gray')}
{s.mix_and_match}
) : (
{kind === 'font' ? s.sim_font_not_supported : s.sim_aod_not_supported}
)} {error ?
{s.operation_failed}{error}
: null}
)}
); } const ThemeStoreApp: 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 ThemeStoreApp;