mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-30 14:28:55 +00:00
346 lines
No EOL
10 KiB
TypeScript
346 lines
No EOL
10 KiB
TypeScript
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
|
|
import { mmkvStorage } from '../services/mmkvStorage';
|
|
import { settingsEmitter } from '../hooks/useSettings';
|
|
import { colors as defaultColors } from '../styles/colors';
|
|
|
|
// Define the Theme interface
|
|
export interface Theme {
|
|
id: string;
|
|
name: string;
|
|
colors: typeof defaultColors;
|
|
isEditable: boolean;
|
|
}
|
|
|
|
// Default built-in themes
|
|
export const DEFAULT_THEMES: Theme[] = [
|
|
{
|
|
id: 'default',
|
|
name: 'Default Dark',
|
|
colors: defaultColors,
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'ocean',
|
|
name: 'Ocean Blue',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#3498db',
|
|
secondary: '#2ecc71',
|
|
darkBackground: '#0a192f',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'sunset',
|
|
name: 'Sunset',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#ff7e5f',
|
|
secondary: '#feb47b',
|
|
darkBackground: '#1a0f0b',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'moonlight',
|
|
name: 'Moonlight',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#c084fc',
|
|
secondary: '#60a5fa',
|
|
darkBackground: '#060609',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'emerald',
|
|
name: 'Emerald',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#2ecc71',
|
|
secondary: '#3498db',
|
|
darkBackground: '#0e1e13',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'ruby',
|
|
name: 'Ruby',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#e74c3c',
|
|
secondary: '#9b59b6',
|
|
darkBackground: '#1a0a0a',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'amethyst',
|
|
name: 'Amethyst',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#9b59b6',
|
|
secondary: '#3498db',
|
|
darkBackground: '#140a1c',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'amber',
|
|
name: 'Amber',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#f39c12',
|
|
secondary: '#d35400',
|
|
darkBackground: '#1a140a',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'mint',
|
|
name: 'Mint',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#1abc9c',
|
|
secondary: '#16a085',
|
|
darkBackground: '#0a1a17',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'slate',
|
|
name: 'Slate',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#7f8c8d',
|
|
secondary: '#95a5a6',
|
|
darkBackground: '#10191a',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'neon',
|
|
name: 'Neon',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#00ff00',
|
|
secondary: '#ff00ff',
|
|
darkBackground: '#0a0a0a',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
{
|
|
id: 'retro',
|
|
name: 'Retro Wave',
|
|
colors: {
|
|
...defaultColors,
|
|
primary: '#ff00ff',
|
|
secondary: '#00ffff',
|
|
darkBackground: '#150036',
|
|
},
|
|
isEditable: false,
|
|
},
|
|
];
|
|
|
|
// Theme context props
|
|
interface ThemeContextProps {
|
|
currentTheme: Theme;
|
|
availableThemes: Theme[];
|
|
setCurrentTheme: (themeId: string) => void;
|
|
addCustomTheme: (theme: Omit<Theme, 'id' | 'isEditable'>) => void;
|
|
updateCustomTheme: (theme: Theme) => void;
|
|
deleteCustomTheme: (themeId: string) => void;
|
|
}
|
|
|
|
// Create the context
|
|
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
|
|
|
|
// Storage keys (kept for backward compatibility). Primary source of truth is app_settings
|
|
const CURRENT_THEME_KEY = 'current_theme';
|
|
const CUSTOM_THEMES_KEY = 'custom_themes';
|
|
|
|
// Provider component
|
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
const [currentTheme, setCurrentThemeState] = useState<Theme>(DEFAULT_THEMES[0]);
|
|
const [availableThemes, setAvailableThemes] = useState<Theme[]>(DEFAULT_THEMES);
|
|
|
|
// Load themes from app_settings (scoped), with legacy fallbacks
|
|
useEffect(() => {
|
|
const loadThemes = async () => {
|
|
try {
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
const appSettingsJson = await mmkvStorage.getItem(`@user:${scope}:app_settings`);
|
|
const appSettings = appSettingsJson ? JSON.parse(appSettingsJson) : {};
|
|
const savedThemeId = appSettings.themeId || (await mmkvStorage.getItem(CURRENT_THEME_KEY));
|
|
const customThemesJson = appSettings.customThemes ? JSON.stringify(appSettings.customThemes) : await mmkvStorage.getItem(CUSTOM_THEMES_KEY);
|
|
const customThemes = customThemesJson ? JSON.parse(customThemesJson) : [];
|
|
const allThemes = [...DEFAULT_THEMES, ...customThemes];
|
|
setAvailableThemes(allThemes);
|
|
if (savedThemeId) {
|
|
const theme = allThemes.find(t => t.id === savedThemeId);
|
|
if (theme) setCurrentThemeState(theme);
|
|
}
|
|
} catch (error) {
|
|
if (__DEV__) console.error('Failed to load themes:', error);
|
|
}
|
|
};
|
|
loadThemes();
|
|
// Stop live refresh from remote; only refresh on app restart or local changes
|
|
return () => {};
|
|
}, []);
|
|
|
|
// Set current theme
|
|
const setCurrentTheme = async (themeId: string) => {
|
|
const theme = availableThemes.find(t => t.id === themeId);
|
|
if (theme) {
|
|
setCurrentThemeState(theme);
|
|
// Persist into scoped app_settings and legacy key for backward compat
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
const key = `@user:${scope}:app_settings`;
|
|
let settings = {} as any;
|
|
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
|
|
settings.themeId = themeId;
|
|
await mmkvStorage.setItem(key, JSON.stringify(settings));
|
|
await mmkvStorage.setItem(CURRENT_THEME_KEY, themeId);
|
|
// Do not emit global settings sync for themes (sync on app restart only)
|
|
}
|
|
};
|
|
|
|
// Add custom theme
|
|
const addCustomTheme = async (themeData: Omit<Theme, 'id' | 'isEditable'>) => {
|
|
try {
|
|
// Generate unique ID
|
|
const id = `custom_${Date.now()}`;
|
|
|
|
// Create new theme object
|
|
const newTheme: Theme = {
|
|
id,
|
|
...themeData,
|
|
isEditable: true,
|
|
};
|
|
|
|
// Add to available themes
|
|
const customThemes = availableThemes.filter(t => t.isEditable);
|
|
const updatedCustomThemes = [...customThemes, newTheme];
|
|
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
|
|
|
|
// Save to storage (scoped app_settings + legacy key)
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
const key = `@user:${scope}:app_settings`;
|
|
let settings = {} as any;
|
|
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
|
|
settings.customThemes = updatedCustomThemes;
|
|
await mmkvStorage.setItem(key, JSON.stringify(settings));
|
|
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
|
|
|
|
// Update state
|
|
setAvailableThemes(updatedAllThemes);
|
|
|
|
// Set as current theme
|
|
setCurrentThemeState(newTheme);
|
|
await mmkvStorage.setItem(CURRENT_THEME_KEY, id);
|
|
// Do not emit global settings sync for themes
|
|
} catch (error) {
|
|
if (__DEV__) console.error('Failed to add custom theme:', error);
|
|
}
|
|
};
|
|
|
|
// Update custom theme
|
|
const updateCustomTheme = async (updatedTheme: Theme) => {
|
|
try {
|
|
if (!updatedTheme.isEditable) {
|
|
throw new Error('Cannot edit built-in themes');
|
|
}
|
|
|
|
// Find and update the theme
|
|
const customThemes = availableThemes.filter(t => t.isEditable);
|
|
const updatedCustomThemes = customThemes.map(t =>
|
|
t.id === updatedTheme.id ? updatedTheme : t
|
|
);
|
|
|
|
// Update available themes
|
|
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
|
|
|
|
// Save to storage (scoped app_settings + legacy key)
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
const key = `@user:${scope}:app_settings`;
|
|
let settings = {} as any;
|
|
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
|
|
settings.customThemes = updatedCustomThemes;
|
|
await mmkvStorage.setItem(key, JSON.stringify(settings));
|
|
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
|
|
|
|
// Update state
|
|
setAvailableThemes(updatedAllThemes);
|
|
|
|
// Update current theme if needed
|
|
if (currentTheme.id === updatedTheme.id) {
|
|
setCurrentThemeState(updatedTheme);
|
|
}
|
|
// Do not emit global settings sync for themes
|
|
} catch (error) {
|
|
if (__DEV__) console.error('Failed to update custom theme:', error);
|
|
}
|
|
};
|
|
|
|
// Delete custom theme
|
|
const deleteCustomTheme = async (themeId: string) => {
|
|
try {
|
|
// Find theme to delete
|
|
const themeToDelete = availableThemes.find(t => t.id === themeId);
|
|
|
|
if (!themeToDelete || !themeToDelete.isEditable) {
|
|
throw new Error('Cannot delete built-in themes or theme not found');
|
|
}
|
|
|
|
// Filter out the theme
|
|
const customThemes = availableThemes.filter(t => t.isEditable && t.id !== themeId);
|
|
const updatedAllThemes = [...DEFAULT_THEMES, ...customThemes];
|
|
|
|
// Save to storage (scoped app_settings + legacy key)
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
const key = `@user:${scope}:app_settings`;
|
|
let settings = {} as any;
|
|
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
|
|
settings.customThemes = customThemes;
|
|
await mmkvStorage.setItem(key, JSON.stringify(settings));
|
|
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(customThemes));
|
|
|
|
// Update state
|
|
setAvailableThemes(updatedAllThemes);
|
|
|
|
// Reset to default theme if current theme was deleted
|
|
if (currentTheme.id === themeId) {
|
|
setCurrentThemeState(DEFAULT_THEMES[0]);
|
|
await mmkvStorage.setItem(CURRENT_THEME_KEY, DEFAULT_THEMES[0].id);
|
|
}
|
|
// Do not emit global settings sync for themes
|
|
} catch (error) {
|
|
if (__DEV__) console.error('Failed to delete custom theme:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ThemeContext.Provider
|
|
value={{
|
|
currentTheme,
|
|
availableThemes,
|
|
setCurrentTheme,
|
|
addCustomTheme,
|
|
updateCustomTheme,
|
|
deleteCustomTheme,
|
|
}}
|
|
>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
// Custom hook to use the theme context
|
|
export function useTheme() {
|
|
const context = useContext(ThemeContext);
|
|
if (context === undefined) {
|
|
throw new Error('useTheme must be used within a ThemeProvider');
|
|
}
|
|
return context;
|
|
}
|