From 34d2b48578532d387b1466c82ae4a761cd1d1a4f Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 2 Nov 2025 20:13:57 +0000 Subject: feat: Add view options to show tag/title and control image fit. Fixes #1960 --- apps/web/components/dashboard/ViewOptions.tsx | 154 ++++++++++++++++++++- .../bookmarks/BookmarkLayoutAdaptingCard.tsx | 61 +++++--- apps/web/lib/i18n/locales/en/translation.json | 7 +- apps/web/lib/userLocalSettings/bookmarksLayout.tsx | 6 + apps/web/lib/userLocalSettings/types.ts | 3 + .../web/lib/userLocalSettings/userLocalSettings.ts | 12 ++ 6 files changed, 214 insertions(+), 29 deletions(-) (limited to 'apps') diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx index c86f43fb..e7b459fe 100644 --- a/apps/web/components/dashboard/ViewOptions.tsx +++ b/apps/web/components/dashboard/ViewOptions.tsx @@ -1,6 +1,12 @@ "use client"; -import { createElement, useEffect, useState } from "react"; +import { + createElement, + useEffect, + useOptimistic, + useState, + useTransition, +} from "react"; import { ButtonWithTooltip } from "@/components/ui/button"; import { DropdownMenu, @@ -21,10 +27,15 @@ import { import { updateBookmarksLayout, updateGridColumns, + updateImageFit, updateShowNotes, + updateShowTags, + updateShowTitle, } from "@/lib/userLocalSettings/userLocalSettings"; import { Check, + Heading, + Image, LayoutDashboard, LayoutGrid, LayoutList, @@ -32,6 +43,7 @@ import { LucideIcon, NotepadText, Settings, + Tag, } from "lucide-react"; type LayoutType = "masonry" | "grid" | "list" | "compact"; @@ -47,16 +59,73 @@ export default function ViewOptions() { const { t } = useTranslation(); const layout = useBookmarkLayout(); const gridColumns = useGridColumns(); - const { showNotes } = useBookmarkDisplaySettings(); + const actualDisplaySettings = useBookmarkDisplaySettings(); const [tempColumns, setTempColumns] = useState(gridColumns); + const [, startTransition] = useTransition(); + + // Optimistic state for all toggles + const [optimisticDisplaySettings, setOptimisticDisplaySettings] = + useOptimistic(actualDisplaySettings); + + const [optimisticLayout, setOptimisticLayout] = useOptimistic(layout); - const showColumnSlider = layout === "grid" || layout === "masonry"; + const [optimisticImageFit, setOptimisticImageFit] = useOptimistic( + actualDisplaySettings.imageFit, + ); + + const showColumnSlider = + optimisticLayout === "grid" || optimisticLayout === "masonry"; // Update temp value when actual value changes useEffect(() => { setTempColumns(gridColumns); }, [gridColumns]); + // Handlers with optimistic updates + const handleLayoutChange = (newLayout: LayoutType) => { + startTransition(async () => { + setOptimisticLayout(newLayout); + await updateBookmarksLayout(newLayout); + }); + }; + + const handleShowNotesChange = (checked: boolean) => { + startTransition(async () => { + setOptimisticDisplaySettings({ + ...optimisticDisplaySettings, + showNotes: checked, + }); + await updateShowNotes(checked); + }); + }; + + const handleShowTagsChange = (checked: boolean) => { + startTransition(async () => { + setOptimisticDisplaySettings({ + ...optimisticDisplaySettings, + showTags: checked, + }); + await updateShowTags(checked); + }); + }; + + const handleShowTitleChange = (checked: boolean) => { + startTransition(async () => { + setOptimisticDisplaySettings({ + ...optimisticDisplaySettings, + showTitle: checked, + }); + await updateShowTitle(checked); + }); + }; + + const handleImageFitChange = (fit: "cover" | "contain") => { + startTransition(async () => { + setOptimisticImageFit(fit); + await updateImageFit(fit); + }); + }; + return ( @@ -76,13 +145,16 @@ export default function ViewOptions() { await updateBookmarksLayout(key as LayoutType)} + onSelect={(e) => { + e.preventDefault(); + handleLayoutChange(key); + }} >
{createElement(iconMap[key as LayoutType], { size: 18 })} {t(`layouts.${key}`)}
- {layout === key && } + {optimisticLayout === key && }
))} @@ -131,11 +203,79 @@ export default function ViewOptions() { + + +
+ + +
+ +
+ +
+ + +
+ {t("view_options.image_options")} +
+ +
+ { + e.preventDefault(); + handleImageFitChange("cover"); + }} + > +
+ + {t("view_options.image_fit_cover")} +
+ {optimisticImageFit === "cover" && ( + + )} +
+ { + e.preventDefault(); + handleImageFitChange("contain"); + }} + > +
+ + {t("view_options.image_fit_contain")} +
+ {optimisticImageFit === "contain" && ( + + )} +
+
); diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index 26c1c72b..b53a14c5 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -18,6 +18,7 @@ import { useTheme } from "next-themes"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; +import { switchCase } from "@karakeep/shared/utils/switch"; import BookmarkActionBar from "./BookmarkActionBar"; import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt"; @@ -117,7 +118,12 @@ function ListView({ footer, className, }: Props) { - const { showNotes } = useBookmarkDisplaySettings(); + const { showNotes, showTags, showTitle, imageFit } = + useBookmarkDisplaySettings(); + const imgFitClass = switchCase(imageFit, { + cover: "object-cover", + contain: "object-contain", + }); const note = showNotes ? bookmark.note?.trim() : undefined; return ( @@ -129,23 +135,25 @@ function ListView({ >
- {image("list", "object-cover rounded-lg size-32")} + {image("list", cn("size-32 rounded-lg", imgFitClass))}
- {title && ( + {showTitle && title && (
{title}
)} {content &&
{content}
} {note && } -
- -
+ {showTags && ( +
+ +
+ )}
@@ -164,9 +172,17 @@ function GridView({ layout, fitHeight = false, }: Props & { layout: BookmarksLayoutTypes }) { - const { showNotes } = useBookmarkDisplaySettings(); + const { showNotes, showTags, showTitle, imageFit } = + useBookmarkDisplaySettings(); + const imgFitClass = switchCase(imageFit, { + cover: "object-cover", + contain: "object-contain", + }); const note = showNotes ? bookmark.note?.trim() : undefined; - const img = image("grid", "h-52 min-h-52 w-full object-cover rounded-t-lg"); + const img = image( + "grid", + cn("h-52 min-h-52 w-full rounded-t-lg", imgFitClass), + ); return (
{img}
}
- {title && ( + {showTitle && title && (
{title}
)} {content &&
{content}
} {note && } -
- -
+ {showTags && ( +
+ +
+ )}
@@ -202,6 +220,7 @@ function GridView({ } function CompactView({ bookmark, title, footer, className }: Props) { + const { showTitle } = useBookmarkDisplaySettings(); return (
)} - { + {showTitle && (
{title ?? "Untitled"}
- } + )} {footer && (

•{footer}

)} diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 9b40416d..5aa2913d 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -52,7 +52,12 @@ "layout": "Layout", "columns": "Columns", "display_options": "Display Options", - "show_note_previews": "Show Notes" + "show_note_previews": "Show Notes", + "show_tags": "Show Tags", + "show_title": "Show Title", + "image_options": "Image Options", + "image_fit_cover": "Cover (Fill)", + "image_fit_contain": "Contain (Fit)" }, "actions": { "change_layout": "Change Layout", diff --git a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx index 504d8d8c..3bd77e4a 100644 --- a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx +++ b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx @@ -15,6 +15,9 @@ export const UserLocalSettingsCtx = createContext< lang: fallbackLng, gridColumns: 3, showNotes: false, + showTags: true, + showTitle: true, + imageFit: "cover", }); function useUserLocalSettings() { @@ -25,6 +28,9 @@ export function useBookmarkDisplaySettings() { const settings = useUserLocalSettings(); return { showNotes: settings.showNotes, + showTags: settings.showTags, + showTitle: settings.showTitle, + imageFit: settings.imageFit, }; } diff --git a/apps/web/lib/userLocalSettings/types.ts b/apps/web/lib/userLocalSettings/types.ts index 54b75b80..949c9229 100644 --- a/apps/web/lib/userLocalSettings/types.ts +++ b/apps/web/lib/userLocalSettings/types.ts @@ -10,6 +10,9 @@ export const zUserLocalSettings = z.object({ lang: z.string().optional().default("en"), gridColumns: z.number().min(1).max(6).optional().default(3), showNotes: z.boolean().optional().default(false), + showTags: z.boolean().optional().default(true), + showTitle: z.boolean().optional().default(true), + imageFit: z.enum(["cover", "contain"]).optional().default("cover"), }); export type UserLocalSettings = z.infer; diff --git a/apps/web/lib/userLocalSettings/userLocalSettings.ts b/apps/web/lib/userLocalSettings/userLocalSettings.ts index aff3645c..55070cd6 100644 --- a/apps/web/lib/userLocalSettings/userLocalSettings.ts +++ b/apps/web/lib/userLocalSettings/userLocalSettings.ts @@ -46,3 +46,15 @@ export async function updateGridColumns(gridColumns: number) { export async function updateShowNotes(showNotes: boolean) { await readModifyWrite(() => ({ showNotes })); } + +export async function updateShowTags(showTags: boolean) { + await readModifyWrite(() => ({ showTags })); +} + +export async function updateShowTitle(showTitle: boolean) { + await readModifyWrite(() => ({ showTitle })); +} + +export async function updateImageFit(imageFit: "cover" | "contain") { + await readModifyWrite(() => ({ imageFit })); +} -- cgit v1.2.3-70-g09d2