mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:53:25 +00:00
Compare commits
1 Commits
4ae5c3bc9b
...
a981beae11
Author | SHA1 | Date | |
---|---|---|---|
|
a981beae11 |
@@ -2,6 +2,3 @@ import "expo-router/entry";
|
|||||||
import "react-native-gesture-handler";
|
import "react-native-gesture-handler";
|
||||||
import "@react-native-anywhere/polyfill-base64";
|
import "@react-native-anywhere/polyfill-base64";
|
||||||
import "text-encoding-polyfill";
|
import "text-encoding-polyfill";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
import crypto from "react-native-quick-crypto";
|
|
||||||
|
@@ -96,11 +96,7 @@ export default function Page() {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap="$4"
|
gap="$4"
|
||||||
>
|
>
|
||||||
<MWButton
|
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||||
type="purple"
|
|
||||||
onPress={() => mutation.mutate()}
|
|
||||||
isLoading={mutation.isPending}
|
|
||||||
>
|
|
||||||
Login
|
Login
|
||||||
</MWButton>
|
</MWButton>
|
||||||
{mutation.isError && (
|
{mutation.isError && (
|
||||||
|
@@ -6,7 +6,7 @@ import { Avatar } from "~/components/account/Avatar";
|
|||||||
import { ColorPicker, colors } from "~/components/account/ColorPicker";
|
import { ColorPicker, colors } from "~/components/account/ColorPicker";
|
||||||
import {
|
import {
|
||||||
expoIcons,
|
expoIcons,
|
||||||
getDbIconFromExpoIcon,
|
expoIconsToDbIcons,
|
||||||
UserIconPicker,
|
UserIconPicker,
|
||||||
} from "~/components/account/UserIconPicker";
|
} from "~/components/account/UserIconPicker";
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
@@ -19,9 +19,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const [deviceName, setDeviceName] = useState("");
|
const [deviceName, setDeviceName] = useState("");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [colorA, setColorA] = useState<string>(colors[0]);
|
const [colorA, setColorA] = useState<(typeof colors)[number]>(colors[0]);
|
||||||
const [colorB, setColorB] = useState<string>(colors[0]);
|
const [colorB, setColorB] = useState<(typeof colors)[number]>(colors[0]);
|
||||||
const [icon, setIcon] = useState<string>(expoIcons[0]);
|
const [icon, setIcon] = useState<(typeof expoIcons)[number]>(expoIcons[0]);
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (!deviceName) {
|
if (!deviceName) {
|
||||||
@@ -34,7 +34,7 @@ export default function Page() {
|
|||||||
deviceName,
|
deviceName,
|
||||||
colorA,
|
colorA,
|
||||||
colorB,
|
colorB,
|
||||||
icon: getDbIconFromExpoIcon(icon),
|
icon: expoIconsToDbIcons[icon],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -95,11 +95,7 @@ export default function Page() {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MWButton
|
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||||
type="purple"
|
|
||||||
onPress={() => mutation.mutate()}
|
|
||||||
isLoading={mutation.isPending}
|
|
||||||
>
|
|
||||||
Create account
|
Create account
|
||||||
</MWButton>
|
</MWButton>
|
||||||
</MWCard.Footer>
|
</MWCard.Footer>
|
||||||
|
@@ -1,21 +1,15 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { H5, Spinner, Text, View, XStack, YStack } from "tamagui";
|
import { H3, Spinner, Text, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
base64ToBuffer,
|
base64ToBuffer,
|
||||||
decryptData,
|
decryptData,
|
||||||
editUser,
|
|
||||||
encryptData,
|
|
||||||
getSessions,
|
getSessions,
|
||||||
removeSession,
|
removeSession,
|
||||||
updateSession,
|
|
||||||
updateSettings,
|
|
||||||
} from "@movie-web/api";
|
} from "@movie-web/api";
|
||||||
|
|
||||||
import { useAuth } from "~/hooks/useAuth";
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
import { useSettingsState } from "~/hooks/useSettingsState";
|
|
||||||
import { useAuthStore } from "~/stores/settings";
|
import { useAuthStore } from "~/stores/settings";
|
||||||
import ScreenLayout from "../layout/ScreenLayout";
|
import ScreenLayout from "../layout/ScreenLayout";
|
||||||
import { MWButton } from "../ui/Button";
|
import { MWButton } from "../ui/Button";
|
||||||
@@ -23,20 +17,12 @@ import { MWCard } from "../ui/Card";
|
|||||||
import { MWInput } from "../ui/Input";
|
import { MWInput } from "../ui/Input";
|
||||||
import { MWSeparator } from "../ui/Separator";
|
import { MWSeparator } from "../ui/Separator";
|
||||||
import { Avatar } from "./Avatar";
|
import { Avatar } from "./Avatar";
|
||||||
import { ChangeProfileModal } from "./ChangeProfileModal";
|
|
||||||
import { DeleteAccountAlert } from "./DeleteAccountAlert";
|
import { DeleteAccountAlert } from "./DeleteAccountAlert";
|
||||||
import { getDbIconFromExpoIcon, getExpoIconFromDbIcon } from "./UserIconPicker";
|
import { getExpoIconFromDbIcon } from "./UserIconPicker";
|
||||||
|
|
||||||
export function AccountInformation() {
|
export function AccountInformation() {
|
||||||
const account = useAuthStore((state) => state.account);
|
const account = useAuthStore((state) => state.account);
|
||||||
const backendUrl = useAuthStore((state) => state.backendUrl);
|
const backendUrl = useAuthStore((state) => state.backendUrl);
|
||||||
const proxySet = useAuthStore((s) => s.proxySet);
|
|
||||||
const setProxySet = useAuthStore((s) => s.setProxySet);
|
|
||||||
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
|
||||||
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { decryptedName, logout } = useAuth();
|
const { decryptedName, logout } = useAuth();
|
||||||
@@ -64,15 +50,14 @@ export function AccountInformation() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const deviceListSorted = useMemo(() => {
|
const deviceListSorted = useMemo(() => {
|
||||||
if (!sessions.data || !account) return [];
|
|
||||||
let list =
|
let list =
|
||||||
sessions.data?.map((session) => {
|
sessions.data?.map((session) => {
|
||||||
const decryptedName = decryptData(
|
const decryptedName = decryptData(
|
||||||
session.device,
|
session.device,
|
||||||
base64ToBuffer(account.seed),
|
base64ToBuffer(account!.seed),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
current: session.id === account.sessionId,
|
current: session.id === account!.sessionId,
|
||||||
id: session.id,
|
id: session.id,
|
||||||
name: decryptedName,
|
name: decryptedName,
|
||||||
};
|
};
|
||||||
@@ -85,222 +70,96 @@ export function AccountInformation() {
|
|||||||
return list;
|
return list;
|
||||||
}, [sessions.data, account]);
|
}, [sessions.data, account]);
|
||||||
|
|
||||||
const state = useSettingsState(decryptedName, proxySet, account?.profile);
|
|
||||||
|
|
||||||
const saveChanges = useCallback(async () => {
|
|
||||||
if (account && backendUrl) {
|
|
||||||
if (state.proxyUrls.changed) {
|
|
||||||
await updateSettings(backendUrl, account, {
|
|
||||||
proxyUrls: state.proxyUrls.state?.filter((v) => v !== "") ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (state.deviceName.changed) {
|
|
||||||
const newDeviceName = await encryptData(
|
|
||||||
state.deviceName.state,
|
|
||||||
base64ToBuffer(account.seed),
|
|
||||||
);
|
|
||||||
await updateSession(backendUrl, account, {
|
|
||||||
deviceName: newDeviceName,
|
|
||||||
});
|
|
||||||
updateDeviceName(newDeviceName);
|
|
||||||
}
|
|
||||||
if (state.profile.changed) {
|
|
||||||
await editUser(backendUrl, account, {
|
|
||||||
profile: state.profile.state,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setProxySet(state.proxyUrls.state?.filter((v) => v !== "") ?? null);
|
|
||||||
|
|
||||||
if (state.profile.state) {
|
|
||||||
updateProfile(state.profile.state);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
account,
|
|
||||||
backendUrl,
|
|
||||||
setProxySet,
|
|
||||||
state.deviceName.changed,
|
|
||||||
state.deviceName.state,
|
|
||||||
state.profile.changed,
|
|
||||||
state.profile.state,
|
|
||||||
state.proxyUrls.changed,
|
|
||||||
state.proxyUrls.state,
|
|
||||||
updateDeviceName,
|
|
||||||
updateProfile,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const saveChangesMutation = useMutation({
|
|
||||||
mutationKey: ["saveChanges"],
|
|
||||||
mutationFn: saveChanges,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ScreenLayout>
|
||||||
<ScreenLayout>
|
<YStack gap="$6">
|
||||||
<YStack gap="$6">
|
<YStack gap="$4">
|
||||||
<YStack gap="$4">
|
<Text fontSize="$7" fontWeight="$bold">
|
||||||
<Text fontSize="$7" fontWeight="$bold">
|
Account
|
||||||
Account
|
</Text>
|
||||||
</Text>
|
<MWSeparator />
|
||||||
<MWSeparator />
|
|
||||||
|
|
||||||
{state.profile.state && (
|
<MWCard bordered padded>
|
||||||
<MWCard bordered padded>
|
<XStack gap="$4" alignItems="center">
|
||||||
<XStack gap="$4" alignItems="center">
|
<Avatar
|
||||||
<ChangeProfileModal
|
{...account.profile}
|
||||||
colorA={state.profile.state.colorA}
|
icon={getExpoIconFromDbIcon(account.profile.icon)}
|
||||||
setColorA={(v) => {
|
width="$7"
|
||||||
state.profile.set((s) =>
|
height="$7"
|
||||||
s ? { ...s, colorA: v } : undefined,
|
/>
|
||||||
);
|
<YStack gap="$4">
|
||||||
}}
|
<Text fontWeight="$bold">Device name</Text>
|
||||||
colorB={state.profile.state.colorB}
|
<MWInput
|
||||||
setColorB={(v) =>
|
type="authentication"
|
||||||
state.profile.set((s) =>
|
value={decryptedName}
|
||||||
s ? { ...s, colorB: v } : undefined,
|
minWidth={"80%"}
|
||||||
)
|
maxWidth={"80%"}
|
||||||
}
|
/>
|
||||||
icon={state.profile.state.icon}
|
|
||||||
setUserIcon={(v) =>
|
|
||||||
state.profile.set((s) =>
|
|
||||||
s
|
|
||||||
? { ...s, icon: getDbIconFromExpoIcon(v) }
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
open={open}
|
|
||||||
setOpen={setOpen}
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
{...state.profile.state}
|
|
||||||
icon={getExpoIconFromDbIcon(state.profile.state.icon)}
|
|
||||||
width="$7"
|
|
||||||
height="$7"
|
|
||||||
bottomItem={
|
|
||||||
<XStack
|
|
||||||
backgroundColor="$shade200"
|
|
||||||
px="$2"
|
|
||||||
py="$1"
|
|
||||||
borderRadius="$4"
|
|
||||||
gap="$1.5"
|
|
||||||
alignItems="center"
|
|
||||||
onPress={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="edit" size={10} color="white" />
|
|
||||||
<Text fontSize="$2">Edit</Text>
|
|
||||||
</XStack>
|
|
||||||
}
|
|
||||||
onPress={() => setOpen(true)}
|
|
||||||
/>
|
|
||||||
<YStack gap="$4">
|
|
||||||
<Text fontWeight="$bold">Device name</Text>
|
|
||||||
<MWInput
|
|
||||||
type="authentication"
|
|
||||||
value={state.deviceName.state}
|
|
||||||
onChangeText={state.deviceName.set}
|
|
||||||
alignSelf="flex-start"
|
|
||||||
width="$14"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MWButton
|
<MWButton
|
||||||
type="danger"
|
type="danger"
|
||||||
onPress={() => logoutMutation.mutate()}
|
width={"50%"}
|
||||||
alignSelf="flex-start"
|
onPress={() => logoutMutation.mutate()}
|
||||||
isLoading={logoutMutation.isPending}
|
>
|
||||||
>
|
Logout
|
||||||
Logout
|
</MWButton>
|
||||||
</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"
|
|
||||||
isLoading={removeSessionMutation.isPending}
|
|
||||||
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">
|
|
||||||
<H5 fontWeight="$bold">Delete account</H5>
|
|
||||||
<Text color="$ash300" fontWeight="$semibold">
|
|
||||||
This action is irreversible. All data will be deleted and
|
|
||||||
nothing can be recovered.
|
|
||||||
</Text>
|
|
||||||
<DeleteAccountAlert />
|
|
||||||
</YStack>
|
</YStack>
|
||||||
</MWCard>
|
</XStack>
|
||||||
</YStack>
|
</MWCard>
|
||||||
</YStack>
|
</YStack>
|
||||||
</ScreenLayout>
|
|
||||||
|
|
||||||
{state.changed && (
|
<YStack gap="$4">
|
||||||
<View position="absolute" alignItems="center" bottom="$2" px="$2">
|
<Text fontSize="$7" fontWeight="$bold">
|
||||||
<XStack
|
Devices
|
||||||
width="100%"
|
</Text>
|
||||||
padding="$4"
|
<MWSeparator />
|
||||||
backgroundColor="$shade800"
|
{sessions.isLoading && <Spinner />}
|
||||||
justifyContent="space-between"
|
{sessions.isError && (
|
||||||
borderRadius="$4"
|
<Text fontWeight="$bold" color="$rose200">
|
||||||
animation="bounce"
|
Error loading sessions
|
||||||
enterStyle={{
|
</Text>
|
||||||
y: 10,
|
)}
|
||||||
opacity: 0,
|
{deviceListSorted.map((device) => (
|
||||||
}}
|
<MWCard bordered padded key={device.id}>
|
||||||
opacity={1}
|
<XStack gap="$4" alignItems="center">
|
||||||
scale={1}
|
<YStack gap="$1" flexGrow={1}>
|
||||||
>
|
<Text fontWeight="$semibold" color="$ash300">
|
||||||
<MWButton type="cancel" onPress={state.reset}>
|
Device name
|
||||||
Reset
|
</Text>
|
||||||
</MWButton>
|
<Text fontWeight="$bold">{device.name}</Text>
|
||||||
<MWButton
|
</YStack>
|
||||||
type="purple"
|
{!device.current && (
|
||||||
onPress={() => saveChangesMutation.mutate()}
|
<MWButton
|
||||||
isLoading={saveChangesMutation.isPending}
|
type="danger"
|
||||||
>
|
onPress={() => removeSessionMutation.mutate(device.id)}
|
||||||
Save changes
|
>
|
||||||
</MWButton>
|
Remove
|
||||||
</XStack>
|
</MWButton>
|
||||||
</View>
|
)}
|
||||||
)}
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import type { CircleProps } from "tamagui";
|
import type { CircleProps } from "tamagui";
|
||||||
import { FontAwesome6 } from "@expo/vector-icons";
|
import { FontAwesome6 } from "@expo/vector-icons";
|
||||||
import { Circle, View } from "tamagui";
|
import { Circle } from "tamagui";
|
||||||
import { LinearGradient } from "tamagui/linear-gradient";
|
import { LinearGradient } from "tamagui/linear-gradient";
|
||||||
|
|
||||||
|
import type { expoIcons } from "./UserIconPicker";
|
||||||
|
|
||||||
export interface AvatarProps {
|
export interface AvatarProps {
|
||||||
colorA: string;
|
colorA: string;
|
||||||
colorB: string;
|
colorB: string;
|
||||||
icon: string;
|
icon: (typeof expoIcons)[number];
|
||||||
bottomItem?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar(props: AvatarProps & CircleProps) {
|
export function Avatar(props: AvatarProps & CircleProps) {
|
||||||
@@ -32,9 +33,6 @@ export function Avatar(props: AvatarProps & CircleProps) {
|
|||||||
>
|
>
|
||||||
<FontAwesome6 name={props.icon} size={24} color="white" />
|
<FontAwesome6 name={props.icon} size={24} color="white" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
<View position="absolute" bottom={0}>
|
|
||||||
{props.bottomItem}
|
|
||||||
</View>
|
|
||||||
</Circle>
|
</Circle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,81 +0,0 @@
|
|||||||
import { H3, Sheet, Text, XStack, YStack } from "tamagui";
|
|
||||||
|
|
||||||
import { MWButton } from "../ui/Button";
|
|
||||||
import { Avatar } from "./Avatar";
|
|
||||||
import { ColorPicker } from "./ColorPicker";
|
|
||||||
import { UserIconPicker } from "./UserIconPicker";
|
|
||||||
|
|
||||||
export function ChangeProfileModal(props: {
|
|
||||||
colorA: string;
|
|
||||||
setColorA: (s: string) => void;
|
|
||||||
colorB: string;
|
|
||||||
setColorB: (s: string) => void;
|
|
||||||
icon: string;
|
|
||||||
setUserIcon: (s: string) => void;
|
|
||||||
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (b: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Sheet
|
|
||||||
forceRemoveScrollEnabled={props.open}
|
|
||||||
modal
|
|
||||||
open={props.open}
|
|
||||||
onOpenChange={props.setOpen}
|
|
||||||
dismissOnSnapToBottom
|
|
||||||
dismissOnOverlayPress
|
|
||||||
animation="fast"
|
|
||||||
snapPoints={[65]}
|
|
||||||
>
|
|
||||||
<Sheet.Handle backgroundColor="$shade100" />
|
|
||||||
<Sheet.Frame
|
|
||||||
backgroundColor="$shade800"
|
|
||||||
padding="$4"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<YStack padding="$4" gap="$4">
|
|
||||||
<XStack gap="$4" alignItems="center">
|
|
||||||
<H3 flexGrow={1} fontWeight="$bold">
|
|
||||||
Edit profile picture
|
|
||||||
</H3>
|
|
||||||
<Avatar
|
|
||||||
colorA={props.colorA}
|
|
||||||
colorB={props.colorB}
|
|
||||||
icon={props.icon}
|
|
||||||
/>
|
|
||||||
</XStack>
|
|
||||||
|
|
||||||
<YStack gap="$2">
|
|
||||||
<Text fontWeight="$bold">Profile color one</Text>
|
|
||||||
<ColorPicker value={props.colorA} onInput={props.setColorA} />
|
|
||||||
</YStack>
|
|
||||||
|
|
||||||
<YStack gap="$2">
|
|
||||||
<Text fontWeight="$bold">Profile color two</Text>
|
|
||||||
<ColorPicker value={props.colorB} onInput={props.setColorB} />
|
|
||||||
</YStack>
|
|
||||||
|
|
||||||
<YStack gap="$2">
|
|
||||||
<Text fontWeight="$bold">User icon</Text>
|
|
||||||
<UserIconPicker value={props.icon} onInput={props.setUserIcon} />
|
|
||||||
</YStack>
|
|
||||||
</YStack>
|
|
||||||
|
|
||||||
<MWButton
|
|
||||||
type="purple"
|
|
||||||
width="100%"
|
|
||||||
onPress={() => props.setOpen(false)}
|
|
||||||
>
|
|
||||||
Finish editing
|
|
||||||
</MWButton>
|
|
||||||
</Sheet.Frame>
|
|
||||||
<Sheet.Overlay
|
|
||||||
animation="lazy"
|
|
||||||
backgroundColor="rgba(0, 0, 0, 0.8)"
|
|
||||||
enterStyle={{ opacity: 0 }}
|
|
||||||
exitStyle={{ opacity: 0 }}
|
|
||||||
/>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -11,8 +11,8 @@ export const colors = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function ColorPicker(props: {
|
export function ColorPicker(props: {
|
||||||
value: string;
|
value: (typeof colors)[number];
|
||||||
onInput: (v: string) => void;
|
onInput: (v: (typeof colors)[number]) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<XStack gap="$2">
|
<XStack gap="$2">
|
||||||
|
@@ -75,12 +75,7 @@ export function DeleteAccountAlert() {
|
|||||||
asChild
|
asChild
|
||||||
onPress={() => deleteAccountMutation.mutate()}
|
onPress={() => deleteAccountMutation.mutate()}
|
||||||
>
|
>
|
||||||
<MWButton
|
<MWButton type="purple">I am sure</MWButton>
|
||||||
type="purple"
|
|
||||||
isLoading={deleteAccountMutation.isPending}
|
|
||||||
>
|
|
||||||
I am sure
|
|
||||||
</MWButton>
|
|
||||||
</AlertDialog.Action>
|
</AlertDialog.Action>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
@@ -24,13 +24,9 @@ export const getExpoIconFromDbIcon = (icon: string) => {
|
|||||||
) as (typeof expoIcons)[number];
|
) as (typeof expoIcons)[number];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDbIconFromExpoIcon = (icon: string) => {
|
|
||||||
return expoIconsToDbIcons[icon as (typeof expoIcons)[number]];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function UserIconPicker(props: {
|
export function UserIconPicker(props: {
|
||||||
value: string;
|
value: (typeof expoIcons)[number];
|
||||||
onInput: (v: string) => void;
|
onInput: (v: (typeof expoIcons)[number]) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<XStack gap="$2">
|
<XStack gap="$2">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Spinner, styled, withStaticProperties } from "tamagui";
|
import { Button, styled } from "tamagui";
|
||||||
|
|
||||||
const MWButtonFrame = styled(Button, {
|
export const MWButton = styled(Button, {
|
||||||
variants: {
|
variants: {
|
||||||
type: {
|
type: {
|
||||||
primary: {
|
primary: {
|
||||||
@@ -71,22 +71,3 @@ const MWButtonFrame = styled(Button, {
|
|||||||
},
|
},
|
||||||
} as const,
|
} as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ButtonComponent = MWButtonFrame.styleable<{
|
|
||||||
isLoading?: boolean;
|
|
||||||
}>(function Button(props, ref) {
|
|
||||||
const spinnerColor =
|
|
||||||
// @ts-expect-error this is a hack to get the color from the variant
|
|
||||||
MWButtonFrame.staticConfig.variants?.type?.[props.type!]?.color as string;
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
<MWButtonFrame {...props} ref={ref}>
|
|
||||||
{props.isLoading && (
|
|
||||||
<Spinner size="small" color={spinnerColor ?? "white"} />
|
|
||||||
)}
|
|
||||||
{props.children}
|
|
||||||
</MWButtonFrame>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const MWButton = withStaticProperties(ButtonComponent, {});
|
|
||||||
|
@@ -1,91 +0,0 @@
|
|||||||
import type { Dispatch, SetStateAction } from "react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
function isEqual(x: unknown, y: unknown): boolean {
|
|
||||||
const ok = Object.keys,
|
|
||||||
tx = typeof x,
|
|
||||||
ty = typeof y;
|
|
||||||
return x && y && tx === "object" && tx === ty
|
|
||||||
? ok(x).length === ok(y).length &&
|
|
||||||
ok(x).every((key) =>
|
|
||||||
isEqual(x[key as keyof typeof x], y[key as keyof typeof y]),
|
|
||||||
)
|
|
||||||
: x === y;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDerived<T>(
|
|
||||||
initial: T,
|
|
||||||
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
|
|
||||||
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
|
||||||
useEffect(() => {
|
|
||||||
setOverwrite(undefined);
|
|
||||||
}, [initial]);
|
|
||||||
const changed = useMemo(
|
|
||||||
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
|
||||||
[overwrite, initial],
|
|
||||||
);
|
|
||||||
const setter = useCallback<Dispatch<SetStateAction<T>>>(
|
|
||||||
(inp) => {
|
|
||||||
if (!(inp instanceof Function)) setOverwrite(inp);
|
|
||||||
else setOverwrite((s) => inp(s ?? initial));
|
|
||||||
},
|
|
||||||
[initial, setOverwrite],
|
|
||||||
);
|
|
||||||
const data = overwrite ?? initial;
|
|
||||||
|
|
||||||
const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
|
|
||||||
|
|
||||||
return [data, setter, reset, changed];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSettingsState(
|
|
||||||
deviceName: string,
|
|
||||||
proxyUrls: string[] | null,
|
|
||||||
profile:
|
|
||||||
| {
|
|
||||||
colorA: string;
|
|
||||||
colorB: string;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
| undefined,
|
|
||||||
) {
|
|
||||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
|
||||||
useDerived(proxyUrls);
|
|
||||||
|
|
||||||
const [
|
|
||||||
deviceNameState,
|
|
||||||
setDeviceNameState,
|
|
||||||
resetDeviceName,
|
|
||||||
deviceNameChanged,
|
|
||||||
] = useDerived(deviceName);
|
|
||||||
const [profileState, setProfileState, resetProfile, profileChanged] =
|
|
||||||
useDerived(profile);
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
resetProxyUrls();
|
|
||||||
resetDeviceName();
|
|
||||||
resetProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
const changed = deviceNameChanged || proxyUrlsChanged || profileChanged;
|
|
||||||
|
|
||||||
return {
|
|
||||||
reset,
|
|
||||||
changed,
|
|
||||||
deviceName: {
|
|
||||||
state: deviceNameState,
|
|
||||||
set: setDeviceNameState,
|
|
||||||
changed: deviceNameChanged,
|
|
||||||
},
|
|
||||||
proxyUrls: {
|
|
||||||
state: proxyUrlsState,
|
|
||||||
set: setProxyUrls,
|
|
||||||
changed: proxyUrlsChanged,
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
state: profileState,
|
|
||||||
set: setProfileState,
|
|
||||||
changed: profileChanged,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
Reference in New Issue
Block a user