mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
Combines LightWrite (beat/lyrics editor) and Mukke (iOS music player) into a single web-based music workspace app. Archives the old Mukke mobile app. - Rename: @lightwrite/* → @mukke/*, all branding, configs, Dockerfiles - New DB schemas: songs, playlists, playlist_songs + songId FK on projects - New backend modules: SongModule, PlaylistModule, LibraryModule - New web: app shell with sidebar, library (songs/albums/artists/genres), web player (queue/shuffle/repeat/MediaSession), playlists, search, upload, dashboard, album/artist/genre detail pages - Auth: add forgot-password + reset-password pages, extend auth store - Tests: 40 backend unit tests (song, playlist, library services) - Config: env generation, MinIO bucket, docker-compose prod, cloudflare - Docs: update CLAUDE.md, auth guidelines with SvelteKit checklist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
4.6 KiB
TypeScript
190 lines
4.6 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
import { useColorScheme } from 'react-native';
|
|
|
|
export type ThemeVariant = 'classic' | 'ocean' | 'sunset';
|
|
export type ThemeMode = 'light' | 'dark';
|
|
|
|
export interface ThemeColors {
|
|
primary: string;
|
|
primaryDark: string;
|
|
accent: string;
|
|
background: string;
|
|
backgroundSecondary: string;
|
|
backgroundTertiary: string;
|
|
text: string;
|
|
textSecondary: string;
|
|
textTertiary: string;
|
|
border: string;
|
|
card: string;
|
|
success: string;
|
|
warning: string;
|
|
error: string;
|
|
shadow: string;
|
|
}
|
|
|
|
const THEME_VARIANTS = {
|
|
classic: {
|
|
primary: '#FF6B35',
|
|
primaryDark: '#E55A2B',
|
|
accent: '#FF8F65',
|
|
},
|
|
ocean: {
|
|
primary: '#2196F3',
|
|
primaryDark: '#1976D2',
|
|
accent: '#42A5F5',
|
|
},
|
|
sunset: {
|
|
primary: '#FF6B6B',
|
|
primaryDark: '#FF5252',
|
|
accent: '#FF8A80',
|
|
},
|
|
};
|
|
|
|
const getThemeColors = (variant: ThemeVariant, isDark: boolean): ThemeColors => {
|
|
const variantColors = THEME_VARIANTS[variant];
|
|
|
|
if (isDark) {
|
|
return {
|
|
primary: variantColors.primary,
|
|
primaryDark: variantColors.primaryDark,
|
|
accent: variantColors.accent,
|
|
background: '#121212',
|
|
backgroundSecondary: '#1E1E1E',
|
|
backgroundTertiary: '#2D2D2D',
|
|
text: '#FFFFFF',
|
|
textSecondary: '#AAAAAA',
|
|
textTertiary: '#888888',
|
|
border: '#333333',
|
|
card: '#1E1E1E',
|
|
success: '#4CAF50',
|
|
warning: '#FF9800',
|
|
error: '#FF6B6B',
|
|
shadow: '#000000',
|
|
};
|
|
} else {
|
|
return {
|
|
primary: variantColors.primary,
|
|
primaryDark: variantColors.primaryDark,
|
|
accent: variantColors.accent,
|
|
background: '#F5F5F5',
|
|
backgroundSecondary: '#FFFFFF',
|
|
backgroundTertiary: '#F0F0F0',
|
|
text: '#000000',
|
|
textSecondary: '#666666',
|
|
textTertiary: '#999999',
|
|
border: '#E0E0E0',
|
|
card: '#FFFFFF',
|
|
success: '#4CAF50',
|
|
warning: '#FF9800',
|
|
error: '#F44336',
|
|
shadow: '#000000',
|
|
};
|
|
}
|
|
};
|
|
|
|
type ThemeContextType = {
|
|
isDarkMode: boolean;
|
|
themeVariant: ThemeVariant;
|
|
colors: ThemeColors;
|
|
toggleTheme: () => void;
|
|
setDarkMode: (isDark: boolean) => void;
|
|
setThemeVariant: (variant: ThemeVariant) => void;
|
|
};
|
|
|
|
const ThemeContext = createContext<ThemeContextType>({
|
|
isDarkMode: false,
|
|
themeVariant: 'classic',
|
|
colors: getThemeColors('classic', false),
|
|
toggleTheme: () => {},
|
|
setDarkMode: () => {},
|
|
setThemeVariant: () => {},
|
|
});
|
|
|
|
export const useTheme = () => useContext(ThemeContext);
|
|
|
|
const THEME_PREFERENCE_KEY = '@mukke_theme_preference';
|
|
const THEME_VARIANT_KEY = '@mukke_theme_variant';
|
|
|
|
type ThemeProviderProps = {
|
|
children: React.ReactNode | ((themeProps: ThemeContextType) => React.ReactNode);
|
|
};
|
|
|
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|
const systemColorScheme = useColorScheme();
|
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
const [themeVariant, setThemeVariantState] = useState<ThemeVariant>('classic');
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadThemePreferences = async () => {
|
|
try {
|
|
const [savedPreference, savedVariant] = await Promise.all([
|
|
AsyncStorage.getItem(THEME_PREFERENCE_KEY),
|
|
AsyncStorage.getItem(THEME_VARIANT_KEY),
|
|
]);
|
|
|
|
if (savedPreference !== null) {
|
|
setIsDarkMode(savedPreference === 'dark');
|
|
} else {
|
|
setIsDarkMode(systemColorScheme === 'dark');
|
|
}
|
|
|
|
if (savedVariant !== null && ['classic', 'ocean', 'sunset'].includes(savedVariant)) {
|
|
setThemeVariantState(savedVariant as ThemeVariant);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load theme preferences', error);
|
|
} finally {
|
|
setIsLoaded(true);
|
|
}
|
|
};
|
|
|
|
loadThemePreferences();
|
|
}, [systemColorScheme]);
|
|
|
|
useEffect(() => {
|
|
if (isLoaded) {
|
|
AsyncStorage.setItem(THEME_PREFERENCE_KEY, isDarkMode ? 'dark' : 'light').catch((error) =>
|
|
console.error('Failed to save theme preference', error)
|
|
);
|
|
}
|
|
}, [isDarkMode, isLoaded]);
|
|
|
|
useEffect(() => {
|
|
if (isLoaded) {
|
|
AsyncStorage.setItem(THEME_VARIANT_KEY, themeVariant).catch((error) =>
|
|
console.error('Failed to save theme variant', error)
|
|
);
|
|
}
|
|
}, [themeVariant, isLoaded]);
|
|
|
|
const toggleTheme = () => {
|
|
setIsDarkMode((prev) => !prev);
|
|
};
|
|
|
|
const setDarkMode = (isDark: boolean) => {
|
|
setIsDarkMode(isDark);
|
|
};
|
|
|
|
const setThemeVariant = (variant: ThemeVariant) => {
|
|
setThemeVariantState(variant);
|
|
};
|
|
|
|
const colors = getThemeColors(themeVariant, isDarkMode);
|
|
|
|
const themeContextValue = {
|
|
isDarkMode,
|
|
themeVariant,
|
|
colors,
|
|
toggleTheme,
|
|
setDarkMode,
|
|
setThemeVariant,
|
|
};
|
|
|
|
return (
|
|
<ThemeContext.Provider value={themeContextValue}>
|
|
{typeof children === 'function' ? children(themeContextValue) : children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
};
|