From 49f38efdbe3718055d2c84847d7dab92ae359be9 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Tue, 1 Jul 2025 20:26:25 +0000 Subject: feat: Add a proper reader mode --- apps/web/app/layout.tsx | 6 +- apps/web/app/reader/[bookmarkId]/page.tsx | 312 +++++++++++++++++++++ .../components/dashboard/lists/ShareListModal.tsx | 2 +- .../dashboard/preview/BookmarkHtmlHighlighter.tsx | 3 + .../dashboard/preview/LinkContentSection.tsx | 237 ++++++---------- .../components/dashboard/preview/ReaderView.tsx | 121 ++++++++ .../dashboard/preview/TextContentSection.tsx | 2 +- apps/web/components/ui/slider.tsx | 27 ++ apps/web/package.json | 1 + 9 files changed, 548 insertions(+), 163 deletions(-) create mode 100644 apps/web/app/reader/[bookmarkId]/page.tsx create mode 100644 apps/web/components/dashboard/preview/ReaderView.tsx create mode 100644 apps/web/components/ui/slider.tsx (limited to 'apps') diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index d5af9e35..e8673c78 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -50,11 +50,7 @@ export default async function RootLayout({ const userSettings = await getUserLocalSettings(); const isRTL = userSettings.lang === "ar"; return ( - + { + if (window.history.length > 1) { + router.back(); + } else { + router.push("/dashboard"); + } + }; + + const handlePrint = () => { + window.print(); + }; + + return ( +
+ {/* Header */} +
+
+
+ + Reader View +
+ +
+ + + + + + + +
+
+ +

Reading Settings

+
+ +
+
+ + +
+ +
+
+ + + {fontSize[0]}px + +
+
+ + + +
+
+ +
+
+ + + {lineHeight[0]} + +
+ +
+
+
+
+
+ + +
+
+
+ +
+ {/* Mobile backdrop */} + {showHighlights && ( + +
+
+ + +
+
+ {highlights.highlights.map((highlight) => ( + + ))} +
+
+ + + )} + + + ); +} diff --git a/apps/web/components/dashboard/lists/ShareListModal.tsx b/apps/web/components/dashboard/lists/ShareListModal.tsx index 16668e67..4b8218a9 100644 --- a/apps/web/components/dashboard/lists/ShareListModal.tsx +++ b/apps/web/components/dashboard/lists/ShareListModal.tsx @@ -4,13 +4,13 @@ import { Dialog, DialogClose, DialogContent, + DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { useTranslation } from "@/lib/i18n/client"; -import { DialogDescription } from "@radix-ui/react-dialog"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; diff --git a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx index dc446112..19499d3e 100644 --- a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx +++ b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx @@ -92,6 +92,7 @@ export interface Highlight { interface HTMLHighlighterProps { htmlContent: string; + style?: React.CSSProperties; className?: string; highlights?: Highlight[]; onHighlight?: (highlight: Highlight) => void; @@ -102,6 +103,7 @@ interface HTMLHighlighterProps { function BookmarkHTMLHighlighter({ htmlContent, className, + style, highlights = [], onHighlight, onUpdateHighlight, @@ -345,6 +347,7 @@ function BookmarkHTMLHighlighter({ dangerouslySetInnerHTML={{ __html: htmlContent }} onPointerUp={handlePointerUp} className={className} + style={style} /> - data.content.type == BookmarkTypes.LINK - ? data.content.htmlContent - : null, - }, - ); - - const { mutate: createHighlight } = useCreateHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been created!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - const { mutate: updateHighlight } = useUpdateHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been updated!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - const { mutate: deleteHighlight } = useDeleteHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been deleted!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - let content; - if (isCachedContentLoading) { - content = ; - } else if (!cachedContent) { - content = ( -
Failed to fetch link content ...
- ); - } else { - content = ( - - deleteHighlight({ - highlightId: h.id, - }) - } - onUpdateHighlight={(h) => - updateHighlight({ - highlightId: h.id, - color: h.color, - }) - } - onHighlight={(h) => - createHighlight({ - startOffset: h.startOffset, - endOffset: h.endOffset, - color: h.color, - bookmarkId, - text: h.text, - note: null, - }) - } - /> - ); - } - return {content}; -} - function VideoSection({ link }: { link: ZBookmarkedLink }) { return (
@@ -182,7 +82,14 @@ export default function LinkContentSection({ let content; if (section === "cached") { - content = ; + content = ( + + + + ); } else if (section === "archive") { content = ; } else if (section === "video") { @@ -193,50 +100,68 @@ export default function LinkContentSection({ return (
- +
+ + {section === "cached" && ( + + + + + + + FullScreen + + )} +
{content}
); diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx new file mode 100644 index 00000000..bf4c27a5 --- /dev/null +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -0,0 +1,121 @@ +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; + +import { + useCreateHighlight, + useDeleteHighlight, + useUpdateHighlight, +} from "@karakeep/shared-react/hooks/highlights"; +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; + +import BookmarkHTMLHighlighter from "./BookmarkHtmlHighlighter"; + +export default function ReaderView({ + bookmarkId, + className, + style, +}: { + bookmarkId: string; + className?: string; + style?: React.CSSProperties; +}) { + const { data: highlights } = api.highlights.getForBookmark.useQuery({ + bookmarkId, + }); + const { data: cachedContent, isPending: isCachedContentLoading } = + api.bookmarks.getBookmark.useQuery( + { + bookmarkId, + includeContent: true, + }, + { + select: (data) => + data.content.type == BookmarkTypes.LINK + ? data.content.htmlContent + : null, + }, + ); + + const { mutate: createHighlight } = useCreateHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been created!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + const { mutate: updateHighlight } = useUpdateHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been updated!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + const { mutate: deleteHighlight } = useDeleteHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been deleted!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + let content; + if (isCachedContentLoading) { + content = ; + } else if (!cachedContent) { + content = ( +
Failed to fetch link content ...
+ ); + } else { + content = ( + + deleteHighlight({ + highlightId: h.id, + }) + } + onUpdateHighlight={(h) => + updateHighlight({ + highlightId: h.id, + color: h.color, + }) + } + onHighlight={(h) => + createHighlight({ + startOffset: h.startOffset, + endOffset: h.endOffset, + color: h.color, + bookmarkId, + text: h.text, + note: null, + }) + } + /> + ); + } + return content; +} diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx index 4e33bb92..a4510698 100644 --- a/apps/web/components/dashboard/preview/TextContentSection.tsx +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; -import { ScrollArea } from "@radix-ui/react-scroll-area"; +import { ScrollArea } from "@/components/ui/scroll-area"; import type { ZBookmarkTypeText } from "@karakeep/shared/types/bookmarks"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; diff --git a/apps/web/components/ui/slider.tsx b/apps/web/components/ui/slider.tsx new file mode 100644 index 00000000..a789595b --- /dev/null +++ b/apps/web/components/ui/slider.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/web/package.json b/apps/web/package.json index bcff4f65..df88638e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", -- cgit v1.2.3-70-g09d2