feat: support primary color customization

This commit is contained in:
codytseng
2025-10-18 23:18:44 +08:00
parent b17846f264
commit 28dad7373f
21 changed files with 644 additions and 43 deletions

View File

@@ -2,6 +2,7 @@ import Icon from '@/assets/Icon'
import Logo from '@/assets/Logo'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useTheme } from '@/providers/ThemeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { ChevronsLeft, ChevronsRight } from 'lucide-react'
import AccountButton from './AccountButton'
@@ -15,6 +16,7 @@ import SettingsButton from './SettingsButton'
export default function PrimaryPageSidebar() {
const { isSmallScreen } = useScreenSize()
const { themeSetting } = useTheme()
const { sidebarCollapse, updateSidebarCollapse } = useUserPreferences()
if (isSmallScreen) return null
@@ -46,7 +48,10 @@ export default function PrimaryPageSidebar() {
</div>
<AccountButton collapse={sidebarCollapse} />
<button
className="absolute flex flex-col justify-center items-center top-5 right-0 w-5 h-6 p-0 rounded-l-md hover:shadow-md text-muted-foreground hover:text-foreground hover:bg-background transition-colors [&_svg]:size-4"
className={cn(
'absolute flex flex-col justify-center items-center right-0 w-5 h-6 p-0 rounded-l-md hover:shadow-md text-muted-foreground hover:text-foreground hover:bg-background transition-colors [&_svg]:size-4',
themeSetting === 'pure-black' ? 'top-3' : 'top-5'
)}
onClick={(e) => {
e.stopPropagation()
updateSidebarCollapse(!sidebarCollapse)

View File

@@ -49,6 +49,7 @@ export const StorageKey = {
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys',
SIDEBAR_COLLAPSE: 'sidebarCollapse',
PRIMARY_COLOR: 'primaryColor',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
@@ -156,3 +157,241 @@ export const MEDIA_AUTO_LOAD_POLICY = {
} as const
export const MAX_PINNED_NOTES = 10
export const PRIMARY_COLORS = {
DEFAULT: {
name: 'Default',
light: {
primary: '259 43% 56%',
'primary-hover': '259 43% 65%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '259 43% 56%',
'primary-hover': '259 43% 65%',
'primary-foreground': '240 5.9% 10%'
}
},
RED: {
name: 'Red',
light: {
primary: '0 65% 55%',
'primary-hover': '0 65% 65%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '0 65% 55%',
'primary-hover': '0 65% 65%',
'primary-foreground': '240 5.9% 10%'
}
},
ORANGE: {
name: 'Orange',
light: {
primary: '30 100% 50%',
'primary-hover': '30 100% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '30 100% 50%',
'primary-hover': '30 100% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
AMBER: {
name: 'Amber',
light: {
primary: '42 100% 50%',
'primary-hover': '42 100% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '42 100% 50%',
'primary-hover': '42 100% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
YELLOW: {
name: 'Yellow',
light: {
primary: '54 100% 50%',
'primary-hover': '54 100% 60%',
'primary-foreground': '0 0% 10%'
},
dark: {
primary: '54 100% 50%',
'primary-hover': '54 100% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
LIME: {
name: 'Lime',
light: {
primary: '90 60% 50%',
'primary-hover': '90 60% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '90 60% 50%',
'primary-hover': '90 60% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
GREEN: {
name: 'Green',
light: {
primary: '140 60% 40%',
'primary-hover': '140 60% 50%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '140 60% 40%',
'primary-hover': '140 60% 50%',
'primary-foreground': '240 5.9% 10%'
}
},
EMERALD: {
name: 'Emerald',
light: {
primary: '160 70% 40%',
'primary-hover': '160 70% 50%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '160 70% 40%',
'primary-hover': '160 70% 50%',
'primary-foreground': '240 5.9% 10%'
}
},
TEAL: {
name: 'Teal',
light: {
primary: '180 70% 40%',
'primary-hover': '180 70% 50%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '180 70% 40%',
'primary-hover': '180 70% 50%',
'primary-foreground': '240 5.9% 10%'
}
},
CYAN: {
name: 'Cyan',
light: {
primary: '200 70% 40%',
'primary-hover': '200 70% 50%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '200 70% 40%',
'primary-hover': '200 70% 50%',
'primary-foreground': '240 5.9% 10%'
}
},
SKY: {
name: 'Sky',
light: {
primary: '210 70% 50%',
'primary-hover': '210 70% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '210 70% 50%',
'primary-hover': '210 70% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
BLUE: {
name: 'Blue',
light: {
primary: '220 80% 50%',
'primary-hover': '220 80% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '220 80% 50%',
'primary-hover': '220 80% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
INDIGO: {
name: 'Indigo',
light: {
primary: '230 80% 50%',
'primary-hover': '230 80% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '230 80% 50%',
'primary-hover': '230 80% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
VIOLET: {
name: 'Violet',
light: {
primary: '250 80% 50%',
'primary-hover': '250 80% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '250 80% 50%',
'primary-hover': '250 80% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
PURPLE: {
name: 'Purple',
light: {
primary: '280 80% 50%',
'primary-hover': '280 80% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '280 80% 50%',
'primary-hover': '280 80% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
FUCHSIA: {
name: 'Fuchsia',
light: {
primary: '310 80% 50%',
'primary-hover': '310 80% 60%',
'primary-foreground': '0 0% 98%'
},
dark: {
primary: '310 80% 50%',
'primary-hover': '310 80% 60%',
'primary-foreground': '240 5.9% 10%'
}
},
PINK: {
name: 'Pink',
light: {
primary: '330 80% 60%',
'primary-hover': '330 80% 70%',
'primary-foreground': '0 0% 10%'
},
dark: {
primary: '330 80% 60%',
'primary-hover': '330 80% 70%',
'primary-foreground': '240 5.9% 10%'
}
},
ROSE: {
name: 'Rose',
light: {
primary: '350 80% 60%',
'primary-hover': '350 80% 70%',
'primary-foreground': '0 0% 10%'
},
dark: {
primary: '350 80% 60%',
'primary-hover': '350 80% 70%',
'primary-foreground': '240 5.9% 10%'
}
}
} as const
export type TPrimaryColor = keyof typeof PRIMARY_COLORS

View File

@@ -457,6 +457,25 @@ export default {
'Unpin from profile': 'إلغاء التثبيت من الملف الشخصي',
'Pin to profile': 'تثبيت في الملف الشخصي',
Appearance: 'المظهر',
'Pure Black': 'أسود نقي'
'Pure Black': 'أسود نقي',
Default: 'افتراضي',
Red: 'أحمر',
Orange: 'برتقالي',
Amber: 'عنبر',
Yellow: 'أصفر',
Lime: 'ليموني',
Green: 'أخضر',
Emerald: 'زمردي',
Teal: 'أزرق مخضر',
Cyan: 'سماوي',
Sky: 'سماء',
Blue: 'أزرق',
Indigo: 'نيلي',
Violet: 'بنفسجي',
Purple: 'أرجواني',
Fuchsia: 'فوشيا',
Pink: 'وردي',
Rose: 'وردة',
'Primary color': 'اللون الأساسي'
}
}

View File

@@ -471,6 +471,25 @@ export default {
'Unpin from profile': 'Vom Profil lösen',
'Pin to profile': 'An Profil anheften',
Appearance: 'Aussehen',
'Pure Black': 'Reines Schwarz'
'Pure Black': 'Reines Schwarz',
Default: 'Standard',
Red: 'Rot',
Orange: 'Orange',
Amber: 'Bernstein',
Yellow: 'Gelb',
Lime: 'Limette',
Green: 'Grün',
Emerald: 'Smaragd',
Teal: 'Blaugrün',
Cyan: 'Cyan',
Sky: 'Himmelblau',
Blue: 'Blau',
Indigo: 'Indigo',
Violet: 'Violett',
Purple: 'Lila',
Fuchsia: 'Fuchsia',
Pink: 'Rosa',
Rose: 'Rose',
'Primary color': 'Primärfarbe'
}
}

View File

@@ -456,6 +456,25 @@ export default {
'Unpin from profile': 'Unpin from profile',
'Pin to profile': 'Pin to profile',
Appearance: 'Appearance',
'Pure Black': 'Pure Black'
'Pure Black': 'Pure Black',
Default: 'Default',
Red: 'Red',
Orange: 'Orange',
Amber: 'Amber',
Yellow: 'Yellow',
Lime: 'Lime',
Green: 'Green',
Emerald: 'Emerald',
Teal: 'Teal',
Cyan: 'Cyan',
Sky: 'Sky',
Blue: 'Blue',
Indigo: 'Indigo',
Violet: 'Violet',
Purple: 'Purple',
Fuchsia: 'Fuchsia',
Pink: 'Pink',
Rose: 'Rose',
'Primary color': 'Primary color'
}
}

View File

@@ -465,6 +465,25 @@ export default {
'Unpin from profile': 'Desfijar del perfil',
'Pin to profile': 'Fijar al perfil',
Appearance: 'Apariencia',
'Pure Black': 'Negro Puro'
'Pure Black': 'Negro Puro',
Default: 'Predeterminado',
Red: 'Rojo',
Orange: 'Naranja',
Amber: 'Ámbar',
Yellow: 'Amarillo',
Lime: 'Lima',
Green: 'Verde',
Emerald: 'Esmeralda',
Teal: 'Verde azulado',
Cyan: 'Cian',
Sky: 'Cielo',
Blue: 'Azul',
Indigo: 'Índigo',
Violet: 'Violeta',
Purple: 'Púrpura',
Fuchsia: 'Fucsia',
Pink: 'Rosa',
Rose: 'Rosa',
'Primary color': 'Color primario'
}
}

View File

@@ -460,6 +460,25 @@ export default {
'Unpin from profile': 'لغو پین از پروفایل',
'Pin to profile': 'پین به پروفایل',
Appearance: 'ظاهر',
'Pure Black': 'سیاه خالص'
'Pure Black': 'سیاه خالص',
Default: 'پیش‌فرض',
Red: 'قرمز',
Orange: 'نارنجی',
Amber: 'کهربایی',
Yellow: 'زرد',
Lime: 'لیمویی',
Green: 'سبز',
Emerald: 'زمردی',
Teal: 'سبز آبی',
Cyan: 'فیروزه‌ای',
Sky: 'آسمانی',
Blue: 'آبی',
Indigo: 'نیلی',
Violet: 'بنفش',
Purple: 'ارغوانی',
Fuchsia: 'فوشیا',
Pink: 'صورتی',
Rose: 'گلی',
'Primary color': 'رنگ اصلی'
}
}

View File

@@ -470,6 +470,25 @@ export default {
'Unpin from profile': 'Retirer lépingle du profil',
'Pin to profile': 'Épingler au profil',
Appearance: 'Apparence',
'Pure Black': 'Noir pur'
'Pure Black': 'Noir pur',
Default: 'Par défaut',
Red: 'Rouge',
Orange: 'Orange',
Amber: 'Ambre',
Yellow: 'Jaune',
Lime: 'Citron vert',
Green: 'Vert',
Emerald: 'Émeraude',
Teal: 'Sarcelle',
Cyan: 'Cyan',
Sky: 'Bleu ciel',
Blue: 'Bleu',
Indigo: 'Indigo',
Violet: 'Violet',
Purple: 'Pourpre',
Fuchsia: 'Fuchsia',
Pink: 'Rose',
Rose: 'Rose',
'Primary color': 'Couleur principale'
}
}

View File

@@ -462,6 +462,25 @@ export default {
'Unpin from profile': 'प्रोफ़ाइल से पिन हटाएं',
'Pin to profile': 'प्रोफ़ाइल पर पिन करें',
Appearance: 'दिखावट',
'Pure Black': 'शुद्ध काला'
'Pure Black': 'शुद्ध काला',
Default: 'डिफ़ॉल्ट',
Red: 'लाल',
Orange: 'नारंगी',
Amber: 'एम्बर',
Yellow: 'पीला',
Lime: 'नींबू',
Green: 'हरा',
Emerald: 'पन्ना',
Teal: 'टील',
Cyan: 'सियान',
Sky: 'आसमानी',
Blue: 'नीला',
Indigo: 'नील',
Violet: 'बैंगनी',
Purple: 'जामुनी',
Fuchsia: 'फुशिया',
Pink: 'गुलाबी',
Rose: 'गुलाब',
'Primary color': 'प्राथमिक रंग'
}
}

View File

@@ -465,6 +465,25 @@ export default {
'Unpin from profile': 'Rimuovi fissaggio dal profilo',
'Pin to profile': 'Fissa al profilo',
Appearance: 'Aspetto',
'Pure Black': 'Nero Puro'
'Pure Black': 'Nero Puro',
Default: 'Predefinito',
Red: 'Rosso',
Orange: 'Arancione',
Amber: 'Ambra',
Yellow: 'Giallo',
Lime: 'Lime',
Green: 'Verde',
Emerald: 'Smeraldo',
Teal: 'Turchese',
Cyan: 'Ciano',
Sky: 'Cielo',
Blue: 'Blu',
Indigo: 'Indaco',
Violet: 'Viola',
Purple: 'Porpora',
Fuchsia: 'Fucsia',
Pink: 'Rosa',
Rose: 'Rosa',
'Primary color': 'Colore primario'
}
}

View File

@@ -461,6 +461,25 @@ export default {
'Unpin from profile': 'プロフィールから固定解除',
'Pin to profile': 'プロフィールに固定',
Appearance: '外観',
'Pure Black': '純黒'
'Pure Black': '純黒',
Default: 'デフォルト',
Red: '赤',
Orange: 'オレンジ',
Amber: 'アンバー',
Yellow: '黄色',
Lime: 'ライム',
Green: '緑',
Emerald: 'エメラルド',
Teal: 'ティール',
Cyan: 'シアン',
Sky: 'スカイ',
Blue: '青',
Indigo: 'インディゴ',
Violet: 'バイオレット',
Purple: '紫',
Fuchsia: 'フクシア',
Pink: 'ピンク',
Rose: 'ローズ',
'Primary color': '主要な色'
}
}

View File

@@ -461,6 +461,25 @@ export default {
'Unpin from profile': '프로필에서 고정 해제',
'Pin to profile': '프로필에 고정',
Appearance: '외관',
'Pure Black': '순수한 검은색'
'Pure Black': '순수한 검은색',
Default: '기본',
Red: '빨강',
Orange: '주황',
Amber: '호박색',
Yellow: '노랑',
Lime: '라임',
Green: '초록',
Emerald: '에메랄드',
Teal: '청록',
Cyan: '시안',
Sky: '하늘색',
Blue: '파랑',
Indigo: '남색',
Violet: '보라',
Purple: '자주',
Fuchsia: '자홍',
Pink: '분홍',
Rose: '장미',
'Primary color': '기본 색상'
}
}

View File

@@ -465,6 +465,25 @@ export default {
'Unpin from profile': 'Odpiń z profilu',
'Pin to profile': 'Przypnij do profilu',
Appearance: 'Wygląd',
'Pure Black': 'Czysta Czerń'
'Pure Black': 'Czysta Czerń',
Default: 'Domyślny',
Red: 'Czerwony',
Orange: 'Pomarańczowy',
Amber: 'Bursztynowy',
Yellow: 'Żółty',
Lime: 'Limonkowy',
Green: 'Zielony',
Emerald: 'Szmaragdowy',
Teal: 'Morski',
Cyan: 'Cyjan',
Sky: 'Niebieski',
Blue: 'Niebieski',
Indigo: 'Indygo',
Violet: 'Fioletowy',
Purple: 'Purpurowy',
Fuchsia: 'Fuksja',
Pink: 'Różowy',
Rose: 'Różany',
'Primary color': 'Kolor podstawowy'
}
}

View File

@@ -462,6 +462,25 @@ export default {
'Unpin from profile': 'Desafixar do perfil',
'Pin to profile': 'Fixar no perfil',
Appearance: 'Aparência',
'Pure Black': 'Preto Puro'
'Pure Black': 'Preto Puro',
Default: 'Padrão',
Red: 'Vermelho',
Orange: 'Laranja',
Amber: 'Âmbar',
Yellow: 'Amarelo',
Lime: 'Lima',
Green: 'Verde',
Emerald: 'Esmeralda',
Teal: 'Turquesa',
Cyan: 'Ciano',
Sky: 'Céu',
Blue: 'Azul',
Indigo: 'Índigo',
Violet: 'Violeta',
Purple: 'Roxo',
Fuchsia: 'Fúcsia',
Pink: 'Rosa',
Rose: 'Rosa',
'Primary color': 'Cor primária'
}
}

View File

@@ -465,6 +465,25 @@ export default {
'Unpin from profile': 'Desafixar do perfil',
'Pin to profile': 'Fixar no perfil',
Appearance: 'Aparência',
'Pure Black': 'Preto Puro'
'Pure Black': 'Preto Puro',
Default: 'Padrão',
Red: 'Vermelho',
Orange: 'Laranja',
Amber: 'Âmbar',
Yellow: 'Amarelo',
Lime: 'Lima',
Green: 'Verde',
Emerald: 'Esmeralda',
Teal: 'Turquesa',
Cyan: 'Ciano',
Sky: 'Céu',
Blue: 'Azul',
Indigo: 'Índigo',
Violet: 'Violeta',
Purple: 'Roxo',
Fuchsia: 'Fúcsia',
Pink: 'Rosa',
Rose: 'Rosa',
'Primary color': 'Cor primária'
}
}

View File

@@ -467,6 +467,25 @@ export default {
'Unpin from profile': 'Открепить из профиля',
'Pin to profile': 'Закрепить в профиле',
Appearance: 'Внешний вид',
'Pure Black': 'Чистый Черный'
'Pure Black': 'Чистый Черный',
Default: 'По умолчанию',
Red: 'Красный',
Orange: 'Оранжевый',
Amber: 'Янтарный',
Yellow: 'Желтый',
Lime: 'Лаймовый',
Green: 'Зеленый',
Emerald: 'Изумрудный',
Teal: 'Бирюзовый',
Cyan: 'Голубой',
Sky: 'Небесный',
Blue: 'Синий',
Indigo: 'Индиго',
Violet: 'Фиолетовый',
Purple: 'Пурпурный',
Fuchsia: 'Фуксия',
Pink: 'Розовый',
Rose: 'Роза',
'Primary color': 'Основной цвет'
}
}

View File

@@ -455,6 +455,25 @@ export default {
'Unpin from profile': 'ยกเลิกปักหมุดจากโปรไฟล์',
'Pin to profile': 'ปักหมุดไปที่โปรไฟล์',
Appearance: 'รูปลักษณ์',
'Pure Black': 'สีดำล้วน'
'Pure Black': 'สีดำล้วน',
Default: 'ค่าเริ่มต้น',
Red: 'แดง',
Orange: 'ส้ม',
Amber: 'อำพัน',
Yellow: 'เหลือง',
Lime: 'มะนาว',
Green: 'เขียว',
Emerald: 'มรกต',
Teal: 'ฟ้าเขียว',
Cyan: 'ฟ้าน้ำทะเล',
Sky: 'ฟ้า',
Blue: 'น้ำเงิน',
Indigo: 'คราม',
Violet: 'ม่วงอ่อน',
Purple: 'ม่วง',
Fuchsia: 'บานเย็น',
Pink: 'ชมพู',
Rose: 'กุหลาบ',
'Primary color': 'สีหลัก'
}
}

View File

@@ -453,6 +453,25 @@ export default {
'Unpin from profile': '从个人资料取消置顶',
'Pin to profile': '置顶到个人资料',
Appearance: '外观',
'Pure Black': '纯黑'
'Pure Black': '纯黑',
Default: '默认',
Red: '红色',
Orange: '橙色',
Amber: '琥珀色',
Yellow: '黄色',
Lime: '青柠色',
Green: '绿色',
Emerald: '翡翠色',
Teal: '蓝绿色',
Cyan: '青色',
Sky: '天空色',
Blue: '蓝色',
Indigo: '靛蓝色',
Violet: '紫罗兰色',
Purple: '紫色',
Fuchsia: '紫红色',
Pink: '粉色',
Rose: '玫瑰色',
'Primary color': '主色调'
}
}

View File

@@ -1,4 +1,5 @@
import { Label } from '@/components/ui/label'
import { PRIMARY_COLORS, TPrimaryColor } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn } from '@/lib/utils'
import { useTheme } from '@/providers/ThemeProvider'
@@ -15,11 +16,11 @@ const THEMES = [
const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { themeSetting, setThemeSetting } = useTheme()
const { themeSetting, setThemeSetting, primaryColor, setPrimaryColor } = useTheme()
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Appearance')}>
<div className="space-y-4 mt-3">
<div className="space-y-4 my-3">
<div className="flex flex-col gap-2 px-4">
<Label className="text-base">{t('Theme')}</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
@@ -31,7 +32,9 @@ const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) =
}}
className={cn(
'flex flex-col items-center gap-2 py-4 rounded-lg border-2 transition-all',
themeSetting === key ? 'border-primary' : 'border-border hover:border-primary/60'
themeSetting === key
? 'border-primary'
: 'border-border hover:border-muted-foreground/40'
)}
>
<div className="flex items-center justify-center w-8 h-8">{icon}</div>
@@ -40,6 +43,31 @@ const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) =
))}
</div>
</div>
<div className="flex flex-col gap-2 px-4">
<Label className="text-base">{t('Primary color')}</Label>
<div className="grid grid-cols-4 gap-4 w-full">
{Object.entries(PRIMARY_COLORS).map(([key, config]) => (
<button
key={key}
onClick={() => setPrimaryColor(key as TPrimaryColor)}
className={cn(
'flex flex-col items-center gap-2 py-4 rounded-lg border-2 transition-all',
primaryColor === key
? 'border-primary'
: 'border-border hover:border-muted-foreground/40'
)}
>
<div
className="size-8 rounded-full shadow-md"
style={{
backgroundColor: `hsl(${config.light.primary})`
}}
/>
<span className="text-xs font-medium">{t(config.name)}</span>
</button>
))}
</div>
</div>
</div>
</SecondaryPageLayout>
)

View File

@@ -1,36 +1,36 @@
import { PRIMARY_COLORS, StorageKey, TPrimaryColor } from '@/constants'
import storage from '@/services/local-storage.service'
import { TTheme, TThemeSetting } from '@/types'
import { createContext, useContext, useEffect, useState } from 'react'
type ThemeProviderState = {
themeSetting: TThemeSetting
setThemeSetting: (themeSetting: TThemeSetting) => Promise<void>
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
setThemeSetting: (themeSetting: TThemeSetting) => void
primaryColor: TPrimaryColor
setPrimaryColor: (color: TPrimaryColor) => void
}
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined)
const updateCSSVariables = (color: TPrimaryColor, currentTheme: TTheme) => {
const root = window.document.documentElement
const colorConfig = PRIMARY_COLORS[color] ?? PRIMARY_COLORS.DEFAULT
const config = currentTheme === 'light' ? colorConfig.light : colorConfig.dark
root.style.setProperty('--primary', config.primary)
root.style.setProperty('--primary-hover', config['primary-hover'])
root.style.setProperty('--primary-foreground', config['primary-foreground'])
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [themeSetting, setThemeSetting] = useState<TThemeSetting>(
(localStorage.getItem('themeSetting') as TThemeSetting | null) ?? 'system'
(localStorage.getItem(StorageKey.THEME_SETTING) as TThemeSetting) ?? 'system'
)
const [theme, setTheme] = useState<TTheme>('light')
useEffect(() => {
const init = async () => {
const themeSetting = storage.getThemeSetting()
if (themeSetting === 'system') {
setTheme(getSystemTheme())
return
}
setTheme(themeSetting)
}
init()
}, [])
const [primaryColor, setPrimaryColor] = useState<TPrimaryColor>(
(localStorage.getItem(StorageKey.PRIMARY_COLOR) as TPrimaryColor) ?? 'DEFAULT'
)
useEffect(() => {
if (themeSetting !== 'system') {
@@ -65,16 +65,27 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
updateTheme()
}, [theme])
const updateThemeSetting = async (themeSetting: TThemeSetting) => {
useEffect(() => {
updateCSSVariables(primaryColor, theme)
}, [theme, primaryColor])
const updateThemeSetting = (themeSetting: TThemeSetting) => {
storage.setThemeSetting(themeSetting)
setThemeSetting(themeSetting)
}
const updatePrimaryColor = (color: TPrimaryColor) => {
storage.setPrimaryColor(color)
setPrimaryColor(color)
}
return (
<ThemeProviderContext.Provider
value={{
themeSetting: themeSetting,
setThemeSetting: updateThemeSetting
themeSetting,
setThemeSetting: updateThemeSetting,
primaryColor,
setPrimaryColor: updatePrimaryColor
}}
>
{children}

View File

@@ -4,7 +4,8 @@ import {
MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE,
SUPPORTED_KINDS,
StorageKey
StorageKey,
TPrimaryColor
} from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
@@ -49,6 +50,7 @@ class LocalStorageService {
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
private sidebarCollapse: boolean = false
private primaryColor: TPrimaryColor = 'DEFAULT'
constructor() {
if (!LocalStorageService.instance) {
@@ -196,6 +198,9 @@ class LocalStorageService {
this.sidebarCollapse = window.localStorage.getItem(StorageKey.SIDEBAR_COLLAPSE) === 'true'
this.primaryColor =
(window.localStorage.getItem(StorageKey.PRIMARY_COLOR) as TPrimaryColor) ?? 'DEFAULT'
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -488,6 +493,15 @@ class LocalStorageService {
this.sidebarCollapse = collapse
window.localStorage.setItem(StorageKey.SIDEBAR_COLLAPSE, collapse.toString())
}
getPrimaryColor() {
return this.primaryColor
}
setPrimaryColor(color: TPrimaryColor) {
this.primaryColor = color
window.localStorage.setItem(StorageKey.PRIMARY_COLOR, color)
}
}
const instance = new LocalStorageService()