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'; export const STYLE_THEMES: { value: StyleTheme, label: string }[] = [ { value: 'simple', label: 'Jednoduchý' }, { value: 'umamusume', label: 'Umamusume' }, { value: 'mlp', label: 'My Little Pony' }, { value: 'hellgirl', label: 'Hell Girl' }, { value: 'kon', label: 'K-ON!' }, { value: 'bocchi', label: 'Bocchi the Rock!' }, ]; 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!) 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, } }