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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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