mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-30 19:54:16 -04:00
Rework account internals storage
This commit is contained in:
parent
25b5f0036d
commit
d38a46de22
@ -17,71 +17,3 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* 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 { 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 { removeAccounts, updateAccount } from "./account-store";
|
||||
import { useSetError } from "./error-provider";
|
||||
import { useStoreValue } from "./settings";
|
||||
|
||||
const AccountContext = createContext<{
|
||||
apiUrl: string;
|
||||
@ -31,22 +34,19 @@ export const AccountProvider = ({
|
||||
}
|
||||
|
||||
const setError = useSetError();
|
||||
|
||||
const [accStr] = useMMKVString("accounts");
|
||||
const accounts = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
||||
const accounts = useStoreValue("accounts", z.array(AccountP)) ?? [];
|
||||
|
||||
const ret = useMemo(() => {
|
||||
const acc = accounts.find((x) => x.selected);
|
||||
return {
|
||||
apiUrl: acc.apiUrl,
|
||||
authToken: acc.token,
|
||||
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl,
|
||||
authToken: acc?.token,
|
||||
selectedAccount: acc,
|
||||
accounts:
|
||||
accounts?.map((account) => ({
|
||||
...account,
|
||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||
remove: () => removeAccounts((x) => x.id === account.id),
|
||||
})) ?? [],
|
||||
accounts: accounts.map((account) => ({
|
||||
...account,
|
||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||
remove: () => removeAccounts((x) => x.id === account.id),
|
||||
})),
|
||||
};
|
||||
}, [accounts]);
|
||||
|
||||
@ -62,10 +62,10 @@ export const AccountProvider = ({
|
||||
path: ["auth", "me"],
|
||||
parser: UserP,
|
||||
placeholderData: ret.selectedAccount,
|
||||
enabled: ret.selectedAccount,
|
||||
enabled: !!ret.selectedAccount,
|
||||
options: {
|
||||
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
|
||||
|
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 { createQueryClient } from "~/query";
|
||||
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
||||
import { AccountProvider } from "./account-provider";
|
||||
|
||||
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
@ -26,7 +27,9 @@ export const Providers = ({ children }: { children: ReactNode }) => {
|
||||
<QueryProvider>
|
||||
<ThemeProvider>
|
||||
<ErrorProvider>
|
||||
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
||||
<AccountProvider>
|
||||
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
||||
</AccountProvider>
|
||||
</ErrorProvider>
|
||||
</ThemeProvider>
|
||||
</QueryProvider>
|
||||
|
@ -1,7 +1,7 @@
|
||||
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) {
|
||||
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));
|
||||
if (ret === undefined) return undefined;
|
||||
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);
|
||||
if (!val) return val;
|
||||
return parser ? parser.parse(JSON.parse(val)) : val;
|
||||
if (val === undefined) return 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