aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/app/dashboard/bookmarks
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-08-26 15:47:05 +0300
committerGitHub <noreply@github.com>2025-08-26 13:47:05 +0100
commited86f7ef012fb558fe8a8974e1e162ce75cbfd15 (patch)
treea3470b0e1a01aede90b75bc61eeba2545e51fe83 /apps/mobile/app/dashboard/bookmarks
parentec56ea33b5e37d02e87e480da305038a5ce7de49 (diff)
downloadkarakeep-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/dashboard/bookmarks')
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx11
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx287
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx7
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx26
-rw-r--r--apps/mobile/app/dashboard/bookmarks/new.tsx8
5 files changed, 198 insertions, 141 deletions
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>
);