Allow the theme to be changed in the settings

This commit is contained in:
Zoe Roux 2023-12-20 03:55:33 +01:00
parent c7061c8c75
commit 9963bf6179
10 changed files with 79 additions and 33 deletions

View File

@ -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",

View File

@ -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) };
};

View File

@ -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;

View File

@ -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 (

View File

@ -20,6 +20,7 @@
export * from "./accounts";
export { storage } from "./account-internal";
export * from "./theme";
export * from "./resources";
export * from "./traits";
export * from "./page";

View 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);
};

View File

@ -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>

View File

@ -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>

View File

@ -75,7 +75,7 @@
"settings": {
"theme": {
"label": "Theme",
"system": "System",
"auto": "System",
"light": "Light",
"dark": "Dark"
}

View File

@ -75,7 +75,7 @@
"settings": {
"theme": {
"label": "Thème",
"system": "Système",
"auto": "Système",
"light": "Clair",
"dark": "Sombre"
}