rcgit

/ karakeep

Commit 66fcf022

SHA 66fcf022695283268e80855365f10262ae6ec907
Author MohamedBassem <me at mbassem dot com>
Author Date 2024-09-14 17:08 +0100
Committer MohamedBassem <me at mbassem dot com>
Commit Date 2024-09-14 17:36 +0100
Parent(s) b9c7857c5bb1 (diff)
Tree 07cc278b1476

patch snapshot

feature(mobile): Add settings page for configuring the theme
File + - Graph
M apps/mobile/app/_layout.tsx +7 -1
M apps/mobile/app/dashboard/(tabs)/settings.tsx +27 -6
M apps/mobile/app/dashboard/_layout.tsx +11 -0
A apps/mobile/app/dashboard/settings/theme.tsx +47 -0
M apps/mobile/lib/settings.ts +14 -4
5 file(s) changed, 106 insertions(+), 11 deletions(-)

apps/mobile/app/_layout.tsx

diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 0f38165b..41186842 100644
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -10,6 +10,7 @@ import { ShareIntentProvider, useShareIntent } from "expo-share-intent";
 import { StatusBar } from "expo-status-bar";
 import { StyledStack } from "@/components/navigation/stack";
 import { Providers } from "@/lib/providers";
+import useAppSettings from "@/lib/settings";
 import { cn } from "@/lib/utils";
 import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
 import { useColorScheme } from "nativewind";
@@ -17,7 +18,8 @@ import { useColorScheme } from "nativewind";
 export default function RootLayout() {
   const router = useRouter();
   const { hasShareIntent } = useShareIntent();
-  const { colorScheme } = useColorScheme();
+  const { colorScheme, setColorScheme } = useColorScheme();
+  const { settings } = useAppSettings();
 
   useEffect(() => {
     if (hasShareIntent) {
@@ -27,6 +29,10 @@ export default function RootLayout() {
     }
   }, [hasShareIntent]);
 
+  useEffect(() => {
+    setColorScheme(settings.theme);
+  }, [settings.theme]);
+
   return (
     <ShareIntentProvider>
       <Providers>

apps/mobile/app/dashboard/(tabs)/settings.tsx

diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx
index 18d3d243..db118df8 100644
--- a/apps/mobile/app/dashboard/(tabs)/settings.tsx
+++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx
@@ -1,7 +1,8 @@
 import { useEffect } from "react";
-import { Text, View } from "react-native";
+import { Pressable, Text, View } from "react-native";
 import { Slider } from "react-native-awesome-slider";
 import { useSharedValue } from "react-native-reanimated";
+import { Link } from "expo-router";
 import { Button } from "@/components/ui/Button";
 import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView";
 import { Divider } from "@/components/ui/Divider";
@@ -9,6 +10,7 @@ import PageTitle from "@/components/ui/PageTitle";
 import { useSession } from "@/lib/session";
 import useAppSettings from "@/lib/settings";
 import { api } from "@/lib/trpc";
+import { ChevronRight } from "lucide-react-native";
 
 export default function Dashboard() {
   const { logout } = useSession();
@@ -36,18 +38,35 @@ export default function Dashboard() {
     <CustomSafeAreaView>
       <PageTitle title="Settings" />
       <View className="flex h-full w-full items-center gap-3 px-4 py-2">
-        <View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent">
+        <View className="flex w-full gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent">
           <Text className="text-lg text-accent-foreground">
             {isSettingsLoading ? "Loading ..." : settings.address}
           </Text>
-        </View>
-        <View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent">
+          <Divider orientation="horizontal" />
           <Text className="text-lg text-accent-foreground">
             {isLoading ? "Loading ..." : data?.email}
           </Text>
         </View>
-        <Button className="w-full" label="Log Out" onPress={logout} />
-        <Divider orientation="horizontal" />
+        <Text className="w-full p-1 text-2xl font-bold text-foreground">
+          App Settings
+        </Text>
+        <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent">
+          <Link asChild href="/dashboard/settings/theme" className="flex-1">
+            <Pressable className="flex flex-row justify-between">
+              <Text className="text-lg text-accent-foreground">Theme</Text>
+              <View className="flex flex-row items-center gap-2">
+                <Text className="text-lg text-muted-foreground">
+                  {
+                    { light: "Light", dark: "Dark", system: "System" }[
+                      settings.theme
+                    ]
+                  }
+                </Text>
+                <ChevronRight color="rgb(0, 122, 255)" />
+              </View>
+            </Pressable>
+          </Link>
+        </View>
         <Text className="w-full p-1 text-2xl font-bold text-foreground">
           Upload Settings
         </Text>
@@ -70,6 +89,8 @@ export default function Dashboard() {
             />
           </View>
         </View>
+        <Divider orientation="horizontal" />
+        <Button className="w-full" label="Log Out" onPress={logout} />
       </View>
     </CustomSafeAreaView>
   );

apps/mobile/app/dashboard/_layout.tsx

diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx
index db4fd251..22d1ed07 100644
--- a/apps/mobile/app/dashboard/_layout.tsx
+++ b/apps/mobile/app/dashboard/_layout.tsx
@@ -33,6 +33,9 @@ export default function Dashboard() {
     <StyledStack
       contentClassName="bg-gray-100 dark:bg-background"
       headerClassName="bg-gray-100 dark:bg-background text-foreground"
+      screenOptions={{
+        headerTransparent: true,
+      }}
     >
       <Stack.Screen
         name="(tabs)"
@@ -54,6 +57,14 @@ export default function Dashboard() {
           headerTransparent: true,
         }}
       />
+      <Stack.Screen
+        name="settings/theme"
+        options={{
+          title: "Theme",
+          headerTitle: "Theme",
+          headerBackTitle: "Back",
+        }}
+      />
     </StyledStack>
   );
 }

apps/mobile/app/dashboard/settings/theme.tsx

diff --git a/apps/mobile/app/dashboard/settings/theme.tsx b/apps/mobile/app/dashboard/settings/theme.tsx
new file mode 100644
index 00000000..dc7ba367
--- /dev/null
+++ b/apps/mobile/app/dashboard/settings/theme.tsx
@@ -0,0 +1,47 @@
+import { Pressable, Text, View } from "react-native";
+import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView";
+import { Divider } from "@/components/ui/Divider";
+import useAppSettings from "@/lib/settings";
+import { Check } from "lucide-react-native";
+
+export default function ThemePage() {
+  const { settings, setSettings } = useAppSettings();
+
+  const options = (["light", "dark", "system"] as const)
+    .map((theme) => {
+      const isChecked = settings.theme === theme;
+      return [
+        <Pressable
+          onPress={() => setSettings({ ...settings, theme })}
+          className="flex flex-row justify-between"
+          key={theme}
+        >
+          <Text className="text-lg text-accent-foreground">
+            {
+              { light: "Light Mode", dark: "Dark Mode", system: "System" }[
+                theme
+              ]
+            }
+          </Text>
+          {isChecked && <Check />}
+        </Pressable>,
+        <Divider
+          key={theme + "-divider"}
+          orientation="horizontal"
+          className="my-3 h-0.5 w-full"
+        />,
+      ];
+    })
+    .flat();
+  options.pop();
+
+  return (
+    <CustomSafeAreaView>
+      <View className="flex h-full w-full items-center px-4 py-2">
+        <View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent">
+          {options}
+        </View>
+      </View>
+    </CustomSafeAreaView>
+  );
+}

apps/mobile/lib/settings.ts

diff --git a/apps/mobile/lib/settings.ts b/apps/mobile/lib/settings.ts
index 085f4f01..58b0817f 100644
--- a/apps/mobile/lib/settings.ts
+++ b/apps/mobile/lib/settings.ts
@@ -9,6 +9,7 @@ const zSettingsSchema = z.object({
   apiKeyId: z.string().optional(),
   address: z.string(),
   imageQuality: z.number().optional().default(0.2),
+  theme: z.enum(["light", "dark", "system"]).optional().default("system"),
 });
 
 export type Settings = z.infer<typeof zSettingsSchema>;
@@ -22,7 +23,7 @@ interface AppSettingsState {
 const useSettings = create<AppSettingsState>((set, get) => ({
   settings: {
     isLoading: true,
-    settings: { address: "", imageQuality: 0.2 },
+    settings: { address: "", imageQuality: 0.2, theme: "system" },
   },
   setSettings: async (settings) => {
     await SecureStore.setItemAsync(SETTING_NAME, JSON.stringify(settings));
@@ -39,9 +40,18 @@ const useSettings = create<AppSettingsState>((set, get) => ({
       }));
       return;
     }
-    // TODO Wipe the state if invalid
-    const parsed = zSettingsSchema.parse(JSON.parse(strVal));
-    set((_state) => ({ settings: { isLoading: false, settings: parsed } }));
+    const parsed = zSettingsSchema.safeParse(JSON.parse(strVal));
+    if (!parsed.success) {
+      // Wipe the state if invalid
+      set((state) => ({
+        settings: { isLoading: false, settings: state.settings.settings },
+      }));
+      return;
+    }
+
+    set((_state) => ({
+      settings: { isLoading: false, settings: parsed.data },
+    }));
   },
 }));