Skip to content

Comments

Add Flow PDF Viewer#55

Merged
iamEvanYT merged 16 commits intomainfrom
evan/better-pdf-viewer
May 19, 2025
Merged

Add Flow PDF Viewer#55
iamEvanYT merged 16 commits intomainfrom
evan/better-pdf-viewer

Conversation

@iamEvanYT
Copy link
Member

@iamEvanYT iamEvanYT commented May 18, 2025

Summary by CodeRabbit

  • New Features

    • Introduced an experimental built-in PDF viewer with thumbnails, outline navigation, attachments, annotation tools, zoom controls, search, and document properties.
    • Added a resizable, tabbed sidebar for PDF navigation and attachments.
    • Enabled advanced annotation tools including ink and free-text editing with customizable colors and settings.
    • Provided a detailed document properties modal and a comprehensive actions menu for PDF management.
    • Added query parameter support for improved navigation and sharing within the app.
    • Introduced an experimental setting to enable or disable the built-in PDF viewer.
  • Improvements

    • Enhanced platform detection robustness in the renderer to handle missing APIs gracefully.
    • Improved URL handling to support new PDF viewer routes and protocols.
    • Streamlined session and protocol initialization with updated intercept rules and preload script registration.
  • Chores

    • Added new dependencies for PDF viewing, UI components, icons, and D3 utilities.
    • Refactored internal session and protocol handling to support new features and streamline initialization.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 18, 2025

Walkthrough

These changes introduce a comprehensive, feature-rich PDF viewer to the application. The update spans backend session and protocol management, utility refactoring, and a significant expansion of the renderer with a modular PDF viewer interface. The viewer includes a toolbar, sidebar with tabs (thumbnails, outline, attachments), annotation tools, search, zoom, and document info modals. Supporting modules and UI libraries are added, and settings are extended for experimental features.

Changes

File(s) / Path(s) Change Summary
package.json Added dependencies: @pdfslick/react, use-query-params. Added devDependencies: @headlessui/react, @radix-ui/react-alert-dialog, @types/d3-drag, @types/d3-selection, d3-drag, d3-selection, react-icons. Reordered @radix-ui/react-alert-dialog.
src/main/browser/sessions.ts Enhanced session initialization: added registerPreloadScript call, introduced exported defaultSessionReady promise for default session setup with protocol registrations, intercept rules, and preload script registration.
src/main/browser/utility/intercept-rules.ts New module. Exports setupInterceptRules to configure Electron session web request interception: CORS bypass for custom protocols, PDF viewer redirect/intercept, and origin header adjustments.
src/main/browser/utility/protocols.ts Removed entire file. Previously contained comprehensive protocol registration and request handling for custom schemes (flow, flow-internal, flow-external), static file serving, favicon and extension icon retrieval, and CORS bypass. These responsibilities are now split into separate modules.
src/main/browser/utility/protocols/_protocols/flow.ts New module. Registers custom flow protocol handling static files, favicons, assets, extension icons, and PDF cache with strict validation and error handling. Maintains PDF response cache.
src/main/browser/utility/protocols/_protocols/flow-internal.ts New module. Registers flow-internal protocol handling active tab favicons and domain-restricted static files with caching and validation.
src/main/browser/utility/protocols/_protocols/flow-external.ts New module. Registers flow-external protocol serving static files from allowed external domains.
src/main/browser/utility/protocols/index.ts New module. Registers custom schemes (flow-internal, flow, flow-external) with privileges. Exports registerProtocolsWithSession to register flow and flow-external protocols, and registerPreloadScript to register preload script for sessions.
src/main/browser/utility/protocols/utils.ts New module. Provides serveStaticFile utility to serve static files with dev server support and error handling. Defines AllowedDomains interface.
src/main/browser/utility/utils.ts Deleted file. Removed utilities: webui extension ID getter/setter, null guards, generateID, sleep.
src/main/modules/utils.ts Added sleep(ms) and generateID() utility functions, using crypto.randomUUID.
src/main/browser/tabs/tab.ts
src/main/ipc/session/profiles.ts
src/main/ipc/session/spaces.ts
src/main/sessions/profiles.ts
Updated import path for generateID from utility module to @/modules/utils. In tab.ts, removed direct preload script reference, updated utility import.
src/main/browser/window.ts
src/main/onboarding/main.ts
src/main/settings/main.ts
Changed import path for defaultSessionReady to new location in sessions.ts.
src/main/modules/basic-settings.ts Added experimental setting "enableFlowPdfViewer" (default: false) and corresponding settings card.
src/preload/index.ts Moved getPlatform method to end of appAPI, annotated as a global exception. Updated flowAPI.app to allow getPlatform everywhere (permission: "all").
src/renderer/src/App.tsx Wrapped app in QueryParamProvider for query param management. Added route for flow://pdf-viewer rendering PDFViewerRoute.
src/renderer/src/components/main/platform.tsx Wrapped flow.app.getPlatform() call in try-catch to handle missing flow in iframes, fallback to "platform-unknown".
src/renderer/src/lib/url.ts Extended transformUrl to handle flow://pdf-viewer URLs, extracting the url query parameter.
src/renderer/src/routes/pdf-viewer/page.tsx New PDF viewer page component. Extracts url query param, renders PDFViewerApp if present. Conditional theme provider wrapper.
src/renderer/src/routes/pdf-viewer/route.tsx New route component. Dynamically imports and renders page.tsx as the PDF viewer page.
src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx New PDFViewerApp component. Loads PDF using @pdfslick/react, tracks loading progress, manages sidebar state, renders toolbar, thumbsbar, and viewer.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/index.tsx New Thumbsbar sidebar component. Tabbed (thumbnails, outline, attachments), resizable via D3 drag, visibility controlled by prop/state.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Thumbnails.tsx New Thumbnails component. Responsive PDF page thumbnail grid, navigates to pages, highlights current, dynamically adjusts columns.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Outline.tsx New Outline component. Renders nested, collapsible PDF outline (table of contents) with navigation.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx New Attachments component. Lists and allows download/open of PDF attachments.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx New ButtonsBar component. Vertical tab bar for sidebar, toggles between thumbnails, outline, attachments.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx New Toolbar component. Provides controls: sidebar toggle, zoom, page nav/input, search, annotation tools, info modal, download, more actions menu.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/ZoomSelector.tsx New ZoomSelector component. Dropdown and buttons for zooming PDF viewer in/out, preset levels.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/InkMenu.tsx New InkMenu component. Toolbar menu for ink annotation mode, color, thickness, opacity.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx New FreetextMenu component. Toolbar menu for free-text annotation mode, color, font size.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx New DocumentInfo component. Button to open document info modal.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx New DocumentInfoModal component. Modal dialog displaying PDF properties and metadata.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx New MoreActionsMenu component. Dropdown menu for PDF actions: presentation, save, print, rotate, scroll/spread modes, document info.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/SearchBar.tsx New SearchBar component. PDF search UI: input, options (highlight, case, word), prev/next, keyboard shortcuts.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx New Splitter component. Vertical divider for toolbar.
src/renderer/src/routes/pdf-viewer/pdf-viewer/Tooltip.tsx New Tooltip component. Customizable tooltip for UI elements.
src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts New color palette module. Exports color names, strengths, and color map for annotation tools.
src/renderer/src/routes/pdf-viewer/pdf-viewer/README.md Added README.md describing PDF viewer origin and modifications.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant PDFViewerApp
    participant Toolbar
    participant Thumbsbar
    participant PDFSlick (PDF engine)

    User->>App: Navigates to flow://pdf-viewer?url=...
    App->>PDFViewerApp: Renders with pdfFilePath=url
    PDFViewerApp->>PDFSlick: Loads PDF (with progress)
    PDFSlick-->>PDFViewerApp: Loading progress updates
    PDFSlick-->>PDFViewerApp: PDF loaded
    PDFViewerApp->>Thumbsbar: Open sidebar (on load)
    User->>Toolbar: Interacts (zoom, page nav, annotation, search, info)
    Toolbar->>PDFSlick: Dispatches actions/events
    User->>Thumbsbar: Switches tabs, selects page/outline/attachment
    Thumbsbar->>PDFSlick: Navigates or downloads content
    PDFSlick-->>PDFViewerApp: Updates state
    PDFViewerApp-->>App: Rerenders with updated state
Loading

Poem

🐇
A hop and a skip, a PDF to view,
With thumbnails, outlines, and annotations too!
The sidebar resizes, the toolbar delights,
Search, zoom, and info—endless reader nights.
With colors and menus, the rabbit feels clever—
Flow’s PDF viewer: the best update ever!
📄✨

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Contributor

Build is in progress... 🚀

Currently building for this pull request!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

🧹 Nitpick comments (38)
package.json (2)

54-56: Review new UI devDependencies
The @headlessui/react and @radix-ui/react-alert-dialog libraries provide accessible UI components for the viewer toolbar and dialogs. Verify that these are tree-shakeable in your production build and that their bundle size impact is acceptable.


74-75: Inspect D3 modules and type definitions
The D3 packages (d3-drag, d3-selection) and their corresponding type definitions are introduced for interactive sidebar resizing in the PDF viewer. Confirm that only the needed D3 sub-modules are bundled (e.g., using selective imports) to minimize bundle size.

Also applies to: 83-84

src/main/modules/utils.ts (1)

70-77: Function duplication with existing waitFor.

The newly added sleep function duplicates the functionality of the existing waitFor function (lines 33-35). Consider consolidating these functions to avoid maintaining two identical utilities with different names.

You could either:

  1. Use waitFor everywhere and remove sleep
  2. Refactor waitFor to use sleep internally:
export function waitFor(ms: number) {
-  return new Promise((resolve) => setTimeout(resolve, ms));
+  return sleep(ms);
}
src/renderer/src/routes/pdf-viewer/page.tsx (1)

25-33: Consider adding loading states and error boundaries.

The App component would benefit from loading state management and error boundaries to handle PDF loading failures gracefully.

Wrap the Page component with an error boundary:

+import { ErrorBoundary } from 'react-error-boundary';

function App() {
  const ThemeProviderOrFragment = THEME_PROVIDER_ENABLED ? ThemeProvider : Fragment;

  return (
    <ThemeProviderOrFragment>
+      <ErrorBoundary fallback={<div className="p-4">Something went wrong loading the PDF viewer.</div>}>
        <Page />
+      </ErrorBoundary>
    </ThemeProviderOrFragment>
  );
}
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (2)

22-34: Improve accessibility of the document info button.

The button lacks appropriate ARIA attributes for screen readers. Also, consider memoizing the click handler.

Enhance the button with accessibility attributes and optimize the event handler:

+  const handleOpenModal = useCallback(() => {
+    openModal();
+  }, []);

  return (
    <>
      <button
        className={`enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent`}
-        onClick={() => {
-          openModal();
-        }}
+        onClick={handleOpenModal}
+        aria-label="Document Properties"
+        title="Document Properties"
      >
        <VscInfo className="w-4 h-4" />
        <Tooltip position="bottom" alignX="right">
          <p className="whitespace-nowrap">Document Properties</p>
        </Tooltip>
      </button>

      <DocumentInfoModal {...{ usePDFSlickStore, isOpen, closeModal }} />
    </>
  );

24-34: Consider extracting the button styles to a shared class.

The long inline class string could be refactored into a shared class or component for better maintainability.

Extract the button styles to a shared class or component:

+// In a shared styles file or component
+const toolbarButtonClass = `enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent`;

// In this component
  return (
    <>
      <button
-        className={`enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent`}
+        className={toolbarButtonClass}
        onClick={() => {
          openModal();
        }}
      >
src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts (1)

1-211: Consider extracting this color palette to a shared location.

This comprehensive color palette appears to be a reusable resource that could benefit other parts of the application beyond the PDF viewer. Consider moving it to a shared UI utilities directory to promote reuse across the application.

Additionally, adding documentation comments explaining the color system's purpose and how to use it would make it easier for other developers to understand and utilize these colors correctly.

src/main/browser/sessions.ts (1)

101-101: Document why a 50ms sleep is necessary.

The code includes a 50ms delay before resolving the promise, but it's unclear why this specific timeout value is needed. Adding a comment explaining the purpose of this delay would help future developers understand why it's there and whether it can be safely modified.

-  // wait for 50 ms before returning
+  // A small delay is needed to ensure all session initialization is complete
+  // before other components that depend on the session can use it
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Outline.tsx (2)

23-28: Use a more accessible approach for hiding checkbox inputs.

Positioning elements thousands of pixels off-screen is a non-standard approach and may cause accessibility issues. Consider using the sr-only class from Tailwind or visually-hidden pattern.

-            className="peer absolute -top-[10000px] -left-[10000px]"
+            className="peer sr-only"

61-71: Add empty state handling and memoization for better performance.

The component doesn't handle the case when there's no outline data, and it could benefit from memoization to prevent unnecessary re-renders.

-const Outline = ({ usePDFSlickStore, show }: OutlineProps) => {
+import { useMemo } from "react";
+
+const Outline = ({ usePDFSlickStore, show }: OutlineProps) => {
   const documentOutline = usePDFSlickStore((s) => s.documentOutline);
   const pdfSlick = usePDFSlickStore((s) => s.pdfSlick);
+  
+  const outlineContent = useMemo(() => {
+    if (!documentOutline || documentOutline.length === 0) {
+      return (
+        <div className="p-4 text-center text-slate-500">
+          No outline available for this document.
+        </div>
+      );
+    }
+    
+    return renderOutlineItems({ outline: documentOutline, pdfSlick });
+  }, [documentOutline, pdfSlick]);

   return (
     <div className={clsx("overflow-auto absolute inset-0", { invisible: !show })}>
       <div className="p-2 pl-0.5 text-slate-700 text-sm">
-        {renderOutlineItems({ outline: documentOutline, pdfSlick })}
+        {outlineContent}
       </div>
     </div>
   );
 };
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx (1)

31-43: Add empty state handling and improve accessibility.

The component doesn't handle the case when there are no attachments, which would provide a better user experience.

 const Attachments = ({ usePDFSlickStore, show }: AttachmentsProps) => {
   const attachments = usePDFSlickStore((s) => s.attachments);
+  const hasAttachments = attachments.size > 0;

   return (
     <div className={clsx("overflow-auto absolute inset-0", { invisible: !show })}>
       <div className="p-2 text-slate-700 text-sm">
-        {Array.from(attachments.entries()).map(([key, { filename, content }]) => (
-          <AttachmentButton key={key} {...{ usePDFSlickStore, filename, content }} />
-        ))}
+        {hasAttachments ? (
+          Array.from(attachments.entries()).map(([key, { filename, content }]) => (
+            <AttachmentButton key={key} {...{ usePDFSlickStore, filename, content }} />
+          ))
+        ) : (
+          <div className="p-4 text-center text-slate-500">
+            No attachments found in this document.
+          </div>
+        )}
       </div>
     </div>
   );
 };
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/SearchBar.tsx (5)

17-17: Consider renaming state setter for consistency

The state variable is term but you're using setTerms for the setter. For consistency, consider renaming to setTerm.

- const [term, setTerms] = useState("");
+ const [term, setTerm] = useState("");

77-89: Add debouncing for search-as-you-type

The search is triggered on every input change, which can cause performance issues in large PDF documents. Consider implementing debouncing to reduce the number of search operations.

+ import { useCallback } from "react";
+ import debounce from "lodash/debounce";

// Inside component, before return:
+ const debouncedSearch = useCallback(
+   debounce((query) => {
+     pdfSlick?.eventBus.dispatch("find", {
+       type: "",
+       query,
+       caseSensitive,
+       highlightAll,
+       entireWord
+     });
+   }, 300),
+   [pdfSlick, caseSensitive, highlightAll, entireWord]
+ );

// Then in the onInput handler:
onInput={(e) => {
  const query = (e.target as HTMLInputElement).value;
-  pdfSlick?.eventBus.dispatch("find", {
-    type: "",
-    query,
-    caseSensitive,
-    highlightAll,
-    entireWord
-  });
+  debouncedSearch(query);
-  setTerms(query);
+  setTerm(query);
}}

21-35: Add support for F3 and Shift+F3 keyboard shortcuts

The component handles Cmd+F to open, but doesn't support common shortcuts like F3 or Shift+F3 for cycling through search results. Consider adding these for better user experience.

const handleEscape = (e: KeyboardEvent) => {
  let handled = false;

  if (e.key === "Escape") {
    setIsOpen(false);
  } else if (e.key === "f" && e.metaKey) {
    handled = true;
    setIsOpen(true);
+  } else if (e.key === "F3") {
+    handled = true;
+    if (term) {
+      pdfSlick?.eventBus.dispatch("find", {
+        type: "again",
+        query: term,
+        caseSensitive,
+        highlightAll,
+        entireWord,
+        findPrevious: e.shiftKey
+      });
+    }
  }

  if (handled) {
    e.preventDefault();
    return;
  }
};

88-88: Update function name to match state variable

For consistency with the state variable name (term), use setTerm instead of setTerms.

- setTerms(query);
+ setTerm(query);

54-64: Add ARIA attributes to improve accessibility

The search button lacks proper ARIA attributes for better screen reader support.

<button
  onClick={() => setIsOpen(!isOpen)}
+ aria-label="Search document"
+ aria-expanded={isOpen}
+ aria-controls="search-panel"
  className={`enabled:hover:bg-slate-200 enabled:hover:text-black  disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent ${
    isOpen ? "bg-slate-200 text-black" : "text-slate-500"
  }`}
>
  <VscSearch className="h-4 w-4" />
</button>

{isOpen && (
  <div 
+   id="search-panel"
    className="absolute top-full mt-2 left-0 w-fit">
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (4)

74-82: Remove unnecessary preventDefault calls

The e.preventDefault() calls in button click handlers are unnecessary since these are button click events, not form submissions.

onClick={() => pdfSlick?.requestPresentationMode()}

For the rotation handlers:

onClick={(e) => {
-  e.preventDefault();
  pdfSlick?.setRotation(pagesRotation + 90);
}}
onClick={(e) => {
-  e.preventDefault();
  pdfSlick?.setRotation(pagesRotation - 90);
}}

Also applies to: 174-177, 191-194


30-40: Consider adding error handling for Zustand store operations

The component accesses multiple properties from the Zustand store but doesn't handle cases where these might be undefined. Consider adding error handling or fallbacks.

const { pdfSlick, pagesRotation, pageNumber, numPages, scrollMode, spreadMode } = usePDFSlickStore(
  (s) => ({
    pdfSlick: s.pdfSlick,
    pagesRotation: s.pagesRotation ?? 0,
    pageNumber: s.pageNumber ?? 1,
    numPages: s.numPages ?? 1,
    scrollMode: s.scrollMode ?? ScrollMode.VERTICAL,
    spreadMode: s.spreadMode ?? SpreadMode.NONE
  }),
  shallow
);

69-361: Extract menu item rendering to reduce duplication

The menu items have a lot of duplicated code for styling and structure. Consider extracting a reusable menu item component to reduce duplication.

You could create a helper component like:

type MenuItemProps = {
  onClick: () => void;
  disabled?: boolean;
  icon: React.ReactNode;
  label: string;
  isActive?: boolean;
};

const MenuItem = ({ onClick, disabled, icon, label, isActive }: MenuItemProps) => {
  return (
    <Menu.Item>
      {({ active }) => (
        <button
          disabled={disabled}
          onClick={onClick}
          className={clsx(
            "w-full items-center flex space-x-2 box-border text-left px-2 py-1.5 text-xs disabled:opacity-50",
            {
              "bg-slate-100 text-gray-900": active,
              "text-gray-700": !active
            }
          )}
        >
          {icon}
          <span className="flex-1">{label}</span>
          {isActive && <VscCheck className="w-3" />}
        </button>
      )}
    </Menu.Item>
  );
};

Then use it like:

<MenuItem 
  onClick={() => pdfSlick?.requestPresentationMode()}
  icon={<VscPlay className="w-4 h-4" />}
  label="Presentation Mode"
/>

57-58: Add aria-haspopup attribute to improve accessibility

The menu button should include additional ARIA attributes for better accessibility.

<Menu.Button
  disabled={!pdfSlick}
+ aria-haspopup="true"
  className="enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent"
>
  <span className="sr-only">Open more actions menu</span>
  <VscKebabVertical className="w-4 h-4" />
</Menu.Button>
src/renderer/src/routes/pdf-viewer/pdf-viewer/Tooltip.tsx (3)

10-62: Add a delay to tooltip display

The tooltip appears immediately on hover, which could lead to flickering when moving the mouse across multiple elements. Consider adding a small delay.

const Tooltip = ({ children, position = "right", alignX = "center" }: TooltipProps) => {
  return (
    <div
      role="tooltip"
      className={clsx(
        "absolute z-10 inline-block text-xs transition-opacity duration-300",
-       "rounded-sm shadow opacity-0 invisible",
+       "rounded-sm shadow opacity-0 invisible transition-all delay-300",
        "text-gray-500 bg-slate-50 border border-slate-300",
        "group-hover:visible group-hover:opacity-100",
        {
          "left-full top-1/2 -translate-y-1/2 ml-4": position === "right",
          "top-full mt-2": position === "bottom",
          "right-1/2 translate-x-1/2": position === "bottom" && alignX === "center",
          "right-0": position === "bottom" && alignX === "right",
          "left-0": position === "bottom" && alignX === "left"
        }
      )}
    >

4-8: Improve documentation of component usage

The Tooltip component depends on the parent element having a group class, but this isn't documented in the props interface.

type TooltipProps = {
  children: ReactNode;
  position?: "bottom" | "right";
  alignX?: "center" | "right" | "left";
+  /**
+   * Note: This component must be wrapped in a parent element with the 'group' class
+   * for the hover functionality to work properly.
+   */
};

1-64: Add mobile/touch device support

The tooltip relies on CSS hover, which doesn't work well on touch devices. Consider adding support for touch interactions.

To properly implement this, you would need to:

  1. Add state to track whether the tooltip is visible
  2. Add touch event handlers to toggle the tooltip
  3. Use the state to conditionally apply classes instead of just CSS hover

Basic implementation:

import { ReactNode, useState } from "react";
import clsx from "clsx";

type TooltipProps = {
  children: ReactNode;
  position?: "bottom" | "right";
  alignX?: "center" | "right" | "left";
};

const Tooltip = ({ children, position = "right", alignX = "center" }: TooltipProps) => {
  const [isVisible, setIsVisible] = useState(false);
  
  return (
    <div
      role="tooltip"
      className={clsx(
        "absolute z-10 inline-block text-xs transition-opacity duration-300",
        "rounded-sm shadow opacity-0 invisible",
        "text-gray-500 bg-slate-50 border border-slate-300",
        {
          "opacity-100 visible": isVisible,
          "group-hover:visible group-hover:opacity-100": true,
          "left-full top-1/2 -translate-y-1/2 ml-4": position === "right",
          "top-full mt-2": position === "bottom",
          "right-1/2 translate-x-1/2": position === "bottom" && alignX === "center",
          "right-0": position === "bottom" && alignX === "right",
          "left-0": position === "bottom" && alignX === "left"
        }
      )}
    >
      {/* ... rest of component */}
    </div>
  );
};
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx (4)

83-89: Strengthen page number validation

The page number validation is good, but could be more robust by using a regex check for numeric input before parsing.

onSubmit={(e) => {
  e.preventDefault();
- const newPageNumber = parseInt(wantedPageNumber + "");
- if (Number.isInteger(newPageNumber) && newPageNumber > 0 && newPageNumber <= numPages) {
+ const pageStr = String(wantedPageNumber);
+ if (/^\d+$/.test(pageStr)) {
+   const newPageNumber = parseInt(pageStr, 10);
+   if (newPageNumber > 0 && newPageNumber <= numPages) {
      pdfSlick?.linkService.goToPage(newPageNumber);
+   } else {
+     setWantedPageNumber(pageNumber);
+   }
  } else {
    setWantedPageNumber(pageNumber);
  }
}}

35-35: Add type safety to updatePageNumber function

The updatePageNumber function should have stronger typing to ensure pageNumber is always a number.

- const updatePageNumber = ({ pageNumber }: { pageNumber: number }) => setWantedPageNumber(pageNumber);
+ const updatePageNumber = ({ pageNumber }: { pageNumber: number }) => setWantedPageNumber(pageNumber || 1);

125-149: Implement a more responsive layout for smaller screens

Some UI elements are already hidden on small screens (sm breakpoint), but the toolbar could still be crowded on very small screens. Consider implementing a more responsive design that collapses more elements into the "More" menu at smaller sizes.

You could:

  1. Move more elements into the MoreActionsMenu component
  2. Use additional responsive breakpoints (like md or lg)
  3. Consider implementing a collapsible toolbar that shows only essential controls initially

For example:

<div className="px-1 space-x-1 flex items-center justify-end">
- <FreetextMenu {...{ usePDFSlickStore }} />
- <InkMenu {...{ usePDFSlickStore }} />
+ <div className="hidden xs:flex items-center space-x-1">
+   <FreetextMenu {...{ usePDFSlickStore }} />
+   <InkMenu {...{ usePDFSlickStore }} />
+ </div>

  <Splitter />

  <div className="items-center space-x-1 hidden sm:flex">

And then add these elements to the MoreActionsMenu when they're hidden.


46-55: Add tooltip to sidebar toggle button

The sidebar toggle button lacks a tooltip, unlike other buttons in the toolbar. For consistency, add a tooltip.

<button
  className={`enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent`}
  onClick={() => setIsThumbsbarOpen(!isThumbsbarOpen)}
>
  {isThumbsbarOpen ? (
    <VscLayoutSidebarLeftOff className="h-4 w-4" />
  ) : (
    <VscLayoutSidebarLeft className="h-4 w-4" />
  )}
+ <Tooltip position="bottom">
+   <p className="whitespace-nowrap">{isThumbsbarOpen ? "Hide sidebar" : "Show sidebar"}</p>
+ </Tooltip>
</button>
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (2)

108-115: Consider using dynamic locale instead of hardcoded "en-US".

The date formatting currently uses a hardcoded "en-US" locale. Consider using the user's system locale for a more localized experience:

-new Intl.DateTimeFormat("en-US", {
+new Intl.DateTimeFormat(navigator.language || "en-US", {

Also applies to: 122-129


62-180: Consider extracting repeated property display pattern.

There's significant repetition in the property display pattern. Consider extracting this into a reusable component to improve maintainability:

+type PropertyRowProps = {
+  label: string;
+  value: React.ReactNode;
+};
+
+const PropertyRow = ({ label, value }: PropertyRowProps) => (
+  <div className="grid grid-cols-3 gap-4 px-6 py-1 my-0.5 hover:bg-slate-50">
+    <dt className="text-sm font-medium text-gray-500">{label}</dt>
+    <dd className="text-sm text-gray-900 col-span-2">
+      {value ?? <span className="text-xs text-gray-500">N/A</span>}
+    </dd>
+  </div>
+);

Then use it throughout the component.

src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx (1)

28-32: Extract common button styles.

All three buttons share similar styling logic. Consider extracting the common styles to reduce duplication:

+const buttonBaseClasses = "enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent";
+const buttonActiveClasses = "enabled:bg-blue-100 enabled:text-black border-blue-200";

// Then use them like:
className={clsx(
  buttonBaseClasses,
  {
    [buttonActiveClasses]: tab === "thumbnails"
  }
)}

Also applies to: 43-47, 59-63

src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx (1)

45-49: Improve progress indicator appearance.

The current progress bar is very thin (h-px) which might be hard to see on high-DPI displays. Consider making it slightly taller and adding a subtle animation:

<div
-  className="fixed top-0 left-0 h-px bg-blue-600 z-50 transition-all duration-150 ease-out"
+  className="fixed top-0 left-0 h-1 bg-blue-600 z-50 transition-all duration-150 ease-out animate-pulse"
  style={{ width: `${loadedPerc}%` }}
></div>
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/ZoomSelector.tsx (1)

49-49: Replace bitwise operator with more readable Math function.

The code uses the bitwise NOT operator twice (~~) as a clever way to round numbers, but this reduces readability. Consider using standard Math methods:

- {scaleValue && presets.has(scaleValue) ? presets.get(scaleValue) : `${~~(scale * 100)}%`}
+ {scaleValue && presets.has(scaleValue) ? presets.get(scaleValue) : `${Math.floor(scale * 100)}%`}
src/main/browser/utility/intercept-rules.ts (2)

13-14: Consider using the modern URL API instead of deprecated URL.parse

The code is using the deprecated URL.parse() method. It's recommended to use the modern URL constructor instead.

- const protocol = URL.parse(currentUrl ?? "")?.protocol;
+ const protocol = new URL(currentUrl ?? "").protocol;

48-52: Add early return pattern for improved readability

The code could benefit from using an early return pattern when the URL object is null, which would make the logic clearer and reduce nesting.

  const url = details.url;
  const urlObject = URL.parse(url);
  if (!urlObject) {
-   return callback({});
+   callback({});
+   return;
  }
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx (1)

54-55: Remove commented code

There are commented-out <Menu.Item> tags that should be removed to maintain clean code.

- // <Menu.Item key={`${name}-${s}`}>
  <button
    key={`${name}-${s}`}
    className="p-2 rounded-full border border-slate-200 hover:border-slate-400 hover:shadow-sm focus:ring-blue-500"
    style={{
      backgroundColor: colors[name][s]
    }}
    onClick={() => {
      pdfSlick?.setAnnotationEditorMode(AnnotationEditorType.FREETEXT);
      pdfSlick?.setAnnotationEditorParams([
        {
          type: AnnotationEditorParamsType.FREETEXT_COLOR,
          value: colors[name][s]
        }
      ]);
    }}
  />
- // </Menu.Item>

Also applies to: 71-72

src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/index.tsx (1)

23-36: Extract magic numbers to named constants

The code uses magic numbers (233 and 620) for the minimum and maximum width constraints. It would be better to define these as named constants at the top of the component for better readability and maintainability.

const Thumbsbar = ({ usePDFSlickStore, isThumbsbarOpen, thumbsRef }: ThumbsbarProps) => {
  const [tab, setTab] = useState<"thumbnails" | "outline" | "attachments">("thumbnails");

+  const MIN_SIDEBAR_WIDTH = 233;
+  const MAX_SIDEBAR_WIDTH = 620;
+
  const containerRef = useRef<HTMLDivElement>(null);
  const resizerRef = useRef<HTMLDivElement>(null);
  const [isResizing, setIsResizing] = useState(false);
- const [width, setWidth] = useState(233);
+ const [width, setWidth] = useState(MIN_SIDEBAR_WIDTH);

  useEffect(() => {
    let newWidth = 0;

    const dragResize = drag<HTMLDivElement, unknown>()
      .on("start", () => {
        newWidth = containerRef.current?.clientWidth ?? 0;
        setIsResizing(true);
      })
      .on("drag", (e) => {
        newWidth += e.dx;
-       const width = Math.min(620, Math.max(233, newWidth));
+       const width = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, newWidth));
        setWidth(width);
      })
src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Thumbnails.tsx (2)

40-41: Shadowing width/height causes mental overhead

({ pageNumber, width, height, … }) shadows the outer width from useMeasure, making the code harder to scan. A small rename improves readability:

-          {({ pageNumber, width, height, src, pageLabel, loaded }) => (
+          {({ pageNumber, width: thumbW, height: thumbH, src, pageLabel, loaded }) => (-                        width: `${width + 2}px`,
-                        height: `${height + 2}px`
+                        width: `${thumbW + 2}px`,
+                        height: `${thumbH + 2}px`-                      {src && <img src={src} width={width} height={height} />}
+                      {src && <img src={src} width={thumbW} height={thumbH} />}

25-29: Minor ref forwarding ergonomics

ref(el!) will throw if el is null (when unmounting) because the non-null assertion forces the type. Forwarding the value as-is avoids the unnecessary assertion:

-      ref={(el) => {
-        ref(el!);
-      }}
+      ref={ref}

This works because the ref function returned by useMeasure matches the expected signature.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between ef642ca and aa9cfb0.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • package.json (4 hunks)
  • src/main/browser/sessions.ts (3 hunks)
  • src/main/browser/tabs/tab.ts (2 hunks)
  • src/main/browser/utility/intercept-rules.ts (1 hunks)
  • src/main/browser/utility/protocols.ts (6 hunks)
  • src/main/browser/utility/utils.ts (0 hunks)
  • src/main/browser/window.ts (1 hunks)
  • src/main/ipc/session/profiles.ts (1 hunks)
  • src/main/ipc/session/spaces.ts (1 hunks)
  • src/main/modules/basic-settings.ts (2 hunks)
  • src/main/modules/utils.ts (2 hunks)
  • src/main/onboarding/main.ts (1 hunks)
  • src/main/sessions/profiles.ts (1 hunks)
  • src/main/settings/main.ts (1 hunks)
  • src/preload/index.ts (2 hunks)
  • src/renderer/src/App.tsx (3 hunks)
  • src/renderer/src/components/main/platform.tsx (1 hunks)
  • src/renderer/src/lib/url.ts (2 hunks)
  • src/renderer/src/routes/pdf-viewer/page.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Outline.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Thumbnails.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/InkMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/SearchBar.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/ZoomSelector.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Tooltip.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/route.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/browser/utility/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/renderer/src/routes/pdf-viewer/page.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx (1)
  • PDFViewerApp (10-53)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1)
  • DocumentInfoModal (20-198)
src/renderer/src/App.tsx (2)
src/renderer/src/routes/pdf-viewer/route.tsx (1)
  • Route (3-14)
src/renderer/src/components/main/platform.tsx (1)
  • PlatformProvider (5-29)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts (2)
  • colorStrengths (209-209)
  • colorNames (208-208)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1)
  • DocumentInfoModal (20-198)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/InkMenu.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts (2)
  • colorStrengths (209-209)
  • colorNames (208-208)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (1)
  • DocumentInfo (11-39)
src/main/browser/utility/protocols.ts (2)
src/main/modules/paths.ts (1)
  • PATHS (18-23)
src/main/browser/utility/intercept-rules.ts (1)
  • setupInterceptRules (78-84)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (33)
package.json (2)

97-97: Approve addition of react-icons
Including react-icons will simplify iconography across the PDF viewer UI. Ensure that only the required icon sets are imported to avoid including the entire icon library in the bundle.


33-47:

Details

❓ Verification inconclusive

Validate new runtime dependencies
The addition of @pdfslick/react and use-query-params is crucial for the new PDF viewer and URL state management. Please confirm these package versions are compatible with your existing React/Electron setup, check for any licensing constraints, and ensure there are no known security vulnerabilities.

Run the following script to audit these new dependencies:


🏁 Script executed:

#!/bin/bash
npm install
npm audit --json | jq '.advisories | to_entries[] | select(.value.module_name == "pdfslick-react" or .value.module_name == "use-query-params")'

Length of output: 736


🏁 Script executed:

#!/bin/bash
# Generate a lockfile and rerun the audit for the two new dependencies
npm install --package-lock-only
npm audit --json | jq '.advisories 
  | to_entries[] 
  | select(.value.module_name == "@pdfslick/react" or .value.module_name == "use-query-params")'

Length of output: 763


Manual verification required for new runtime dependencies
I wasn’t able to generate a lockfile or run a full npm audit in CI (no existing lockfile and audit errors), so please double-check the following for both @pdfslick/react and use-query-params:

  • Compatibility: confirm these versions work with your current React and Electron versions.
  • Licensing: run npm view @pdfslick/react license and npm view use-query-params license to ensure there are no restrictive terms.
  • Security: add or regenerate a lockfile (npm install --package-lock-only), then run npm audit or an equivalent scan over these two modules.
src/main/ipc/session/profiles.ts (1)

2-2: Approve updated import path
The generateID utility import has been correctly updated to reflect its new location in @/modules/utils, aligning with the refactoring of utility functions.

src/main/settings/main.ts (1)

3-3: Approve updated session import
The defaultSessionReady constant import path has been updated to @/browser/sessions, matching the relocation in your refactoring.

src/main/onboarding/main.ts (1)

4-4: Approve updated session import
The import path for defaultSessionReady has been updated to the new @/browser/sessions module, maintaining consistency with related files.

src/main/ipc/session/spaces.ts (1)

14-14: Approve updated import path
The generateID import has been correctly switched to @/modules/utils following the utility module relocation.

src/main/sessions/profiles.ts (1)

8-8: Import path update to reflect utility function relocation.

The import path for generateID has been updated from @/browser/utility/utils to @/modules/utils as part of the broader refactoring where utility functions have been consolidated.

src/main/browser/window.ts (1)

13-13: Import path update to reflect session initialization centralization.

The import path for defaultSessionReady has been updated from @/browser/utility/protocols to @/browser/sessions as part of centralizing session initialization, which includes protocol registrations and preload script setup.

src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx (1)

1-5: Well-designed UI separator component.

This component creates a visually clean vertical line separator using Tailwind CSS. The implementation is simple, focused, and reusable, making it ideal for separating toolbar elements in the PDF viewer.

src/main/browser/tabs/tab.ts (3)

9-9: Removed unused PATHS import.

The import of PATHS is now commented out as it's no longer needed after moving the preload script implementation.


13-13: Import path update to reflect utility function relocation.

The import path for generateID has been updated from @/browser/utility/utils to @/modules/utils as part of the broader utility functions consolidation.


97-101: Improved tab transparency and preload script handling.

Two changes here:

  1. Added transparent: true to support transparency in web contents views, which is necessary for the PDF viewer.
  2. Removed the direct preload script path, replaced with a comment indicating it's now handled in protocols.ts.

This improves the architecture by centralizing preload script management at the session level rather than hardcoding it in each tab.

src/renderer/src/routes/pdf-viewer/route.tsx (1)

1-14: Well-implemented dynamic route loading.

This component implements code splitting through dynamic imports, which is a good practice for improving application performance. The PDF viewer page will only be loaded when this route is accessed, reducing the initial bundle size.

The implementation follows React best practices for code splitting and lazy loading.

src/renderer/src/components/main/platform.tsx (1)

8-23: Robust error handling for platform detection.

Adding the try-catch block is a good defensive programming practice that prevents runtime errors when the flow object is unavailable in contexts like iframes. The comments clearly explain the rationale behind this change, which is helpful for future maintenance.

src/renderer/src/lib/url.ts (1)

84-87: Support for PDF viewer protocol implementation.

The implementation for handling flow://pdf-viewer URLs is consistent with the existing pattern for special Flow URLs. The nullish coalescing operator (??) ensures a sensible default when the URL parameter is absent.

src/main/modules/basic-settings.ts (2)

211-218: Experimental feature toggle for PDF viewer.

Adding the PDF viewer as an opt-in experimental feature is a good approach. Setting the default value to false is appropriate for a new feature that may still need refinement.


273-278: New settings category for experimental features.

Creating a dedicated "Experimental Settings" category provides a clear distinction between stable and experimental features. This helps set appropriate user expectations for features that may change or have limited functionality.

src/main/modules/utils.ts (1)

79-85: Efficient ID generation implementation.

The generateID function provides a clean approach to generate short random identifiers by using the first segment of a UUID. This is more efficient than generating and storing full UUIDs when only short identifiers are needed.

src/renderer/src/App.tsx (4)

5-6: Good addition of query parameter support.

The integration of QueryParamProvider and WindowHistoryAdapter is a sensible choice for managing URL parameters throughout the application.


24-24: Appropriate import for the new PDF viewer route.

Adding this import follows the established pattern for route components in the codebase.


63-65: Well-structured route definition for the PDF viewer.

The new route follows the same pattern as existing routes, providing consistency in the routing architecture.


72-77: Correct implementation of the updated App component.

Wrapping the application with QueryParamProvider ensures query parameter functionality is available throughout the app. The component hierarchy is maintained with PlatformProvider as a child component.

src/renderer/src/routes/pdf-viewer/page.tsx (2)

1-7: Appropriate imports for PDF viewer functionality.

The imports include the necessary components for theme support, query parameter handling, and PDF rendering. The PDF viewer CSS is correctly imported to ensure proper styling.


8-9:

Details

✅ Verification successful

Consider addressing theming issues.

The comment suggests there are unresolved issues with the theme provider. It would be beneficial to investigate and fix these issues rather than disabling the theme functionality.


🏁 Script executed:

#!/bin/bash
# Search for other theme provider issues or workarounds in the codebase
rg -n "Theme makes it go all weird" --context 3
rg -n "THEME_PROVIDER_ENABLED" --context 3

Length of output: 1548


Intentionally disabling ThemeProvider for PDF viewer
The PDF viewer’s bundled CSS (@pdfslick/react/dist/pdf_viewer.css) conflicts with our theme styles, so disabling the ThemeProvider here is an intentional workaround. No further theming changes are required for this page.

src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (2)

1-9: Well-structured imports and props definition.

The component has clear type definitions and imports the necessary dependencies for its functionality.


11-21: Good state management for modal visibility.

The component correctly uses React useState for managing modal visibility state with clear open/close functions.

src/preload/index.ts (1)

502-504: Consistent permission override implementation.

The permission override for getPlatform follows the established pattern for other API methods that require special access, making it a maintainable solution.

src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1)

5-12: Good utility function for byte formatting.

The formatBytes function is well implemented with proper handling of different size units and decimal precision.

src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx (1)

58-58:

Details

✅ Verification successful

Verify attachments type.

The check attachments.size < 1 assumes attachments is a Map or Set. Ensure this is consistent with how attachments are stored in the PDF store.


🏁 Script executed:

#!/bin/bash
# Finding the attachments type in the PDFSlick store definition
rg -A 3 -B 3 "attachments" --glob "*.ts*" | grep -i "type" 

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Search for the definition and usage of `attachments` across the TypeScript codebase
rg -n -C3 "attachments" --glob "*.ts*" .

Length of output: 6176


🏁 Script executed:

#!/bin/bash
# Locate the PDFSlick store definition and inspect attachments type
store_file=$(rg -l "export const usePDFSlickStore" --glob "*.ts*")
echo "Store file: $store_file"
rg -n -C3 "attachments" "$store_file"

Length of output: 172


🏁 Script executed:

#!/bin/bash
# Locate all occurrences of the usePDFSlickStore hook to find its import and definition
rg -n "usePDFSlickStore" -C2 --glob "*.ts*" .

Length of output: 25301


🏁 Script executed:

#!/bin/bash
FILE="src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx"
echo "Showing import section of $FILE"
sed -n '1,50p' "$FILE"

Length of output: 2481


Attachments type confirmed
The attachments prop returned by usePDFSlickStore is a Map (as shown by its use with attachments.entries() in Attachments.tsx), so using attachments.size and attachments.size < 1 is valid. No changes are needed here.

src/main/browser/utility/intercept-rules.ts (2)

66-74: Handle potential error when accessing urlObject.origin

The code assumes that urlObject.origin will always be available, but it might be undefined for some URL types. Consider adding a fallback.

- const newHeaders = { ...details.requestHeaders, Origin: urlObject.origin };
+ const origin = urlObject.origin || `${urlObject.protocol}//${urlObject.host}`;
+ const newHeaders = { ...details.requestHeaders, Origin: origin };
  callback({ requestHeaders: newHeaders });

77-84: Well-structured setup function

The setupInterceptRules function is well-structured, providing a clear entry point for setting up both interception rules. The code is modular and follows good separation of concerns.

src/main/browser/utility/protocols.ts (2)

47-49: No objections to domain-whitelist extension

Adding extensions and pdf-viewer to FLOW_PROTOCOL_ALLOWED_DOMAINS looks consistent with the new feature set and doesn’t introduce any obvious security concerns beyond those already present for the existing domains.


352-357:

Details

❓ Verification inconclusive

session.registerPreloadScript may not exist in current Electron versions

Electron ≤ v29 exposes session.setPreloads() (static list) and, in newer nightlies, session.addPreload(); a registerPreloadScript() method is not present in any stable release at the time of writing. Calling an undefined API will throw at runtime and break the session initialisation path.

-export function registerPreloadScript(session: Session) {
-  session.registerPreloadScript({
-    id: "flow-preload",
-    type: "frame",
-    filePath: PATHS.PRELOAD
-  });
+export function registerPreloadScript(session: Session) {
+  // Prefer upcoming API when available, else fall back to the stable one.
+  // NOTE: keep this block until the project locks to an Electron version
+  if (typeof (session as any).registerPreloadScript === "function") {
+    (session as any).registerPreloadScript({
+      id: "flow-preload",
+      type: "frame",
+      filePath: PATHS.PRELOAD
+    });
+  } else if (typeof session.setPreloads === "function") {
+    // Avoid duplicates if registerPreloadScript gets called twice for the same session
+    const current = new Set(session.getPreloads?.() ?? []);
+    current.add(PATHS.PRELOAD);
+    session.setPreloads([...current]);
+  } else {
+    console.error("No supported preload-registration API found on Session");
+  }
+}

Please verify which Electron version the application ships with and adjust accordingly.


#!/bin/bash
# Retrieve the Electron version from package.json
if [ -f package.json ]; then
  jq -r '(.dependencies.electron // .devDependencies.electron // .peerDependencies.electron) // "not specified"' package.json
else
  echo "package.json not found"
fi

🏁 Script executed:

#!/bin/bash
# 1. Electron version from package.json
if [ -f package.json ]; then
  echo "Electron version from package.json:"
  jq -r '(.dependencies.electron // .devDependencies.electron // .peerDependencies.electron) // "not specified"' package.json
else
  echo "package.json not found"
fi

# 2. Search for any occurrences of registerPreloadScript in the codebase
echo "Searching for registerPreloadScript references in codebase:"
rg "registerPreloadScript" -n .

Length of output: 829


Ensure Electron preload API compatibility
The project is using Electron “github:castlabs/electron-releases#v35.3.0+wvcus,” which may not expose session.registerPreloadScript. Verify whether your custom build includes this API; if not, fall back to the stable session.setPreloads() approach to avoid runtime errors.

Files to review:

  • src/main/browser/utility/protocols.ts (lines 352–357)
  • src/main/browser/sessions.ts (calls at lines 73 & 98)

Suggested update in protocols.ts:

 export function registerPreloadScript(session: Session) {
-  session.registerPreloadScript({
-    id: "flow-preload",
-    type: "frame",
-    filePath: PATHS.PRELOAD
-  });
+  // Use new API if available; otherwise fall back to stable setPreloads()
+  if (typeof (session as any).registerPreloadScript === "function") {
+    (session as any).registerPreloadScript({
+      id: "flow-preload",
+      type: "frame",
+      filePath: PATHS.PRELOAD
+    });
+  } else if (typeof session.setPreloads === "function") {
+    const current = new Set(session.getPreloads?.() ?? []);
+    current.add(PATHS.PRELOAD);
+    session.setPreloads([...current]);
+  } else {
+    console.error("No supported preload-registration API found on Session");
+  }
 }

Please confirm your Electron fork’s API surface and adjust accordingly.

@iamEvanYT iamEvanYT force-pushed the evan/better-pdf-viewer branch from aa9cfb0 to eb98c1a Compare May 18, 2025 22:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (1)

343-343: Fix incorrect button disable condition

The "Document Properties" button is incorrectly disabled when on the last page. Document properties should be accessible regardless of the current page.

-disabled={pageNumber === numPages}
🧹 Nitpick comments (6)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (6)

73-85: Consider adding keyboard shortcuts for quick access

The presentation mode button (and other frequently used actions) would benefit from keyboard shortcuts to improve user experience and accessibility.

Add a keyboard shortcut indicator to the presentation mode button:

 <button
   onClick={() => pdfSlick?.requestPresentationMode()}
   className={clsx(
     "w-full items-center flex space-x-2 box-border text-left px-2 py-1.5 text-xs disabled:opacity-50",
     {
       "bg-slate-100 text-gray-900": active,
       "text-gray-700": !active
     }
   )}
 >
   <VscPlay className="w-4 h-4" />
-  <span>Presentation Mode</span>
+  <span className="flex-1">Presentation Mode</span>
+  <span className="text-xs text-gray-500">F11</span>
 </button>

27-40: Optimize component rendering with useCallback

Event handlers are recreated on each render, which can impact performance. Using useCallback would maintain referential equality between renders.

 const MoreActionsMenu = ({ usePDFSlickStore }: MoreActionsMenuProps) => {
   const [isOpen, setIsOpen] = useState(false);
 
   const { pdfSlick, pagesRotation, pageNumber, numPages, scrollMode, spreadMode } = usePDFSlickStore(
     (s) => ({
       pdfSlick: s.pdfSlick,
       pagesRotation: s.pagesRotation,
       pageNumber: s.pageNumber,
       numPages: s.numPages,
       scrollMode: s.scrollMode,
       spreadMode: s.spreadMode
     }),
     shallow
   );
+
+  const handleSave = useCallback(() => pdfSlick?.downloadOrSave(), [pdfSlick]);
+  const handlePrint = useCallback(() => pdfSlick?.triggerPrinting(), [pdfSlick]);
+  const handleRotateClockwise = useCallback(() => pdfSlick?.setRotation(pagesRotation + 90), [pdfSlick, pagesRotation]);

Then use these callbacks in your click handlers.


50-69: Enhance menu accessibility with ARIA attributes

The menu button has good screen reader support, but the menu items could benefit from additional ARIA attributes for improved accessibility.

Add ARIA attributes to the menu component:

   return (
     <>
-      <Menu as="span" className="">
+      <Menu as="span" className="" aria-label="PDF viewer options">
         <Menu.Button
           disabled={!pdfSlick}
           className="enabled:hover:bg-slate-200 enabled:hover:text-black text-slate-500 disabled:text-slate-300 p-1 rounded-sm transition-all group relative focus:border-blue-400 focus:ring-0 focus:shadow outline-none border border-transparent"
+          aria-haspopup="true"
         >
           <span className="sr-only">Open more actions menu</span>
           <VscKebabVertical className="w-4 h-4" />
         </Menu.Button>
         <Transition
           as={Fragment}
           enter="transition ease-out duration-100"
           enterFrom="transform opacity-0 scale-95"
           enterTo="transform opacity-100 scale-100"
           leave="transition ease-in duration-75"
           leaveFrom="transform opacity-100 scale-100"
           leaveTo="transform opacity-0 scale-95"
         >
-          <Menu.Items className="absolute right-2 w-52 z-30 mt-2 origin-top-right divide-y divide-slate-200 rounded text-left bg-white shadow-lg ring-1 ring-black/5 focus:outline-none">
+          <Menu.Items className="absolute right-2 w-52 z-30 mt-2 origin-top-right divide-y divide-slate-200 rounded text-left bg-white shadow-lg ring-1 ring-black/5 focus:outline-none" aria-orientation="vertical" role="menu">

74-106: Extract repeated menu item pattern into a reusable component

There's significant code duplication in the structure and styling of menu items. Extracting this pattern would improve maintainability.

Create a reusable menu item component:

type MenuItemProps = {
  icon: React.ReactNode;
  label: string;
  onClick: () => void;
  disabled?: boolean;
  isActive?: boolean;
  rightElement?: React.ReactNode;
};

const MenuItem = ({ icon, label, onClick, disabled = false, rightElement }: MenuItemProps) => (
  <Menu.Item>
    {({ active }) => (
      <button
        onClick={onClick}
        disabled={disabled}
        className={clsx(
          "w-full items-center flex space-x-2 box-border text-left px-2 py-1.5 text-xs disabled:opacity-50",
          {
            "bg-slate-100 text-gray-900": active,
            "text-gray-700": !active
          }
        )}
      >
        {icon}
        <span className="flex-1">{label}</span>
        {rightElement}
      </button>
    )}
  </Menu.Item>
);

Then use it in the menu:

<MenuItem 
  icon={<VscDesktopDownload className="w-4 h-4" />}
  label="Save"
  onClick={() => pdfSlick?.downloadOrSave()}
/>

173-183: Improve handling of rotation functions

The current implementation directly sets rotation angles, but doesn't normalize them to stay within 0-359 degrees, which could lead to unnecessarily large values after multiple rotations.

Consider normalizing the rotation value:

   <button
     onClick={(e) => {
       e.preventDefault();
-      pdfSlick?.setRotation(pagesRotation + 90);
+      pdfSlick?.setRotation((pagesRotation + 90) % 360);
     }}
     className={clsx("w-full flex space-x-2 box-border text-left px-2 py-1.5 text-xs", {
       "bg-slate-100 text-gray-900": active,
       "text-gray-700": !active
     })}
   >
     <VscRefresh className="w-4 h-4" />

23-25: Type definition for usePDFSlickStore could be more specific

The current type TUsePDFSlickStore is imported from a third-party library, but it might be beneficial to define more specific types for your use case.

Consider creating a more specific type that only includes the properties you're using:

+import { type StoreApi, type UseBoundStore } from 'zustand';
+
+interface PDFSlickState {
+  pdfSlick: any; // Replace with more specific type if available
+  pagesRotation: number;
+  pageNumber: number;
+  numPages: number;
+  scrollMode: ScrollMode;
+  spreadMode: SpreadMode;
+}
+
 type MoreActionsMenuProps = {
-  usePDFSlickStore: TUsePDFSlickStore;
+  usePDFSlickStore: UseBoundStore<StoreApi<PDFSlickState>>;
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between aa9cfb0 and eb98c1a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • package.json (4 hunks)
  • src/main/browser/sessions.ts (3 hunks)
  • src/main/browser/tabs/tab.ts (2 hunks)
  • src/main/browser/utility/intercept-rules.ts (1 hunks)
  • src/main/browser/utility/protocols.ts (6 hunks)
  • src/main/browser/utility/utils.ts (0 hunks)
  • src/main/browser/window.ts (1 hunks)
  • src/main/ipc/session/profiles.ts (1 hunks)
  • src/main/ipc/session/spaces.ts (1 hunks)
  • src/main/modules/basic-settings.ts (2 hunks)
  • src/main/modules/utils.ts (2 hunks)
  • src/main/onboarding/main.ts (1 hunks)
  • src/main/sessions/profiles.ts (1 hunks)
  • src/main/settings/main.ts (1 hunks)
  • src/preload/index.ts (2 hunks)
  • src/renderer/src/App.tsx (3 hunks)
  • src/renderer/src/components/main/platform.tsx (1 hunks)
  • src/renderer/src/lib/url.ts (2 hunks)
  • src/renderer/src/routes/pdf-viewer/page.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Outline.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Thumbnails.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/InkMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/SearchBar.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/ZoomSelector.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Tooltip.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/route.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/browser/utility/utils.ts
🚧 Files skipped from review as they are similar to previous changes (35)
  • src/main/onboarding/main.ts
  • src/main/ipc/session/profiles.ts
  • src/main/browser/window.ts
  • src/main/ipc/session/spaces.ts
  • src/renderer/src/components/main/platform.tsx
  • src/main/sessions/profiles.ts
  • src/main/settings/main.ts
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx
  • src/renderer/src/routes/pdf-viewer/route.tsx
  • src/main/browser/tabs/tab.ts
  • src/main/modules/utils.ts
  • src/renderer/src/lib/url.ts
  • src/main/modules/basic-settings.ts
  • src/renderer/src/App.tsx
  • package.json
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/colors.ts
  • src/renderer/src/routes/pdf-viewer/page.tsx
  • src/preload/index.ts
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/SearchBar.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Tooltip.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Outline.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/index.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/FreetextMenu.tsx
  • src/main/browser/sessions.ts
  • src/main/browser/utility/protocols.ts
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/ZoomSelector.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/ButtonsBar.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/InkMenu.tsx
  • src/main/browser/utility/intercept-rules.ts
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Thumbnails.tsx
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/index.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/MoreActionsMenu.tsx (1)
src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfoModal.tsx (1)
  • DocumentInfoModal (20-198)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (macos-latest)
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-latest)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/browser/utility/intercept-rules.ts (1)

43-58: ⚠️ Potential issue

Security issue: Missing URL scheme validation

The code only checks if the pathname ends with ".pdf" but doesn't validate the URL scheme, which could be a security risk if a malicious URL with a PDF extension is processed.

Implement secure URL validation by checking the protocol:

  const { pathname } = urlObject;
- if (pathname && pathname.toLowerCase().endsWith(".pdf") && getSettingValueById("enableFlowPdfViewer") === true) {
+ // Only redirect HTTP/HTTPS URLs or other trusted protocols
+ const isValidProtocol = url.startsWith('http:') || url.startsWith('https:') || url.startsWith('file:');
+ if (pathname && pathname.toLowerCase().endsWith(".pdf") && isValidProtocol && getSettingValueById("enableFlowPdfViewer") === true) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 57-57: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🧹 Nitpick comments (9)
src/main/browser/utility/intercept-rules.ts (2)

57-57: Use optional chaining for code safety

The static analysis hint suggests using optional chaining here for better safety.

- if (pathname && pathname.toLowerCase().endsWith(".pdf") && getSettingValueById("enableFlowPdfViewer") === true) {
+ if (pathname?.toLowerCase().endsWith(".pdf") && getSettingValueById("enableFlowPdfViewer") === true) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 57-57: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


90-101: Origin header handling may be too broad

The function is replacing the Origin header for all requests, not just PDF-related ones. This could potentially affect other requests unnecessarily.

Consider limiting the Origin header modification to only PDF-related requests:

  webRequest.onBeforeSendHeaders((details, callback) => {
    const url = details.url;
    const urlObject = URL.parse(url);
    if (!urlObject) {
      return callback({});
    }

+   // Only modify headers for PDF-related URLs
+   const { pathname } = urlObject;
+   if (pathname?.toLowerCase().endsWith(".pdf")) {
      const newHeaders = { ...details.requestHeaders, Origin: urlObject.origin };
      callback({ requestHeaders: newHeaders });
+     return;
+   }

    callback({});
  });
src/main/browser/utility/protocols/_protocols/flow-external.ts (1)

29-36: Misleading comment in protocol handler

The comment // flow://:path doesn't match the actual protocol being handled (flow-external).

  protocol.handle("flow-external", async (request) => {
    const urlString = request.url;
    const url = new URL(urlString);

-   // flow://:path
+   // flow-external://:path
    return await handleDomainRequest(request, url);
  });
src/main/browser/utility/protocols/utils.ts (3)

39-47: Well-implemented development server integration

The code properly checks for development mode with hot reload enabled and attempts to serve files from the development server when appropriate.

Consider adding a timeout for the fetch operation to prevent hanging in case the development server is slow to respond.

 if (ping) {
-      return await fetchFromDevServer(transformedPath, request);
+      // Add timeout to prevent hanging if dev server is slow
+      return await Promise.race([
+        fetchFromDevServer(transformedPath, request),
+        new Promise<Response>((_, reject) => 
+          setTimeout(() => reject(new Error('Dev server timeout')), 5000)
+        ).catch(error => {
+          console.warn('Dev server timeout:', error);
+          return new Response('Development server timeout', { status: 504 });
+        })
+      ]);
 }

49-65: Well-structured file serving implementation

The code correctly checks for directories, reads file contents, determines appropriate content types, and returns proper responses.

Consider adding caching headers to improve performance for static assets:

 return new Response(buffer, {
   headers: {
-        "Content-Type": contentType
+        "Content-Type": contentType,
+        "Cache-Control": "public, max-age=86400"
   }
 });

66-69: Error handling could be more specific

The current implementation logs the error and returns a generic 404 response for all error types.

Consider differentiating between common error types for better debugging and user experience:

 } catch (error) {
   console.error("Error serving file:", error);
-  return new Response("File not found", { status: 404 });
+  if (error.code === 'ENOENT') {
+    return new Response("File not found", { status: 404 });
+  } else if (error.code === 'EACCES') {
+    return new Response("Permission denied", { status: 403 });
+  } else {
+    return new Response("Internal server error", { status: 500 });
+  }
 }
src/main/browser/utility/protocols/_protocols/flow.ts (3)

22-34: PDF caching implementation needs documentation

The PDF cache implementation is functional but lacks explanatory comments.

Add documentation to explain the purpose and lifecycle of cached PDF responses:

+/**
+ * In-memory cache for PDF responses.
+ * Used to temporarily store PDF content that needs to be displayed
+ * in the PDF viewer without re-downloading or processing it again.
+ */
 const PDF_CACHE = new Map<string, Response>();

+/**
+ * Adds a PDF response to the cache with the specified key.
+ * @param key - Unique identifier for the cached response
+ * @param response - The PDF response to cache
+ */
 export function addPdfResponseToCache(key: string, response: Response) {
   PDF_CACHE.set(key, response);
 }

+/**
+ * Retrieves a PDF response from the cache.
+ * @param key - The key to look up
+ * @returns The cached response or undefined if not found
+ */
 function getPdfResponseFromCache(key: string): Response | undefined {
   return PDF_CACHE.get(key);
 }

+/**
+ * Removes a PDF response from the cache.
+ * @param key - The key of the response to remove
+ */
 function removePdfResponseFromCache(key: string) {
   PDF_CACHE.delete(key);
 }

82-98: Asset serving error handling could be improved

The error handling for asset requests is basic but functional.

Consider improving error handling with more specific error types and adding logging of the specific asset path for debugging:

 try {
   // Read file contents
   const buffer = await fsPromises.readFile(filePath);

   // Determine content type based on file extension
   const contentType = getContentType(filePath);

   return new Response(buffer, {
     headers: {
       "Content-Type": contentType
     }
   });
 } catch (error) {
-  console.error("Error serving asset:", error);
+  console.error(`Error serving asset '${normalizedPath}':`, error);
+  
+  if (error.code === 'ENOENT') {
     return new Response("Asset not found", { status: 404 });
+  } else if (error.code === 'EACCES') {
+    return new Response("Permission denied", { status: 403 });
+  } else {
+    return new Response("Internal server error", { status: 500 });
+  }
 }

135-150: PDF cache request handler should document cleanup

The PDF cache handler removes the response from cache after retrieving it, but this behavior isn't obvious.

Add a comment explaining the one-time use nature of the cached PDF responses:

 const handlePdfCacheRequest = async (_request: Request, url: URL) => {
   const pdfURL = url.searchParams.get("url");
   const key = url.searchParams.get("key");
   if (!pdfURL || !key) {
     return new Response("Invalid request path", { status: 400 });
   }

   const pdfResponse = getPdfResponseFromCache(key);
   if (!pdfResponse) {
     // redirect to actual url
     return Response.redirect(pdfURL);
   }

+  // Remove from cache after use - PDFs are meant to be retrieved only once
   removePdfResponseFromCache(key);
   return pdfResponse;
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between eb98c1a and 8bdd10c.

📒 Files selected for processing (10)
  • src/main/browser/sessions.ts (3 hunks)
  • src/main/browser/utility/intercept-rules.ts (1 hunks)
  • src/main/browser/utility/protocols.ts (0 hunks)
  • src/main/browser/utility/protocols/_protocols/flow-external.ts (1 hunks)
  • src/main/browser/utility/protocols/_protocols/flow-internal.ts (1 hunks)
  • src/main/browser/utility/protocols/_protocols/flow.ts (1 hunks)
  • src/main/browser/utility/protocols/index.ts (1 hunks)
  • src/main/browser/utility/protocols/utils.ts (1 hunks)
  • src/renderer/src/routes/pdf-viewer/page.tsx (1 hunks)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/README.md (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/browser/utility/protocols.ts
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/src/routes/pdf-viewer/pdf-viewer/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/browser/sessions.ts
  • src/renderer/src/routes/pdf-viewer/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/browser/utility/protocols/_protocols/flow-external.ts (1)
src/main/browser/utility/protocols/utils.ts (2)
  • AllowedDomains (10-12)
  • serveStaticFile (14-70)
src/main/browser/utility/protocols/index.ts (3)
src/main/browser/utility/protocols/_protocols/flow.ts (1)
  • registerFlowProtocol (36-179)
src/main/browser/utility/protocols/_protocols/flow-external.ts (1)
  • registerFlowExternalProtocol (15-36)
src/main/modules/paths.ts (1)
  • PATHS (18-23)
src/main/browser/utility/protocols/_protocols/flow-internal.ts (2)
src/main/browser/utility/protocols/utils.ts (2)
  • AllowedDomains (10-12)
  • serveStaticFile (14-70)
src/main/index.ts (1)
  • browser (14-14)
src/main/browser/utility/protocols/utils.ts (4)
src/main/modules/paths.ts (1)
  • PATHS (18-23)
src/main/modules/flags.ts (1)
  • FLAGS (16-41)
src/main/browser/utility/hot-reload.ts (3)
  • setupHotReloadFileDescriptors (9-15)
  • isDevelopmentServerRunning (21-27)
  • fetchFromDevServer (45-81)
src/main/modules/utils.ts (1)
  • getContentType (24-26)
src/main/browser/utility/protocols/_protocols/flow.ts (4)
src/main/browser/utility/protocols/utils.ts (2)
  • AllowedDomains (10-12)
  • serveStaticFile (14-70)
src/main/modules/paths.ts (1)
  • PATHS (18-23)
src/main/modules/utils.ts (1)
  • getContentType (24-26)
src/main/index.ts (1)
  • browser (14-14)
🪛 Biome (1.9.4)
src/main/browser/utility/intercept-rules.ts

[error] 57-57: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (ubuntu-latest)
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (macos-latest)
🔇 Additional comments (25)
src/main/browser/utility/intercept-rules.ts (6)

4-7: Imports and dependencies look good

The file correctly imports all required dependencies for the interceptor functions.


8-37: CORS bypass implementation for custom protocols

The CORS bypass implementation for Flow protocols is well-structured with proper header manipulation. It correctly identifies whitelisted protocols, removes existing CORS headers, and adds a wildcard CORS header.


39-42: Setup function for PDF viewer correctly created

The setup function for the PDF viewer with appropriate web request creation looks good.


58-72: PDF content type verification looks good

The implementation correctly fetches the PDF, verifies its content type, and handles potential errors.


73-84: PDF caching and redirection logic is well-implemented

The codebase properly generates cache keys, caches PDF responses, and constructs appropriate redirect URLs.


103-110: Export function is well-structured

The export function correctly sets up both intercept rules for the session.

src/main/browser/utility/protocols/_protocols/flow-external.ts (3)

1-4: Imports and dependencies look good

The file correctly imports all necessary dependencies including AllowedDomains, serveStaticFile, and the Protocol type.


4-13: Well-documented domain whitelist

The FLOW_EXTERNAL_ALLOWED_DOMAINS whitelist is well-structured and properly documented, including sources for the games.


15-27: Protocol handler function properly validates hostnames

The handler function correctly validates if the requested hostname is in the allowed domains list and returns a 400 status for invalid hosts.

src/main/browser/utility/protocols/index.ts (3)

1-19: Privilege registration for protocols is well-structured

The code correctly registers three protocols with appropriate privileges, which is important for security and functionality.


21-25: Protocol registration with session is clean and modular

The registerProtocolsWithSession function follows good separation of concerns by correctly delegating the registration to the respective protocol handlers.


27-33: Preload script registration is well-implemented

The registerPreloadScript function properly registers the preload script with the correct parameters.

src/main/browser/utility/protocols/_protocols/flow-internal.ts (4)

1-13: Domain whitelist is well-defined

The allowed domains for the flow-internal protocol are properly defined with a clear structure.


26-71: Favicon handler implementation handles edge cases well

The handleActiveFaviconRequest function properly validates input parameters and handles edge cases like missing tab, favicon, or profile.


73-84: Domain request handler follows secure practices

The domain request handler validates hostnames against the whitelist and returns appropriate error responses for invalid requests.


86-98: Protocol handler routing is clear and effective

The protocol.handle function appropriately routes requests to specific handlers based on the URL host.

src/main/browser/utility/protocols/utils.ts (4)

10-12: Well-defined interface for allowed domains

The AllowedDomains interface provides a clear contract for defining which domains are permitted and how they should be handled.


14-19: Function signature looks good

The function has meaningful parameter names, appropriate defaults, and good type annotations.


20-26: Good path normalization

The path normalization logic handles both leading and trailing slashes correctly, ensuring consistent path handling.


28-30: Proper default file handling

Correctly defaults to serving "index.html" when an empty path is provided, following web server conventions.

src/main/browser/utility/protocols/_protocols/flow.ts (5)

12-20: Well-defined protocol allowed domains

The allowed domains are clearly specified and include all necessary system pages.


72-73: Good path normalization for security

The implementation properly normalizes paths and prevents directory traversal attacks by removing parent directory references.


77-80: Proper path validation for security

The code ensures the requested path is within the allowed directory, which is an important security measure.


100-133: Extension icon handler has thorough error checking

The handler properly validates inputs and handles all potential error cases in the process of retrieving extension icons.


152-178: Well-structured protocol handler with clear routing

The main protocol handler efficiently routes requests to the appropriate specialized handlers based on the URL host.

@iamEvanYT iamEvanYT merged commit 1e6958a into main May 19, 2025
8 checks passed
@iamEvanYT iamEvanYT deleted the evan/better-pdf-viewer branch May 19, 2025 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant