mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -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 { ThemeSelector } from "@kyoo/primitives";
|
||||
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 { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||
import i18next from "i18next";
|
||||
@ -111,9 +111,12 @@ SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function Root() {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
const theme = useColorScheme();
|
||||
let theme = useUserTheme();
|
||||
const systemTheme = useColorScheme();
|
||||
const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black });
|
||||
|
||||
if (theme === "auto") theme = systemTheme ?? "light";
|
||||
|
||||
if (!fontsLoaded) return null;
|
||||
return (
|
||||
<PersistQueryClientProvider
|
||||
@ -125,7 +128,7 @@ export default function Root() {
|
||||
}}
|
||||
>
|
||||
<ThemeSelector
|
||||
theme={theme ?? "light"}
|
||||
theme={theme}
|
||||
font={{
|
||||
normal: "Poppins_400Regular",
|
||||
"300": "Poppins_300Light",
|
||||
|
@ -25,12 +25,14 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { HiddenIfNoJs, TouchOnlyCss, SkeletonCss, ThemeSelector } from "@kyoo/primitives";
|
||||
import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
|
||||
import {
|
||||
AccountP,
|
||||
AccountProvider,
|
||||
createQueryClient,
|
||||
fetchQuery,
|
||||
getTokenWJ,
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
useUserTheme,
|
||||
} from "@kyoo/models";
|
||||
import { useState } from "react";
|
||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||
@ -41,11 +43,7 @@ import Head from "next/head";
|
||||
import { withTranslations } from "../i18n";
|
||||
import arrayShuffle from "array-shuffle";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import {
|
||||
getCurrentAccount,
|
||||
readAccountCookie,
|
||||
updateAccount,
|
||||
} from "@kyoo/models/src/account-internal";
|
||||
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||
|
||||
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 [queryClient] = useState(() => createQueryClient());
|
||||
const { queryState, token, randomItems, account, ...props } = superjson.deserialize<any>(
|
||||
const { queryState, token, randomItems, account, theme, ...props } = superjson.deserialize<any>(
|
||||
pageProps ?? { json: {} },
|
||||
);
|
||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||
const { Layout, props: layoutProps } =
|
||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||
|
||||
const userTheme = useUserTheme(theme);
|
||||
useMobileHover();
|
||||
|
||||
// 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}>
|
||||
<AccountProvider ssrAccount={account}>
|
||||
<HydrationBoundary state={queryState}>
|
||||
<ThemeSelector theme="auto" font={{ normal: "inherit" }}>
|
||||
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
||||
<GlobalCssTheme />
|
||||
<Layout
|
||||
page={
|
||||
@ -183,11 +182,12 @@ App.getInitialProps = async (ctx: AppContext) => {
|
||||
...(getLayoutUrl ? getLayoutUrl(ctx.router.query as any, items) : []),
|
||||
];
|
||||
|
||||
const account = readAccountCookie(ctx.ctx.req?.headers.cookie);
|
||||
const [authToken, token] = await getTokenWJ(account ?? null);
|
||||
const account = readCookie(ctx.ctx.req?.headers.cookie, "account", AccountP);
|
||||
const [authToken, token] = await getTokenWJ(account);
|
||||
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
||||
appProps.pageProps.token = token;
|
||||
appProps.pageProps.account = account;
|
||||
appProps.pageProps.theme = readCookie(ctx.ctx.req?.headers.cookie, "theme") ?? "auto";
|
||||
|
||||
return { pageProps: superjson.serialize(appProps.pageProps) };
|
||||
};
|
||||
|
@ -18,7 +18,7 @@
|
||||
* 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 { MMKV } from "react-native-mmkv";
|
||||
|
||||
@ -34,21 +34,25 @@ const writeAccounts = (accounts: Account[]) => {
|
||||
storage.set("accounts", JSON.stringify(accounts));
|
||||
};
|
||||
|
||||
export const setAccountCookie = (account?: Account) => {
|
||||
let value = JSON.stringify(account);
|
||||
export const setCookie = (key: string, val?: unknown) => {
|
||||
let value = JSON.stringify(val);
|
||||
// Remove illegal values from json. There should not be one in the account anyways.
|
||||
value = value?.replaceAll(";", "");
|
||||
const d = new Date();
|
||||
// A year
|
||||
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
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;
|
||||
};
|
||||
|
||||
export const readAccountCookie = (cookies?: string) => {
|
||||
export const readCookie = <T extends ZodRawShape>(
|
||||
cookies: string | undefined,
|
||||
key: string,
|
||||
parser?: ZodObject<T>,
|
||||
) => {
|
||||
if (!cookies) return null;
|
||||
const name = "account=";
|
||||
const name = `${key}=`;
|
||||
const decodedCookie = decodeURIComponent(cookies);
|
||||
const ca = decodedCookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
@ -58,7 +62,8 @@ export const readAccountCookie = (cookies?: string) => {
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
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;
|
||||
|
@ -22,7 +22,7 @@ import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from
|
||||
import { User, UserP } from "./resources";
|
||||
import { z } from "zod";
|
||||
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 { Platform } from "react-native";
|
||||
import { useFetch } from "./query";
|
||||
@ -122,7 +122,7 @@ export const AccountProvider = ({
|
||||
oldSelectedId.current = selected?.id;
|
||||
|
||||
// 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]);
|
||||
|
||||
return (
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
export * from "./accounts";
|
||||
export { storage } from "./account-internal";
|
||||
export * from "./theme";
|
||||
export * from "./resources";
|
||||
export * from "./traits";
|
||||
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 { Button } from "./button";
|
||||
|
||||
export const Select = ({
|
||||
export const Select = <Value extends string>({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
@ -31,10 +31,10 @@ export const Select = ({
|
||||
getLabel,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (v: string) => void;
|
||||
values: string[];
|
||||
getLabel: (key: string) => string;
|
||||
value: Value;
|
||||
onValueChange: (v: Value) => void;
|
||||
values: Value[];
|
||||
getLabel: (key: Value) => string;
|
||||
}) => {
|
||||
return (
|
||||
<Menu Trigger={Button} text={getLabel(value)} icon={<Icon icon={ExpandMore} />}>
|
||||
@ -43,7 +43,7 @@ export const Select = ({
|
||||
key={x}
|
||||
label={getLabel(x)}
|
||||
selected={x === value}
|
||||
onSelect={() => onValueChange(value)}
|
||||
onSelect={() => onValueChange(x)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
|
@ -18,7 +18,7 @@
|
||||
* 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 { DefaultLayout } from "../layout";
|
||||
import { ReactNode } from "react";
|
||||
@ -56,15 +56,16 @@ const query: QueryIdentifier<User> = {
|
||||
export const SettingsPage: QueryPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useUserTheme("auto");
|
||||
return (
|
||||
<ScrollView>
|
||||
<Container>
|
||||
<Preference label={t("settings.theme.label")}>
|
||||
<Select
|
||||
label={t("settings.theme.label")}
|
||||
value="system"
|
||||
onValueChange={() => {}}
|
||||
values={["system", "light", "dark"]}
|
||||
value={theme}
|
||||
onValueChange={(value) => setUserTheme(value)}
|
||||
values={["auto", "light", "dark"]}
|
||||
getLabel={(key) => t(`settings.theme.${key}`)}
|
||||
/>
|
||||
</Preference>
|
||||
|
@ -75,7 +75,7 @@
|
||||
"settings": {
|
||||
"theme": {
|
||||
"label": "Theme",
|
||||
"system": "System",
|
||||
"auto": "System",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
}
|
||||
|
@ -75,7 +75,7 @@
|
||||
"settings": {
|
||||
"theme": {
|
||||
"label": "Thème",
|
||||
"system": "Système",
|
||||
"auto": "Système",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user