mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-08 18:54:22 -04:00
Rework account internals storage
This commit is contained in:
parent
0a8553653c
commit
ea7cb8b4d1
@ -17,71 +17,3 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import { type ZodTypeAny, z } from "zod";
|
|
||||||
import { type Account, AccountP } from "./accounts";
|
|
||||||
|
|
||||||
const readAccounts = () => {
|
|
||||||
const acc = storage.getString("accounts");
|
|
||||||
if (!acc) return [];
|
|
||||||
return z.array(AccountP).parse(JSON.parse(acc));
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeAccounts = (accounts: Account[]) => {
|
|
||||||
storage.set("accounts", JSON.stringify(accounts));
|
|
||||||
if (Platform.OS === "web") {
|
|
||||||
const selected = accounts.find((x) => x.selected);
|
|
||||||
if (!selected) return;
|
|
||||||
setCookie("account", selected);
|
|
||||||
// cookie used for images and videos since we can't add Authorization headers in img or video tags.
|
|
||||||
setCookie("X-Bearer", selected?.token.access_token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentAccount = () => {
|
|
||||||
const accounts = readAccounts();
|
|
||||||
return accounts.find((x) => x.selected);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addAccount = (account: Account) => {
|
|
||||||
const accounts = readAccounts();
|
|
||||||
|
|
||||||
// Prevent the user from adding the same account twice.
|
|
||||||
if (accounts.find((x) => x.id === account.id)) {
|
|
||||||
updateAccount(account.id, account);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const acc of accounts) acc.selected = false;
|
|
||||||
accounts.push(account);
|
|
||||||
writeAccounts(accounts);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
|
||||||
let accounts = readAccounts();
|
|
||||||
accounts = accounts.filter((x) => !filter(x));
|
|
||||||
if (!accounts.find((x) => x.selected) && accounts.length > 0) {
|
|
||||||
accounts[0].selected = true;
|
|
||||||
}
|
|
||||||
writeAccounts(accounts);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateAccount = (id: string, account: Account) => {
|
|
||||||
const accounts = readAccounts();
|
|
||||||
const idx = accounts.findIndex((x) => x.id === id);
|
|
||||||
if (idx === -1) return;
|
|
||||||
|
|
||||||
const selected = account.selected;
|
|
||||||
if (selected) {
|
|
||||||
for (const acc of accounts) acc.selected = false;
|
|
||||||
// if account was already on the accounts list, we keep it selected.
|
|
||||||
account.selected = selected;
|
|
||||||
} else if (accounts[idx].selected) {
|
|
||||||
// we just unselected the current account, focus another one.
|
|
||||||
if (accounts.length > 0) accounts[0].selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts[idx] = account;
|
|
||||||
writeAccounts(accounts);
|
|
||||||
};
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { type ReactNode, createContext, useEffect, useMemo } from "react";
|
import { type ReactNode, createContext, useEffect, useMemo } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { type Account, type Token, type User, UserP } from "~/models";
|
import { z } from "zod";
|
||||||
|
import { type Account, AccountP, type Token, type User, UserP } from "~/models";
|
||||||
import { useFetch } from "~/query";
|
import { useFetch } from "~/query";
|
||||||
|
import { removeAccounts, updateAccount } from "./account-store";
|
||||||
import { useSetError } from "./error-provider";
|
import { useSetError } from "./error-provider";
|
||||||
|
import { useStoreValue } from "./settings";
|
||||||
|
|
||||||
const AccountContext = createContext<{
|
const AccountContext = createContext<{
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
@ -31,22 +34,19 @@ export const AccountProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setError = useSetError();
|
const setError = useSetError();
|
||||||
|
const accounts = useStoreValue("accounts", z.array(AccountP)) ?? [];
|
||||||
const [accStr] = useMMKVString("accounts");
|
|
||||||
const accounts = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
|
||||||
|
|
||||||
const ret = useMemo(() => {
|
const ret = useMemo(() => {
|
||||||
const acc = accounts.find((x) => x.selected);
|
const acc = accounts.find((x) => x.selected);
|
||||||
return {
|
return {
|
||||||
apiUrl: acc.apiUrl,
|
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl,
|
||||||
authToken: acc.token,
|
authToken: acc?.token,
|
||||||
selectedAccount: acc,
|
selectedAccount: acc,
|
||||||
accounts:
|
accounts: accounts.map((account) => ({
|
||||||
accounts?.map((account) => ({
|
...account,
|
||||||
...account,
|
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
remove: () => removeAccounts((x) => x.id === account.id),
|
||||||
remove: () => removeAccounts((x) => x.id === account.id),
|
})),
|
||||||
})) ?? [],
|
|
||||||
};
|
};
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
|
||||||
@ -62,10 +62,10 @@ export const AccountProvider = ({
|
|||||||
path: ["auth", "me"],
|
path: ["auth", "me"],
|
||||||
parser: UserP,
|
parser: UserP,
|
||||||
placeholderData: ret.selectedAccount,
|
placeholderData: ret.selectedAccount,
|
||||||
enabled: ret.selectedAccount,
|
enabled: !!ret.selectedAccount,
|
||||||
options: {
|
options: {
|
||||||
apiUrl: ret.apiUrl,
|
apiUrl: ret.apiUrl,
|
||||||
authToken: ret.authToken,
|
authToken: ret.authToken?.access_token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Use a ref here because we don't want the effect to trigger when the selected
|
// Use a ref here because we don't want the effect to trigger when the selected
|
||||||
|
58
front/src/providers/account-store.ts
Normal file
58
front/src/providers/account-store.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { type Account, AccountP } from "~/models";
|
||||||
|
import { readValue, setCookie, storeValue } from "./settings";
|
||||||
|
|
||||||
|
const writeAccounts = (accounts: Account[]) => {
|
||||||
|
storeValue("accounts", accounts);
|
||||||
|
if (Platform.OS === "web") {
|
||||||
|
const selected = accounts.find((x) => x.selected);
|
||||||
|
if (!selected) return;
|
||||||
|
setCookie("account", selected);
|
||||||
|
// cookie used for images and videos since we can't add Authorization headers in img or video tags.
|
||||||
|
setCookie("X-Bearer", selected?.token.access_token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addAccount = (account: Account) => {
|
||||||
|
const accounts = readValue("accounts", z.array(AccountP)) ?? [];
|
||||||
|
|
||||||
|
// Prevent the user from adding the same account twice.
|
||||||
|
if (accounts.find((x) => x.id === account.id)) {
|
||||||
|
updateAccount(account.id, account);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const acc of accounts) acc.selected = false;
|
||||||
|
account.selected = true;
|
||||||
|
accounts.push(account);
|
||||||
|
writeAccounts(accounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
||||||
|
let accounts = readValue("accounts", z.array(AccountP)) ?? [];
|
||||||
|
accounts = accounts.filter((x) => !filter(x));
|
||||||
|
if (!accounts.find((x) => x.selected) && accounts.length > 0) {
|
||||||
|
accounts[0].selected = true;
|
||||||
|
}
|
||||||
|
writeAccounts(accounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAccount = (id: string, account: Account) => {
|
||||||
|
const accounts = readValue("accounts", z.array(AccountP)) ?? [];
|
||||||
|
const idx = accounts.findIndex((x) => x.id === id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
const selected = account.selected;
|
||||||
|
if (selected) {
|
||||||
|
for (const acc of accounts) acc.selected = false;
|
||||||
|
// if account was already on the accounts list, we keep it selected.
|
||||||
|
account.selected = selected;
|
||||||
|
} else if (accounts[idx].selected) {
|
||||||
|
// we just unselected the current account, focus another one.
|
||||||
|
if (accounts.length > 0) accounts[0].selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[idx] = account;
|
||||||
|
writeAccounts(accounts);
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { type ReactNode, useState } from "react";
|
|||||||
import { ThemeSelector } from "~/primitives/theme";
|
import { ThemeSelector } from "~/primitives/theme";
|
||||||
import { createQueryClient } from "~/query";
|
import { createQueryClient } from "~/query";
|
||||||
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
||||||
|
import { AccountProvider } from "./account-provider";
|
||||||
|
|
||||||
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
@ -26,7 +27,9 @@ export const Providers = ({ children }: { children: ReactNode }) => {
|
|||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
<AccountProvider>
|
||||||
|
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
||||||
|
</AccountProvider>
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MMKV, useMMKVString } from "react-native-mmkv";
|
import { MMKV, useMMKVString } from "react-native-mmkv";
|
||||||
import type { ZodTypeAny } from "zod";
|
import type { ZodTypeAny, z } from "zod";
|
||||||
|
|
||||||
export const storage = new MMKV();
|
const storage = new MMKV();
|
||||||
|
|
||||||
function toBase64(utf8: string) {
|
function toBase64(utf8: string) {
|
||||||
if (typeof window !== "undefined") return window.btoa(utf8);
|
if (typeof window !== "undefined") return window.btoa(utf8);
|
||||||
@ -35,11 +35,21 @@ export const readCookie = <T extends ZodTypeAny>(
|
|||||||
const ret = ca.find((x) => x.trimStart().startsWith(name));
|
const ret = ca.find((x) => x.trimStart().startsWith(name));
|
||||||
if (ret === undefined) return undefined;
|
if (ret === undefined) return undefined;
|
||||||
const str = fromBase64(ret.substring(name.length));
|
const str = fromBase64(ret.substring(name.length));
|
||||||
return parser ? parser.parse(JSON.parse(str)) : str;
|
return parser ? (parser.parse(JSON.parse(str)) as z.infer<T>) : str;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useStoreValue = <T extends ZodTypeAny>(key: string, parser?: T) => {
|
export const useStoreValue = <T extends ZodTypeAny>(key: string, parser: T) => {
|
||||||
const [val] = useMMKVString(key);
|
const [val] = useMMKVString(key);
|
||||||
if (!val) return val;
|
if (val === undefined) return val;
|
||||||
return parser ? parser.parse(JSON.parse(val)) : val;
|
return parser.parse(JSON.parse(val)) as z.infer<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const storeValue = (key: string, value: unknown) => {
|
||||||
|
storage.set(key, JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readValue = <T extends ZodTypeAny>(key: string, parser: T) => {
|
||||||
|
const val = storage.getString(key);
|
||||||
|
if (val === undefined) return val;
|
||||||
|
return parser.parse(JSON.parse(val)) as z.infer<T>;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user