mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 11:13:25 +00:00
Compare commits
5 Commits
a000848412
...
a981beae11
Author | SHA1 | Date | |
---|---|---|---|
|
a981beae11 | ||
|
b12562d249 | ||
|
100435af3c | ||
|
7cce25a261 | ||
|
a89ef8a901 |
@@ -1,60 +1,10 @@
|
|||||||
import { Link } from "expo-router";
|
import { AccountInformation } from "~/components/account/AccountInformation";
|
||||||
import { H3, H5, Paragraph, View } from "tamagui";
|
import { AccountGetStarted } from "~/components/account/GetStarted";
|
||||||
|
|
||||||
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 { useAuthStore } from "~/stores/settings";
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
|
||||||
export default function MovieWebScreen() {
|
export default function MovieWebScreen() {
|
||||||
const { backendUrl, setBackendUrl } = useAuthStore();
|
const account = useAuthStore((state) => state.account);
|
||||||
|
|
||||||
return (
|
if (account) return <AccountInformation />;
|
||||||
<ScreenLayout
|
return <AccountGetStarted />;
|
||||||
contentContainerStyle={{
|
|
||||||
flexGrow: 1,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MWCard bordered padded>
|
|
||||||
<MWCard.Header>
|
|
||||||
<H3 fontWeight="$bold" paddingBottom="$1">
|
|
||||||
Sync to the cloud
|
|
||||||
</H3>
|
|
||||||
<H5 color="$shade200" fontWeight="$semibold" paddingVertical="$3">
|
|
||||||
Share your watch progress between devices and keep them synced.
|
|
||||||
</H5>
|
|
||||||
<Paragraph color="$shade200">
|
|
||||||
First choose the backend you want to use. If you do not know what
|
|
||||||
this does, use the default and click on 'Get started'.
|
|
||||||
</Paragraph>
|
|
||||||
</MWCard.Header>
|
|
||||||
|
|
||||||
<View padding="$4">
|
|
||||||
<MWInput
|
|
||||||
placeholder={backendUrl}
|
|
||||||
type="authentication"
|
|
||||||
value={backendUrl}
|
|
||||||
onChangeText={setBackendUrl}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<MWCard.Footer padded justifyContent="center">
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: "/sync/trust/[url]",
|
|
||||||
params: { url: backendUrl },
|
|
||||||
}}
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<MWButton type="purple" width="100%">
|
|
||||||
Get started
|
|
||||||
</MWButton>
|
|
||||||
</Link>
|
|
||||||
</MWCard.Footer>
|
|
||||||
</MWCard>
|
|
||||||
</ScreenLayout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,40 @@
|
|||||||
import { Stack } from "expo-router";
|
import { useState } from "react";
|
||||||
|
import { Link, Stack, useRouter } from "expo-router";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { H4, Label, Paragraph, Text, YStack } from "tamagui";
|
import { H4, Label, Paragraph, Text, YStack } from "tamagui";
|
||||||
|
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { MWButton } from "~/components/ui/Button";
|
import { MWButton } from "~/components/ui/Button";
|
||||||
import { MWCard } from "~/components/ui/Card";
|
import { MWCard } from "~/components/ui/Card";
|
||||||
import { MWInput } from "~/components/ui/Input";
|
import { MWInput } from "~/components/ui/Input";
|
||||||
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const backendUrl = useAuthStore((state) => state.backendUrl);
|
||||||
|
const router = useRouter();
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const [passphrase, setPassphrase] = useState("");
|
||||||
|
const [deviceName, setDeviceName] = useState("");
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: ["login", backendUrl, passphrase, deviceName],
|
||||||
|
mutationFn: () =>
|
||||||
|
login({
|
||||||
|
mnemonic: passphrase,
|
||||||
|
userData: {
|
||||||
|
device: deviceName,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
return router.push("/(tabs)/movie-web");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
@@ -46,6 +74,8 @@ export default function Page() {
|
|||||||
placeholder="Passphrase"
|
placeholder="Passphrase"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
value={passphrase}
|
||||||
|
onChangeText={setPassphrase}
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$1">
|
<YStack gap="$1">
|
||||||
@@ -54,6 +84,8 @@ export default function Page() {
|
|||||||
type="authentication"
|
type="authentication"
|
||||||
placeholder="Personal phone"
|
placeholder="Personal phone"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
value={deviceName}
|
||||||
|
onChangeText={setDeviceName}
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -64,13 +96,22 @@ export default function Page() {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap="$4"
|
gap="$4"
|
||||||
>
|
>
|
||||||
<MWButton type="purple">Login</MWButton>
|
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||||
|
Login
|
||||||
|
</MWButton>
|
||||||
|
{mutation.isError && (
|
||||||
|
<Text color="$rose200" textAlign="center">
|
||||||
|
{mutation.error.message}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
||||||
Don't have an account yet?{"\n"}
|
Don't have an account yet?{"\n"}
|
||||||
|
<Link href={{ pathname: "/sync/register" }} asChild>
|
||||||
<Text color="$purple100" fontWeight="$bold">
|
<Text color="$purple100" fontWeight="$bold">
|
||||||
Create an account.
|
Create an account.
|
||||||
</Text>
|
</Text>
|
||||||
|
</Link>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</MWCard.Footer>
|
</MWCard.Footer>
|
||||||
</MWCard>
|
</MWCard>
|
||||||
|
@@ -1,115 +1,43 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, Stack } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
import { H4, Label, Paragraph, View, YStack } from "tamagui";
|
||||||
import { Circle, H4, Label, Paragraph, View, XStack, YStack } from "tamagui";
|
|
||||||
import { LinearGradient } from "tamagui/linear-gradient";
|
|
||||||
|
|
||||||
|
import { Avatar } from "~/components/account/Avatar";
|
||||||
|
import { ColorPicker, colors } from "~/components/account/ColorPicker";
|
||||||
|
import {
|
||||||
|
expoIcons,
|
||||||
|
expoIconsToDbIcons,
|
||||||
|
UserIconPicker,
|
||||||
|
} from "~/components/account/UserIconPicker";
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { MWButton } from "~/components/ui/Button";
|
import { MWButton } from "~/components/ui/Button";
|
||||||
import { MWCard } from "~/components/ui/Card";
|
import { MWCard } from "~/components/ui/Card";
|
||||||
import { MWInput } from "~/components/ui/Input";
|
import { MWInput } from "~/components/ui/Input";
|
||||||
|
|
||||||
const colors = ["#0A54FF", "#CF2E68", "#F9DD7F", "#7652DD", "#2ECFA8"] as const;
|
|
||||||
|
|
||||||
function ColorPicker(props: {
|
|
||||||
value: (typeof colors)[number];
|
|
||||||
onInput: (v: (typeof colors)[number]) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<XStack gap="$2">
|
|
||||||
{colors.map((color) => {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
onPress={() => props.onInput(color)}
|
|
||||||
flexGrow={1}
|
|
||||||
height="$4"
|
|
||||||
borderRadius="$4"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
backgroundColor={color}
|
|
||||||
key={color}
|
|
||||||
>
|
|
||||||
{props.value === color ? (
|
|
||||||
<Ionicons name="checkmark-circle" size={24} color="white" />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</XStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const icons = [
|
|
||||||
"user-group",
|
|
||||||
"couch",
|
|
||||||
"mobile-screen",
|
|
||||||
"ticket",
|
|
||||||
"handcuffs",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function UserIconPicker(props: {
|
|
||||||
value: (typeof icons)[number];
|
|
||||||
onInput: (v: (typeof icons)[number]) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<XStack gap="$2">
|
|
||||||
{icons.map((icon) => {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
flexGrow={1}
|
|
||||||
height="$4"
|
|
||||||
borderRadius="$4"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
backgroundColor={props.value === icon ? "$purple400" : "$shade400"}
|
|
||||||
borderColor={props.value === icon ? "$purple200" : "$shade400"}
|
|
||||||
borderWidth={1}
|
|
||||||
key={icon}
|
|
||||||
onPress={() => props.onInput(icon)}
|
|
||||||
>
|
|
||||||
<FontAwesome6 name={icon} size={24} color="white" />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</XStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AvatarProps {
|
|
||||||
colorA: string;
|
|
||||||
colorB: string;
|
|
||||||
icon: (typeof icons)[number];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Avatar(props: AvatarProps) {
|
|
||||||
return (
|
|
||||||
<Circle
|
|
||||||
backgroundColor={props.colorA}
|
|
||||||
height="$6"
|
|
||||||
width="$6"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[props.colorA, props.colorB]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
borderRadius="$12"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<FontAwesome6 name={props.icon} size={24} color="white" />
|
|
||||||
</LinearGradient>
|
|
||||||
</Circle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [color, setColor] = useState<(typeof colors)[number]>(colors[0]);
|
const router = useRouter();
|
||||||
const [color2, setColor2] = useState<(typeof colors)[number]>(colors[0]);
|
|
||||||
const [icon, setIcon] = useState<(typeof icons)[number]>(icons[0]);
|
const [deviceName, setDeviceName] = useState("");
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [colorA, setColorA] = useState<(typeof colors)[number]>(colors[0]);
|
||||||
|
const [colorB, setColorB] = useState<(typeof colors)[number]>(colors[0]);
|
||||||
|
const [icon, setIcon] = useState<(typeof expoIcons)[number]>(expoIcons[0]);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (!deviceName) {
|
||||||
|
setErrorMessage("Please enter a device name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return router.push({
|
||||||
|
pathname: "/sync/register/confirm",
|
||||||
|
params: {
|
||||||
|
deviceName,
|
||||||
|
colorA,
|
||||||
|
colorB,
|
||||||
|
icon: expoIconsToDbIcons[icon],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
@@ -129,7 +57,7 @@ export default function Page() {
|
|||||||
<MWCard bordered padded>
|
<MWCard bordered padded>
|
||||||
<MWCard.Header>
|
<MWCard.Header>
|
||||||
<View alignItems="center" marginBottom="$3">
|
<View alignItems="center" marginBottom="$3">
|
||||||
<Avatar colorA={color} colorB={color2} icon={icon} />
|
<Avatar colorA={colorA} colorB={colorB} icon={icon} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<H4 fontWeight="$bold" textAlign="center">
|
<H4 fontWeight="$bold" textAlign="center">
|
||||||
@@ -152,18 +80,19 @@ export default function Page() {
|
|||||||
<Label fontWeight="$bold">Device name</Label>
|
<Label fontWeight="$bold">Device name</Label>
|
||||||
<MWInput
|
<MWInput
|
||||||
type="authentication"
|
type="authentication"
|
||||||
placeholder="Passphrase"
|
placeholder="Personal phone"
|
||||||
secureTextEntry
|
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
value={deviceName}
|
||||||
|
onChangeText={setDeviceName}
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$1">
|
<YStack gap="$1">
|
||||||
<Label fontWeight="$bold">Profile color one</Label>
|
<Label fontWeight="$bold">Profile color one</Label>
|
||||||
<ColorPicker value={color} onInput={(color) => setColor(color)} />
|
<ColorPicker value={colorA} onInput={(color) => setColorA(color)} />
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$1">
|
<YStack gap="$1">
|
||||||
<Label fontWeight="$bold">Profile color two</Label>
|
<Label fontWeight="$bold">Profile color two</Label>
|
||||||
<ColorPicker value={color2} onInput={(color) => setColor2(color)} />
|
<ColorPicker value={colorB} onInput={(color) => setColorB(color)} />
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack gap="$1">
|
<YStack gap="$1">
|
||||||
<Label fontWeight="$bold">User icon</Label>
|
<Label fontWeight="$bold">User icon</Label>
|
||||||
@@ -172,17 +101,14 @@ export default function Page() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
|
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
|
||||||
<Link
|
{errorMessage && (
|
||||||
href={{
|
<Paragraph color="$rose200" textAlign="center">
|
||||||
pathname: "/sync/register/confirm",
|
{errorMessage}
|
||||||
}}
|
</Paragraph>
|
||||||
replace
|
)}
|
||||||
asChild
|
<MWButton type="purple" width="100%" onPress={handleNext}>
|
||||||
>
|
|
||||||
<MWButton type="purple" width="100%">
|
|
||||||
Next
|
Next
|
||||||
</MWButton>
|
</MWButton>
|
||||||
</Link>
|
|
||||||
</MWCard.Footer>
|
</MWCard.Footer>
|
||||||
</MWCard>
|
</MWCard>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
|
@@ -1,12 +1,47 @@
|
|||||||
import { Link, Stack } from "expo-router";
|
import { useState } from "react";
|
||||||
|
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { H4, Label, Paragraph, YStack } from "tamagui";
|
import { H4, Label, Paragraph, YStack } from "tamagui";
|
||||||
|
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { MWButton } from "~/components/ui/Button";
|
import { MWButton } from "~/components/ui/Button";
|
||||||
import { MWCard } from "~/components/ui/Card";
|
import { MWCard } from "~/components/ui/Card";
|
||||||
import { MWInput } from "~/components/ui/Input";
|
import { MWInput } from "~/components/ui/Input";
|
||||||
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
// Requires type casting, typecheck fails for type-safe params
|
||||||
|
const { deviceName, colorA, colorB, icon } =
|
||||||
|
useLocalSearchParams() as unknown as {
|
||||||
|
deviceName: string;
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
const { register } = useAuth();
|
||||||
|
|
||||||
|
const [passphrase, setPassphrase] = useState("");
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: ["register", deviceName, colorA, colorB, icon],
|
||||||
|
mutationFn: () =>
|
||||||
|
register({
|
||||||
|
// TODO: "Add recaptchaToken",
|
||||||
|
mnemonic: passphrase,
|
||||||
|
userData: {
|
||||||
|
device: deviceName,
|
||||||
|
profile: { colorA, colorB, icon },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
return router.push("/(tabs)/movie-web");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
@@ -47,20 +82,22 @@ export default function Page() {
|
|||||||
placeholder="Passphrase"
|
placeholder="Passphrase"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
value={passphrase}
|
||||||
|
onChangeText={setPassphrase}
|
||||||
/>
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
|
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
|
||||||
<Link
|
{mutation.isError && (
|
||||||
href={{
|
<Paragraph color="$rose200" textAlign="center">
|
||||||
pathname: "/(tabs)/movie-web",
|
{mutation.error.message}
|
||||||
}}
|
</Paragraph>
|
||||||
replace
|
)}
|
||||||
asChild
|
|
||||||
>
|
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||||
<MWButton type="purple">Create account</MWButton>
|
Create account
|
||||||
</Link>
|
</MWButton>
|
||||||
</MWCard.Footer>
|
</MWCard.Footer>
|
||||||
</MWCard>
|
</MWCard>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
|
@@ -28,7 +28,7 @@ function PassphraseWord({ word }: { word: string }) {
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const words = genMnemonic().split(" ");
|
const words = genMnemonic();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
@@ -86,7 +86,7 @@ export default function Page() {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
await Clipboard.setStringAsync(words.join(""));
|
await Clipboard.setStringAsync(words);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Feather name="copy" size={18} color={theme.shade200.val} />
|
<Feather name="copy" size={18} color={theme.shade200.val} />
|
||||||
@@ -103,7 +103,7 @@ export default function Page() {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
padding="$3"
|
padding="$3"
|
||||||
>
|
>
|
||||||
{words.map((word, index) => (
|
{words.split(" ").map((word, index) => (
|
||||||
<PassphraseWord key={index} word={word} />
|
<PassphraseWord key={index} word={word} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -122,7 +122,13 @@ export default function Page() {
|
|||||||
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
||||||
Already have an account?{"\n"}
|
Already have an account?{"\n"}
|
||||||
<Text color="$purple100" fontWeight="$bold">
|
<Text color="$purple100" fontWeight="$bold">
|
||||||
<Link href="/sync/login">Login here</Link>
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: "/sync/login",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Login here
|
||||||
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</MWCard.Footer>
|
</MWCard.Footer>
|
||||||
|
@@ -7,13 +7,16 @@ import { getBackendMeta } from "@movie-web/api";
|
|||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { MWButton } from "~/components/ui/Button";
|
import { MWButton } from "~/components/ui/Button";
|
||||||
import { MWCard } from "~/components/ui/Card";
|
import { MWCard } from "~/components/ui/Card";
|
||||||
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { url } = useLocalSearchParams<{ url: string }>();
|
const { backendUrl } = useLocalSearchParams<{ backendUrl: string }>();
|
||||||
|
|
||||||
|
const setBackendUrl = useAuthStore((state) => state.setBackendUrl);
|
||||||
|
|
||||||
const meta = useQuery({
|
const meta = useQuery({
|
||||||
queryKey: ["backendMeta", url],
|
queryKey: ["backendMeta", backendUrl],
|
||||||
queryFn: () => getBackendMeta(url as unknown as string),
|
queryFn: () => getBackendMeta(backendUrl as unknown as string),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +55,7 @@ export default function Page() {
|
|||||||
color="white"
|
color="white"
|
||||||
textDecorationLine="underline"
|
textDecorationLine="underline"
|
||||||
>
|
>
|
||||||
{url}
|
{backendUrl}
|
||||||
</Text>
|
</Text>
|
||||||
. Please confirm you trust it before making an account.
|
. Please confirm you trust it before making an account.
|
||||||
</>
|
</>
|
||||||
@@ -95,8 +98,13 @@ export default function Page() {
|
|||||||
pathname: "/sync/register",
|
pathname: "/sync/register",
|
||||||
}}
|
}}
|
||||||
asChild
|
asChild
|
||||||
|
onPress={() => {
|
||||||
|
setBackendUrl(backendUrl as unknown as string);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MWButton type="purple">I trust this server</MWButton>
|
<MWButton type="purple" disabled={!meta.isSuccess}>
|
||||||
|
I trust this server
|
||||||
|
</MWButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
165
apps/expo/src/components/account/AccountInformation.tsx
Normal file
165
apps/expo/src/components/account/AccountInformation.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { H3, Spinner, Text, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
|
import {
|
||||||
|
base64ToBuffer,
|
||||||
|
decryptData,
|
||||||
|
getSessions,
|
||||||
|
removeSession,
|
||||||
|
} from "@movie-web/api";
|
||||||
|
|
||||||
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
import ScreenLayout from "../layout/ScreenLayout";
|
||||||
|
import { MWButton } from "../ui/Button";
|
||||||
|
import { MWCard } from "../ui/Card";
|
||||||
|
import { MWInput } from "../ui/Input";
|
||||||
|
import { MWSeparator } from "../ui/Separator";
|
||||||
|
import { Avatar } from "./Avatar";
|
||||||
|
import { DeleteAccountAlert } from "./DeleteAccountAlert";
|
||||||
|
import { getExpoIconFromDbIcon } from "./UserIconPicker";
|
||||||
|
|
||||||
|
export function AccountInformation() {
|
||||||
|
const account = useAuthStore((state) => state.account);
|
||||||
|
const backendUrl = useAuthStore((state) => state.backendUrl);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { decryptedName, logout } = useAuth();
|
||||||
|
|
||||||
|
const logoutMutation = useMutation({
|
||||||
|
mutationKey: ["logout"],
|
||||||
|
mutationFn: logout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeSessionMutation = useMutation({
|
||||||
|
mutationKey: ["removeSession"],
|
||||||
|
mutationFn: (sessionId: string) =>
|
||||||
|
removeSession(backendUrl, account!.token, sessionId),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["sessions", backendUrl, account],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessions = useQuery({
|
||||||
|
queryKey: ["sessions", backendUrl, account],
|
||||||
|
queryFn: () => getSessions(backendUrl, account!),
|
||||||
|
enabled: !!account,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceListSorted = useMemo(() => {
|
||||||
|
let list =
|
||||||
|
sessions.data?.map((session) => {
|
||||||
|
const decryptedName = decryptData(
|
||||||
|
session.device,
|
||||||
|
base64ToBuffer(account!.seed),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
current: session.id === account!.sessionId,
|
||||||
|
id: session.id,
|
||||||
|
name: decryptedName,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
list = list.sort((a, b) => {
|
||||||
|
if (a.current) return -1;
|
||||||
|
if (b.current) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}, [sessions.data, account]);
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenLayout>
|
||||||
|
<YStack gap="$6">
|
||||||
|
<YStack gap="$4">
|
||||||
|
<Text fontSize="$7" fontWeight="$bold">
|
||||||
|
Account
|
||||||
|
</Text>
|
||||||
|
<MWSeparator />
|
||||||
|
|
||||||
|
<MWCard bordered padded>
|
||||||
|
<XStack gap="$4" alignItems="center">
|
||||||
|
<Avatar
|
||||||
|
{...account.profile}
|
||||||
|
icon={getExpoIconFromDbIcon(account.profile.icon)}
|
||||||
|
width="$7"
|
||||||
|
height="$7"
|
||||||
|
/>
|
||||||
|
<YStack gap="$4">
|
||||||
|
<Text fontWeight="$bold">Device name</Text>
|
||||||
|
<MWInput
|
||||||
|
type="authentication"
|
||||||
|
value={decryptedName}
|
||||||
|
minWidth={"80%"}
|
||||||
|
maxWidth={"80%"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MWButton
|
||||||
|
type="danger"
|
||||||
|
width={"50%"}
|
||||||
|
onPress={() => logoutMutation.mutate()}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</MWButton>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
</MWCard>
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
<YStack gap="$4">
|
||||||
|
<Text fontSize="$7" fontWeight="$bold">
|
||||||
|
Devices
|
||||||
|
</Text>
|
||||||
|
<MWSeparator />
|
||||||
|
{sessions.isLoading && <Spinner />}
|
||||||
|
{sessions.isError && (
|
||||||
|
<Text fontWeight="$bold" color="$rose200">
|
||||||
|
Error loading sessions
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{deviceListSorted.map((device) => (
|
||||||
|
<MWCard bordered padded key={device.id}>
|
||||||
|
<XStack gap="$4" alignItems="center">
|
||||||
|
<YStack gap="$1" flexGrow={1}>
|
||||||
|
<Text fontWeight="$semibold" color="$ash300">
|
||||||
|
Device name
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="$bold">{device.name}</Text>
|
||||||
|
</YStack>
|
||||||
|
{!device.current && (
|
||||||
|
<MWButton
|
||||||
|
type="danger"
|
||||||
|
onPress={() => removeSessionMutation.mutate(device.id)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</MWButton>
|
||||||
|
)}
|
||||||
|
</XStack>
|
||||||
|
</MWCard>
|
||||||
|
))}
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
<YStack gap="$4">
|
||||||
|
<Text fontSize="$7" fontWeight="$bold">
|
||||||
|
Actions
|
||||||
|
</Text>
|
||||||
|
<MWSeparator />
|
||||||
|
<MWCard bordered padded>
|
||||||
|
<YStack gap="$3">
|
||||||
|
<H3 fontWeight="$bold">Delete account</H3>
|
||||||
|
<Text color="$ash300" fontWeight="$semibold">
|
||||||
|
This action is irreversible. All data will be deleted and
|
||||||
|
nothing can be recovered.
|
||||||
|
</Text>
|
||||||
|
<DeleteAccountAlert />
|
||||||
|
</YStack>
|
||||||
|
</MWCard>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</ScreenLayout>
|
||||||
|
);
|
||||||
|
}
|
38
apps/expo/src/components/account/Avatar.tsx
Normal file
38
apps/expo/src/components/account/Avatar.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { CircleProps } from "tamagui";
|
||||||
|
import { FontAwesome6 } from "@expo/vector-icons";
|
||||||
|
import { Circle } from "tamagui";
|
||||||
|
import { LinearGradient } from "tamagui/linear-gradient";
|
||||||
|
|
||||||
|
import type { expoIcons } from "./UserIconPicker";
|
||||||
|
|
||||||
|
export interface AvatarProps {
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: (typeof expoIcons)[number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Avatar(props: AvatarProps & CircleProps) {
|
||||||
|
return (
|
||||||
|
<Circle
|
||||||
|
backgroundColor={props.colorA}
|
||||||
|
height="$6"
|
||||||
|
width="$6"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[props.colorA, props.colorB]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
borderRadius="$12"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<FontAwesome6 name={props.icon} size={24} color="white" />
|
||||||
|
</LinearGradient>
|
||||||
|
</Circle>
|
||||||
|
);
|
||||||
|
}
|
39
apps/expo/src/components/account/ColorPicker.tsx
Normal file
39
apps/expo/src/components/account/ColorPicker.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { View, XStack } from "tamagui";
|
||||||
|
|
||||||
|
export const colors = [
|
||||||
|
"#0A54FF",
|
||||||
|
"#CF2E68",
|
||||||
|
"#F9DD7F",
|
||||||
|
"#7652DD",
|
||||||
|
"#2ECFA8",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function ColorPicker(props: {
|
||||||
|
value: (typeof colors)[number];
|
||||||
|
onInput: (v: (typeof colors)[number]) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<XStack gap="$2">
|
||||||
|
{colors.map((color) => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
onPress={() => props.onInput(color)}
|
||||||
|
flexGrow={1}
|
||||||
|
height="$4"
|
||||||
|
borderRadius="$4"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
backgroundColor={color}
|
||||||
|
key={color}
|
||||||
|
>
|
||||||
|
{props.value === color ? (
|
||||||
|
<Ionicons name="checkmark-circle" size={24} color="white" />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</XStack>
|
||||||
|
);
|
||||||
|
}
|
86
apps/expo/src/components/account/DeleteAccountAlert.tsx
Normal file
86
apps/expo/src/components/account/DeleteAccountAlert.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { AlertDialog, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
|
import { deleteUser } from "@movie-web/api";
|
||||||
|
|
||||||
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
import { MWButton } from "../ui/Button";
|
||||||
|
|
||||||
|
export function DeleteAccountAlert() {
|
||||||
|
const account = useAuthStore((state) => state.account);
|
||||||
|
const backendUrl = useAuthStore((state) => state.backendUrl);
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const logoutMutation = useMutation({
|
||||||
|
mutationKey: ["logout"],
|
||||||
|
mutationFn: logout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteAccountMutation = useMutation({
|
||||||
|
mutationKey: ["deleteAccount"],
|
||||||
|
mutationFn: () => deleteUser(backendUrl, account!),
|
||||||
|
onSuccess: () => {
|
||||||
|
logoutMutation.mutate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog native>
|
||||||
|
<AlertDialog.Trigger asChild>
|
||||||
|
<MWButton type="danger" width="$14" alignSelf="flex-end">
|
||||||
|
Delete account
|
||||||
|
</MWButton>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay
|
||||||
|
key="overlay"
|
||||||
|
animation="quick"
|
||||||
|
opacity={0.5}
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
<AlertDialog.Content
|
||||||
|
bordered
|
||||||
|
elevate
|
||||||
|
key="content"
|
||||||
|
animation={[
|
||||||
|
"quick",
|
||||||
|
{
|
||||||
|
opacity: {
|
||||||
|
overshootClamping: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
enterStyle={{ x: 0, y: -20, opacity: 0, scale: 0.9 }}
|
||||||
|
exitStyle={{ x: 0, y: 10, opacity: 0, scale: 0.95 }}
|
||||||
|
x={0}
|
||||||
|
scale={1}
|
||||||
|
opacity={1}
|
||||||
|
y={0}
|
||||||
|
>
|
||||||
|
<YStack gap="$4">
|
||||||
|
<AlertDialog.Title>Are you sure?</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This action is irreversible. All data will be deleted and nothing
|
||||||
|
can be recovered.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<XStack gap="$3" justifyContent="flex-end">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<MWButton>Cancel</MWButton>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action
|
||||||
|
asChild
|
||||||
|
onPress={() => deleteAccountMutation.mutate()}
|
||||||
|
>
|
||||||
|
<MWButton type="purple">I am sure</MWButton>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
60
apps/expo/src/components/account/GetStarted.tsx
Normal file
60
apps/expo/src/components/account/GetStarted.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Link } from "expo-router";
|
||||||
|
import { H3, H5, Paragraph, View } from "tamagui";
|
||||||
|
|
||||||
|
import { useAuthStore } from "~/stores/settings";
|
||||||
|
import ScreenLayout from "../layout/ScreenLayout";
|
||||||
|
import { MWButton } from "../ui/Button";
|
||||||
|
import { MWCard } from "../ui/Card";
|
||||||
|
import { MWInput } from "../ui/Input";
|
||||||
|
|
||||||
|
export function AccountGetStarted() {
|
||||||
|
const { backendUrl, setBackendUrl } = useAuthStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenLayout
|
||||||
|
contentContainerStyle={{
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MWCard bordered padded>
|
||||||
|
<MWCard.Header>
|
||||||
|
<H3 fontWeight="$bold" paddingBottom="$1">
|
||||||
|
Sync to the cloud
|
||||||
|
</H3>
|
||||||
|
<H5 color="$shade200" fontWeight="$semibold" paddingVertical="$3">
|
||||||
|
Share your watch progress between devices and keep them synced.
|
||||||
|
</H5>
|
||||||
|
<Paragraph color="$shade200">
|
||||||
|
First choose the backend you want to use. If you do not know what
|
||||||
|
this does, use the default and click on 'Get started'.
|
||||||
|
</Paragraph>
|
||||||
|
</MWCard.Header>
|
||||||
|
|
||||||
|
<View padding="$4">
|
||||||
|
<MWInput
|
||||||
|
placeholder={backendUrl}
|
||||||
|
type="authentication"
|
||||||
|
value={backendUrl}
|
||||||
|
onChangeText={setBackendUrl}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<MWCard.Footer padded justifyContent="center">
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: "/sync/trust/[backendUrl]",
|
||||||
|
params: { backendUrl },
|
||||||
|
}}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<MWButton type="purple" width="100%">
|
||||||
|
Get started
|
||||||
|
</MWButton>
|
||||||
|
</Link>
|
||||||
|
</MWCard.Footer>
|
||||||
|
</MWCard>
|
||||||
|
</ScreenLayout>
|
||||||
|
);
|
||||||
|
}
|
53
apps/expo/src/components/account/UserIconPicker.tsx
Normal file
53
apps/expo/src/components/account/UserIconPicker.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FontAwesome6 } from "@expo/vector-icons";
|
||||||
|
import { View, XStack } from "tamagui";
|
||||||
|
|
||||||
|
export const expoIcons = [
|
||||||
|
"user-group",
|
||||||
|
"couch",
|
||||||
|
"mobile-screen",
|
||||||
|
"ticket",
|
||||||
|
"handcuffs",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const expoIconsToDbIcons: Record<(typeof expoIcons)[number], string> = {
|
||||||
|
"user-group": "userGroup",
|
||||||
|
couch: "couch",
|
||||||
|
"mobile-screen": "mobile",
|
||||||
|
ticket: "ticket",
|
||||||
|
handcuffs: "handcuffs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExpoIconFromDbIcon = (icon: string) => {
|
||||||
|
return Object.keys(expoIconsToDbIcons).find(
|
||||||
|
(key) => expoIconsToDbIcons[key as (typeof expoIcons)[number]] === icon,
|
||||||
|
) as (typeof expoIcons)[number];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UserIconPicker(props: {
|
||||||
|
value: (typeof expoIcons)[number];
|
||||||
|
onInput: (v: (typeof expoIcons)[number]) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<XStack gap="$2">
|
||||||
|
{expoIcons.map((icon) => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
flexGrow={1}
|
||||||
|
height="$4"
|
||||||
|
borderRadius="$4"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
backgroundColor={props.value === icon ? "$purple400" : "$shade400"}
|
||||||
|
borderColor={props.value === icon ? "$purple200" : "$shade400"}
|
||||||
|
borderWidth={1}
|
||||||
|
key={icon}
|
||||||
|
onPress={() => props.onInput(icon)}
|
||||||
|
>
|
||||||
|
<FontAwesome6 name={icon} size={24} color="white" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</XStack>
|
||||||
|
);
|
||||||
|
}
|
@@ -10,6 +10,11 @@ export const MWButton = styled(Button, {
|
|||||||
pressStyle: {
|
pressStyle: {
|
||||||
backgroundColor: "$silver100",
|
backgroundColor: "$silver100",
|
||||||
},
|
},
|
||||||
|
disabledStyle: {
|
||||||
|
backgroundColor: "$ash500",
|
||||||
|
color: "$ash200",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
backgroundColor: "$ash700",
|
backgroundColor: "$ash700",
|
||||||
@@ -18,6 +23,11 @@ export const MWButton = styled(Button, {
|
|||||||
pressStyle: {
|
pressStyle: {
|
||||||
backgroundColor: "$ash500",
|
backgroundColor: "$ash500",
|
||||||
},
|
},
|
||||||
|
disabledStyle: {
|
||||||
|
backgroundColor: "$ash900",
|
||||||
|
color: "$ash200",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
backgroundColor: "$purple500",
|
backgroundColor: "$purple500",
|
||||||
@@ -26,6 +36,11 @@ export const MWButton = styled(Button, {
|
|||||||
pressStyle: {
|
pressStyle: {
|
||||||
backgroundColor: "$purple400",
|
backgroundColor: "$purple400",
|
||||||
},
|
},
|
||||||
|
disabledStyle: {
|
||||||
|
backgroundColor: "$purple700",
|
||||||
|
color: "$ash200",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
backgroundColor: "$ash500",
|
backgroundColor: "$ash500",
|
||||||
@@ -34,6 +49,24 @@ export const MWButton = styled(Button, {
|
|||||||
pressStyle: {
|
pressStyle: {
|
||||||
backgroundColor: "$ash300",
|
backgroundColor: "$ash300",
|
||||||
},
|
},
|
||||||
|
disabledStyle: {
|
||||||
|
backgroundColor: "$ash700",
|
||||||
|
color: "$ash200",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
backgroundColor: "$rose300",
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "bold",
|
||||||
|
pressStyle: {
|
||||||
|
backgroundColor: "$rose200",
|
||||||
|
},
|
||||||
|
disabledStyle: {
|
||||||
|
backgroundColor: "$rose500",
|
||||||
|
color: "$ash200",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const,
|
} as const,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AccountWithToken,
|
AccountWithToken,
|
||||||
@@ -8,9 +8,11 @@ import type {
|
|||||||
UserResponse,
|
UserResponse,
|
||||||
} from "@movie-web/api";
|
} from "@movie-web/api";
|
||||||
import {
|
import {
|
||||||
|
base64ToBuffer,
|
||||||
bookmarkMediaToInput,
|
bookmarkMediaToInput,
|
||||||
bytesToBase64,
|
bytesToBase64,
|
||||||
bytesToBase64Url,
|
bytesToBase64Url,
|
||||||
|
decryptData,
|
||||||
encryptData,
|
encryptData,
|
||||||
getBookmarks,
|
getBookmarks,
|
||||||
getLoginChallengeToken,
|
getLoginChallengeToken,
|
||||||
@@ -192,9 +194,18 @@ export function useAuth() {
|
|||||||
[backendUrl, syncData, logout],
|
[backendUrl, syncData, logout],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const decryptedName = useMemo(() => {
|
||||||
|
if (!currentAccount) return "";
|
||||||
|
return decryptData(
|
||||||
|
currentAccount.deviceName,
|
||||||
|
base64ToBuffer(currentAccount.seed),
|
||||||
|
);
|
||||||
|
}, [currentAccount]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loggedIn,
|
loggedIn,
|
||||||
profile,
|
profile,
|
||||||
|
decryptedName,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
register,
|
register,
|
||||||
|
@@ -38,7 +38,10 @@ export async function f<T>(url: string, ops?: FetcherOptions): Promise<T> {
|
|||||||
const fullUrl = makeFullUrl(url, ops);
|
const fullUrl = makeFullUrl(url, ops);
|
||||||
const response = await fetch(fullUrl, {
|
const response = await fetch(fullUrl, {
|
||||||
method: ops?.method ?? "GET",
|
method: ops?.method ?? "GET",
|
||||||
headers: ops?.headers,
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...ops?.headers,
|
||||||
|
},
|
||||||
body: ops?.body ? JSON.stringify(ops.body) : undefined,
|
body: ops?.body ? JSON.stringify(ops.body) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user