diff options
| -rw-r--r-- | .prettierignore | 3 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/settings.tsx | 14 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/BookmarkCard.tsx | 10 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/NotePreview.tsx | 83 | ||||
| -rw-r--r-- | apps/mobile/lib/settings.ts | 2 | ||||
| -rw-r--r-- | apps/web/.oxlintrc.json | 2 | ||||
| -rw-r--r-- | apps/web/components/dashboard/ViewOptions.tsx | 57 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx | 28 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/NotePreview.tsx | 66 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 8 | ||||
| -rw-r--r-- | apps/web/lib/userLocalSettings/bookmarksLayout.tsx | 8 | ||||
| -rw-r--r-- | apps/web/lib/userLocalSettings/types.ts | 1 | ||||
| -rw-r--r-- | apps/web/lib/userLocalSettings/userLocalSettings.ts | 11 |
13 files changed, 268 insertions, 25 deletions
diff --git a/.prettierignore b/.prettierignore index b9bf8b46..0613380e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,3 +19,6 @@ yarn.lock # Expo build **/.expo/** + +apps/mobile/android +apps/mobile/ios diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 6d76308d..76216e00 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { ActivityIndicator, Pressable, View } from "react-native"; +import { ActivityIndicator, Pressable, Switch, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; import { Link } from "expo-router"; @@ -87,6 +87,18 @@ export default function Dashboard() { </Pressable> </Link> </View> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> + <Text>Show note preview in bookmark</Text> + <Switch + value={settings.showNotes} + onValueChange={(value) => + setSettings({ + ...settings, + showNotes: value, + }) + } + /> + </View> <Text className="w-full p-1 text-2xl font-bold text-foreground"> Upload Settings </Text> diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 62e74e97..98d8d3e2 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { ActivityIndicator, Alert, @@ -37,6 +36,7 @@ import { Skeleton } from "../ui/Skeleton"; import { useToast } from "../ui/Toast"; import BookmarkAssetImage from "./BookmarkAssetImage"; import BookmarkTextMarkdown from "./BookmarkTextMarkdown"; +import { NotePreview } from "./NotePreview"; import TagPill from "./TagPill"; function ActionBar({ bookmark }: { bookmark: ZBookmark }) { @@ -285,6 +285,7 @@ function LinkCard({ throw new Error("Wrong content type rendered"); } + const note = settings.showNotes ? bookmark.note?.trim() : undefined; const url = bookmark.content.url; const parsedUrl = new URL(url); @@ -329,6 +330,7 @@ function LinkCard({ > {bookmark.title ?? bookmark.content.title ?? parsedUrl.host} </Text> + {note && <NotePreview note={note} bookmarkId={bookmark.id} />} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> @@ -347,9 +349,11 @@ function TextCard({ bookmark: ZBookmark; onOpenBookmark: () => void; }) { + const { settings } = useAppSettings(); if (bookmark.content.type !== BookmarkTypes.TEXT) { throw new Error("Wrong content type rendered"); } + const note = settings.showNotes ? bookmark.note?.trim() : undefined; const content = bookmark.content.text; return ( <View className="flex max-h-96 gap-2 p-2"> @@ -365,6 +369,7 @@ function TextCard({ <BookmarkTextMarkdown text={content} /> </Pressable> </View> + {note && <NotePreview note={note} bookmarkId={bookmark.id} />} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="flex flex-row justify-between p-2"> @@ -382,9 +387,11 @@ function AssetCard({ bookmark: ZBookmark; onOpenBookmark: () => void; }) { + const { settings } = useAppSettings(); if (bookmark.content.type !== BookmarkTypes.ASSET) { throw new Error("Wrong content type rendered"); } + const note = settings.showNotes ? bookmark.note?.trim() : undefined; const title = bookmark.title ?? bookmark.content.fileName; const assetImage = @@ -405,6 +412,7 @@ function AssetCard({ <Text className="line-clamp-2 text-xl font-bold">{title}</Text> )} </Pressable> + {note && <NotePreview note={note} bookmarkId={bookmark.id} />} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> diff --git a/apps/mobile/components/bookmarks/NotePreview.tsx b/apps/mobile/components/bookmarks/NotePreview.tsx new file mode 100644 index 00000000..d529d56e --- /dev/null +++ b/apps/mobile/components/bookmarks/NotePreview.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { Modal, Pressable, ScrollView, View } from "react-native"; +import { router } from "expo-router"; +import { ExternalLink, NotepadText, X } from "lucide-react-native"; +import { useColorScheme } from "nativewind"; + +import { Button } from "../ui/Button"; +import { Text } from "../ui/Text"; + +interface NotePreviewProps { + note: string; + bookmarkId: string; +} + +export function NotePreview({ note, bookmarkId }: NotePreviewProps) { + const [isModalVisible, setIsModalVisible] = useState(false); + const { colorScheme } = useColorScheme(); + const iconColor = colorScheme === "dark" ? "#9ca3af" : "#6b7280"; + const modalIconColor = colorScheme === "dark" ? "#d1d5db" : "#374151"; + + if (!note?.trim()) { + return null; + } + + return ( + <> + <Pressable onPress={() => setIsModalVisible(true)}> + <View className="flex flex-row items-center gap-2"> + <NotepadText size={24} color={iconColor} /> + <Text + className="flex-1 text-sm italic text-gray-500 dark:text-gray-400" + numberOfLines={2} + > + {note} + </Text> + </View> + </Pressable> + + <Modal + visible={isModalVisible} + transparent + animationType="slide" + onRequestClose={() => setIsModalVisible(false)} + > + <View className="flex-1 justify-end bg-black/50"> + <View className="max-h-[80%] rounded-t-3xl bg-card p-6"> + {/* Header */} + <View className="mb-4 flex flex-row items-center justify-between"> + <Text className="text-lg font-semibold">Note</Text> + <Pressable + onPress={() => setIsModalVisible(false)} + className="p-2" + > + <X size={24} color={modalIconColor} /> + </Pressable> + </View> + + {/* Note Content */} + <ScrollView className="mb-4 max-h-96"> + <Text className="text-sm text-gray-700 dark:text-gray-300"> + {note} + </Text> + </ScrollView> + + {/* Action Button */} + <View className="flex flex-row justify-end border-t border-border pt-4"> + <Button + variant="secondary" + onPress={() => { + setIsModalVisible(false); + router.push(`/dashboard/bookmarks/${bookmarkId}/info`); + }} + > + <Text className="text-sm">Edit Notes</Text> + <ExternalLink size={14} color={modalIconColor} /> + </Button> + </View> + </View> + </View> + </Modal> + </> + ); +} diff --git a/apps/mobile/lib/settings.ts b/apps/mobile/lib/settings.ts index 51fa661f..4399e04a 100644 --- a/apps/mobile/lib/settings.ts +++ b/apps/mobile/lib/settings.ts @@ -14,6 +14,7 @@ const zSettingsSchema = z.object({ .enum(["reader", "browser"]) .optional() .default("reader"), + showNotes: z.boolean().optional().default(false), }); export type Settings = z.infer<typeof zSettingsSchema>; @@ -32,6 +33,7 @@ const useSettings = create<AppSettingsState>((set, get) => ({ imageQuality: 0.2, theme: "system", defaultBookmarkView: "reader", + showNotes: false, }, }, setSettings: async (settings) => { diff --git a/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json index d0a51846..4c437efb 100644 --- a/apps/web/.oxlintrc.json +++ b/apps/web/.oxlintrc.json @@ -27,6 +27,8 @@ ".next", "dist", "build", + "public/sw.js", + "public/workbox-*.js", "pnpm-lock.yaml" ] } diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx index 6367421f..c86f43fb 100644 --- a/apps/web/components/dashboard/ViewOptions.tsx +++ b/apps/web/components/dashboard/ViewOptions.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import { createElement, useEffect, useState } from "react"; import { ButtonWithTooltip } from "@/components/ui/button"; import { DropdownMenu, @@ -9,14 +9,19 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Label } from "@/components/ui/label"; import { Slider } from "@/components/ui/slider"; +import { Switch } from "@/components/ui/switch"; +import { useTranslation } from "@/lib/i18n/client"; import { + useBookmarkDisplaySettings, useBookmarkLayout, useGridColumns, } from "@/lib/userLocalSettings/bookmarksLayout"; import { updateBookmarksLayout, updateGridColumns, + updateShowNotes, } from "@/lib/userLocalSettings/userLocalSettings"; import { Check, @@ -25,6 +30,7 @@ import { LayoutList, List, LucideIcon, + NotepadText, Settings, } from "lucide-react"; @@ -37,22 +43,17 @@ const iconMap: Record<LayoutType, LucideIcon> = { compact: List, }; -const layoutNames: Record<LayoutType, string> = { - masonry: "Masonry", - grid: "Grid", - list: "List", - compact: "Compact", -}; - export default function ViewOptions() { + const { t } = useTranslation(); const layout = useBookmarkLayout(); const gridColumns = useGridColumns(); - const [tempColumns, setTempColumns] = React.useState(gridColumns); + const { showNotes } = useBookmarkDisplaySettings(); + const [tempColumns, setTempColumns] = useState(gridColumns); const showColumnSlider = layout === "grid" || layout === "masonry"; // Update temp value when actual value changes - React.useEffect(() => { + useEffect(() => { setTempColumns(gridColumns); }, [gridColumns]); @@ -60,7 +61,7 @@ export default function ViewOptions() { <DropdownMenu> <DropdownMenuTrigger asChild> <ButtonWithTooltip - tooltip="View Options" + tooltip={t("view_options.title")} delayDuration={100} variant="ghost" > @@ -68,7 +69,9 @@ export default function ViewOptions() { </ButtonWithTooltip> </DropdownMenuTrigger> <DropdownMenuContent className="w-56"> - <div className="px-2 py-1.5 text-sm font-semibold">Layout</div> + <div className="px-2 py-1.5 text-sm font-semibold"> + {t("view_options.layout")} + </div> {(Object.keys(iconMap) as LayoutType[]).map((key) => ( <DropdownMenuItem key={key} @@ -76,8 +79,8 @@ export default function ViewOptions() { onClick={async () => await updateBookmarksLayout(key as LayoutType)} > <div className="flex items-center gap-2"> - {React.createElement(iconMap[key as LayoutType], { size: 18 })} - <span>{layoutNames[key]}</span> + {createElement(iconMap[key as LayoutType], { size: 18 })} + <span>{t(`layouts.${key}`)}</span> </div> {layout === key && <Check className="ml-2 size-4" />} </DropdownMenuItem> @@ -88,7 +91,9 @@ export default function ViewOptions() { <DropdownMenuSeparator /> <div className="px-2 py-3"> <div className="mb-2 flex items-center justify-between"> - <span className="text-sm font-semibold">Columns</span> + <span className="text-sm font-semibold"> + {t("view_options.columns")} + </span> <span className="text-sm text-muted-foreground"> {tempColumns} </span> @@ -109,6 +114,28 @@ export default function ViewOptions() { </div> </> )} + + <DropdownMenuSeparator /> + <div className="px-2 py-1.5 text-sm font-semibold"> + {t("view_options.display_options")} + </div> + + <div className="space-y-3 px-2 py-2"> + <div className="flex items-center justify-between"> + <Label + htmlFor="show-notes" + className="flex cursor-pointer items-center gap-2 text-sm" + > + <NotepadText size={16} /> + <span>{t("view_options.show_note_previews")}</span> + </Label> + <Switch + id="show-notes" + checked={showNotes} + onCheckedChange={updateShowNotes} + /> + </div> + </div> </DropdownMenuContent> </DropdownMenu> ); diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index 4b511a3c..26c1c72b 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -1,10 +1,14 @@ +"use client"; + import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types"; -import React, { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import useBulkActionsStore from "@/lib/bulkActions"; import { bookmarkLayoutSwitch, + useBookmarkDisplaySettings, useBookmarkLayout, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; @@ -17,14 +21,15 @@ import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; import BookmarkActionBar from "./BookmarkActionBar"; import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt"; +import { NotePreview } from "./NotePreview"; import TagList from "./TagList"; interface Props { bookmark: ZBookmark; - image: (layout: BookmarksLayoutTypes, className: string) => React.ReactNode; - title?: React.ReactNode; - content?: React.ReactNode; - footer?: React.ReactNode; + image: (layout: BookmarksLayoutTypes, className: string) => ReactNode; + title?: ReactNode; + content?: ReactNode; + footer?: ReactNode; className?: string; fitHeight?: boolean; wrapTags: boolean; @@ -34,7 +39,7 @@ function BottomRow({ footer, bookmark, }: { - footer?: React.ReactNode; + footer?: ReactNode; bookmark: ZBookmark; }) { return ( @@ -112,6 +117,9 @@ function ListView({ footer, className, }: Props) { + const { showNotes } = useBookmarkDisplaySettings(); + const note = showNotes ? bookmark.note?.trim() : undefined; + return ( <div className={cn( @@ -131,6 +139,7 @@ function ListView({ </div> )} {content && <div className="shrink-1 overflow-hidden">{content}</div>} + {note && <NotePreview note={note} bookmarkId={bookmark.id} />} <div className="flex shrink-0 flex-wrap gap-1 overflow-hidden"> <TagList bookmark={bookmark} @@ -155,7 +164,9 @@ function GridView({ layout, fitHeight = false, }: Props & { layout: BookmarksLayoutTypes }) { - const img = image("grid", "h-56 min-h-56 w-full object-cover rounded-t-lg"); + const { showNotes } = useBookmarkDisplaySettings(); + const note = showNotes ? bookmark.note?.trim() : undefined; + const img = image("grid", "h-52 min-h-52 w-full object-cover rounded-t-lg"); return ( <div @@ -166,7 +177,7 @@ function GridView({ )} > <MultiBookmarkSelector bookmark={bookmark} /> - {img && <div className="h-56 w-full shrink-0 overflow-hidden">{img}</div>} + {img && <div className="h-52 w-full shrink-0 overflow-hidden">{img}</div>} <div className="flex h-full flex-col justify-between gap-2 overflow-hidden p-2"> <div className="grow-1 flex flex-col gap-2 overflow-hidden"> {title && ( @@ -175,6 +186,7 @@ function GridView({ </div> )} {content && <div className="shrink-1 overflow-hidden">{content}</div>} + {note && <NotePreview note={note} bookmarkId={bookmark.id} />} <div className="flex shrink-0 flex-wrap gap-1 overflow-hidden"> <TagList className={wrapTags ? undefined : "h-full"} diff --git a/apps/web/components/dashboard/bookmarks/NotePreview.tsx b/apps/web/components/dashboard/bookmarks/NotePreview.tsx new file mode 100644 index 00000000..c32c85a3 --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/NotePreview.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { useTranslation } from "@/lib/i18n/client"; +import { cn } from "@/lib/utils"; +import { ExternalLink, NotepadText } from "lucide-react"; + +interface NotePreviewProps { + note: string; + bookmarkId: string; + className?: string; +} + +export function NotePreview({ note, bookmarkId, className }: NotePreviewProps) { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + if (!note?.trim()) { + return null; + } + + return ( + <Popover open={isOpen} onOpenChange={setIsOpen}> + <PopoverTrigger asChild> + <div + className={cn( + "flex cursor-pointer items-center gap-1.5 text-sm font-light italic text-gray-500 dark:text-gray-400", + className, + )} + > + <NotepadText className="size-5 shrink-0" /> + <div className="line-clamp-2 min-w-0 flex-1 overflow-hidden text-wrap break-words"> + {note} + </div> + </div> + </PopoverTrigger> + <PopoverContent className="w-96 max-w-[calc(100vw-2rem)]" align="start"> + <div className="space-y-3"> + <div className="max-h-60 overflow-y-auto whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-300"> + {note} + </div> + <div className="flex justify-end"> + <Link href={`/dashboard/preview/${bookmarkId}`}> + <Button + variant="outline" + size="sm" + className="gap-2" + onClick={() => setIsOpen(false)} + > + {t("actions.edit_notes")} + <ExternalLink className="size-4" /> + </Button> + </Link> + </div> + </div> + </PopoverContent> + </Popover> + ); +} diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 63e4ae2d..9b40416d 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -47,6 +47,13 @@ "list": "List", "compact": "Compact" }, + "view_options": { + "title": "View Options", + "layout": "Layout", + "columns": "Columns", + "display_options": "Display Options", + "show_note_previews": "Show Notes" + }, "actions": { "change_layout": "Change Layout", "archive": "Archive", @@ -59,6 +66,7 @@ "recrawl": "Recrawl", "download_full_page_archive": "Download Full Page Archive", "edit_tags": "Edit Tags", + "edit_notes": "Edit Notes", "add_to_list": "Add to List", "select_all": "Select All", "unselect_all": "Unselect All", diff --git a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx index 346c85e0..504d8d8c 100644 --- a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx +++ b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx @@ -14,12 +14,20 @@ export const UserLocalSettingsCtx = createContext< bookmarkGridLayout: defaultLayout, lang: fallbackLng, gridColumns: 3, + showNotes: false, }); function useUserLocalSettings() { return useContext(UserLocalSettingsCtx); } +export function useBookmarkDisplaySettings() { + const settings = useUserLocalSettings(); + return { + showNotes: settings.showNotes, + }; +} + export function useBookmarkLayout() { const settings = useUserLocalSettings(); return settings.bookmarkGridLayout; diff --git a/apps/web/lib/userLocalSettings/types.ts b/apps/web/lib/userLocalSettings/types.ts index c87c8c33..54b75b80 100644 --- a/apps/web/lib/userLocalSettings/types.ts +++ b/apps/web/lib/userLocalSettings/types.ts @@ -9,6 +9,7 @@ export const zUserLocalSettings = z.object({ bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"), lang: z.string().optional().default("en"), gridColumns: z.number().min(1).max(6).optional().default(3), + showNotes: z.boolean().optional().default(false), }); export type UserLocalSettings = z.infer<typeof zUserLocalSettings>; diff --git a/apps/web/lib/userLocalSettings/userLocalSettings.ts b/apps/web/lib/userLocalSettings/userLocalSettings.ts index 11bd0a84..25c10e1b 100644 --- a/apps/web/lib/userLocalSettings/userLocalSettings.ts +++ b/apps/web/lib/userLocalSettings/userLocalSettings.ts @@ -48,3 +48,14 @@ export async function updateGridColumns(gridColumns: number) { sameSite: "lax", }); } + +export async function updateShowNotes(showNotes: boolean) { + const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME); + const parsed = parseUserLocalSettings(userSettings?.value); + (await cookies()).set({ + name: USER_LOCAL_SETTINGS_COOKIE_NAME, + value: JSON.stringify({ ...parsed, showNotes }), + maxAge: 34560000, // Chrome caps max age to 400 days + sameSite: "lax", + }); +} |
