import React, { ReactNode, useContext, useEffect, useState } from "react" const BANK_ACCOUNT_NUMBER_KEY = 'bank_account_number'; const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name'; const HIDE_SOUPS_KEY = 'hide_soups'; const THEME_KEY = 'theme_preference'; const ACCENT_HUE_KEY = 'accent_hue'; const LEGACY_COLOR_THEME_KEY = 'color_theme'; const STYLE_THEME_KEY = 'style_theme'; const SHOW_BUTTERFLIES_KEY = 'show_butterflies'; const AUTO_ORDER_CHOICE_KEY = 'auto_order_choice'; export type ThemePreference = 'system' | 'light' | 'dark'; /** Vizuální styl aplikace (nezávislý na světlém/tmavém režimu). */ export type StyleTheme = 'simple' | 'umamusume' | 'mlp' | 'hellgirl' | 'kon' | 'bocchi' | 'evangelion' | 'yurucamp' | 'spyfamily' | 'apothecary' | 'frieren' | 'mlpg5' | 'aggretsuko' | 'sumikko' | 'gudetama' | 'miku' | 'dungeonmeshi' | 'chiikawa' | 'totoro'; export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [ { value: 'simple', label: 'Jednoduchý' }, { value: 'umamusume', label: 'Umamusume' }, { value: 'mlp', label: 'My Little Pony (G4)' }, { value: 'mlpg5', label: 'My Little Pony (G5)' }, { value: 'hellgirl', label: 'Hell Girl' }, { value: 'kon', label: 'K-ON!' }, { value: 'bocchi', label: 'Bocchi the Rock!' }, { value: 'evangelion', label: 'Neon Genesis Evangelion' }, { value: 'yurucamp', label: 'Yuru Camp△' }, { value: 'spyfamily', label: 'Spy × Family' }, { value: 'apothecary', label: 'Apothecary Diaries' }, { value: 'frieren', label: 'Frieren' }, { value: 'aggretsuko', label: 'Aggretsuko' }, { value: 'sumikko', label: 'Sumikko Gurashi' }, { value: 'gudetama', label: 'Gudetama' }, { value: 'miku', label: 'Hatsune Miku' }, { value: 'dungeonmeshi', label: 'Dungeon Meshi' }, { value: 'chiikawa', label: 'Chiikawa' }, { value: 'totoro', label: 'Můj soused Totoro' }, ]; export type SettingsContextProps = { bankAccount?: string, holderName?: string, hideSoups?: boolean, themePreference: ThemePreference, accentHue: number, effectiveDark: boolean, styleTheme: StyleTheme, showButterflies: boolean, autoOrderChoice: boolean, setBankAccountNumber: (accountNumber?: string) => void, setBankAccountHolderName: (holderName?: string) => void, setHideSoupsOption: (hideSoups?: boolean) => void, setThemePreference: (theme: ThemePreference) => void, setAccentHue: (hue: number) => void, setStyleTheme: (theme: StyleTheme) => void, setShowButterflies: (show: boolean) => void, setAutoOrderChoice: (enabled: boolean) => void, } type ContextProps = { children: ReactNode } const settingsContext = React.createContext(null); export function ProvideSettings(props: Readonly) { const settings = useProvideSettings(); return {props.children} } export const useSettings = () => { return useContext(settingsContext); } function getInitialTheme(): ThemePreference { try { const saved = localStorage.getItem(THEME_KEY) as ThemePreference | null; if (saved && ['system', 'light', 'dark'].includes(saved)) { return saved; } } catch (e) { // localStorage nedostupný } return 'system'; } function getInitialStyleTheme(): StyleTheme { try { const saved = localStorage.getItem(STYLE_THEME_KEY); const known = STYLE_THEMES.find(t => t.value === saved); if (known) { return known.value; } } catch { // localStorage nedostupný } return 'simple'; } function getInitialShowButterflies(): boolean { try { return localStorage.getItem(SHOW_BUTTERFLIES_KEY) !== 'false'; } catch { // localStorage nedostupný } return true; } function getInitialAutoOrderChoice(): boolean { try { return localStorage.getItem(AUTO_ORDER_CHOICE_KEY) === 'true'; } catch { // localStorage nedostupný } return false; } function getInitialAccentHue(): number { try { const saved = localStorage.getItem(ACCENT_HUE_KEY); if (saved !== null) { const n = parseInt(saved, 10); if (!isNaN(n) && n >= 0 && n <= 360) return n; } // Migrace ze starého string formátu (green/blue/purple) const old = localStorage.getItem(LEGACY_COLOR_THEME_KEY); if (old === 'blue') return 217; if (old === 'purple') return 263; } catch { // localStorage nedostupný } return 142; } // Převod HSL na relativní jas dle WCAG (pro výpočet kontrastu s bílým textem) function hslToRelativeLuminance(h: number, s: number, l: number): number { const sn = s / 100, ln = l / 100; const a = sn * Math.min(ln, 1 - ln); const ch = (n: number) => { const k = (n + h / 30) % 12; return ln - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); }; const toLinear = (c: number) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); return 0.2126 * toLinear(ch(0)) + 0.7152 * toLinear(ch(8)) + 0.0722 * toLinear(ch(4)); } // Najde nejnižší světlost, při které má barva dostatečný kontrast s bílým textem (WCAG AA 4.5:1) function adjustedL(hue: number, sat: number, targetL: number): number { let l = targetL; while (l >= 5) { const lum = hslToRelativeLuminance(hue, sat, l); if (1.05 / (lum + 0.05) >= 4.5) return l; l -= 1; } return l; } const ACCENT_PROPERTIES = ['--luncher-primary', '--luncher-primary-hover', '--luncher-primary-light', '--luncher-action-icon', '--luncher-success']; function clearAccentColors(): void { ACCENT_PROPERTIES.forEach(p => document.documentElement.style.removeProperty(p)); } function applyAccentColors(hue: number, isDark: boolean): void { const sat = 70; const baseL = adjustedL(hue, sat, isDark ? 55 : 38); const hoverL = isDark ? Math.min(baseL + 10, 80) : Math.max(baseL - 10, 10); const root = document.documentElement; root.style.setProperty('--luncher-primary', `hsl(${hue} ${sat}% ${baseL}%)`); root.style.setProperty('--luncher-primary-hover', `hsl(${hue} ${sat}% ${hoverL}%)`); root.style.setProperty('--luncher-primary-light', isDark ? `hsl(${hue} 60% 12%)` : `hsl(${hue} 60% 92%)`); root.style.setProperty('--luncher-action-icon', `hsl(${hue} ${sat}% ${baseL}%)`); root.style.setProperty('--luncher-success', `hsl(${hue} ${sat}% ${baseL}%)`); } function useProvideSettings(): SettingsContextProps { const [bankAccount, setBankAccount] = useState(); const [holderName, setHolderName] = useState(); const [hideSoups, setHideSoups] = useState(); const [themePreference, setTheme] = useState(getInitialTheme); const [accentHue, setHue] = useState(getInitialAccentHue); const [styleTheme, setStyle] = useState(getInitialStyleTheme); const [showButterflies, setShowButterfliesState] = useState(getInitialShowButterflies); const [autoOrderChoice, setAutoOrderChoiceState] = useState(getInitialAutoOrderChoice); const [effectiveDark, setEffectiveDark] = useState(() => { try { const pref = localStorage.getItem(THEME_KEY) as ThemePreference | null; if (pref === 'dark') return true; if (pref === 'light') return false; } catch { /* noop */ } return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; }); useEffect(() => { const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY); if (accountNumber) { setBankAccount(accountNumber); } const holderName = localStorage.getItem(BANK_ACCOUNT_HOLDER_KEY); if (holderName) { setHolderName(holderName); } const hideSoups = localStorage.getItem(HIDE_SOUPS_KEY); if (hideSoups !== null) { setHideSoups(hideSoups === 'true'); } }, []) useEffect(() => { if (bankAccount) { localStorage.setItem(BANK_ACCOUNT_NUMBER_KEY, bankAccount) } else { localStorage.removeItem(BANK_ACCOUNT_NUMBER_KEY); } }, [bankAccount]); useEffect(() => { if (holderName) { localStorage.setItem(BANK_ACCOUNT_HOLDER_KEY, holderName); } else { localStorage.removeItem(BANK_ACCOUNT_HOLDER_KEY); } }, [holderName]); useEffect(() => { if (hideSoups) { localStorage.setItem(HIDE_SOUPS_KEY, hideSoups ? 'true' : 'false'); } else { localStorage.removeItem(HIDE_SOUPS_KEY); } }, [hideSoups]); useEffect(() => { localStorage.setItem(THEME_KEY, themePreference); }, [themePreference]); useEffect(() => { const applyTheme = (dark: boolean) => { document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light'); setEffectiveDark(dark); }; if (themePreference === 'system') { const mq = window.matchMedia('(prefers-color-scheme: dark)'); applyTheme(mq.matches); const handler = (e: MediaQueryListEvent) => applyTheme(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); } else { applyTheme(themePreference === 'dark'); } }, [themePreference]); useEffect(() => { localStorage.setItem(STYLE_THEME_KEY, styleTheme); document.documentElement.setAttribute('data-luncher-theme', styleTheme); }, [styleTheme]); useEffect(() => { localStorage.setItem(SHOW_BUTTERFLIES_KEY, showButterflies ? 'true' : 'false'); }, [showButterflies]); useEffect(() => { localStorage.setItem(AUTO_ORDER_CHOICE_KEY, autoOrderChoice ? 'true' : 'false'); }, [autoOrderChoice]); // Aplikuje accent barvy při změně hue nebo přepnutí světlý/tmavý. // Tematické styly (Umamusume, My Little Pony, Hell Girl, K-ON!, Bocchi the Rock!, Evangelion, Yuru Camp△, Spy × Family, Apothecary Diaries, Frieren, My Little Pony G5, Aggretsuko, Sumikko Gurashi, Gudetama, Hatsune Miku, Dungeon Meshi, Chiikawa, Můj soused Totoro) mají vlastní pevnou paletu, accent barvu proto uplatňuje jen jednoduchý styl. useEffect(() => { localStorage.setItem(ACCENT_HUE_KEY, String(accentHue)); if (styleTheme === 'simple') { applyAccentColors(accentHue, effectiveDark); } else { clearAccentColors(); } }, [accentHue, effectiveDark, styleTheme]); function setBankAccountNumber(bankAccount?: string) { setBankAccount(bankAccount); } function setBankAccountHolderName(holderName?: string) { setHolderName(holderName); } function setHideSoupsOption(hideSoups?: boolean) { setHideSoups(hideSoups); } function setThemePreference(theme: ThemePreference) { setTheme(theme); } function setAccentHue(hue: number) { setHue(hue); } function setStyleTheme(theme: StyleTheme) { setStyle(theme); } function setShowButterflies(show: boolean) { setShowButterfliesState(show); } function setAutoOrderChoice(enabled: boolean) { setAutoOrderChoiceState(enabled); } return { bankAccount, holderName, hideSoups, themePreference, accentHue, effectiveDark, setBankAccountNumber, setBankAccountHolderName, setHideSoupsOption, setThemePreference, setAccentHue, styleTheme, setStyleTheme, showButterflies, setShowButterflies, autoOrderChoice, setAutoOrderChoice, } }