mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 06:02:39 +00:00
add custom theme
This commit is contained in:
parent
87851c1167
commit
cc029c8c0a
12 changed files with 671 additions and 9 deletions
|
|
@ -1200,6 +1200,7 @@
|
||||||
"activeTheme": "Active",
|
"activeTheme": "Active",
|
||||||
"themes": {
|
"themes": {
|
||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
|
"custom": "Custom",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"gray": "Gray",
|
"gray": "Gray",
|
||||||
"red": "Red",
|
"red": "Red",
|
||||||
|
|
@ -1223,6 +1224,11 @@
|
||||||
"frost": "Frost",
|
"frost": "Frost",
|
||||||
"christmas": "Christmas"
|
"christmas": "Christmas"
|
||||||
},
|
},
|
||||||
|
"customParts": {
|
||||||
|
"primary": "Primary",
|
||||||
|
"secondary": "Secondary",
|
||||||
|
"tertiary": "Tertiary"
|
||||||
|
},
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"options": {
|
"options": {
|
||||||
"discover": "Discover section",
|
"discover": "Discover section",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import { getAuthHeaders } from "@/backend/accounts/auth";
|
||||||
import { AccountWithToken } from "@/stores/auth";
|
import { AccountWithToken } from "@/stores/auth";
|
||||||
import { KeyboardShortcuts } from "@/utils/keyboardShortcuts";
|
import { KeyboardShortcuts } from "@/utils/keyboardShortcuts";
|
||||||
|
|
||||||
|
export interface CustomThemeSettings {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsInput {
|
export interface SettingsInput {
|
||||||
applicationLanguage?: string;
|
applicationLanguage?: string;
|
||||||
applicationTheme?: string | null;
|
applicationTheme?: string | null;
|
||||||
|
|
@ -38,6 +44,7 @@ export interface SettingsInput {
|
||||||
enableDoubleClickToSeek?: boolean;
|
enableDoubleClickToSeek?: boolean;
|
||||||
enableAutoResumeOnPlaybackError?: boolean;
|
enableAutoResumeOnPlaybackError?: boolean;
|
||||||
keyboardShortcuts?: KeyboardShortcuts;
|
keyboardShortcuts?: KeyboardShortcuts;
|
||||||
|
customTheme?: CustomThemeSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsResponse {
|
export interface SettingsResponse {
|
||||||
|
|
@ -74,6 +81,7 @@ export interface SettingsResponse {
|
||||||
enableDoubleClickToSeek?: boolean;
|
enableDoubleClickToSeek?: boolean;
|
||||||
enableAutoResumeOnPlaybackError?: boolean;
|
enableAutoResumeOnPlaybackError?: boolean;
|
||||||
keyboardShortcuts?: KeyboardShortcuts;
|
keyboardShortcuts?: KeyboardShortcuts;
|
||||||
|
customTheme?: CustomThemeSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSettings(
|
export function updateSettings(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { SubtitleStyling } from "@/stores/subtitles";
|
import { SubtitleStyling } from "@/stores/subtitles";
|
||||||
import { usePreviewThemeStore } from "@/stores/theme";
|
import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
|
||||||
|
|
||||||
export function useDerived<T>(
|
export function useDerived<T>(
|
||||||
initial: T,
|
initial: T,
|
||||||
|
|
@ -81,6 +81,11 @@ export function useSettingsState(
|
||||||
manualSourceSelection: boolean,
|
manualSourceSelection: boolean,
|
||||||
enableDoubleClickToSeek: boolean,
|
enableDoubleClickToSeek: boolean,
|
||||||
enableAutoResumeOnPlaybackError: boolean,
|
enableAutoResumeOnPlaybackError: boolean,
|
||||||
|
customTheme: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||||
useDerived(proxyUrls);
|
useDerived(proxyUrls);
|
||||||
|
|
@ -272,6 +277,13 @@ export function useSettingsState(
|
||||||
resetEnableAutoResumeOnPlaybackError,
|
resetEnableAutoResumeOnPlaybackError,
|
||||||
enableAutoResumeOnPlaybackErrorChanged,
|
enableAutoResumeOnPlaybackErrorChanged,
|
||||||
] = useDerived(enableAutoResumeOnPlaybackError);
|
] = useDerived(enableAutoResumeOnPlaybackError);
|
||||||
|
const [
|
||||||
|
customThemeState,
|
||||||
|
setCustomThemeState,
|
||||||
|
resetCustomTheme,
|
||||||
|
customThemeChanged,
|
||||||
|
] = useDerived(customTheme);
|
||||||
|
const setCustomThemeStore = useThemeStore((s) => s.setCustomTheme);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
resetTheme();
|
resetTheme();
|
||||||
|
|
@ -311,6 +323,7 @@ export function useSettingsState(
|
||||||
resetManualSourceSelection();
|
resetManualSourceSelection();
|
||||||
resetEnableDoubleClickToSeek();
|
resetEnableDoubleClickToSeek();
|
||||||
resetEnableAutoResumeOnPlaybackError();
|
resetEnableAutoResumeOnPlaybackError();
|
||||||
|
resetCustomTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
|
|
@ -350,7 +363,8 @@ export function useSettingsState(
|
||||||
homeSectionOrderChanged ||
|
homeSectionOrderChanged ||
|
||||||
manualSourceSelectionChanged ||
|
manualSourceSelectionChanged ||
|
||||||
enableDoubleClickToSeekChanged ||
|
enableDoubleClickToSeekChanged ||
|
||||||
enableAutoResumeOnPlaybackErrorChanged;
|
enableAutoResumeOnPlaybackErrorChanged ||
|
||||||
|
customThemeChanged;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reset,
|
reset,
|
||||||
|
|
@ -540,5 +554,13 @@ export function useSettingsState(
|
||||||
set: setEnableAutoResumeOnPlaybackErrorState,
|
set: setEnableAutoResumeOnPlaybackErrorState,
|
||||||
changed: enableAutoResumeOnPlaybackErrorChanged,
|
changed: enableAutoResumeOnPlaybackErrorChanged,
|
||||||
},
|
},
|
||||||
|
customTheme: {
|
||||||
|
state: customThemeState,
|
||||||
|
set: (v: { primary: string; secondary: string; tertiary: string }) => {
|
||||||
|
setCustomThemeState(v);
|
||||||
|
setCustomThemeStore(v);
|
||||||
|
},
|
||||||
|
changed: customThemeChanged,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,9 +290,25 @@ export function SettingsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const activeTheme = useThemeStore((s) => s.theme);
|
const activeTheme = useThemeStore((s) => s.theme);
|
||||||
const setTheme = useThemeStore((s) => s.setTheme);
|
const setTheme = useThemeStore((s) => s.setTheme);
|
||||||
|
const customTheme = useThemeStore((s) => s.customTheme);
|
||||||
|
const setCustomTheme = useThemeStore((s) => s.setCustomTheme);
|
||||||
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
|
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
|
||||||
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
|
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
|
||||||
|
|
||||||
|
// Baseline for custom theme so "changed" is detected when only colors change.
|
||||||
|
// Only updated on load from backend or after save; prevents useDerived from
|
||||||
|
// resetting when we update the store for preview.
|
||||||
|
const [customThemeBaseline, setCustomThemeBaseline] = useState<{
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
} | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (customThemeBaseline === null) {
|
||||||
|
setCustomThemeBaseline(customTheme);
|
||||||
|
}
|
||||||
|
}, [customTheme, customThemeBaseline]);
|
||||||
|
|
||||||
// Simple text search with highlighting
|
// Simple text search with highlighting
|
||||||
const handleSearchChange = useCallback((value: string, _force: boolean) => {
|
const handleSearchChange = useCallback((value: string, _force: boolean) => {
|
||||||
setSearchQuery(value);
|
setSearchQuery(value);
|
||||||
|
|
@ -539,16 +555,140 @@ export function SettingsPage() {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
if (account && backendUrl) {
|
if (account && backendUrl) {
|
||||||
const settings = await getSettings(backendUrl, account);
|
const settings = await getSettings(backendUrl, account);
|
||||||
if (settings.febboxKey) {
|
if (settings.applicationTheme !== undefined) {
|
||||||
|
setTheme(settings.applicationTheme);
|
||||||
|
}
|
||||||
|
if (settings.applicationLanguage) {
|
||||||
|
setAppLanguage(settings.applicationLanguage);
|
||||||
|
}
|
||||||
|
if (settings.proxyUrls !== undefined) {
|
||||||
|
setProxySet(settings.proxyUrls?.filter((v) => v !== "") ?? null);
|
||||||
|
}
|
||||||
|
if (settings.febboxKey !== undefined) {
|
||||||
setFebboxKey(settings.febboxKey);
|
setFebboxKey(settings.febboxKey);
|
||||||
}
|
}
|
||||||
if (settings.debridToken) {
|
if (settings.debridToken !== undefined) {
|
||||||
setdebridToken(settings.debridToken);
|
setdebridToken(settings.debridToken);
|
||||||
}
|
}
|
||||||
|
if (settings.debridService) {
|
||||||
|
setdebridService(settings.debridService);
|
||||||
|
}
|
||||||
|
if (settings.enableThumbnails !== undefined) {
|
||||||
|
setEnableThumbnails(settings.enableThumbnails);
|
||||||
|
}
|
||||||
|
if (settings.enableAutoplay !== undefined) {
|
||||||
|
setEnableAutoplay(settings.enableAutoplay);
|
||||||
|
}
|
||||||
|
if (settings.enableSkipCredits !== undefined) {
|
||||||
|
setEnableSkipCredits(settings.enableSkipCredits);
|
||||||
|
}
|
||||||
|
if (settings.enableAutoSkipSegments !== undefined) {
|
||||||
|
setEnableAutoSkipSegments(settings.enableAutoSkipSegments);
|
||||||
|
}
|
||||||
|
if (settings.enableDiscover !== undefined) {
|
||||||
|
setEnableDiscover(settings.enableDiscover);
|
||||||
|
}
|
||||||
|
if (settings.enableFeatured !== undefined) {
|
||||||
|
setEnableFeatured(settings.enableFeatured);
|
||||||
|
}
|
||||||
|
if (settings.enableDetailsModal !== undefined) {
|
||||||
|
setEnableDetailsModal(settings.enableDetailsModal);
|
||||||
|
}
|
||||||
|
if (settings.enableImageLogos !== undefined) {
|
||||||
|
setEnableImageLogos(settings.enableImageLogos);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
settings.sourceOrder !== undefined &&
|
||||||
|
Array.isArray(settings.sourceOrder)
|
||||||
|
) {
|
||||||
|
setSourceOrder(settings.sourceOrder);
|
||||||
|
}
|
||||||
|
if (settings.enableSourceOrder !== undefined) {
|
||||||
|
setEnableSourceOrder(settings.enableSourceOrder);
|
||||||
|
}
|
||||||
|
if (settings.lastSuccessfulSource !== undefined) {
|
||||||
|
setLastSuccessfulSource(settings.lastSuccessfulSource);
|
||||||
|
}
|
||||||
|
if (settings.enableLastSuccessfulSource !== undefined) {
|
||||||
|
setEnableLastSuccessfulSource(settings.enableLastSuccessfulSource);
|
||||||
|
}
|
||||||
|
if (settings.proxyTmdb !== undefined) {
|
||||||
|
setProxyTmdb(settings.proxyTmdb);
|
||||||
|
}
|
||||||
|
if (settings.enableCarouselView !== undefined) {
|
||||||
|
setEnableCarouselView(settings.enableCarouselView);
|
||||||
|
}
|
||||||
|
if (settings.enableMinimalCards !== undefined) {
|
||||||
|
setEnableMinimalCards(settings.enableMinimalCards);
|
||||||
|
}
|
||||||
|
if (settings.forceCompactEpisodeView !== undefined) {
|
||||||
|
setForceCompactEpisodeView(settings.forceCompactEpisodeView);
|
||||||
|
}
|
||||||
|
if (settings.enableLowPerformanceMode !== undefined) {
|
||||||
|
setEnableLowPerformanceMode(settings.enableLowPerformanceMode);
|
||||||
|
}
|
||||||
|
if (settings.enableHoldToBoost !== undefined) {
|
||||||
|
setEnableHoldToBoost(settings.enableHoldToBoost);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
settings.homeSectionOrder !== undefined &&
|
||||||
|
Array.isArray(settings.homeSectionOrder)
|
||||||
|
) {
|
||||||
|
setHomeSectionOrder(settings.homeSectionOrder);
|
||||||
|
}
|
||||||
|
if (settings.manualSourceSelection !== undefined) {
|
||||||
|
setManualSourceSelection(settings.manualSourceSelection);
|
||||||
|
}
|
||||||
|
if (settings.enableDoubleClickToSeek !== undefined) {
|
||||||
|
setEnableDoubleClickToSeek(settings.enableDoubleClickToSeek);
|
||||||
|
}
|
||||||
|
if (settings.enableAutoResumeOnPlaybackError !== undefined) {
|
||||||
|
setEnableAutoResumeOnPlaybackError(
|
||||||
|
settings.enableAutoResumeOnPlaybackError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (settings.customTheme) {
|
||||||
|
setCustomTheme(settings.customTheme);
|
||||||
|
setCustomThemeBaseline(settings.customTheme);
|
||||||
|
} else {
|
||||||
|
setCustomThemeBaseline(useThemeStore.getState().customTheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [account, backendUrl, setFebboxKey, setdebridToken, setdebridService]);
|
}, [
|
||||||
|
account,
|
||||||
|
backendUrl,
|
||||||
|
setTheme,
|
||||||
|
setAppLanguage,
|
||||||
|
setProxySet,
|
||||||
|
setFebboxKey,
|
||||||
|
setdebridToken,
|
||||||
|
setdebridService,
|
||||||
|
setEnableThumbnails,
|
||||||
|
setEnableAutoplay,
|
||||||
|
setEnableSkipCredits,
|
||||||
|
setEnableAutoSkipSegments,
|
||||||
|
setEnableDiscover,
|
||||||
|
setEnableFeatured,
|
||||||
|
setEnableDetailsModal,
|
||||||
|
setEnableImageLogos,
|
||||||
|
setSourceOrder,
|
||||||
|
setEnableSourceOrder,
|
||||||
|
setLastSuccessfulSource,
|
||||||
|
setEnableLastSuccessfulSource,
|
||||||
|
setProxyTmdb,
|
||||||
|
setEnableCarouselView,
|
||||||
|
setEnableMinimalCards,
|
||||||
|
setForceCompactEpisodeView,
|
||||||
|
setEnableLowPerformanceMode,
|
||||||
|
setEnableHoldToBoost,
|
||||||
|
setHomeSectionOrder,
|
||||||
|
setManualSourceSelection,
|
||||||
|
setEnableDoubleClickToSeek,
|
||||||
|
setEnableAutoResumeOnPlaybackError,
|
||||||
|
setCustomTheme,
|
||||||
|
]);
|
||||||
|
|
||||||
const state = useSettingsState(
|
const state = useSettingsState(
|
||||||
activeTheme,
|
activeTheme,
|
||||||
|
|
@ -588,6 +728,7 @@ export function SettingsPage() {
|
||||||
manualSourceSelection,
|
manualSourceSelection,
|
||||||
enableDoubleClickToSeek,
|
enableDoubleClickToSeek,
|
||||||
enableAutoResumeOnPlaybackError,
|
enableAutoResumeOnPlaybackError,
|
||||||
|
customThemeBaseline ?? customTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableSources = useMemo(() => {
|
const availableSources = useMemo(() => {
|
||||||
|
|
@ -655,7 +796,8 @@ export function SettingsPage() {
|
||||||
state.homeSectionOrder.changed ||
|
state.homeSectionOrder.changed ||
|
||||||
state.manualSourceSelection.changed ||
|
state.manualSourceSelection.changed ||
|
||||||
state.enableDoubleClickToSeek.changed ||
|
state.enableDoubleClickToSeek.changed ||
|
||||||
state.enableAutoResumeOnPlaybackError
|
state.enableAutoResumeOnPlaybackError.changed ||
|
||||||
|
state.customTheme.changed
|
||||||
) {
|
) {
|
||||||
await updateSettings(backendUrl, account, {
|
await updateSettings(backendUrl, account, {
|
||||||
applicationLanguage: state.appLanguage.state,
|
applicationLanguage: state.appLanguage.state,
|
||||||
|
|
@ -687,6 +829,7 @@ export function SettingsPage() {
|
||||||
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
|
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
|
||||||
enableAutoResumeOnPlaybackError:
|
enableAutoResumeOnPlaybackError:
|
||||||
state.enableAutoResumeOnPlaybackError.state,
|
state.enableAutoResumeOnPlaybackError.state,
|
||||||
|
customTheme: state.customTheme.state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (state.deviceName.changed) {
|
if (state.deviceName.changed) {
|
||||||
|
|
@ -746,6 +889,8 @@ export function SettingsPage() {
|
||||||
setEnableAutoResumeOnPlaybackError(
|
setEnableAutoResumeOnPlaybackError(
|
||||||
state.enableAutoResumeOnPlaybackError.state,
|
state.enableAutoResumeOnPlaybackError.state,
|
||||||
);
|
);
|
||||||
|
setCustomTheme(state.customTheme.state);
|
||||||
|
setCustomThemeBaseline(state.customTheme.state);
|
||||||
|
|
||||||
if (state.profile.state) {
|
if (state.profile.state) {
|
||||||
updateProfile(state.profile.state);
|
updateProfile(state.profile.state);
|
||||||
|
|
@ -806,6 +951,7 @@ export function SettingsPage() {
|
||||||
setManualSourceSelection,
|
setManualSourceSelection,
|
||||||
setEnableDoubleClickToSeek,
|
setEnableDoubleClickToSeek,
|
||||||
setEnableAutoResumeOnPlaybackError,
|
setEnableAutoResumeOnPlaybackError,
|
||||||
|
setCustomTheme,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
|
|
@ -921,6 +1067,8 @@ export function SettingsPage() {
|
||||||
homeSectionOrder={state.homeSectionOrder.state}
|
homeSectionOrder={state.homeSectionOrder.state}
|
||||||
setHomeSectionOrder={state.homeSectionOrder.set}
|
setHomeSectionOrder={state.homeSectionOrder.set}
|
||||||
enableLowPerformanceMode={state.enableLowPerformanceMode.state}
|
enableLowPerformanceMode={state.enableLowPerformanceMode.state}
|
||||||
|
customTheme={state.customTheme.state}
|
||||||
|
setCustomTheme={state.customTheme.set}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||||
import { useGroupOrderStore } from "@/stores/groupOrder";
|
import { useGroupOrderStore } from "@/stores/groupOrder";
|
||||||
|
import {
|
||||||
|
primaryOptions,
|
||||||
|
secondaryOptions,
|
||||||
|
tertiaryOptions,
|
||||||
|
} from "@themes/custom";
|
||||||
|
|
||||||
const availableThemes = [
|
const availableThemes = [
|
||||||
{
|
{
|
||||||
|
|
@ -130,6 +135,11 @@ const availableThemes = [
|
||||||
selector: "theme-christmas",
|
selector: "theme-christmas",
|
||||||
key: "settings.appearance.themes.christmas",
|
key: "settings.appearance.themes.christmas",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
selector: "theme-custom",
|
||||||
|
key: "settings.appearance.themes.custom",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ThemePreview(props: {
|
function ThemePreview(props: {
|
||||||
|
|
@ -225,6 +235,46 @@ function ThemePreview(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ColorOption(props: {
|
||||||
|
active: boolean;
|
||||||
|
colors: Record<string, string>;
|
||||||
|
onClick: () => void;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
const c1 =
|
||||||
|
props.colors["--colors-type-logo"] ||
|
||||||
|
props.colors["--colors-background-main"] ||
|
||||||
|
props.colors["--colors-type-text"];
|
||||||
|
const c2 =
|
||||||
|
props.colors["--colors-lightBar-light"] ||
|
||||||
|
props.colors["--colors-modal-background"] ||
|
||||||
|
props.colors["--colors-utils-divider"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"cursor-pointer p-1 rounded-full border-2 transition-all",
|
||||||
|
props.active
|
||||||
|
? "border-type-link scale-110"
|
||||||
|
: "border-transparent hover:border-white/20 hover:scale-105",
|
||||||
|
)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
title={props.title}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 rounded-full overflow-hidden flex transform rotate-45">
|
||||||
|
<div
|
||||||
|
className="flex-1 h-full"
|
||||||
|
style={{ backgroundColor: `rgb(${c1})` }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="flex-1 h-full"
|
||||||
|
style={{ backgroundColor: `rgb(${c2})` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AppearancePart(props: {
|
export function AppearancePart(props: {
|
||||||
active: string;
|
active: string;
|
||||||
inUse: string;
|
inUse: string;
|
||||||
|
|
@ -255,9 +305,23 @@ export function AppearancePart(props: {
|
||||||
setHomeSectionOrder: (v: string[]) => void;
|
setHomeSectionOrder: (v: string[]) => void;
|
||||||
|
|
||||||
enableLowPerformanceMode: boolean;
|
enableLowPerformanceMode: boolean;
|
||||||
|
|
||||||
|
customTheme: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
};
|
||||||
|
setCustomTheme: (v: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
}) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const customTheme = props.customTheme;
|
||||||
|
const setCustomTheme = props.setCustomTheme;
|
||||||
|
|
||||||
const carouselRef = useRef<HTMLDivElement>(null);
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
const activeThemeRef = useRef<HTMLDivElement>(null);
|
const activeThemeRef = useRef<HTMLDivElement>(null);
|
||||||
const [isAtTop, setIsAtTop] = useState(true);
|
const [isAtTop, setIsAtTop] = useState(true);
|
||||||
|
|
@ -628,6 +692,65 @@ export function AppearancePart(props: {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{props.active === "custom" && (
|
||||||
|
<div className="animate-fade-in space-y-6 pt-4 border-t border-utils-divider">
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.appearance.customParts.primary")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{primaryOptions.map((opt) => (
|
||||||
|
<ColorOption
|
||||||
|
key={opt.id}
|
||||||
|
active={customTheme.primary === opt.id}
|
||||||
|
colors={opt.colors}
|
||||||
|
onClick={() =>
|
||||||
|
setCustomTheme({ ...customTheme, primary: opt.id })
|
||||||
|
}
|
||||||
|
title={t(`settings.appearance.themes.${opt.id}`)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.appearance.customParts.secondary")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{secondaryOptions.map((opt) => (
|
||||||
|
<ColorOption
|
||||||
|
key={opt.id}
|
||||||
|
active={customTheme.secondary === opt.id}
|
||||||
|
colors={opt.colors}
|
||||||
|
onClick={() =>
|
||||||
|
setCustomTheme({ ...customTheme, secondary: opt.id })
|
||||||
|
}
|
||||||
|
title={t(`settings.appearance.themes.${opt.id}`)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.appearance.customParts.tertiary")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{tertiaryOptions.map((opt) => (
|
||||||
|
<ColorOption
|
||||||
|
key={opt.id}
|
||||||
|
active={customTheme.tertiary === opt.id}
|
||||||
|
colors={opt.colors}
|
||||||
|
onClick={() =>
|
||||||
|
setCustomTheme({ ...customTheme, tertiary: opt.id })
|
||||||
|
}
|
||||||
|
title={t(`settings.appearance.themes.${opt.id}`)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,25 @@ import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
import {
|
||||||
|
primaryOptions,
|
||||||
|
secondaryOptions,
|
||||||
|
tertiaryOptions,
|
||||||
|
} from "@themes/custom";
|
||||||
|
|
||||||
export interface ThemeStore {
|
export interface ThemeStore {
|
||||||
theme: string | null;
|
theme: string | null;
|
||||||
|
customTheme: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
};
|
||||||
setTheme(v: string | null): void;
|
setTheme(v: string | null): void;
|
||||||
|
setCustomTheme(v: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
}): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|
@ -19,11 +35,21 @@ export const useThemeStore = create(
|
||||||
persist(
|
persist(
|
||||||
immer<ThemeStore>((set) => ({
|
immer<ThemeStore>((set) => ({
|
||||||
theme: is420 ? "green" : isHalloween ? "autumn" : null,
|
theme: is420 ? "green" : isHalloween ? "autumn" : null,
|
||||||
|
customTheme: {
|
||||||
|
primary: "classic",
|
||||||
|
secondary: "classic",
|
||||||
|
tertiary: "classic",
|
||||||
|
},
|
||||||
setTheme(v) {
|
setTheme(v) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.theme = v;
|
s.theme = v;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setCustomTheme(v) {
|
||||||
|
set((s) => {
|
||||||
|
s.customTheme = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "__MW::theme",
|
name: "__MW::theme",
|
||||||
|
|
@ -53,12 +79,36 @@ export function ThemeProvider(props: {
|
||||||
}) {
|
}) {
|
||||||
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
|
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
|
||||||
const theme = useThemeStore((s) => s.theme);
|
const theme = useThemeStore((s) => s.theme);
|
||||||
|
const customTheme = useThemeStore((s) => s.customTheme);
|
||||||
|
|
||||||
const themeToDisplay = previewTheme ?? theme;
|
const themeToDisplay = previewTheme ?? theme;
|
||||||
const themeSelector = themeToDisplay ? `theme-${themeToDisplay}` : undefined;
|
const themeSelector = themeToDisplay ? `theme-${themeToDisplay}` : undefined;
|
||||||
|
|
||||||
|
let styleContent = "";
|
||||||
|
if (themeToDisplay === "custom" && customTheme) {
|
||||||
|
const primary =
|
||||||
|
primaryOptions.find((o) => o.id === customTheme.primary)?.colors || {};
|
||||||
|
const secondary =
|
||||||
|
secondaryOptions.find((o) => o.id === customTheme.secondary)?.colors ||
|
||||||
|
{};
|
||||||
|
const tertiary =
|
||||||
|
tertiaryOptions.find((o) => o.id === customTheme.tertiary)?.colors || {};
|
||||||
|
|
||||||
|
const vars = { ...primary, ...secondary, ...tertiary };
|
||||||
|
const cssVars = Object.entries(vars)
|
||||||
|
.map(([k, v]) => `${k}: ${v};`)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
styleContent = `.theme-custom { ${cssVars} }`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={themeSelector}>
|
<div className={themeSelector}>
|
||||||
|
{styleContent ? (
|
||||||
|
<Helmet>
|
||||||
|
<style>{styleContent}</style>
|
||||||
|
</Helmet>
|
||||||
|
) : null}
|
||||||
{props.applyGlobal ? (
|
{props.applyGlobal ? (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<body className={themeSelector} />
|
<body className={themeSelector} />
|
||||||
|
|
|
||||||
67
src/utils/color.ts
Normal file
67
src/utils/color.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
export function hexToRgb(hex: string): string | null {
|
||||||
|
// Remove hash
|
||||||
|
hex = hex.replace(/^#/, "");
|
||||||
|
|
||||||
|
// Convert 3-char hex to 6-char
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map((c) => c + c)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse hex
|
||||||
|
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(
|
||||||
|
hex,
|
||||||
|
);
|
||||||
|
return result
|
||||||
|
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(
|
||||||
|
result[3],
|
||||||
|
16,
|
||||||
|
)}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert HSL/HSLA to RGB
|
||||||
|
// hsla(240, 25%, 6%, 1) -> 15 15 19 (approx)
|
||||||
|
function hslToRgb(h: number, s: number, l: number): string {
|
||||||
|
s /= 100;
|
||||||
|
l /= 100;
|
||||||
|
|
||||||
|
const k = (n: number) => (n + h / 30) % 12;
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
const f = (n: number) =>
|
||||||
|
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
||||||
|
|
||||||
|
return `${Math.round(255 * f(0))} ${Math.round(255 * f(8))} ${Math.round(
|
||||||
|
255 * f(4),
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHsla(hsla: string): string | null {
|
||||||
|
// matches hsla(H, S%, L%, A) or hsl(H, S%, L%)
|
||||||
|
// simple regex, assuming comma separation and valid syntax
|
||||||
|
const match = hsla.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*[\d.]+)?\)/);
|
||||||
|
if (match) {
|
||||||
|
return hslToRgb(
|
||||||
|
parseInt(match[1], 10),
|
||||||
|
parseInt(match[2], 10),
|
||||||
|
parseInt(match[3], 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToRgbString(color: string): string {
|
||||||
|
if (color.startsWith("#")) {
|
||||||
|
const rgb = hexToRgb(color);
|
||||||
|
if (rgb) return rgb;
|
||||||
|
} else if (color.startsWith("hsl")) {
|
||||||
|
const rgb = parseHsla(color);
|
||||||
|
if (rgb) return rgb;
|
||||||
|
}
|
||||||
|
// If parsing fails, assume it's already in RGB or named color
|
||||||
|
// However, returning "red" for Tailwind opacity utility will likely fail (as it expects RGB components)
|
||||||
|
// But returning original string allows basic non-opacity usage to potentially work or fail gracefully.
|
||||||
|
return color;
|
||||||
|
}
|
||||||
232
themes/custom.ts
Normal file
232
themes/custom.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import merge from "lodash.merge";
|
||||||
|
import { createTheme } from "./types";
|
||||||
|
import { defaultTheme } from "./default";
|
||||||
|
import classic from "./list/classic";
|
||||||
|
import blue from "./list/blue";
|
||||||
|
import red from "./list/red";
|
||||||
|
import teal from "./list/teal";
|
||||||
|
import green from "./list/green";
|
||||||
|
import pink from "./list/pink";
|
||||||
|
import autumn from "./list/autumn";
|
||||||
|
import frost from "./list/frost";
|
||||||
|
import grape from "./list/grape";
|
||||||
|
import { colorToRgbString } from "../src/utils/color";
|
||||||
|
|
||||||
|
const availableThemes = [
|
||||||
|
{ id: "classic", theme: classic },
|
||||||
|
{ id: "blue", theme: blue },
|
||||||
|
{ id: "red", theme: red },
|
||||||
|
{ id: "teal", theme: teal },
|
||||||
|
{ id: "green", theme: green },
|
||||||
|
{ id: "pink", theme: pink },
|
||||||
|
{ id: "autumn", theme: autumn },
|
||||||
|
{ id: "frost", theme: frost },
|
||||||
|
{ id: "grape", theme: grape },
|
||||||
|
];
|
||||||
|
|
||||||
|
function cssVarName(path: string) {
|
||||||
|
return `--colors-${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the custom theme structure with CSS variables
|
||||||
|
function generateCustomThemeStructure(theme: any, prefix = ""): any {
|
||||||
|
const result: any = {};
|
||||||
|
for (const key in theme) {
|
||||||
|
if (typeof theme[key] === "object" && theme[key] !== null) {
|
||||||
|
result[key] = generateCustomThemeStructure(theme[key], `${prefix}${key}-`);
|
||||||
|
} else {
|
||||||
|
result[key] = `rgb(var(${cssVarName(`${prefix}${key}`)}) / <alpha-value>)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customTheme = createTheme({
|
||||||
|
name: "custom",
|
||||||
|
extend: {
|
||||||
|
colors: generateCustomThemeStructure(defaultTheme.extend.colors),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define parts
|
||||||
|
const parts = {
|
||||||
|
primary: [
|
||||||
|
"lightBar.light",
|
||||||
|
"type.logo",
|
||||||
|
"buttons.primary",
|
||||||
|
"buttons.primaryText",
|
||||||
|
"buttons.primaryHover",
|
||||||
|
"buttons.toggle",
|
||||||
|
"buttons.toggleDisabled",
|
||||||
|
"buttons.purple",
|
||||||
|
"buttons.purpleHover",
|
||||||
|
"global.accentA",
|
||||||
|
"global.accentB",
|
||||||
|
"pill.highlight",
|
||||||
|
"progress.filled",
|
||||||
|
"video.audio.set",
|
||||||
|
"video.context.type.accent",
|
||||||
|
"video.context.sliderFilled",
|
||||||
|
"video.scraping.loading",
|
||||||
|
"onboarding.good",
|
||||||
|
"onboarding.best",
|
||||||
|
"onboarding.link",
|
||||||
|
"onboarding.barFilled",
|
||||||
|
"settings.sidebar.type.iconActivated",
|
||||||
|
"settings.sidebar.type.activated",
|
||||||
|
"type.link",
|
||||||
|
"type.linkHover",
|
||||||
|
"largeCard.icon",
|
||||||
|
"mediaCard.barFillColor",
|
||||||
|
],
|
||||||
|
secondary: [
|
||||||
|
"type.text",
|
||||||
|
"type.dimmed",
|
||||||
|
"type.secondary",
|
||||||
|
"type.emphasis",
|
||||||
|
"type.divider",
|
||||||
|
"type.danger",
|
||||||
|
"type.success",
|
||||||
|
"buttons.secondary",
|
||||||
|
"buttons.secondaryText",
|
||||||
|
"buttons.secondaryHover",
|
||||||
|
"buttons.danger",
|
||||||
|
"buttons.dangerHover",
|
||||||
|
"buttons.cancel",
|
||||||
|
"buttons.cancelHover",
|
||||||
|
"utils.divider",
|
||||||
|
"search.text",
|
||||||
|
"search.placeholder",
|
||||||
|
"search.icon",
|
||||||
|
"dropdown.text",
|
||||||
|
"dropdown.secondary",
|
||||||
|
"dropdown.border",
|
||||||
|
"authentication.border",
|
||||||
|
"authentication.inputBg",
|
||||||
|
"authentication.inputBgHover",
|
||||||
|
"authentication.wordBackground",
|
||||||
|
"authentication.copyText",
|
||||||
|
"authentication.copyTextHover",
|
||||||
|
"authentication.errorText",
|
||||||
|
"settings.sidebar.activeLink",
|
||||||
|
"settings.sidebar.badge",
|
||||||
|
"settings.sidebar.type.secondary",
|
||||||
|
"settings.sidebar.type.inactive",
|
||||||
|
"settings.sidebar.type.icon",
|
||||||
|
"settings.card.border",
|
||||||
|
"onboarding.bar",
|
||||||
|
"onboarding.divider",
|
||||||
|
"onboarding.border",
|
||||||
|
"errors.border",
|
||||||
|
"errors.type.secondary",
|
||||||
|
"about.circle",
|
||||||
|
"about.circleText",
|
||||||
|
"editBadge.bg",
|
||||||
|
"editBadge.bgHover",
|
||||||
|
"editBadge.text",
|
||||||
|
"progress.background",
|
||||||
|
"progress.preloaded",
|
||||||
|
"pill.background",
|
||||||
|
"pill.backgroundHover",
|
||||||
|
"pill.activeBackground",
|
||||||
|
"video.buttonBackground",
|
||||||
|
"video.autoPlay.background",
|
||||||
|
"video.autoPlay.hover",
|
||||||
|
"video.scraping.error",
|
||||||
|
"video.scraping.success",
|
||||||
|
"video.scraping.noresult",
|
||||||
|
"video.context.light",
|
||||||
|
"video.context.border",
|
||||||
|
"video.context.hoverColor",
|
||||||
|
"video.context.buttonFocus",
|
||||||
|
"video.context.inputBg",
|
||||||
|
"video.context.buttonOverInputHover",
|
||||||
|
"video.context.inputPlaceholder",
|
||||||
|
"video.context.cardBorder",
|
||||||
|
"video.context.slider",
|
||||||
|
"video.context.error",
|
||||||
|
"video.context.buttons.list",
|
||||||
|
"video.context.buttons.active",
|
||||||
|
"video.context.closeHover",
|
||||||
|
"video.context.type.main",
|
||||||
|
"video.context.type.secondary",
|
||||||
|
"mediaCard.barColor",
|
||||||
|
"mediaCard.badge",
|
||||||
|
"mediaCard.badgeText",
|
||||||
|
],
|
||||||
|
tertiary: [
|
||||||
|
"background.main",
|
||||||
|
"background.secondary",
|
||||||
|
"background.secondaryHover",
|
||||||
|
"background.accentA",
|
||||||
|
"background.accentB",
|
||||||
|
"modal.background",
|
||||||
|
"mediaCard.shadow",
|
||||||
|
"mediaCard.hoverBackground",
|
||||||
|
"mediaCard.hoverAccent",
|
||||||
|
"mediaCard.hoverShadow",
|
||||||
|
"search.background",
|
||||||
|
"search.hoverBackground",
|
||||||
|
"search.focused",
|
||||||
|
"dropdown.background",
|
||||||
|
"dropdown.altBackground",
|
||||||
|
"dropdown.hoverBackground",
|
||||||
|
"dropdown.contentBackground",
|
||||||
|
"dropdown.highlight",
|
||||||
|
"dropdown.highlightHover",
|
||||||
|
"largeCard.background",
|
||||||
|
"settings.card.background",
|
||||||
|
"settings.card.altBackground",
|
||||||
|
"settings.saveBar.background",
|
||||||
|
"onboarding.card",
|
||||||
|
"onboarding.cardHover",
|
||||||
|
"errors.card",
|
||||||
|
"themePreview.primary",
|
||||||
|
"themePreview.secondary",
|
||||||
|
"themePreview.ghost",
|
||||||
|
"video.scraping.card",
|
||||||
|
"video.context.background",
|
||||||
|
"video.context.flagBg",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNestedValue(obj: any, path: string) {
|
||||||
|
return path.split(".").reduce((o, i) => (o ? o[i] : undefined), obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractColors(theme: any, keys: string[]) {
|
||||||
|
const colors: Record<string, string> = {};
|
||||||
|
// We need to flatten the structure to css vars
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const value = getNestedValue(theme.extend.colors, key);
|
||||||
|
if (value) {
|
||||||
|
colors[cssVarName(key.replace(/\./g, "-"))] = colorToRgbString(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate options for each part
|
||||||
|
export const primaryOptions = availableThemes.map((t) => {
|
||||||
|
const merged = merge({}, defaultTheme, t.theme);
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
colors: extractColors(merged, parts.primary),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const secondaryOptions = availableThemes.map((t) => {
|
||||||
|
const merged = merge({}, defaultTheme, t.theme);
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
colors: extractColors(merged, parts.secondary),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tertiaryOptions = availableThemes.map((t) => {
|
||||||
|
const merged = merge({}, defaultTheme, t.theme);
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
colors: extractColors(merged, parts.tertiary),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { allThemes } from "./all";
|
import { allThemes } from "./all";
|
||||||
|
import { customTheme } from "./custom";
|
||||||
|
|
||||||
export { defaultTheme } from "./default";
|
export { defaultTheme } from "./default";
|
||||||
export { allThemes } from "./all";
|
export { allThemes } from "./all";
|
||||||
|
|
||||||
export const safeThemeList = allThemes
|
export const safeThemeList = [customTheme, ...allThemes]
|
||||||
.flatMap((v) => v.selectors)
|
.flatMap((v) => v.selectors)
|
||||||
.filter((v) => v.startsWith("."))
|
.filter((v) => v.startsWith("."))
|
||||||
.map((v) => v.slice(1)); // remove dot from selector
|
.map((v) => v.slice(1)); // remove dot from selector
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { DeepPartial } from "vite-plugin-checker/dist/esm/types";
|
|
||||||
import { defaultTheme } from "./default";
|
import { defaultTheme } from "./default";
|
||||||
|
|
||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||||
|
};
|
||||||
|
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
name: string;
|
name: string;
|
||||||
extend: DeepPartial<(typeof defaultTheme)["extend"]>;
|
extend: DeepPartial<(typeof defaultTheme)["extend"]>;
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./*"],
|
||||||
|
"@themes/*": ["../themes/*"],
|
||||||
"@sozialhelden/ietf-language-tags": [
|
"@sozialhelden/ietf-language-tags": [
|
||||||
"../node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
|
"../node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "themes"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ export default defineConfig(({ mode }) => {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
"@themes": path.resolve(__dirname, "./themes"),
|
||||||
"@sozialhelden/ietf-language-tags": path.resolve(
|
"@sozialhelden/ietf-language-tags": path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
"./node_modules/@sozialhelden/ietf-language-tags/dist/cjs",
|
"./node_modules/@sozialhelden/ietf-language-tags/dist/cjs",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue