diff options
| author | Mohamed Bassem <me@mbassem.com> | 2026-02-08 22:45:32 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2026-02-09 00:17:31 +0000 |
| commit | e455e46852900c6d2b3e77b7a77e1b9da41b2ca8 (patch) | |
| tree | 2d2042bd43d704b6432332c1465619b4b907dc71 /apps/mobile/app/dashboard | |
| parent | 4186c4c64c68892248ce8671d9b8e67fc7f884a0 (diff) | |
| download | karakeep-e455e46852900c6d2b3e77b7a77e1b9da41b2ca8.tar.zst | |
feat(mobile): more native screens
Diffstat (limited to 'apps/mobile/app/dashboard')
15 files changed, 534 insertions, 405 deletions
diff --git a/apps/mobile/app/dashboard/(tabs)/(highlights)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/(highlights)/_layout.tsx new file mode 100644 index 00000000..961df836 --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(highlights)/_layout.tsx @@ -0,0 +1,18 @@ +import { Stack } from "expo-router/stack"; + +export default function Layout() { + return ( + <Stack + screenOptions={{ + headerLargeTitle: true, + headerTransparent: true, + headerBlurEffect: "systemMaterial", + headerShadowVisible: false, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { backgroundColor: "transparent" }, + }} + > + <Stack.Screen name="index" options={{ title: "Highlights" }} /> + </Stack> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/highlights.tsx b/apps/mobile/app/dashboard/(tabs)/(highlights)/index.tsx index 8d6e37a4..48a190c1 100644 --- a/apps/mobile/app/dashboard/(tabs)/highlights.tsx +++ b/apps/mobile/app/dashboard/(tabs)/(highlights)/index.tsx @@ -1,9 +1,6 @@ -import { View } from "react-native"; import FullPageError from "@/components/FullPageError"; import HighlightList from "@/components/highlights/HighlightList"; -import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import PageTitle from "@/components/ui/PageTitle"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "@karakeep/shared-react/trpc"; @@ -42,19 +39,12 @@ export default function Highlights() { }; return ( - <CustomSafeAreaView edges={["top"]}> - <HighlightList - highlights={data.pages.flatMap((p) => p.highlights)} - header={ - <View className="flex flex-row justify-between"> - <PageTitle title="Highlights" /> - </View> - } - onRefresh={onRefresh} - fetchNextPage={fetchNextPage} - isFetchingNextPage={isFetchingNextPage} - isRefreshing={isPending || isPlaceholderData} - /> - </CustomSafeAreaView> + <HighlightList + highlights={data.pages.flatMap((p) => p.highlights)} + onRefresh={onRefresh} + fetchNextPage={fetchNextPage} + isFetchingNextPage={isFetchingNextPage} + isRefreshing={isPending || isPlaceholderData} + /> ); } diff --git a/apps/mobile/app/dashboard/(tabs)/(home)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/(home)/_layout.tsx new file mode 100644 index 00000000..1ba65211 --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(home)/_layout.tsx @@ -0,0 +1,18 @@ +import { Stack } from "expo-router/stack"; + +export default function Layout() { + return ( + <Stack + screenOptions={{ + headerLargeTitle: true, + headerTransparent: true, + headerBlurEffect: "systemMaterial", + headerShadowVisible: false, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { backgroundColor: "transparent" }, + }} + > + <Stack.Screen name="index" options={{ title: "Home" }} /> + </Stack> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/index.tsx b/apps/mobile/app/dashboard/(tabs)/(home)/index.tsx index 3e49e6f2..65034419 100644 --- a/apps/mobile/app/dashboard/(tabs)/index.tsx +++ b/apps/mobile/app/dashboard/(tabs)/(home)/index.tsx @@ -1,11 +1,9 @@ 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"; +import { router, Stack } from "expo-router"; 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"; @@ -76,34 +74,35 @@ function HeaderRight({ export default function Home() { return ( - <CustomSafeAreaView edges={["top"]}> + <> + <Stack.Screen + options={{ + headerRight: () => ( + <HeaderRight + openNewBookmarkModal={() => + router.push("/dashboard/bookmarks/new") + } + /> + ), + }} + /> <UpdatingBookmarkList query={{ archived: false }} header={ - <View className="flex flex-col gap-1"> - <View className="flex flex-row justify-between"> - <PageTitle title="Home" className="pb-2" /> - <HeaderRight - openNewBookmarkModal={() => - router.push("/dashboard/bookmarks/new") - } - /> - </View> - <Pressable - 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" - comp={(styles) => ( - <Search size={16} color={styles?.color?.toString()} /> - )} - /> - <Text className="text-muted">Search</Text> - </Pressable> - </View> + <Pressable + 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" + comp={(styles) => ( + <Search size={16} color={styles?.color?.toString()} /> + )} + /> + <Text className="text-muted">Search</Text> + </Pressable> } /> - </CustomSafeAreaView> + </> ); } diff --git a/apps/mobile/app/dashboard/(tabs)/(lists)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/(lists)/_layout.tsx new file mode 100644 index 00000000..398ba650 --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(lists)/_layout.tsx @@ -0,0 +1,18 @@ +import { Stack } from "expo-router/stack"; + +export default function Layout() { + return ( + <Stack + screenOptions={{ + headerLargeTitle: true, + headerTransparent: true, + headerBlurEffect: "systemMaterial", + headerShadowVisible: false, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { backgroundColor: "transparent" }, + }} + > + <Stack.Screen name="index" options={{ title: "Lists" }} /> + </Stack> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/(lists)/index.tsx index 3f81a36e..4c98ef2c 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/(lists)/index.tsx @@ -1,12 +1,10 @@ import { useEffect, useMemo, useState } from "react"; import { FlatList, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; -import { Link, router } from "expo-router"; +import { Link, router, Stack } from "expo-router"; import FullPageError from "@/components/FullPageError"; 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 { useColorScheme } from "@/lib/useColorScheme"; import { condProps } from "@/lib/utils"; @@ -186,27 +184,33 @@ export default function Lists() { }); return ( - <CustomSafeAreaView edges={["top"]}> - <FlatList - className="h-full" - ListHeaderComponent={ - <View className="flex flex-row justify-between"> - <PageTitle title="Lists" /> + <> + <Stack.Screen + options={{ + headerRight: () => ( <HeaderRight openNewListModal={() => router.push("/dashboard/lists/new")} /> - </View> - } + ), + }} + /> + <FlatList + className="h-full" + contentInsetAdjustmentBehavior="automatic" contentContainerStyle={{ gap: 6, + paddingBottom: 20, }} renderItem={(l) => ( <View className="mx-2 flex flex-row items-center rounded-xl bg-card px-4 py-2" - style={condProps({ - condition: l.item.level > 0, - props: { marginLeft: l.item.level * 20 }, - })} + style={{ + borderCurve: "continuous", + ...condProps({ + condition: l.item.level > 0, + props: { marginLeft: l.item.level * 20 }, + }), + }} > {hasAnyListsWithChildren && ( <View style={{ width: 32 }}> @@ -275,6 +279,6 @@ export default function Lists() { refreshing={refreshing} onRefresh={onRefresh} /> - </CustomSafeAreaView> + </> ); } diff --git a/apps/mobile/app/dashboard/(tabs)/(settings)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/(settings)/_layout.tsx new file mode 100644 index 00000000..8c51d5a3 --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(settings)/_layout.tsx @@ -0,0 +1,18 @@ +import { Stack } from "expo-router/stack"; + +export default function Layout() { + return ( + <Stack + screenOptions={{ + headerLargeTitle: true, + headerTransparent: true, + headerBlurEffect: "systemMaterial", + headerShadowVisible: false, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { backgroundColor: "transparent" }, + }} + > + <Stack.Screen name="index" options={{ title: "Settings" }} /> + </Stack> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx b/apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx new file mode 100644 index 00000000..de17ff5a --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx @@ -0,0 +1,225 @@ +import { useEffect } from "react"; +import { + ActivityIndicator, + Pressable, + ScrollView, + Switch, + View, +} from "react-native"; +import { Slider } from "react-native-awesome-slider"; +import { useSharedValue } from "react-native-reanimated"; +import Constants from "expo-constants"; +import { Link } from "expo-router"; +import { UserProfileHeader } from "@/components/settings/UserProfileHeader"; +import ChevronRight from "@/components/ui/ChevronRight"; +import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; +import { useServerVersion } from "@/lib/hooks"; +import { useSession } from "@/lib/session"; +import useAppSettings from "@/lib/settings"; +import { useQuery } from "@tanstack/react-query"; + +import { useTRPC } from "@karakeep/shared-react/trpc"; + +function SectionHeader({ title }: { title: string }) { + return ( + <Text className="px-4 pb-1 pt-4 text-xs uppercase tracking-wide text-muted-foreground"> + {title} + </Text> + ); +} + +export default function Settings() { + const { logout } = useSession(); + const { + settings, + setSettings, + isLoading: isSettingsLoading, + } = useAppSettings(); + const api = useTRPC(); + + const imageQuality = useSharedValue(0); + const imageQualityMin = useSharedValue(0); + const imageQualityMax = useSharedValue(100); + + useEffect(() => { + imageQuality.value = settings.imageQuality * 100; + }, [settings]); + + const { data, error } = useQuery(api.users.whoami.queryOptions()); + const { + data: serverVersion, + isLoading: isServerVersionLoading, + error: serverVersionError, + } = useServerVersion(); + + if (error?.data?.code === "UNAUTHORIZED") { + logout(); + } + + return ( + <ScrollView + contentInsetAdjustmentBehavior="automatic" + contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }} + > + <UserProfileHeader + image={data?.image} + name={data?.name} + email={data?.email} + /> + + <SectionHeader title="Appearance" /> + <View + className="w-full rounded-xl bg-card py-2" + style={{ borderCurve: "continuous" }} + > + <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> + <Link asChild href="/dashboard/settings/theme" className="flex-1"> + <Pressable className="flex flex-row justify-between"> + <Text>Theme</Text> + <View className="flex flex-row items-center gap-2"> + <Text className="text-muted-foreground"> + { + { light: "Light", dark: "Dark", system: "System" }[ + settings.theme + ] + } + </Text> + <ChevronRight /> + </View> + </Pressable> + </Link> + </View> + <Divider orientation="horizontal" className="mx-6 my-1" /> + <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> + <Link + asChild + href="/dashboard/settings/bookmark-default-view" + className="flex-1" + > + <Pressable className="flex flex-row justify-between"> + <Text>Default Bookmark View</Text> + <View className="flex flex-row items-center gap-2"> + {isSettingsLoading ? ( + <ActivityIndicator size="small" /> + ) : ( + <Text className="text-muted-foreground"> + {settings.defaultBookmarkView === "reader" + ? "Reader" + : "Browser"} + </Text> + )} + <ChevronRight /> + </View> + </Pressable> + </Link> + </View> + </View> + + <SectionHeader title="Reading" /> + <View + className="w-full rounded-xl bg-card py-2" + style={{ borderCurve: "continuous" }} + > + <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> + <Link + asChild + href="/dashboard/settings/reader-settings" + className="flex-1" + > + <Pressable className="flex flex-row justify-between"> + <Text>Reader Text Settings</Text> + <ChevronRight /> + </Pressable> + </Link> + </View> + <Divider orientation="horizontal" className="mx-6 my-1" /> + <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> + <Text className="flex-1" numberOfLines={1}> + Show notes in bookmark card + </Text> + <Switch + className="shrink-0" + value={settings.showNotes} + onValueChange={(value) => + setSettings({ + ...settings, + showNotes: value, + }) + } + /> + </View> + </View> + + <SectionHeader title="Media" /> + <View + className="w-full rounded-xl bg-card py-2" + style={{ borderCurve: "continuous" }} + > + <View className="flex w-full flex-row items-center justify-between gap-8 px-4 py-1"> + <Text>Upload 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)}% + </Text> + <Slider + onSlidingComplete={(value) => + setSettings({ + ...settings, + imageQuality: Math.round(value) / 100, + }) + } + progress={imageQuality} + minimumValue={imageQualityMin} + maximumValue={imageQualityMax} + /> + </View> + </View> + </View> + + <SectionHeader title="Account" /> + <View + className="w-full rounded-xl bg-card py-2" + style={{ borderCurve: "continuous" }} + > + <Pressable + className="flex flex-row items-center px-4 py-1" + onPress={logout} + > + <Text className="text-destructive">Log Out</Text> + </Pressable> + </View> + + <SectionHeader title="About" /> + <View + className="w-full rounded-xl bg-card py-2" + style={{ borderCurve: "continuous" }} + > + <View className="flex flex-row items-center justify-between px-4 py-1"> + <Text className="text-muted-foreground">Server</Text> + <Text className="text-sm text-muted-foreground"> + {isSettingsLoading ? "Loading..." : settings.address} + </Text> + </View> + <Divider orientation="horizontal" className="mx-6 my-1" /> + <View className="flex flex-row items-center justify-between px-4 py-1"> + <Text className="text-muted-foreground">App Version</Text> + <Text className="text-sm text-muted-foreground"> + {Constants.expoConfig?.version ?? "unknown"} + </Text> + </View> + <Divider orientation="horizontal" className="mx-6 my-1" /> + <View className="flex flex-row items-center justify-between px-4 py-1"> + <Text className="text-muted-foreground">Server Version</Text> + <Text className="text-sm text-muted-foreground"> + {isServerVersionLoading + ? "Loading..." + : serverVersionError + ? "unavailable" + : (serverVersion ?? "unknown")} + </Text> + </View> + </View> + </ScrollView> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/(tags)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/(tags)/_layout.tsx new file mode 100644 index 00000000..3b56548f --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(tags)/_layout.tsx @@ -0,0 +1,18 @@ +import { Stack } from "expo-router/stack"; + +export default function Layout() { + return ( + <Stack + screenOptions={{ + headerLargeTitle: true, + headerTransparent: true, + headerBlurEffect: "systemMaterial", + headerShadowVisible: false, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { backgroundColor: "transparent" }, + }} + > + <Stack.Screen name="index" options={{ title: "Tags" }} /> + </Stack> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx b/apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx new file mode 100644 index 00000000..4903d681 --- /dev/null +++ b/apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from "react"; +import { FlatList, Pressable, View } from "react-native"; +import { Link } from "expo-router"; +import FullPageError from "@/components/FullPageError"; +import ChevronRight from "@/components/ui/ChevronRight"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { SearchInput } from "@/components/ui/SearchInput"; +import { Text } from "@/components/ui/Text"; +import { useQueryClient } from "@tanstack/react-query"; + +import { usePaginatedSearchTags } from "@karakeep/shared-react/hooks/tags"; +import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; +import { useTRPC } from "@karakeep/shared-react/trpc"; + +interface TagItem { + id: string; + name: string; + numBookmarks: number; + href: string; +} + +export default function Tags() { + const [refreshing, setRefreshing] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const api = useTRPC(); + const queryClient = useQueryClient(); + + // Debounce search query to avoid too many API calls + const debouncedSearch = useDebounce(searchQuery, 300); + + // Fetch tags sorted by usage (most used first) + const { + data, + isPending, + error, + refetch, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = usePaginatedSearchTags({ + limit: 50, + sortBy: debouncedSearch ? "relevance" : "usage", + nameContains: debouncedSearch, + }); + + useEffect(() => { + setRefreshing(isPending); + }, [isPending]); + + if (error) { + return <FullPageError error={error.message} onRetry={() => refetch()} />; + } + + if (!data) { + return <FullPageSpinner />; + } + + const onRefresh = () => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + }; + + const tags: TagItem[] = data.tags.map((tag) => ({ + id: tag.id, + name: tag.name, + numBookmarks: tag.numBookmarks, + href: `/dashboard/tags/${tag.id}`, + })); + + const handleLoadMore = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + + return ( + <FlatList + className="h-full" + contentInsetAdjustmentBehavior="automatic" + ListHeaderComponent={ + <SearchInput + containerClassName="mx-2 mb-2" + placeholder="Search tags..." + value={searchQuery} + onChangeText={setSearchQuery} + /> + } + contentContainerStyle={{ + gap: 6, + paddingBottom: 20, + }} + renderItem={(item) => ( + <View + className="mx-2 flex flex-row items-center rounded-xl bg-card px-4 py-2" + style={{ borderCurve: "continuous" }} + > + <Link + asChild + key={item.item.id} + href={item.item.href} + className="flex-1" + > + <Pressable className="flex flex-row items-center justify-between"> + <View className="flex-1"> + <Text className="font-medium">{item.item.name}</Text> + <Text className="text-sm text-muted-foreground"> + {item.item.numBookmarks}{" "} + {item.item.numBookmarks === 1 ? "bookmark" : "bookmarks"} + </Text> + </View> + <ChevronRight /> + </Pressable> + </Link> + </View> + )} + data={tags} + refreshing={refreshing} + onRefresh={onRefresh} + onEndReached={handleLoadMore} + onEndReachedThreshold={0.5} + ListFooterComponent={ + isFetchingNextPage ? ( + <View className="py-4"> + <Text className="text-center text-muted-foreground"> + Loading more... + </Text> + </View> + ) : null + } + ListEmptyComponent={ + !isPending ? ( + <View className="py-8"> + <Text className="text-center text-muted-foreground"> + No tags yet + </Text> + </View> + ) : null + } + /> + ); +} diff --git a/apps/mobile/app/dashboard/(tabs)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/_layout.tsx index f3db822e..fd5798b9 100644 --- a/apps/mobile/app/dashboard/(tabs)/_layout.tsx +++ b/apps/mobile/app/dashboard/(tabs)/_layout.tsx @@ -12,7 +12,7 @@ export default function TabLayout() { const { colors } = useColorScheme(); return ( <NativeTabs backgroundColor={colors.grey6} minimizeBehavior="onScrollDown"> - <NativeTabs.Trigger name="index"> + <NativeTabs.Trigger name="(home)"> <Icon sf="house.fill" androidSrc={ @@ -22,7 +22,7 @@ export default function TabLayout() { <Label>Home</Label> </NativeTabs.Trigger> - <NativeTabs.Trigger name="lists"> + <NativeTabs.Trigger name="(lists)"> <Icon sf="list.clipboard.fill" androidSrc={ @@ -32,7 +32,7 @@ export default function TabLayout() { <Label>Lists</Label> </NativeTabs.Trigger> - <NativeTabs.Trigger name="tags"> + <NativeTabs.Trigger name="(tags)"> <Icon sf="tag.fill" androidSrc={<VectorIcon family={MaterialCommunityIcons} name="tag" />} @@ -40,7 +40,7 @@ export default function TabLayout() { <Label>Tags</Label> </NativeTabs.Trigger> - <NativeTabs.Trigger name="highlights"> + <NativeTabs.Trigger name="(highlights)"> <Icon sf="highlighter" androidSrc={ @@ -50,7 +50,7 @@ export default function TabLayout() { <Label>Highlights</Label> </NativeTabs.Trigger> - <NativeTabs.Trigger name="settings"> + <NativeTabs.Trigger name="(settings)"> <Icon sf="gearshape.fill" androidSrc={<VectorIcon family={MaterialCommunityIcons} name="cog" />} diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx deleted file mode 100644 index ba38d9e6..00000000 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useEffect } from "react"; -import { ActivityIndicator, Pressable, Switch, View } from "react-native"; -import { Slider } from "react-native-awesome-slider"; -import { useSharedValue } from "react-native-reanimated"; -import Constants from "expo-constants"; -import { Link } from "expo-router"; -import { UserProfileHeader } from "@/components/settings/UserProfileHeader"; -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 { Text } from "@/components/ui/Text"; -import { useServerVersion } from "@/lib/hooks"; -import { useSession } from "@/lib/session"; -import useAppSettings from "@/lib/settings"; -import { useQuery } from "@tanstack/react-query"; - -import { useTRPC } from "@karakeep/shared-react/trpc"; - -export default function Dashboard() { - const { logout } = useSession(); - const { - settings, - setSettings, - isLoading: isSettingsLoading, - } = useAppSettings(); - const api = useTRPC(); - - const imageQuality = useSharedValue(0); - const imageQualityMin = useSharedValue(0); - const imageQualityMax = useSharedValue(100); - - useEffect(() => { - imageQuality.value = settings.imageQuality * 100; - }, [settings]); - - const { data, error } = useQuery(api.users.whoami.queryOptions()); - const { - data: serverVersion, - isLoading: isServerVersionLoading, - error: serverVersionError, - } = useServerVersion(); - - if (error?.data?.code === "UNAUTHORIZED") { - logout(); - } - - return ( - <CustomSafeAreaView edges={["top"]}> - <UserProfileHeader - image={data?.image} - name={data?.name} - email={data?.email} - /> - <View className="flex h-full w-full items-center gap-3 px-4 py-2"> - <View className="w-full rounded-xl bg-card py-2"> - <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> - <Link asChild href="/dashboard/settings/theme" className="flex-1"> - <Pressable className="flex flex-row justify-between"> - <Text>Theme</Text> - <View className="flex flex-row items-center gap-2"> - <Text className="text-muted-foreground"> - { - { light: "Light", dark: "Dark", system: "System" }[ - settings.theme - ] - } - </Text> - <ChevronRight /> - </View> - </Pressable> - </Link> - </View> - <Divider orientation="horizontal" className="mx-6 my-1" /> - <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> - <Link - asChild - href="/dashboard/settings/bookmark-default-view" - className="flex-1" - > - <Pressable className="flex flex-row justify-between"> - <Text>Default Bookmark View</Text> - <View className="flex flex-row items-center gap-2"> - {isSettingsLoading ? ( - <ActivityIndicator size="small" /> - ) : ( - <Text className="text-muted-foreground"> - {settings.defaultBookmarkView === "reader" - ? "Reader" - : "Browser"} - </Text> - )} - <ChevronRight /> - </View> - </Pressable> - </Link> - </View> - <Divider orientation="horizontal" className="mx-6 my-1" /> - <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> - <Link - asChild - href="/dashboard/settings/reader-settings" - className="flex-1" - > - <Pressable className="flex flex-row justify-between"> - <Text>Reader Text Settings</Text> - <ChevronRight /> - </Pressable> - </Link> - </View> - <Divider orientation="horizontal" className="mx-6 my-1" /> - <View className="flex flex-row items-center justify-between gap-8 px-4 py-1"> - <Text className="flex-1" numberOfLines={1}> - Show notes in bookmark card - </Text> - <Switch - className="shrink-0" - value={settings.showNotes} - onValueChange={(value) => - setSettings({ - ...settings, - showNotes: value, - }) - } - /> - </View> - </View> - - <View className="w-full rounded-xl bg-card py-2"> - <View className="flex w-full flex-row items-center justify-between gap-8 px-4 py-1"> - <Text>Upload 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)}% - </Text> - <Slider - onSlidingComplete={(value) => - setSettings({ - ...settings, - imageQuality: Math.round(value) / 100, - }) - } - progress={imageQuality} - minimumValue={imageQualityMin} - maximumValue={imageQualityMax} - /> - </View> - </View> - </View> - <Button - androidRootClassName="w-full" - onPress={logout} - variant="destructive" - > - <Text>Log Out</Text> - </Button> - <View className="mt-4 w-full gap-1"> - <Text className="text-center text-sm text-muted-foreground"> - {isSettingsLoading ? "Loading..." : settings.address} - </Text> - <Text className="text-center text-sm text-muted-foreground"> - App Version: {Constants.expoConfig?.version ?? "unknown"} - </Text> - <Text className="text-center text-sm text-muted-foreground"> - Server Version:{" "} - {isServerVersionLoading - ? "Loading..." - : serverVersionError - ? "unavailable" - : (serverVersion ?? "unknown")} - </Text> - </View> - </View> - </CustomSafeAreaView> - ); -} diff --git a/apps/mobile/app/dashboard/(tabs)/tags.tsx b/apps/mobile/app/dashboard/(tabs)/tags.tsx deleted file mode 100644 index 8a629305..00000000 --- a/apps/mobile/app/dashboard/(tabs)/tags.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useEffect, useState } from "react"; -import { FlatList, Pressable, View } from "react-native"; -import { Link } from "expo-router"; -import FullPageError from "@/components/FullPageError"; -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 { SearchInput } from "@/components/ui/SearchInput"; -import { Text } from "@/components/ui/Text"; -import { useQueryClient } from "@tanstack/react-query"; - -import { usePaginatedSearchTags } from "@karakeep/shared-react/hooks/tags"; -import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; -import { useTRPC } from "@karakeep/shared-react/trpc"; - -interface TagItem { - id: string; - name: string; - numBookmarks: number; - href: string; -} - -export default function Tags() { - const [refreshing, setRefreshing] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const api = useTRPC(); - const queryClient = useQueryClient(); - - // Debounce search query to avoid too many API calls - const debouncedSearch = useDebounce(searchQuery, 300); - - // Fetch tags sorted by usage (most used first) - const { - data, - isPending, - error, - refetch, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = usePaginatedSearchTags({ - limit: 50, - sortBy: debouncedSearch ? "relevance" : "usage", - nameContains: debouncedSearch, - }); - - useEffect(() => { - setRefreshing(isPending); - }, [isPending]); - - if (error) { - return <FullPageError error={error.message} onRetry={() => refetch()} />; - } - - if (!data) { - return <FullPageSpinner />; - } - - const onRefresh = () => { - queryClient.invalidateQueries(api.tags.list.pathFilter()); - }; - - const tags: TagItem[] = data.tags.map((tag) => ({ - id: tag.id, - name: tag.name, - numBookmarks: tag.numBookmarks, - href: `/dashboard/tags/${tag.id}`, - })); - - const handleLoadMore = () => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - - return ( - <CustomSafeAreaView edges={["top"]}> - <FlatList - className="h-full" - ListHeaderComponent={ - <View> - <PageTitle title="Tags" /> - <SearchInput - containerClassName="mx-2 mb-2" - placeholder="Search tags..." - value={searchQuery} - onChangeText={setSearchQuery} - /> - </View> - } - contentContainerStyle={{ - gap: 6, - }} - renderItem={(item) => ( - <View className="mx-2 flex flex-row items-center rounded-xl bg-card px-4 py-2"> - <Link - asChild - key={item.item.id} - href={item.item.href} - className="flex-1" - > - <Pressable className="flex flex-row items-center justify-between"> - <View className="flex-1"> - <Text className="font-medium">{item.item.name}</Text> - <Text className="text-sm text-muted-foreground"> - {item.item.numBookmarks}{" "} - {item.item.numBookmarks === 1 ? "bookmark" : "bookmarks"} - </Text> - </View> - <ChevronRight /> - </Pressable> - </Link> - </View> - )} - data={tags} - refreshing={refreshing} - onRefresh={onRefresh} - onEndReached={handleLoadMore} - onEndReachedThreshold={0.5} - ListFooterComponent={ - isFetchingNextPage ? ( - <View className="py-4"> - <Text className="text-center text-muted-foreground"> - Loading more... - </Text> - </View> - ) : null - } - ListEmptyComponent={ - !isPending ? ( - <View className="py-8"> - <Text className="text-center text-muted-foreground"> - No tags yet - </Text> - </View> - ) : null - } - /> - </CustomSafeAreaView> - ); -} diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx index 60fbc4fc..78fd7c60 100644 --- a/apps/mobile/app/dashboard/_layout.tsx +++ b/apps/mobile/app/dashboard/_layout.tsx @@ -70,8 +70,10 @@ export default function Dashboard() { options={{ headerTitle: "New Bookmark", headerBackTitle: "Back", - headerTransparent: true, - presentation: "modal", + headerTransparent: false, + presentation: "formSheet", + sheetGrabberVisible: true, + sheetAllowedDetents: [0.35, 0.7], }} /> <Stack.Screen diff --git a/apps/mobile/app/dashboard/bookmarks/new.tsx b/apps/mobile/app/dashboard/bookmarks/new.tsx index 25882d7f..f7be22e1 100644 --- a/apps/mobile/app/dashboard/bookmarks/new.tsx +++ b/apps/mobile/app/dashboard/bookmarks/new.tsx @@ -2,7 +2,6 @@ import React, { useState } from "react"; 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"; @@ -59,25 +58,23 @@ const NoteEditorPage = () => { }; return ( - <CustomSafeAreaView> - <View className="gap-2 px-4"> - {error && ( - <Text className="w-full text-center text-red-500">{error}</Text> - )} - <Input - onChangeText={setText} - className="bg-card" - multiline - placeholder="What's on your mind?" - autoFocus - autoCapitalize={"none"} - textAlignVertical="top" - /> - <Button onPress={onSubmit} disabled={isPending}> - <Text>Save</Text> - </Button> - </View> - </CustomSafeAreaView> + <View className="flex-1 gap-2 px-4 pt-4"> + {error && ( + <Text className="w-full text-center text-red-500">{error}</Text> + )} + <Input + onChangeText={setText} + className="bg-card" + multiline + placeholder="What's on your mind?" + autoFocus + autoCapitalize={"none"} + textAlignVertical="top" + /> + <Button onPress={onSubmit} disabled={isPending}> + <Text>Save</Text> + </Button> + </View> ); }; |
