2024-09-05 02:35:08 +02:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-09-05 12:20:21 +02:00
|
|
|
import { ThemeOptions } from '@mui/material/styles';
|
2024-09-05 02:35:08 +02:00
|
|
|
import { t as translate } from 'i18next';
|
2024-09-07 01:07:02 +02:00
|
|
|
import WebFont from 'webfontloader';
|
2024-09-05 12:20:21 +02:00
|
|
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
2024-09-07 01:07:02 +02:00
|
|
|
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
|
2024-09-05 02:35:08 +02:00
|
|
|
|
2024-09-05 12:20:21 +02:00
|
|
|
type Theme = { isCustom: boolean; getName: () => string; muiTheme: ThemeOptions };
|
2024-09-05 02:35:08 +02:00
|
|
|
|
|
|
|
|
const themes = {
|
|
|
|
|
default: {
|
2024-09-05 12:20:21 +02:00
|
|
|
isCustom: false,
|
2024-09-05 02:35:08 +02:00
|
|
|
getName: () => translate('global.label.default'),
|
2024-09-05 12:20:21 +02:00
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#5b74ef',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#efd65b',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
lavender: {
|
2024-09-05 12:20:21 +02:00
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.lavender'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#5c58dc',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#d8dc58',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
dune: {
|
2024-09-05 12:20:21 +02:00
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.dune'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#897869',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#697a89',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
rosegold: {
|
2024-09-05 12:20:21 +02:00
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.rosegold'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#f1a886',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#86cff1',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-09-05 12:20:21 +02:00
|
|
|
'forest dew': {
|
|
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.forest_dew'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#53a584',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#a55374',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-09-05 12:20:21 +02:00
|
|
|
'montain sunset': {
|
|
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.mountain_sunset'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#c55a77',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#5ac5a8',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
crimson: {
|
2024-09-05 12:20:21 +02:00
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.crimson'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#ff0033',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#00ffcc',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-09-05 12:20:21 +02:00
|
|
|
'minty miracles': {
|
|
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.minty_miracles'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#9defc3',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#ef9dc9',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-09-05 12:20:21 +02:00
|
|
|
'orange juice': {
|
|
|
|
|
isCustom: false,
|
|
|
|
|
getName: () => translate('settings.appearance.theme.themes.orange_juice'),
|
|
|
|
|
muiTheme: {
|
|
|
|
|
palette: {
|
|
|
|
|
primary: {
|
|
|
|
|
main: '#ffb546',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
main: '#4690ff',
|
|
|
|
|
},
|
2024-09-05 02:35:08 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as const satisfies Record<string, Theme>;
|
|
|
|
|
|
2024-09-05 12:20:21 +02:00
|
|
|
export type AppThemes = keyof typeof themes | string;
|
2024-09-05 02:35:08 +02:00
|
|
|
|
2024-09-05 12:20:21 +02:00
|
|
|
export type AppTheme = Theme & { id: AppThemes; isCustom: boolean };
|
2024-09-05 02:35:08 +02:00
|
|
|
|
|
|
|
|
export const appThemes = (Object.entries(themes) as [AppThemes, Theme][]).map(([id, theme]) => ({
|
|
|
|
|
id,
|
|
|
|
|
...theme,
|
|
|
|
|
})) satisfies AppTheme[];
|
|
|
|
|
|
2024-09-05 12:20:21 +02:00
|
|
|
export const getTheme = (id: AppThemes, customThemes: Record<string, AppTheme> = {}): AppTheme => {
|
|
|
|
|
try {
|
|
|
|
|
const allThemes = { ...themes, ...customThemes };
|
|
|
|
|
const theme = (allThemes[id as keyof typeof themes] as AppTheme) ?? themes.default;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// @ts-ignore - custom themes do not have a "getName" function
|
|
|
|
|
getName: () => id,
|
|
|
|
|
...theme,
|
|
|
|
|
};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
defaultPromiseErrorHandler('getTheme')(e);
|
|
|
|
|
}
|
|
|
|
|
return { id, ...themes[id as keyof typeof themes] };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const isThemeNameUnique = (id: string, customThemes: Record<string, AppTheme>): boolean =>
|
|
|
|
|
Object.keys({ ...themes, ...customThemes }).every((themeId) => themeId.toLowerCase() !== id.toLowerCase());
|
2024-09-07 01:07:02 +02:00
|
|
|
|
|
|
|
|
const getFontsFromTheme = (obj: Record<string, any>, fonts: string[] = []): string[] => {
|
|
|
|
|
const tmpFonts = [...fonts];
|
|
|
|
|
|
|
|
|
|
const propertyNames = Object.keys(obj);
|
|
|
|
|
for (const propertyName of propertyNames) {
|
|
|
|
|
const propertyValue = obj[propertyName];
|
|
|
|
|
const propertyType = typeof obj[propertyName];
|
|
|
|
|
|
|
|
|
|
const isValidFontProperty = propertyName === 'fontFamily' && propertyType === 'string';
|
|
|
|
|
if (isValidFontProperty) {
|
|
|
|
|
tmpFonts.push(propertyValue as string);
|
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isObject = propertyType === 'object';
|
|
|
|
|
if (isObject) {
|
|
|
|
|
tmpFonts.push(...getFontsFromTheme(propertyValue as Record<string, any>, tmpFonts));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...new Set(tmpFonts)];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadedFonts: string[] = [];
|
|
|
|
|
export const loadThemeFonts = async (theme: ThemeOptions): Promise<void> => {
|
|
|
|
|
const themeFonts = getFontsFromTheme(theme);
|
|
|
|
|
const missingThemeFonts = themeFonts.filter((font) => !loadedFonts.includes(font));
|
|
|
|
|
|
|
|
|
|
if (!missingThemeFonts.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadFontsPromise = new ControlledPromise();
|
|
|
|
|
try {
|
|
|
|
|
WebFont.load({
|
|
|
|
|
google: {
|
|
|
|
|
families: missingThemeFonts,
|
|
|
|
|
},
|
|
|
|
|
active: () => {
|
|
|
|
|
loadedFonts.push(...missingThemeFonts);
|
|
|
|
|
loadFontsPromise.resolve();
|
|
|
|
|
},
|
|
|
|
|
inactive: () => {
|
|
|
|
|
loadFontsPromise.reject();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
defaultPromiseErrorHandler('AppThemes::loadFonts')(e);
|
|
|
|
|
loadFontsPromise.reject(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await loadFontsPromise.promise;
|
|
|
|
|
};
|