mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Allow the theme to be changed in the settings
This commit is contained in:
parent
c7061c8c75
commit
9963bf6179
@ -23,7 +23,7 @@ import "react-native-reanimated";
|
|||||||
import { PortalProvider } from "@gorhom/portal";
|
import { PortalProvider } from "@gorhom/portal";
|
||||||
import { ThemeSelector } from "@kyoo/primitives";
|
import { ThemeSelector } from "@kyoo/primitives";
|
||||||
import { DownloadProvider } from "@kyoo/ui";
|
import { DownloadProvider } from "@kyoo/ui";
|
||||||
import { AccountProvider, createQueryClient, storage } from "@kyoo/models";
|
import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models";
|
||||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -111,9 +111,12 @@ SplashScreen.preventAutoHideAsync();
|
|||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
const theme = useColorScheme();
|
let theme = useUserTheme();
|
||||||
|
const systemTheme = useColorScheme();
|
||||||
const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black });
|
const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black });
|
||||||
|
|
||||||
|
if (theme === "auto") theme = systemTheme ?? "light";
|
||||||
|
|
||||||
if (!fontsLoaded) return null;
|
if (!fontsLoaded) return null;
|
||||||
return (
|
return (
|
||||||
<PersistQueryClientProvider
|
<PersistQueryClientProvider
|
||||||
@ -125,7 +128,7 @@ export default function Root() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
theme={theme ?? "light"}
|
theme={theme}
|
||||||
font={{
|
font={{
|
||||||
normal: "Poppins_400Regular",
|
normal: "Poppins_400Regular",
|
||||||
"300": "Poppins_300Light",
|
"300": "Poppins_300Light",
|
||||||
|
@ -25,12 +25,14 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|||||||
import { HiddenIfNoJs, TouchOnlyCss, SkeletonCss, ThemeSelector } from "@kyoo/primitives";
|
import { HiddenIfNoJs, TouchOnlyCss, SkeletonCss, ThemeSelector } from "@kyoo/primitives";
|
||||||
import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
|
import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
|
||||||
import {
|
import {
|
||||||
|
AccountP,
|
||||||
AccountProvider,
|
AccountProvider,
|
||||||
createQueryClient,
|
createQueryClient,
|
||||||
fetchQuery,
|
fetchQuery,
|
||||||
getTokenWJ,
|
getTokenWJ,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
QueryPage,
|
QueryPage,
|
||||||
|
useUserTheme,
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||||
@ -41,11 +43,7 @@ import Head from "next/head";
|
|||||||
import { withTranslations } from "../i18n";
|
import { withTranslations } from "../i18n";
|
||||||
import arrayShuffle from "array-shuffle";
|
import arrayShuffle from "array-shuffle";
|
||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
import {
|
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||||
getCurrentAccount,
|
|
||||||
readAccountCookie,
|
|
||||||
updateAccount,
|
|
||||||
} from "@kyoo/models/src/account-internal";
|
|
||||||
|
|
||||||
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
||||||
|
|
||||||
@ -111,13 +109,14 @@ const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
|
|||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
const { queryState, token, randomItems, account, ...props } = superjson.deserialize<any>(
|
const { queryState, token, randomItems, account, theme, ...props } = superjson.deserialize<any>(
|
||||||
pageProps ?? { json: {} },
|
pageProps ?? { json: {} },
|
||||||
);
|
);
|
||||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||||
const { Layout, props: layoutProps } =
|
const { Layout, props: layoutProps } =
|
||||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||||
|
|
||||||
|
const userTheme = useUserTheme(theme);
|
||||||
useMobileHover();
|
useMobileHover();
|
||||||
|
|
||||||
// Set the auth from the server (if the token was refreshed during SSR).
|
// Set the auth from the server (if the token was refreshed during SSR).
|
||||||
@ -136,7 +135,7 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AccountProvider ssrAccount={account}>
|
<AccountProvider ssrAccount={account}>
|
||||||
<HydrationBoundary state={queryState}>
|
<HydrationBoundary state={queryState}>
|
||||||
<ThemeSelector theme="auto" font={{ normal: "inherit" }}>
|
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
||||||
<GlobalCssTheme />
|
<GlobalCssTheme />
|
||||||
<Layout
|
<Layout
|
||||||
page={
|
page={
|
||||||
@ -183,11 +182,12 @@ App.getInitialProps = async (ctx: AppContext) => {
|
|||||||
...(getLayoutUrl ? getLayoutUrl(ctx.router.query as any, items) : []),
|
...(getLayoutUrl ? getLayoutUrl(ctx.router.query as any, items) : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const account = readAccountCookie(ctx.ctx.req?.headers.cookie);
|
const account = readCookie(ctx.ctx.req?.headers.cookie, "account", AccountP);
|
||||||
const [authToken, token] = await getTokenWJ(account ?? null);
|
const [authToken, token] = await getTokenWJ(account);
|
||||||
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
||||||
appProps.pageProps.token = token;
|
appProps.pageProps.token = token;
|
||||||
appProps.pageProps.account = account;
|
appProps.pageProps.account = account;
|
||||||
|
appProps.pageProps.theme = readCookie(ctx.ctx.req?.headers.cookie, "theme") ?? "auto";
|
||||||
|
|
||||||
return { pageProps: superjson.serialize(appProps.pageProps) };
|
return { pageProps: superjson.serialize(appProps.pageProps) };
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { ZodObject, ZodRawShape, z } from "zod";
|
||||||
import { Account, AccountP } from "./accounts";
|
import { Account, AccountP } from "./accounts";
|
||||||
import { MMKV } from "react-native-mmkv";
|
import { MMKV } from "react-native-mmkv";
|
||||||
|
|
||||||
@ -34,21 +34,25 @@ const writeAccounts = (accounts: Account[]) => {
|
|||||||
storage.set("accounts", JSON.stringify(accounts));
|
storage.set("accounts", JSON.stringify(accounts));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAccountCookie = (account?: Account) => {
|
export const setCookie = (key: string, val?: unknown) => {
|
||||||
let value = JSON.stringify(account);
|
let value = JSON.stringify(val);
|
||||||
// Remove illegal values from json. There should not be one in the account anyways.
|
// Remove illegal values from json. There should not be one in the account anyways.
|
||||||
value = value?.replaceAll(";", "");
|
value = value?.replaceAll(";", "");
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
// A year
|
// A year
|
||||||
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||||
const expires = value ? "expires=" + d.toUTCString() : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
const expires = value ? "expires=" + d.toUTCString() : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
||||||
document.cookie = "account=" + value + ";" + expires + ";path=/;samesite=strict";
|
document.cookie = key + "=" + value + ";" + expires + ";path=/;samesite=strict";
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readAccountCookie = (cookies?: string) => {
|
export const readCookie = <T extends ZodRawShape>(
|
||||||
|
cookies: string | undefined,
|
||||||
|
key: string,
|
||||||
|
parser?: ZodObject<T>,
|
||||||
|
) => {
|
||||||
if (!cookies) return null;
|
if (!cookies) return null;
|
||||||
const name = "account=";
|
const name = `${key}=`;
|
||||||
const decodedCookie = decodeURIComponent(cookies);
|
const decodedCookie = decodeURIComponent(cookies);
|
||||||
const ca = decodedCookie.split(";");
|
const ca = decodedCookie.split(";");
|
||||||
for (let i = 0; i < ca.length; i++) {
|
for (let i = 0; i < ca.length; i++) {
|
||||||
@ -58,7 +62,8 @@ export const readAccountCookie = (cookies?: string) => {
|
|||||||
}
|
}
|
||||||
if (c.indexOf(name) == 0) {
|
if (c.indexOf(name) == 0) {
|
||||||
const str = c.substring(name.length, c.length);
|
const str = c.substring(name.length, c.length);
|
||||||
return AccountP.parse(JSON.parse(str));
|
const ret = JSON.parse(str);
|
||||||
|
return parser ? parser.parse(ret) : ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -22,7 +22,7 @@ import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from
|
|||||||
import { User, UserP } from "./resources";
|
import { User, UserP } from "./resources";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "./utils";
|
import { zdate } from "./utils";
|
||||||
import { removeAccounts, setAccountCookie, updateAccount } from "./account-internal";
|
import { removeAccounts, setCookie, updateAccount } from "./account-internal";
|
||||||
import { useMMKVString } from "react-native-mmkv";
|
import { useMMKVString } from "react-native-mmkv";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useFetch } from "./query";
|
import { useFetch } from "./query";
|
||||||
@ -122,7 +122,7 @@ export const AccountProvider = ({
|
|||||||
oldSelectedId.current = selected?.id;
|
oldSelectedId.current = selected?.id;
|
||||||
|
|
||||||
// update cookies for ssr (needs to contains token, theme, language...)
|
// update cookies for ssr (needs to contains token, theme, language...)
|
||||||
if (Platform.OS === "web") setAccountCookie(selected);
|
if (Platform.OS === "web") setCookie("account", selected);
|
||||||
}, [selected, queryClient]);
|
}, [selected, queryClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
export * from "./accounts";
|
export * from "./accounts";
|
||||||
export { storage } from "./account-internal";
|
export { storage } from "./account-internal";
|
||||||
|
export * from "./theme";
|
||||||
export * from "./resources";
|
export * from "./resources";
|
||||||
export * from "./traits";
|
export * from "./traits";
|
||||||
export * from "./page";
|
export * from "./page";
|
||||||
|
36
front/packages/models/src/theme.ts
Normal file
36
front/packages/models/src/theme.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Kyoo - A portable and vast media library solution.
|
||||||
|
* Copyright (c) Kyoo.
|
||||||
|
*
|
||||||
|
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
*
|
||||||
|
* Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* Kyoo is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMMKVString } from "react-native-mmkv";
|
||||||
|
import { setCookie, storage } from "./account-internal";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
|
export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
|
||||||
|
if (Platform.OS === "web" && typeof window === "undefined" && ssrTheme) return ssrTheme;
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [value] = useMMKVString("theme", storage);
|
||||||
|
if (!value) return "auto";
|
||||||
|
return value as "light" | "dark" | "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setUserTheme = (theme: "light" | "dark" | "auto") => {
|
||||||
|
storage.set("theme", theme);
|
||||||
|
if (Platform.OS === "web") setCookie("theme", theme);
|
||||||
|
};
|
@ -23,7 +23,7 @@ import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
|||||||
import { Menu } from "./menu";
|
import { Menu } from "./menu";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
|
|
||||||
export const Select = ({
|
export const Select = <Value extends string>({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
@ -31,10 +31,10 @@ export const Select = ({
|
|||||||
getLabel,
|
getLabel,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: Value;
|
||||||
onValueChange: (v: string) => void;
|
onValueChange: (v: Value) => void;
|
||||||
values: string[];
|
values: Value[];
|
||||||
getLabel: (key: string) => string;
|
getLabel: (key: Value) => string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Menu Trigger={Button} text={getLabel(value)} icon={<Icon icon={ExpandMore} />}>
|
<Menu Trigger={Button} text={getLabel(value)} icon={<Icon icon={ExpandMore} />}>
|
||||||
@ -43,7 +43,7 @@ export const Select = ({
|
|||||||
key={x}
|
key={x}
|
||||||
label={getLabel(x)}
|
label={getLabel(x)}
|
||||||
selected={x === value}
|
selected={x === value}
|
||||||
onSelect={() => onValueChange(value)}
|
onSelect={() => onValueChange(x)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QueryIdentifier, QueryPage, User, UserP } from "@kyoo/models";
|
import { QueryIdentifier, QueryPage, User, UserP, setUserTheme, useUserTheme } from "@kyoo/models";
|
||||||
import { Container, P, Select, ts } from "@kyoo/primitives";
|
import { Container, P, Select, ts } from "@kyoo/primitives";
|
||||||
import { DefaultLayout } from "../layout";
|
import { DefaultLayout } from "../layout";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
@ -56,15 +56,16 @@ const query: QueryIdentifier<User> = {
|
|||||||
export const SettingsPage: QueryPage = () => {
|
export const SettingsPage: QueryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const theme = useUserTheme("auto");
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<Container>
|
<Container>
|
||||||
<Preference label={t("settings.theme.label")}>
|
<Preference label={t("settings.theme.label")}>
|
||||||
<Select
|
<Select
|
||||||
label={t("settings.theme.label")}
|
label={t("settings.theme.label")}
|
||||||
value="system"
|
value={theme}
|
||||||
onValueChange={() => {}}
|
onValueChange={(value) => setUserTheme(value)}
|
||||||
values={["system", "light", "dark"]}
|
values={["auto", "light", "dark"]}
|
||||||
getLabel={(key) => t(`settings.theme.${key}`)}
|
getLabel={(key) => t(`settings.theme.${key}`)}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"theme": {
|
"theme": {
|
||||||
"label": "Theme",
|
"label": "Theme",
|
||||||
"system": "System",
|
"auto": "System",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark"
|
"dark": "Dark"
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"theme": {
|
"theme": {
|
||||||
"label": "Thème",
|
"label": "Thème",
|
||||||
"system": "Système",
|
"auto": "Système",
|
||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"dark": "Sombre"
|
"dark": "Sombre"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user