diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-08-26 15:47:05 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-26 13:47:05 +0100 |
| commit | ed86f7ef012fb558fe8a8974e1e162ce75cbfd15 (patch) | |
| tree | a3470b0e1a01aede90b75bc61eeba2545e51fe83 /apps/mobile/app | |
| parent | ec56ea33b5e37d02e87e480da305038a5ce7de49 (diff) | |
| download | karakeep-ed86f7ef012fb558fe8a8974e1e162ce75cbfd15.tar.zst | |
feat(mobile): Retheme the mobile app (#1872)
* Add nativewindui
* migrate to nativewindui text
* Replace buttons with nativewindui buttons
* Use nativewindui search input
* fix the divider color
* More changes
* fix manage tag icon
* fix styling of bookmark card
* fix ios compilation
* fix search clear
* fix tag pill border color
* Store theme setting in app settings
* fix setting color appearance
* fix coloring of search input
* fix following system theme
* add a save button to info
* fix the grey colors on android
* fix icon active tint color
* drop the use of TextField
Diffstat (limited to 'apps/mobile/app')
| -rw-r--r-- | apps/mobile/app/_layout.tsx | 101 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/_layout.tsx | 3 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/index.tsx | 9 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/lists.tsx | 34 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/settings.tsx | 45 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx | 11 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx | 287 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx | 7 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx | 26 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/new.tsx | 8 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/lists/new.tsx | 9 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/search.tsx | 52 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/settings/bookmark-default-view.tsx | 9 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/settings/theme.tsx | 9 | ||||
| -rw-r--r-- | apps/mobile/app/error.tsx | 5 | ||||
| -rw-r--r-- | apps/mobile/app/sharing.tsx | 14 | ||||
| -rw-r--r-- | apps/mobile/app/signin.tsx | 28 | ||||
| -rw-r--r-- | apps/mobile/app/test-connection.tsx | 14 |
18 files changed, 371 insertions, 300 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index ca3da0cb..1e6128c7 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -3,21 +3,23 @@ import "expo-dev-client"; import { useEffect } from "react"; import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { KeyboardProvider } from "react-native-keyboard-controller"; import { useRouter } from "expo-router"; import { Stack } from "expo-router/stack"; 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 { useColorScheme, useInitialAndroidBarSync } from "@/lib/useColorScheme"; import { cn } from "@/lib/utils"; -import { useColorScheme } from "nativewind"; +import { NAV_THEME } from "@/theme"; +import { ThemeProvider as NavThemeProvider } from "@react-navigation/native"; export default function RootLayout() { + useInitialAndroidBarSync(); const router = useRouter(); const { hasShareIntent } = useShareIntent(); - const { colorScheme, setColorScheme } = useColorScheme(); - const { settings } = useAppSettings(); + const { colorScheme, isDarkColorScheme } = useColorScheme(); useEffect(() => { if (hasShareIntent) { @@ -27,52 +29,55 @@ export default function RootLayout() { } }, [hasShareIntent]); - useEffect(() => { - setColorScheme(settings.theme); - }, [settings.theme]); - return ( <> - <StyledStack - layout={(props) => { - return ( - <GestureHandlerRootView style={{ flex: 1 }}> - <ShareIntentProvider> - <Providers>{props.children}</Providers> - </ShareIntentProvider> - </GestureHandlerRootView> - ); - }} - contentClassName={cn( - "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", - colorScheme == "dark" ? "dark" : "light", - )} - screenOptions={{ - headerTitle: "", - headerTransparent: true, - }} - > - <Stack.Screen name="index" /> - <Stack.Screen - name="signin" - options={{ - headerShown: true, - headerBackVisible: true, - headerBackTitle: "Back", - title: "", - }} - /> - <Stack.Screen name="sharing" /> - <Stack.Screen - name="test-connection" - options={{ - title: "Test Connection", - headerShown: true, - presentation: "modal", - }} - /> - </StyledStack> - <StatusBar style="auto" /> + <KeyboardProvider statusBarTranslucent navigationBarTranslucent> + <NavThemeProvider value={NAV_THEME[colorScheme]}> + <StyledStack + layout={(props) => { + return ( + <GestureHandlerRootView style={{ flex: 1 }}> + <ShareIntentProvider> + <Providers>{props.children}</Providers> + </ShareIntentProvider> + </GestureHandlerRootView> + ); + }} + contentClassName={cn( + "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", + colorScheme == "dark" ? "dark" : "light", + )} + screenOptions={{ + headerTitle: "", + headerTransparent: true, + }} + > + <Stack.Screen name="index" /> + <Stack.Screen + name="signin" + options={{ + headerShown: true, + headerBackVisible: true, + headerBackTitle: "Back", + title: "", + }} + /> + <Stack.Screen name="sharing" /> + <Stack.Screen + name="test-connection" + options={{ + title: "Test Connection", + headerShown: true, + presentation: "modal", + }} + /> + </StyledStack> + </NavThemeProvider> + </KeyboardProvider> + <StatusBar + key={`root-status-bar-${isDarkColorScheme ? "light" : "dark"}`} + style={isDarkColorScheme ? "light" : "dark"} + /> </> ); } diff --git a/apps/mobile/app/dashboard/(tabs)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/_layout.tsx index f1d90ee4..7419c348 100644 --- a/apps/mobile/app/dashboard/(tabs)/_layout.tsx +++ b/apps/mobile/app/dashboard/(tabs)/_layout.tsx @@ -1,9 +1,11 @@ import React, { useLayoutEffect } from "react"; import { Tabs, useNavigation } from "expo-router"; import { StyledTabs } from "@/components/navigation/tabs"; +import { useColorScheme } from "@/lib/useColorScheme"; import { ClipboardList, Home, Settings } from "lucide-react-native"; export default function TabLayout() { + const { colors } = useColorScheme(); const navigation = useNavigation(); // Hide the header on the parent screen useLayoutEffect(() => { @@ -18,6 +20,7 @@ export default function TabLayout() { sceneClassName="bg-gray-100 dark:bg-background" screenOptions={{ headerShown: false, + tabBarActiveTintColor: colors.foreground, }} > <Tabs.Screen diff --git a/apps/mobile/app/dashboard/(tabs)/index.tsx b/apps/mobile/app/dashboard/(tabs)/index.tsx index f70474a9..0a51b817 100644 --- a/apps/mobile/app/dashboard/(tabs)/index.tsx +++ b/apps/mobile/app/dashboard/(tabs)/index.tsx @@ -1,4 +1,4 @@ -import { Platform, Pressable, Text, View } from "react-native"; +import { Platform, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import * as ImagePicker from "expo-image-picker"; import { router } from "expo-router"; @@ -6,6 +6,7 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import useAppSettings from "@/lib/settings"; import { useUploadAsset } from "@/lib/upload"; @@ -89,16 +90,16 @@ export default function Home() { /> </View> <Pressable - className="flex flex-row items-center gap-1 rounded-lg border border-input bg-background px-4 py-2.5" + className="flex flex-row items-center gap-1 rounded-lg border border-input bg-card px-4 py-1" onPress={() => router.push("/dashboard/search")} > <TailwindResolver - className="text-muted-foreground" + className="text-muted" comp={(styles) => ( <Search size={16} color={styles?.color?.toString()} /> )} /> - <Text className="text-muted-foreground">Search</Text> + <Text className="text-muted">Search</Text> </Pressable> </View> } diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index 218c1de4..a2301c36 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -1,15 +1,17 @@ import { useEffect, useState } from "react"; -import { FlatList, Pressable, Text, View } from "react-native"; +import { FlatList, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import { Link, router } from "expo-router"; import FullPageError from "@/components/FullPageError"; -import { TailwindResolver } from "@/components/TailwindResolver"; +import ChevronRight from "@/components/ui/ChevronRight"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; import { api } from "@/lib/trpc"; +import { useColorScheme } from "@/lib/useColorScheme"; import { condProps } from "@/lib/utils"; -import { ChevronRight, Plus } from "lucide-react-native"; +import { Plus } from "lucide-react-native"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils"; @@ -65,6 +67,7 @@ function traverseTree( } export default function Lists() { + const { colors } = useColorScheme(); const [refreshing, setRefreshing] = useState(false); const { data: lists, isPending, error, refetch } = useBookmarkLists(); const [showChildrenOf, setShowChildrenOf] = useState<Record<string, boolean>>( @@ -130,7 +133,7 @@ export default function Lists() { }} renderItem={(l) => ( <View - className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent" + className="mx-2 flex flex-row items-center rounded-xl border border-input bg-card px-4 py-2" style={condProps({ condition: l.item.level > 0, props: { marginLeft: l.item.level * 20 }, @@ -146,28 +149,23 @@ export default function Lists() { })); }} > - <TailwindResolver - className="text-foreground" - comp={(style) => ( - <ChevronRight - color={style?.color?.toString()} - style={{ - transform: [ - { rotate: l.item.collapsed ? "0deg" : "90deg" }, - ], - }} - /> - )} + <ChevronRight + color={colors.foreground} + style={{ + transform: [ + { rotate: l.item.collapsed ? "0deg" : "90deg" }, + ], + }} /> </Pressable> )} <Link asChild key={l.item.id} href={l.item.href} className="flex-1"> <Pressable className="flex flex-row justify-between"> - <Text className="text-lg text-accent-foreground"> + <Text> {l.item.logo} {l.item.name} </Text> - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </Pressable> </Link> </View> diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 7b3dab4f..6d76308d 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -1,16 +1,17 @@ import { useEffect } from "react"; -import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { ActivityIndicator, Pressable, 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 ChevronRight from "@/components/ui/ChevronRight"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; 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(); @@ -38,56 +39,50 @@ 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="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 className="flex w-full gap-3 rounded-lg bg-card px-4 py-2"> + <Text>{isSettingsLoading ? "Loading ..." : settings.address}</Text> <Divider orientation="horizontal" /> - <Text className="text-lg text-accent-foreground"> - {isLoading ? "Loading ..." : data?.email} - </Text> + <Text>{isLoading ? "Loading ..." : data?.email}</Text> </View> <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"> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> <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> + <Text>Theme</Text> <View className="flex flex-row items-center gap-2"> - <Text className="text-lg text-muted-foreground"> + <Text className="text-muted-foreground"> { { light: "Light", dark: "Dark", system: "System" }[ settings.theme ] } </Text> - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </View> </Pressable> </Link> </View> - <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent"> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> <Link asChild href="/dashboard/settings/bookmark-default-view" className="flex-1" > <Pressable className="flex flex-row justify-between"> - <Text className="text-lg text-accent-foreground"> - Default Bookmark View - </Text> + <Text>Default Bookmark View</Text> <View className="flex flex-row items-center gap-2"> {isSettingsLoading ? ( <ActivityIndicator size="small" /> ) : ( - <Text className="text-lg text-muted-foreground"> + <Text className="text-muted-foreground"> {settings.defaultBookmarkView === "reader" ? "Reader" : "Browser"} </Text> )} - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </View> </Pressable> </Link> @@ -95,8 +90,8 @@ export default function Dashboard() { <Text className="w-full p-1 text-2xl font-bold text-foreground"> Upload 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"> - <Text className="text-lg text-accent-foreground">Image Quality</Text> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> + <Text>Image Quality</Text> <View className="flex flex-1 flex-row items-center justify-center gap-2"> <Text className="text-foreground"> {Math.round(settings.imageQuality * 100)}% @@ -115,7 +110,13 @@ export default function Dashboard() { </View> </View> <Divider orientation="horizontal" /> - <Button className="w-full" label="Log Out" onPress={logout} /> + <Button + androidRootClassName="w-full" + onPress={logout} + variant="destructive" + > + <Text>Log Out</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index eafcfc19..3b1300ca 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -25,6 +25,7 @@ import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useAssetUrl } from "@/lib/hooks"; import useAppSettings from "@/lib/settings"; @@ -296,15 +297,17 @@ function BookmarkTextView({ bookmark }: { bookmark: ZBookmark }) { <View className="flex-1"> {isEditing && ( <View className="absolute right-0 top-0 z-10 m-4 flex flex-row gap-1"> - <Button label="Save" variant="default" onPress={Keyboard.dismiss} /> + <Button onPress={Keyboard.dismiss}> + <Text>Save</Text> + </Button> <Button - label="Discard" - variant="destructive" onPress={() => { setContent(initialText); setIsEditing(false); }} - /> + > + <Text>Discard</Text> + </Button> </View> )} <ScrollView className="flex bg-background p-2"> diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx index af124160..1781ec74 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -1,26 +1,22 @@ import React from "react"; +import { Alert, Pressable, View } from "react-native"; import { - Alert, - Keyboard, - Pressable, - Text, - TouchableWithoutFeedback, - View, -} from "react-native"; -import Animated, { - useAnimatedKeyboard, - useAnimatedStyle, -} from "react-native-reanimated"; + KeyboardAwareScrollView, + KeyboardGestureArea, +} from "react-native-keyboard-controller"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { router, Stack, useLocalSearchParams } from "expo-router"; import TagPill from "@/components/bookmarks/TagPill"; import FullPageError from "@/components/FullPageError"; import { Button } from "@/components/ui/Button"; +import ChevronRight from "@/components/ui/ChevronRight"; import { Divider } from "@/components/ui/Divider"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; import { Skeleton } from "@/components/ui/Skeleton"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; -import { ChevronRight } from "lucide-react-native"; +import { cn } from "@/lib/utils"; import { useAutoRefreshingBookmarkQuery, @@ -30,9 +26,21 @@ import { import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; +function InfoSection({ + className, + ...props +}: React.ComponentProps<typeof View>) { + return ( + <View + className={cn("flex gap-2 rounded-lg bg-card p-3", className)} + {...props} + /> + ); +} + function TagList({ bookmark }: { bookmark: ZBookmark }) { return ( - <View className="flex gap-2 rounded-lg bg-white py-3 dark:bg-accent"> + <InfoSection> {isBookmarkStillTagging(bookmark) ? ( <View className="flex gap-4 pb-3"> <Skeleton className="h-4 w-full" /> @@ -41,7 +49,7 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { ) : ( bookmark.tags.length > 0 && ( <> - <View className="flex flex-row flex-wrap gap-2 rounded-lg bg-background p-2"> + <View className="flex flex-row flex-wrap gap-2 rounded-lg p-2"> {bookmark.tags.map((t) => ( <TagPill key={t.id} tag={t} /> ))} @@ -50,94 +58,104 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { </> ) )} - <Pressable - onPress={() => - router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) - } - className="flex w-full flex-row justify-between gap-3 px-4" - > - <Text className="text-lg text-accent-foreground">Manage Tags</Text> - <ChevronRight color="rgb(0, 122, 255)" /> - </Pressable> - </View> + <View> + <Pressable + onPress={() => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) + } + className="flex w-full flex-row justify-between gap-3" + > + <Text>Manage Tags</Text> + <ChevronRight /> + </Pressable> + </View> + </InfoSection> ); } function ManageLists({ bookmark }: { bookmark: ZBookmark }) { return ( - <View className="flex gap-4"> - <Pressable - onPress={() => - router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`) - } - className="flex w-full flex-row justify-between gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent" - > - <Text className="text-lg text-accent-foreground">Manage Lists</Text> - <ChevronRight color="rgb(0, 122, 255)" /> - </Pressable> - </View> + <InfoSection> + <View> + <Pressable + onPress={() => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`) + } + className="flex w-full flex-row justify-between gap-3 rounded-lg" + > + <Text>Manage Lists</Text> + <ChevronRight /> + </Pressable> + </View> + </InfoSection> ); } function TitleEditor({ - bookmarkId, title, + setTitle, + isPending, }: { - bookmarkId: string; - title: string; + title: string | null | undefined; + setTitle: (title: string | null) => void; + isPending: boolean; }) { - const { mutate, isPending } = useUpdateBookmark(); return ( - <View className="flex gap-4"> + <InfoSection> <Input editable={!isPending} - multiline={true} + multiline={false} numberOfLines={1} - loading={isPending} placeholder="Title" - textAlignVertical="top" - onEndEditing={(ev) => - mutate({ - bookmarkId, - title: ev.nativeEvent.text ? ev.nativeEvent.text : null, - }) - } + onChangeText={(text) => setTitle(text)} defaultValue={title ?? ""} /> - </View> + </InfoSection> ); } -function NotesEditor({ bookmark }: { bookmark: ZBookmark }) { - const { mutate, isPending } = useUpdateBookmark(); +function NotesEditor({ + notes, + setNotes, + isPending, +}: { + notes: string | null | undefined; + setNotes: (title: string | null) => void; + isPending: boolean; +}) { return ( - <View className="flex gap-4"> + <InfoSection> <Input editable={!isPending} multiline={true} - numberOfLines={3} - loading={isPending} placeholder="Notes" + inputClasses="h-24" + onChangeText={(text) => setNotes(text)} textAlignVertical="top" - onEndEditing={(ev) => - mutate({ - bookmarkId: bookmark.id, - note: ev.nativeEvent.text, - }) - } - defaultValue={bookmark.note ?? ""} + defaultValue={notes ?? ""} /> - </View> + </InfoSection> ); } const ViewBookmarkPage = () => { + const insets = useSafeAreaInsets(); const { slug } = useLocalSearchParams(); const { toast } = useToast(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } + const { mutate: editBookmark, isPending: isEditPending } = useUpdateBookmark({ + onSuccess: () => { + toast({ + message: "The bookmark has been updated!", + showProgress: false, + }); + setEditedBookmark({}); + }, + }); + const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { @@ -149,12 +167,6 @@ const ViewBookmarkPage = () => { }, }); - const keyboard = useAnimatedKeyboard(); - - const animatedStyles = useAnimatedStyle(() => ({ - marginBottom: keyboard.height.value, - })); - const { data: bookmark, isPending, @@ -163,6 +175,11 @@ const ViewBookmarkPage = () => { bookmarkId: slug, }); + const [editedBookmark, setEditedBookmark] = React.useState<{ + title?: string | null; + note?: string; + }>({}); + if (isPending) { return <FullPageSpinner />; } @@ -188,6 +205,27 @@ const ViewBookmarkPage = () => { ); }; + const onDone = () => { + const doDone = () => { + if (router.canGoBack()) { + router.back(); + } else { + router.replace("dashboard"); + } + }; + if (Object.keys(editedBookmark).length === 0) { + doDone(); + return; + } + Alert.alert("You have unsaved changes", "Do you still want to leave?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Leave", + onPress: doDone, + }, + ]); + }; + let title = null; switch (bookmark.content.type) { case BookmarkTypes.LINK: @@ -201,56 +239,77 @@ const ViewBookmarkPage = () => { break; } return ( - <View> - <Stack.Screen - options={{ - headerShown: true, - headerTransparent: false, - headerTitle: title ?? "Untitled", - headerRight: () => ( - <Pressable - onPress={() => { - if (router.canGoBack()) { - router.back(); - } else { - router.replace("dashboard"); - } - }} + <KeyboardGestureArea interpolator="ios"> + <KeyboardAwareScrollView + className="p-4" + bottomOffset={8} + keyboardDismissMode="interactive" + contentContainerStyle={{ paddingBottom: insets.bottom }} + > + <Stack.Screen + options={{ + headerShown: true, + headerTransparent: false, + headerTitle: title ?? "Untitled", + headerRight: () => ( + <Pressable onPress={onDone}> + <Text>Done</Text> + </Pressable> + ), + }} + /> + <View className="gap-4"> + <TitleEditor + title={title} + setTitle={(title) => + setEditedBookmark((prev) => ({ ...prev, title })) + } + isPending={isEditPending} + /> + <TagList bookmark={bookmark} /> + <ManageLists bookmark={bookmark} /> + <NotesEditor + notes={bookmark.note} + setNotes={(note) => + setEditedBookmark((prev) => ({ ...prev, note: note ?? "" })) + } + isPending={isEditPending} + /> + <View className="flex justify-between gap-3"> + <Button + onPress={() => + editBookmark({ + bookmarkId: bookmark.id, + ...editedBookmark, + }) + } + disabled={isEditPending} > - <Text className="text-foreground">Done</Text> - </Pressable> - ), - }} - /> - <Animated.ScrollView className="p-4" style={[animatedStyles]}> - <TouchableWithoutFeedback onPress={Keyboard.dismiss}> - <View className="h-screen gap-8 px-2"> - <TitleEditor bookmarkId={bookmark.id} title={title ?? ""} /> - <TagList bookmark={bookmark} /> - <ManageLists bookmark={bookmark} /> - <NotesEditor bookmark={bookmark} /> + <Text>Save</Text> + </Button> <Button - onPress={handleDeleteBookmark} variant="destructive" + onPress={handleDeleteBookmark} disabled={isDeletionPending} - label="Delete" - /> - <View className="gap-2"> - <Text className="items-center text-center"> - Created {bookmark.createdAt.toLocaleString()} - </Text> - {bookmark.modifiedAt && - bookmark.modifiedAt.getTime() !== - bookmark.createdAt.getTime() && ( - <Text className="items-center text-center"> - Modified {bookmark.modifiedAt.toLocaleString()} - </Text> - )} - </View> + > + <Text>Delete</Text> + </Button> + </View> + <View className="gap-2"> + <Text className="items-center text-center"> + Created {bookmark.createdAt.toLocaleString()} + </Text> + {bookmark.modifiedAt && + bookmark.modifiedAt.getTime() !== + bookmark.createdAt.getTime() && ( + <Text className="items-center text-center"> + Modified {bookmark.modifiedAt.toLocaleString()} + </Text> + )} </View> - </TouchableWithoutFeedback> - </Animated.ScrollView> - </View> + </View> + </KeyboardAwareScrollView> + </KeyboardGestureArea> ); }; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx index 9f2149ae..7250d06b 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -1,8 +1,9 @@ import React from "react"; -import { FlatList, Pressable, Text, View } from "react-native"; +import { FlatList, Pressable, View } from "react-native"; import Checkbox from "expo-checkbox"; import { useLocalSearchParams } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { @@ -75,13 +76,13 @@ const ListPickerPage = () => { gap: 5, }} renderItem={(l) => ( - <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent"> + <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-card px-4 py-2"> <Pressable key={l.item[l.item.length - 1].id} onPress={() => toggleList(l.item[l.item.length - 1].id)} className="flex w-full flex-row justify-between" > - <Text className="text-lg text-accent-foreground"> + <Text> {l.item.map((item) => `${item.icon} ${item.name}`).join(" / ")} </Text> <Checkbox diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx index 38296626..ea6c2f4d 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx @@ -1,16 +1,11 @@ import React, { useMemo } from "react"; -import { - Pressable, - SectionList, - Text, - TouchableOpacity, - View, -} from "react-native"; +import { Pressable, SectionList, TouchableOpacity, View } from "react-native"; import { Stack, useLocalSearchParams } from "expo-router"; -import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; +import { useColorScheme } from "@/lib/useColorScheme"; import { Check, Plus } from "lucide-react-native"; import { @@ -22,6 +17,7 @@ import { api } from "@karakeep/shared-react/trpc"; const NEW_TAG_ID = "new-tag"; const ListPickerPage = () => { + const { colors } = useColorScheme(); const { slug: bookmarkId } = useLocalSearchParams(); const [search, setSearch] = React.useState(""); @@ -211,20 +207,14 @@ const ListPickerPage = () => { }) } > - <View className="mx-2 flex flex-row items-center gap-2 rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent"> + <View className="mx-2 flex flex-row items-center gap-2 rounded-xl border border-input bg-card px-4 py-2"> {t.section.title == "Existing Tags" && ( - <TailwindResolver - className="text-accent-foreground" - comp={(s) => <Check color={s?.color} />} - /> + <Check color={colors.foreground} /> )} {t.section.title == "All Tags" && t.item.id == NEW_TAG_ID && ( - <TailwindResolver - className="text-accent-foreground" - comp={(s) => <Plus color={s?.color} />} - /> + <Plus color={colors.foreground} /> )} - <Text className="text-center text-lg text-accent-foreground"> + <Text> {t.item.id == NEW_TAG_ID ? `Create new tag '${t.item.name}'` : t.item.name} diff --git a/apps/mobile/app/dashboard/bookmarks/new.tsx b/apps/mobile/app/dashboard/bookmarks/new.tsx index d24c1597..50f8f2a7 100644 --- a/apps/mobile/app/dashboard/bookmarks/new.tsx +++ b/apps/mobile/app/dashboard/bookmarks/new.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { Text, View } from "react-native"; +import { View } from "react-native"; import { router } from "expo-router"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useCreateBookmark } from "@karakeep/shared-react/hooks/bookmarks"; @@ -61,13 +62,16 @@ const NoteEditorPage = () => { )} <Input onChangeText={setText} + className="bg-card" multiline placeholder="What's on your mind?" autoFocus autoCapitalize={"none"} textAlignVertical="top" /> - <Button onPress={onSubmit} label="Save" /> + <Button onPress={onSubmit}> + <Text>Save</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/lists/new.tsx b/apps/mobile/app/dashboard/lists/new.tsx index 2cd690f5..55315e70 100644 --- a/apps/mobile/app/dashboard/lists/new.tsx +++ b/apps/mobile/app/dashboard/lists/new.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { Text, View } from "react-native"; +import { View } from "react-native"; import { router } from "expo-router"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useCreateBookmarkList } from "@karakeep/shared-react/hooks/lists"; @@ -40,14 +41,16 @@ const NewListPage = () => { <View className="flex flex-row items-center gap-1"> <Text className="shrink p-2">🚀</Text> <Input - className="flex-1" + className="flex-1 bg-card" onChangeText={setText} placeholder="List Name" autoFocus autoCapitalize={"none"} /> </View> - <Button disabled={isPending} onPress={onSubmit} label="Save" /> + <Button disabled={isPending} onPress={onSubmit}> + <Text>Save</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/search.tsx b/apps/mobile/app/dashboard/search.tsx index 5cc97575..66423870 100644 --- a/apps/mobile/app/dashboard/search.tsx +++ b/apps/mobile/app/dashboard/search.tsx @@ -1,18 +1,12 @@ import { useMemo, useRef, useState } from "react"; -import { - FlatList, - Keyboard, - Pressable, - Text, - TextInput, - View, -} from "react-native"; -import { router } from "expo-router"; +import { FlatList, Keyboard, Pressable, TextInput, View } from "react-native"; +import { router, Stack } from "expo-router"; import BookmarkList from "@/components/bookmarks/BookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { Input } from "@/components/ui/Input"; +import { SearchInput } from "@/components/ui/SearchInput"; +import { Text } from "@/components/ui/Text"; import { api } from "@/lib/trpc"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { keepPreviousData } from "@tanstack/react-query"; @@ -102,24 +96,26 @@ export default function Search() { return ( <CustomSafeAreaView> - <View className="flex flex-row items-center gap-3 p-3"> - <Input - ref={inputRef} - placeholder="Search" - className="flex-1" - value={search} - onChangeText={setSearch} - onFocus={handleOnFocus} - onBlur={handleOnBlur} - onSubmitEditing={() => handleSearchSubmit(search)} - returnKeyType="search" - autoFocus - autoCapitalize="none" - /> - <Pressable onPress={() => router.back()}> - <Text className="text-foreground">Cancel</Text> - </Pressable> - </View> + <Stack.Screen + options={{ + headerShown: true, + }} + /> + <SearchInput + containerClassName="m-3" + ref={inputRef} + placeholder="Search" + className="flex-1" + value={search} + onChangeText={setSearch} + onFocus={handleOnFocus} + onBlur={handleOnBlur} + onSubmitEditing={() => handleSearchSubmit(search)} + returnKeyType="search" + autoFocus + autoCapitalize="none" + onCancel={router.back} + /> {isInputFocused ? ( <FlatList diff --git a/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx b/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx index c8c522cf..5f4463ae 100644 --- a/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx +++ b/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx @@ -1,7 +1,8 @@ -import { Pressable, Text, View } from "react-native"; +import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import useAppSettings from "@/lib/settings"; import { Check } from "lucide-react-native"; @@ -41,9 +42,7 @@ export default function BookmarkDefaultViewSettings() { className="flex flex-row justify-between" key={mode} > - <Text className="text-lg text-accent-foreground"> - {{ browser: "Browser", reader: "Reader" }[mode]} - </Text> + <Text>{{ browser: "Browser", reader: "Reader" }[mode]}</Text> {isChecked && <Check color="rgb(0, 122, 255)" />} </Pressable>, <Divider @@ -59,7 +58,7 @@ export default function BookmarkDefaultViewSettings() { 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"> + <View className="w-full rounded-lg bg-card bg-card px-4 py-2"> {options} </View> </View> diff --git a/apps/mobile/app/dashboard/settings/theme.tsx b/apps/mobile/app/dashboard/settings/theme.tsx index f7feacdb..a4f0494a 100644 --- a/apps/mobile/app/dashboard/settings/theme.tsx +++ b/apps/mobile/app/dashboard/settings/theme.tsx @@ -1,6 +1,7 @@ -import { Pressable, Text, View } from "react-native"; +import { Pressable, View } from "react-native"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { Check } from "lucide-react-native"; @@ -16,7 +17,7 @@ export default function ThemePage() { className="flex flex-row justify-between" key={theme} > - <Text className="text-lg text-accent-foreground"> + <Text> { { light: "Light Mode", dark: "Dark Mode", system: "System" }[ theme @@ -38,9 +39,7 @@ export default function ThemePage() { 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 className="w-full rounded-lg bg-card px-4 py-2">{options}</View> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/error.tsx b/apps/mobile/app/error.tsx index d0e4a7df..6e975306 100644 --- a/apps/mobile/app/error.tsx +++ b/apps/mobile/app/error.tsx @@ -1,9 +1,10 @@ -import { Text, View } from "react-native"; +import { View } from "react-native"; +import { Text } from "@/components/ui/Text"; export default function ErrorPage() { return ( <View className="flex-1 items-center justify-center gap-4"> - <Text className="text-4xl">Error!</Text> + <Text variant="largeTitle">Error!</Text> </View> ); } diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 506b5100..1e5df4b8 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { ActivityIndicator, Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { useShareIntentContext } from "expo-share-intent"; import { Button } from "@/components/ui/Button"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { useUploadAsset } from "@/lib/upload"; @@ -73,7 +74,7 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { return ( <View className="flex flex-row gap-3"> - <Text className="text-4xl text-foreground">Hoarding</Text> + <Text variant="largeTitle">Hoarding</Text> <ActivityIndicator /> </View> ); @@ -95,18 +96,19 @@ export default function Sharing() { case "success": { comp = ( <View className="items-center gap-4"> - <Text className="text-4xl text-foreground"> + <Text variant="largeTitle"> {mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"} </Text> <Button - label="Manage" onPress={() => { router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`); if (autoCloseTimeoutId.current) { clearTimeout(autoCloseTimeoutId.current); } }} - /> + > + <Text>Manage</Text> + </Button> <Pressable onPress={() => router.replace("dashboard")}> <Text className="text-muted-foreground">Dismiss</Text> </Pressable> @@ -115,7 +117,7 @@ export default function Sharing() { break; } case "error": { - comp = <Text className="text-4xl text-foreground">Error!</Text>; + comp = <Text variant="largeTitle">Error!</Text>; break; } } diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 0d160398..215b6a67 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -4,18 +4,17 @@ import { KeyboardAvoidingView, Platform, Pressable, - Text, TouchableWithoutFeedback, View, } from "react-native"; import { Redirect, useRouter } from "expo-router"; import Logo from "@/components/Logo"; import { TailwindResolver } from "@/components/TailwindResolver"; -import { Button, buttonVariants } from "@/components/ui/Button"; +import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { cn } from "@/lib/utils"; import { Bug } from "lucide-react-native"; enum LoginType { @@ -134,6 +133,7 @@ export default function Signin() { <Text className="font-bold">Server Address</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Server Address" value={formState.serverAddress} autoCapitalize="none" @@ -150,6 +150,7 @@ export default function Signin() { <Text className="font-bold">Email</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Email" keyboardType="email-address" autoCapitalize="none" @@ -163,6 +164,7 @@ export default function Signin() { <Text className="font-bold">Password</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Password" secureTextEntry value={formState.password} @@ -181,6 +183,7 @@ export default function Signin() { <Text className="font-bold">API Key</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="API Key" secureTextEntry value={formState.apiKey} @@ -193,18 +196,17 @@ export default function Signin() { <View className="flex flex-row items-center justify-between gap-2"> <Button - className="flex-1" - label="Sign In" + size="lg" + androidRootClassName="flex-1" onPress={onSignin} disabled={ userNamePasswordRequestIsPending || apiKeyValueRequestIsPending } - /> - <Pressable - className={cn( - buttonVariants({ variant: "default" }), - !settings.address && "bg-gray-500", - )} + > + <Text>Sign In</Text> + </Button> + <Button + size="icon" onPress={() => router.push("/test-connection")} disabled={!settings.address} > @@ -212,9 +214,9 @@ export default function Signin() { comp={(styles) => ( <Bug size={20} color={styles?.color?.toString()} /> )} - className="text-background" + className="text-white" /> - </Pressable> + </Button> </View> <Pressable onPress={toggleLoginType}> <Text className="mt-2 text-center text-gray-500"> diff --git a/apps/mobile/app/test-connection.tsx b/apps/mobile/app/test-connection.tsx index 5639c6bd..a9ec6e5e 100644 --- a/apps/mobile/app/test-connection.tsx +++ b/apps/mobile/app/test-connection.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { Platform, Text, View } from "react-native"; +import { Platform, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { cn } from "@/lib/utils"; import { z } from "zod"; @@ -79,19 +80,22 @@ export default function TestConnection() { <View className="m-4 flex flex-col gap-2 p-2"> <Button className="w-full" - label="Copy Diagnostics Result" onPress={async () => { await Clipboard.setStringAsync(text); }} - /> + > + <Text>Copy Diagnostics Result</Text> + </Button> <Button className="w-full" - label="Retry" + variant="secondary" onPress={() => { setText(""); setRandomId(Math.random()); }} - /> + > + <Text>Retry</Text> + </Button> <View className={cn( "w-full rounded-md p-2", |
