mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 12:23:24 +00:00
feat: api hooks n stuff
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import "expo-router/entry";
|
||||
import "react-native-gesture-handler";
|
||||
import "@react-native-anywhere/polyfill-base64";
|
||||
import "text-encoding-polyfill";
|
||||
|
@@ -75,6 +75,7 @@
|
||||
"react-native-web": "^0.19.10",
|
||||
"subsrt-ts": "^2.1.2",
|
||||
"tamagui": "^1.94.0",
|
||||
"text-encoding-polyfill": "^0.6.7",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -1,12 +1,53 @@
|
||||
import { Link } from "expo-router";
|
||||
import { H2, H5, Paragraph, View } from "tamagui";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { H2, H5, Paragraph, useTheme, View } from "tamagui";
|
||||
|
||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||
import { MWButton } from "~/components/ui/Button";
|
||||
import { MWCard } from "~/components/ui/Card";
|
||||
import { MWInput } from "~/components/ui/Input";
|
||||
import { useAuth } from "~/hooks/useAuth";
|
||||
import { useAuthStore } from "~/stores/settings";
|
||||
|
||||
function TestButtons() {
|
||||
const theme = useTheme();
|
||||
const { login } = useAuth();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<MWButton
|
||||
type="secondary"
|
||||
backgroundColor="$sheetItemBackground"
|
||||
marginBottom="$4"
|
||||
icon={
|
||||
<MaterialCommunityIcons
|
||||
name="login"
|
||||
size={24}
|
||||
color={theme.buttonSecondaryText.val}
|
||||
/>
|
||||
}
|
||||
onPress={async () => {
|
||||
const passhphrase = "";
|
||||
if (!passhphrase) {
|
||||
alert("Please configure your passphrase");
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await login({
|
||||
mnemonic: passhphrase,
|
||||
userData: {
|
||||
device: "phone",
|
||||
},
|
||||
});
|
||||
console.log(account);
|
||||
}}
|
||||
>
|
||||
test login
|
||||
</MWButton>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MovieWebScreen() {
|
||||
const { backendUrl, setBackendUrl } = useAuthStore();
|
||||
|
||||
@@ -18,6 +59,7 @@ export default function MovieWebScreen() {
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TestButtons />
|
||||
<MWCard bordered>
|
||||
<MWCard.Header padded>
|
||||
<H2 fontWeight="$bold" paddingBottom="$1">
|
||||
|
204
apps/expo/src/hooks/useAuth.ts
Normal file
204
apps/expo/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import type {
|
||||
AccountWithToken,
|
||||
BookmarkMediaItem,
|
||||
ProgressMediaItem,
|
||||
SessionResponse,
|
||||
UserResponse,
|
||||
} from "@movie-web/api";
|
||||
import {
|
||||
bookmarkMediaToInput,
|
||||
bytesToBase64,
|
||||
bytesToBase64Url,
|
||||
encryptData,
|
||||
getBookmarks,
|
||||
getLoginChallengeToken,
|
||||
getProgress,
|
||||
getRegisterChallengeToken,
|
||||
getSettings,
|
||||
getUser,
|
||||
importBookmarks,
|
||||
importProgress,
|
||||
keysFromMnemonic,
|
||||
loginAccount,
|
||||
progressMediaItemToInputs,
|
||||
registerAccount,
|
||||
removeSession,
|
||||
signChallenge,
|
||||
} from "@movie-web/api";
|
||||
|
||||
import { useAuthStore } from "~/stores/settings";
|
||||
import { useAuthData } from "./useAuthData";
|
||||
|
||||
export interface RegistrationData {
|
||||
recaptchaToken?: string;
|
||||
mnemonic: string;
|
||||
userData: {
|
||||
device: string;
|
||||
profile: {
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoginData {
|
||||
mnemonic: string;
|
||||
userData: {
|
||||
device: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const currentAccount = useAuthStore((s) => s.account);
|
||||
const profile = useAuthStore((s) => s.account?.profile);
|
||||
const loggedIn = !!useAuthStore((s) => s.account);
|
||||
const backendUrl = useAuthStore((s) => s.backendUrl);
|
||||
const {
|
||||
logout: userDataLogout,
|
||||
login: userDataLogin,
|
||||
syncData,
|
||||
} = useAuthData();
|
||||
|
||||
const login = useCallback(
|
||||
async (loginData: LoginData) => {
|
||||
if (!backendUrl) return;
|
||||
const keys = await keysFromMnemonic(loginData.mnemonic);
|
||||
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
|
||||
const { challenge } = await getLoginChallengeToken(
|
||||
backendUrl,
|
||||
publicKeyBase64Url,
|
||||
);
|
||||
const signature = await signChallenge(keys, challenge);
|
||||
const loginResult = await loginAccount(backendUrl, {
|
||||
challenge: {
|
||||
code: challenge,
|
||||
signature,
|
||||
},
|
||||
publicKey: publicKeyBase64Url,
|
||||
device: await encryptData(loginData.userData.device, keys.seed),
|
||||
});
|
||||
|
||||
const user = await getUser(backendUrl, loginResult.token);
|
||||
const seedBase64 = bytesToBase64(keys.seed);
|
||||
return userDataLogin(loginResult, user.user, user.session, seedBase64);
|
||||
},
|
||||
[userDataLogin, backendUrl],
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
if (!currentAccount || !backendUrl) return;
|
||||
try {
|
||||
await removeSession(
|
||||
backendUrl,
|
||||
currentAccount.token,
|
||||
currentAccount.sessionId,
|
||||
);
|
||||
} catch {
|
||||
// we dont care about failing to delete session
|
||||
}
|
||||
userDataLogout();
|
||||
}, [userDataLogout, backendUrl, currentAccount]);
|
||||
|
||||
const register = useCallback(
|
||||
async (registerData: RegistrationData) => {
|
||||
if (!backendUrl) return;
|
||||
const { challenge } = await getRegisterChallengeToken(
|
||||
backendUrl,
|
||||
registerData.recaptchaToken,
|
||||
);
|
||||
const keys = await keysFromMnemonic(registerData.mnemonic);
|
||||
const signature = await signChallenge(keys, challenge);
|
||||
const registerResult = await registerAccount(backendUrl, {
|
||||
challenge: {
|
||||
code: challenge,
|
||||
signature,
|
||||
},
|
||||
publicKey: bytesToBase64Url(keys.publicKey),
|
||||
device: await encryptData(registerData.userData.device, keys.seed),
|
||||
profile: registerData.userData.profile,
|
||||
});
|
||||
|
||||
return userDataLogin(
|
||||
registerResult,
|
||||
registerResult.user,
|
||||
registerResult.session,
|
||||
bytesToBase64(keys.seed),
|
||||
);
|
||||
},
|
||||
[backendUrl, userDataLogin],
|
||||
);
|
||||
|
||||
const importData = useCallback(
|
||||
async (
|
||||
account: AccountWithToken,
|
||||
progressItems: Record<string, ProgressMediaItem>,
|
||||
bookmarks: Record<string, BookmarkMediaItem>,
|
||||
) => {
|
||||
if (!backendUrl) return;
|
||||
if (
|
||||
Object.keys(progressItems).length === 0 &&
|
||||
Object.keys(bookmarks).length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progressInputs = Object.entries(progressItems).flatMap(
|
||||
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
|
||||
);
|
||||
|
||||
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
|
||||
bookmarkMediaToInput(tmdbId, item),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
importProgress(backendUrl, account, progressInputs),
|
||||
importBookmarks(backendUrl, account, bookmarkInputs),
|
||||
]);
|
||||
},
|
||||
[backendUrl],
|
||||
);
|
||||
|
||||
const restore = useCallback(
|
||||
async (account: AccountWithToken) => {
|
||||
if (!backendUrl) return;
|
||||
let user: { user: UserResponse; session: SessionResponse };
|
||||
try {
|
||||
user = await getUser(backendUrl, account.token);
|
||||
} catch (err) {
|
||||
const anyError = err as { response?: { status: number } };
|
||||
if (
|
||||
anyError?.response?.status === 401 ||
|
||||
anyError?.response?.status === 403 ||
|
||||
anyError?.response?.status === 400
|
||||
) {
|
||||
await logout();
|
||||
return;
|
||||
}
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const [bookmarks, progress, settings] = await Promise.all([
|
||||
getBookmarks(backendUrl, account),
|
||||
getProgress(backendUrl, account),
|
||||
getSettings(backendUrl, account),
|
||||
]);
|
||||
|
||||
syncData(user.user, user.session, progress, bookmarks, settings);
|
||||
},
|
||||
[backendUrl, syncData, logout],
|
||||
);
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
profile,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
restore,
|
||||
importData,
|
||||
};
|
||||
}
|
170
apps/expo/src/hooks/useAuthData.ts
Normal file
170
apps/expo/src/hooks/useAuthData.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import type {
|
||||
BookmarkResponse,
|
||||
LoginResponse,
|
||||
ProgressResponse,
|
||||
SessionResponse,
|
||||
SettingsResponse,
|
||||
UserResponse,
|
||||
} from "@movie-web/api";
|
||||
import type { ScrapeMedia } from "@movie-web/provider-utils";
|
||||
|
||||
import type { ItemData } from "~/components/item/item";
|
||||
import type { WatchHistoryItem } from "~/stores/settings";
|
||||
import type { ThemeStoreOption } from "~/stores/theme";
|
||||
import {
|
||||
useAuthStore,
|
||||
useBookmarkStore,
|
||||
useWatchHistoryStore,
|
||||
} from "~/stores/settings";
|
||||
import { useThemeStore } from "~/stores/theme";
|
||||
|
||||
export function useAuthData() {
|
||||
const loggedIn = !!useAuthStore((s) => s.account);
|
||||
const setAccount = useAuthStore((s) => s.setAccount);
|
||||
const removeAccount = useAuthStore((s) => s.removeAccount);
|
||||
const setBookmarks = useBookmarkStore((s) => s.setBookmarks);
|
||||
const setProxySet = useAuthStore((s) => s.setProxySet);
|
||||
const setWatchHistory = useWatchHistoryStore((s) => s.setWatchHistory);
|
||||
const clearBookmarks = useCallback(() => setBookmarks([]), [setBookmarks]);
|
||||
const clearProgress = useCallback(
|
||||
() => setWatchHistory([]),
|
||||
[setWatchHistory],
|
||||
);
|
||||
const replaceBookmarks = useCallback(
|
||||
(bookmarks: ItemData[]) => setBookmarks(bookmarks),
|
||||
[setBookmarks],
|
||||
);
|
||||
const replaceItems = useCallback(
|
||||
(items: WatchHistoryItem[]) => setWatchHistory(items),
|
||||
[setWatchHistory],
|
||||
);
|
||||
const setTheme = useThemeStore((s) => s.setTheme);
|
||||
|
||||
// const setAppLanguage = useLanguageStore((s) => s.setLanguage);
|
||||
// const importSubtitleLanguage = useSubtitleStore(
|
||||
// (s) => s.importSubtitleLanguage,
|
||||
// );
|
||||
|
||||
const login = useCallback(
|
||||
(
|
||||
loginResponse: LoginResponse,
|
||||
user: UserResponse,
|
||||
session: SessionResponse,
|
||||
seed: string,
|
||||
) => {
|
||||
const account = {
|
||||
token: loginResponse.token,
|
||||
userId: user.id,
|
||||
sessionId: loginResponse.session.id,
|
||||
deviceName: session.device,
|
||||
profile: user.profile,
|
||||
seed,
|
||||
};
|
||||
setAccount(account);
|
||||
return account;
|
||||
},
|
||||
[setAccount],
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
removeAccount();
|
||||
clearBookmarks();
|
||||
clearProgress();
|
||||
}, [removeAccount, clearBookmarks, clearProgress]);
|
||||
|
||||
const syncData = useCallback(
|
||||
(
|
||||
_user: UserResponse,
|
||||
_session: SessionResponse,
|
||||
progress: ProgressResponse[],
|
||||
bookmarks: BookmarkResponse[],
|
||||
settings: SettingsResponse,
|
||||
) => {
|
||||
const bookmarkResponseToItemData = (
|
||||
bookmarks: BookmarkResponse[],
|
||||
): ItemData[] => {
|
||||
return bookmarks.map((bookmark) => ({
|
||||
id: bookmark.tmdbId,
|
||||
title: bookmark.meta.title,
|
||||
type: bookmark.meta.type === "show" ? "tv" : "movie",
|
||||
year: bookmark.meta.year,
|
||||
posterUrl: bookmark.meta.poster ?? "",
|
||||
}));
|
||||
};
|
||||
|
||||
const progressResponseToWatchHistoryItem = (
|
||||
progress: ProgressResponse[],
|
||||
): WatchHistoryItem[] => {
|
||||
return progress.map((entry) => {
|
||||
const isShow = entry.meta.type === "show";
|
||||
const commonMedia = {
|
||||
title: entry.meta.title,
|
||||
releaseYear: entry.meta.year,
|
||||
tmdbId: entry.tmdbId,
|
||||
};
|
||||
|
||||
const media: ScrapeMedia = isShow
|
||||
? {
|
||||
...commonMedia,
|
||||
type: "show",
|
||||
season: {
|
||||
number: entry.season.number ?? 0,
|
||||
tmdbId: entry.season.id ?? "",
|
||||
},
|
||||
episode: {
|
||||
number: entry.episode.number ?? 0,
|
||||
tmdbId: entry.episode.id ?? "",
|
||||
},
|
||||
}
|
||||
: {
|
||||
...commonMedia,
|
||||
type: "movie",
|
||||
};
|
||||
|
||||
return {
|
||||
item: {
|
||||
id: entry.tmdbId,
|
||||
title: entry.meta.title,
|
||||
type: entry.meta.type === "show" ? "tv" : "movie",
|
||||
season: entry.season.number,
|
||||
episode: entry.episode.number,
|
||||
year: entry.meta.year,
|
||||
posterUrl: entry.meta.poster ?? "",
|
||||
},
|
||||
media: media,
|
||||
positionMillis: parseInt(entry.watched, 10),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
replaceBookmarks(bookmarkResponseToItemData(bookmarks));
|
||||
replaceItems(progressResponseToWatchHistoryItem(progress));
|
||||
|
||||
// if (settings.applicationLanguage) {
|
||||
// setAppLanguage(settings.applicationLanguage);
|
||||
// }
|
||||
|
||||
// if (settings.defaultSubtitleLanguage) {
|
||||
// importSubtitleLanguage(settings.defaultSubtitleLanguage);
|
||||
// }
|
||||
|
||||
if (settings.applicationTheme) {
|
||||
setTheme(settings.applicationTheme as unknown as ThemeStoreOption);
|
||||
}
|
||||
|
||||
if (settings.proxyUrls) {
|
||||
setProxySet(settings.proxyUrls);
|
||||
}
|
||||
},
|
||||
[replaceBookmarks, replaceItems, setTheme, setProxySet],
|
||||
);
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
login,
|
||||
logout,
|
||||
syncData,
|
||||
};
|
||||
}
|
@@ -144,7 +144,7 @@ export const useBookmarkStore = create<
|
||||
),
|
||||
);
|
||||
|
||||
interface WatchHistoryItem {
|
||||
export interface WatchHistoryItem {
|
||||
item: ItemData;
|
||||
media: ScrapeMedia;
|
||||
positionMillis: number;
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -197,6 +197,9 @@ importers:
|
||||
tamagui:
|
||||
specifier: ^1.94.0
|
||||
version: 1.94.0(@types/react@18.2.52)(immer@10.0.3)(react-dom@18.2.0)(react-native-web@0.19.10)(react-native@0.73.6)(react@18.2.0)
|
||||
text-encoding-polyfill:
|
||||
specifier: ^0.6.7
|
||||
version: 0.6.7
|
||||
zustand:
|
||||
specifier: ^4.4.7
|
||||
version: 4.4.7(@types/react@18.2.52)(immer@10.0.3)(react@18.2.0)
|
||||
@@ -13083,6 +13086,10 @@ packages:
|
||||
source-map-support: 0.5.21
|
||||
dev: false
|
||||
|
||||
/text-encoding-polyfill@0.6.7:
|
||||
resolution: {integrity: sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg==}
|
||||
dev: false
|
||||
|
||||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
|
Reference in New Issue
Block a user