feat(pwa): add protocol and file handlers for magnet links and torrent files - #783
Conversation
…t files Add PWA protocol handler for magnet: links and file handler for .torrent files, enabling qui to open torrents directly from the OS when installed as a PWA. - Add protocol_handlers, file_handlers, launch_handler to PWA manifest - Create /add route to handle PWA launches with instance selection - Add LaunchQueue API TypeScript declarations - Store add intent across login redirects with error handling - Add timeout/error feedback when file handling fails after auth - Validate magnet URLs in route schema
WalkthroughAdds a new /add route with comprehensive magnet/.torrent ingestion and UI, session-backed AddIntent for post-auth navigation, PWA protocol/file handlers and launch behavior, browser protocol-registration utilities and banner/Settings UI, Launch Queue consumer/types, magnet normalization helpers, and related router wiring and docs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant Browser
participant App
participant Session as SessionStorage
participant Auth
participant Instances
participant LaunchQueue
User->>Browser: Click magnet link / open .torrent
Browser->>App: Navigate to /add or deliver launchQueue event
App->>LaunchQueue: setup consumer / receive event
App->>Session: getAndClearAddIntent()
alt payload found
App->>App: initialize add payload (magnet/files)
end
App->>Auth: Check authentication
alt Not authenticated
App->>Session: storeAddIntent({magnet,hasFiles,openAdd})
App->>Browser: Redirect to /login
Auth->>Browser: User authenticates
Browser->>App: Login callback -> navigateAfterAuth(navigate)
App->>Session: getAndClearAddIntent()
end
App->>Instances: Load active instances
alt no instances
App->>Browser: Redirect to /settings
else multiple instances
App->>User: Render InstanceSelector
User->>App: Select instance
end
App->>App: Render AddTorrentDialog with payload
User->>App: Confirm -> consume payload / navigate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
web/src/hooks/useAuth.ts(3 hunks)web/src/lib/add-intent.ts(1 hunks)web/src/pages/Login.tsx(2 hunks)web/src/routeTree.gen.ts(11 hunks)web/src/routes/add.tsx(1 hunks)web/src/types/launch-queue.d.ts(1 hunks)web/vite.config.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/routes/add.tsx
🧬 Code graph analysis (3)
web/src/pages/Login.tsx (1)
web/src/lib/add-intent.ts (1)
navigateAfterAuth(39-49)
web/src/routes/add.tsx (4)
web/src/hooks/useAuth.ts (1)
useAuth(12-73)web/src/hooks/useInstances.ts (1)
useInstances(10-188)web/src/components/torrents/AddTorrentDialog.tsx (2)
AddTorrentDropPayload(104-106)AddTorrentDialog(164-1671)web/src/lib/add-intent.ts (1)
storeAddIntent(15-21)
web/src/hooks/useAuth.ts (1)
web/src/lib/add-intent.ts (1)
navigateAfterAuth(39-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (6)
web/src/types/launch-queue.d.ts (1)
11-30: Good minimal typing; consider exporting types if you’ll reference them directly elsewhere.
As-is, it’s sufficient forwindow.launchQueue?.setConsumer(...)type inference. If other modules need to nameLaunchParams/LaunchQueue, export them (or put them insidedeclare global) to avoid ad-hoc re-declarations later.web/src/routes/add.tsx (1)
95-113: Dialog flow + instance selection logic looks solid.
TheselectedInstanceId/resolvedInstanceIdresolution and navigation on close/cancel is clear, and the “wait for payload” UI is a good fallback.Also applies to: 146-174
web/src/hooks/useAuth.ts (1)
6-38: Good centralization of post-auth navigation (once storage access is hardened).
UsingnavigateAfterAuth(navigate)in both login/setup aligns auth flows with the new/addintent behavior.web/vite.config.ts (1)
90-107: The current manifest configuration is correct and requires no changes.All three handler fields (
protocol_handlers,file_handlers,launch_handler) are valid per the Chrome Web App Manifest specification and properly emitted by vite-plugin-pwa v1.2.0. Browsers matchfile_handlersprimarily by file extension (.torrent), not by MIME type, so the current configuration will correctly associate.torrentfiles with your app regardless of whether the server sendsapplication/x-bittorrentorapplication/octet-stream. Adding the latter MIME type provides no additional coverage.web/src/pages/Login.tsx (1)
17-17: LGTM! Post-auth navigation now respects add intents.The integration with
navigateAfterAuthcorrectly enables the OIDC callback flow to redirect users to/addwhen they have a pending torrent operation (magnet link or file), preserving the user's intent across the authentication boundary. The fallback to"/"ensures existing behavior is maintained when no intent is stored.Also applies to: 64-64
web/src/routeTree.gen.ts (1)
1-354: Auto-generated routing configuration.This file is automatically generated by TanStack Router and correctly reflects the new
/addroute integration. The additions are consistent with the existing route structure and require no manual changes.
|
@s0up4200 is it possible to add firefox support by calling as part of a button? (only works with https / maybe a flag for non-secure contexts, but not sure). |
Add Firefox support for magnet link handling via navigator.registerProtocolHandler(). This complements the PWA manifest protocol_handlers which only work in Chromium. - Add protocol-handler.ts utility with secure context detection - Add dismissible banner on dashboard prompting registration - Add Browser Integration card in Settings > Security tab - Use withBasePath() for correct URL construction - Show toast feedback on registration attempts
Done. Added a dismissable banner on Dashboard, and a small section in the Security tab in Settings. Should work for both https and localhost. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/src/components/MagnetHandlerBanner.tsx (1)
30-39: Consider auto-dismissing banner after successful registration.The registration handler works correctly with appropriate toast feedback. One optional enhancement: automatically dismiss the banner after successful registration (in addition to showing the toast) to reduce UI clutter once the user has taken action.
Current behavior is acceptable since the browser confirmation prompt itself provides feedback, but auto-dismissing would improve the UX.
Apply this diff if you'd like to auto-dismiss:
const handleRegister = () => { const success = registerMagnetHandler() if (success) { toast.success("Magnet handler registration requested", { description: "If prompted by your browser, please accept to complete registration.", }) + dismissProtocolHandlerBanner() + setDismissed(true) } else { toast.error("Failed to register magnet handler") } }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/src/components/MagnetHandlerBanner.tsx(1 hunks)web/src/lib/protocol-handler.ts(1 hunks)web/src/pages/Dashboard.tsx(2 hunks)web/src/pages/Settings.tsx(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-25T11:39:54.748Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: web/src/pages/Dashboard.tsx:805-831
Timestamp: 2025-11-25T11:39:54.748Z
Learning: In web/src/pages/Dashboard.tsx, the TrackerIconImage component intentionally receives displayDomain (incognito-mapped name) instead of the real domain in incognito mode. This causes icon lookups to fail and show only fallback letters, which is desired behavior for privacy - hiding both tracker names and icons when incognito mode is enabled.
Applied to files:
web/src/pages/Dashboard.tsx
🧬 Code graph analysis (4)
web/src/lib/protocol-handler.ts (1)
web/src/lib/base-url.ts (1)
withBasePath(24-29)
web/src/components/MagnetHandlerBanner.tsx (1)
web/src/lib/protocol-handler.ts (4)
isProtocolHandlerBannerDismissed(39-45)canRegisterProtocolHandler(15-18)registerMagnetHandler(25-34)dismissProtocolHandlerBanner(50-56)
web/src/pages/Settings.tsx (1)
web/src/lib/protocol-handler.ts (2)
canRegisterProtocolHandler(15-18)registerMagnetHandler(25-34)
web/src/pages/Dashboard.tsx (1)
web/src/components/MagnetHandlerBanner.tsx (1)
MagnetHandlerBanner(17-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (11)
web/src/pages/Dashboard.tsx (2)
8-8: LGTM! Clean import addition.The import of
MagnetHandlerBanneris correctly placed with other component imports.
2279-2280: LGTM! Well-placed banner integration.The
MagnetHandlerBanneris appropriately positioned afterPasswordIssuesBanner, keeping related notification UI elements grouped together. The component handles its own conditional rendering, so no wrapper logic is needed here.web/src/pages/Settings.tsx (2)
49-49: LGTM! Protocol handler utilities imported.The imports of
canRegisterProtocolHandlerandregisterMagnetHandlerare clean and follow the project's import conventions.
999-1032: LGTM! Well-integrated browser integration feature.The Browser Integration card provides a clear, user-friendly interface for registering qui as a magnet link handler. Key strengths:
- Conditional rendering ensures it only appears when supported
- Clear description explains the functionality
- Success toast includes helpful prompt about browser confirmation
- Error handling provides feedback on failures
The implementation follows existing Settings page patterns and integrates nicely with the Security tab.
web/src/components/MagnetHandlerBanner.tsx (3)
17-28: LGTM! Clean conditional rendering logic.The component correctly handles visibility conditions:
- Checks browser support via
canRegisterProtocolHandler()- Respects user dismissal preference
- State initialization properly calls the function once on mount
41-44: LGTM! Dismissal logic is correct.The dismiss handler properly persists the dismissal state and updates local state to hide the banner.
46-60: LGTM! Clear and accessible banner UI.The banner design is user-friendly with:
- Clear messaging about the functionality
- Visible icon and action buttons
- Appropriate color scheme for an informational banner
- Responsive layout
web/src/lib/protocol-handler.ts (4)
10-18: LGTM! Robust browser capability detection.The
canRegisterProtocolHandlerfunction correctly checks both:
- API availability via optional chaining
- Secure context requirement (HTTPS or localhost)
The JSDoc comment helpfully explains that this complements PWA manifest protocol handlers for broader browser support.
20-34: LGTM! Correct protocol handler registration.The
registerMagnetHandlerfunction properly:
- Constructs the handler URL with the correct
%splaceholder- Uses
withBasePathfor base URL handling- Wraps the registration call in try-catch
- Returns a boolean indicating if registration was attempted
Note: The function returns
truewhen the browser shows the registration prompt, not when the user confirms it. This is expected behavior since the browser API doesn't provide a callback for user confirmation.
36-45: LGTM! Safe localStorage access with sensible default.The
isProtocolHandlerBannerDismissedfunction safely handles localStorage access and returnsfalseon error, ensuring the banner shows by default if storage is unavailable (fail-open behavior).
47-56: LGTM! Safe persistence with appropriate error handling.The
dismissProtocolHandlerBannerfunction safely persists the dismissal state and logs a warning if localStorage is unavailable. This graceful degradation is appropriate for a non-critical preference.
Nice, exactly what I was looking for.
|
Either by deleting the key from localStorage, or by going into Settings -> Security (couldnt find a better section for it) |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
web/src/routes/add.tsx (2)
19-23: CoerceexpectingFilesfrom the URL.URL query parameters are always strings. The current schema uses
z.boolean().optional()on line 22, which cannot parse the string"true"from/add?expectingFiles=truewithout coercion.🔎 Proposed fix
const addSearchSchema = z.object({ magnet: z.string().regex(/^magnet:\?/i).optional(), instance: z.coerce.number().optional(), - expectingFiles: z.boolean().optional(), + expectingFiles: z.coerce.boolean().optional(), })
52-79: Add cleanup to prevent state updates after unmount.The
setConsumercallback (lines 55–78) capturessetDropPayloadandnavigate. If the component unmounts during an auth redirect before the launch is delivered, the callback will still fire when files arrive, causing a state update on an unmounted component.🔎 Proposed fix with mounted guard
// Handle files from file handler (launchQueue API) useEffect(() => { if (!window.launchQueue) return + + let isMounted = true window.launchQueue.setConsumer(async (launchParams) => { + if (!isMounted) return if (!launchParams.files?.length) return const files: File[] = [] for (const handle of launchParams.files) { try { const file = await handle.getFile() if (file.name.toLowerCase().endsWith(".torrent")) { files.push(file) } } catch (err) { console.error("Failed to get file from handle:", err) } } if (files.length > 0) { + if (!isMounted) return hasReceivedPayload.current = true setDropPayload({ type: "file", files }) } else if (launchParams.files.length > 0) { // Files were provided but none were valid .torrent files + if (!isMounted) return toast.error("No valid .torrent files found") navigate({ to: "/" }) } }) + + return () => { + isMounted = false + } }, [navigate])
🧹 Nitpick comments (1)
web/src/routes/add.tsx (1)
81-93: Consider increasing the timeout duration.The 2-second timeout (line 85) may fire prematurely if the auth redirect takes time or if the browser is slow to deliver the launchQueue event. A longer timeout (3-5 seconds) would reduce false-positive timeout errors while still providing timely feedback.
🔎 Optional adjustment
const timeout = setTimeout(() => { if (!hasReceivedPayload.current) { toast.error("Unable to open torrent file. Please try again.") navigate({ to: "/" }) } - }, 2000) + }, 5000)
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
web/src/lib/add-intent.ts(1 hunks)web/src/routes/add.tsx(1 hunks)web/vite.config.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web/src/lib/add-intent.ts
- web/vite.config.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/routes/add.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (5)
web/src/routes/add.tsx (5)
25-28: LGTM!The route definition correctly uses
createFileRoutewith search validation and component assignment.
30-49: LGTM!The component initialization and magnet payload setup are well-structured. The
hasReceivedPayloadref correctly tracks payload receipt to prevent state confusion after consumption.
95-112: LGTM!The event handlers are well-structured with appropriate
useCallbackdependencies and clear navigation logic.
114-186: LGTM!The rendering logic comprehensively handles all states: auth loading, unauthenticated (with intent storage), instance loading, no instances, multi-instance selection, payload processing, and waiting for launchQueue. The conditional flow is clear and correct.
188-231: LGTM!The
InstanceSelectorcomponent provides a clean UI for multi-instance selection with clear connection status indicators and appropriate button styling.
Only store add intent when magnet or expectingFiles is present. Redirect authenticated users to home if visiting /add with no payload.
Detect PWA standalone mode to distinguish file-handler launches from manual /add navigation. Store hasFiles intent when in standalone mode so files are processed after login. Add timeout fallback for both authenticated and unauthenticated standalone users.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
web/src/routes/add.tsx (2)
20-25: CoerceexpectingFilesfrom the URL.URL query parameters are always strings. When a user navigates directly to
/add?expectingFiles=true, the value is the string"true", not the booleantrue. The currentz.boolean().optional()cannot parse this string without coercion.🔎 Proposed fix
const addSearchSchema = z.object({ magnet: z.string().optional(), url: z.string().optional(), instance: z.coerce.number().optional(), - expectingFiles: z.boolean().optional(), + expectingFiles: z.coerce.boolean().optional(), })
56-90: Add unmount guard to prevent state updates after component unmount.The
setConsumercallback capturessetDropPayloadandnavigatebut has no cleanup or mounted-state guard. If the component unmounts during auth redirect before the launch is delivered, the callback will still fire, causing a React warning about state updates on unmounted components.🔎 Proposed fix using isMounted ref
// Handle files from file handler (launchQueue API) useEffect(() => { if (!window.launchQueue) return + let isMounted = true + window.launchQueue.setConsumer(async (launchParams) => { + if (!isMounted) return + const launchedMagnet = extractMagnetFromTargetURL(launchParams.targetURL) if (launchedMagnet) { hasReceivedPayload.current = true setDropPayload({ type: "url", urls: [launchedMagnet] }) return } if (!launchParams.files?.length) return const files: File[] = [] for (const handle of launchParams.files) { try { const file = await handle.getFile() if (file.name.toLowerCase().endsWith(".torrent")) { files.push(file) } } catch (err) { console.error("Failed to get file from handle:", err) } } + if (!isMounted) return + if (files.length > 0) { hasReceivedPayload.current = true setDropPayload({ type: "file", files }) } else if (launchParams.files.length > 0) { // Files were provided but none were valid .torrent files toast.error("No valid .torrent files found") navigate({ to: "/" }) } }) + + return () => { + isMounted = false + } }, [navigate])
🧹 Nitpick comments (2)
README.md (1)
203-206: Consider restructuring this section.The "Magnet Link Handling" section is placed in the middle of the Configuration section (between the environment variables block and the Tracker Icons config). This placement is confusing since it's a
##heading that breaks the flow of configuration options.Consider moving this section to appear after all Configuration-related content (after line 218) or making it a subsection (
###) if it's intended to be part of Configuration.web/src/lib/protocol-handler.ts (1)
10-22: Consider including Edge in Chromium detection for guidance.The
isChromiumfunction explicitly excludes Edge (/edg/i), but Edge is Chromium-based and likely exhibits similar protocol handler behavior. Users on Edge would receive the generic guidance instead of the Chrome-specific address bar tip.This is minor since the generic guidance still works, but you might want to either:
- Include Edge in the Chromium check, or
- Add a separate
isEdgebranch with appropriate guidance
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
README.mdweb/src/components/MagnetHandlerBanner.tsxweb/src/lib/magnet.tsweb/src/lib/protocol-handler.tsweb/src/pages/Settings.tsxweb/src/routes/add.tsxweb/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- web/vite.config.ts
- web/src/pages/Settings.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/routes/add.tsx
🧬 Code graph analysis (3)
web/src/components/MagnetHandlerBanner.tsx (1)
web/src/lib/protocol-handler.ts (5)
isProtocolHandlerBannerDismissed(72-78)canRegisterProtocolHandler(29-32)registerMagnetHandler(55-67)getMagnetHandlerRegistrationGuidance(34-48)dismissProtocolHandlerBanner(83-89)
web/src/lib/protocol-handler.ts (1)
web/src/lib/base-url.ts (1)
withBasePath(24-29)
web/src/routes/add.tsx (5)
web/src/hooks/useAuth.ts (1)
useAuth(12-73)web/src/hooks/useInstances.ts (1)
useInstances(10-188)web/src/lib/magnet.ts (2)
normalizeMagnetLink(6-27)extractMagnetFromTargetURL(29-42)web/src/components/torrents/AddTorrentDialog.tsx (2)
AddTorrentDropPayload(104-106)AddTorrentDialog(164-1671)web/src/lib/add-intent.ts (1)
storeAddIntent(16-22)
🪛 markdownlint-cli2 (0.18.1)
README.md
16-16: Link fragments should be valid
(MD051, link-fragments)
🔇 Additional comments (11)
web/src/lib/magnet.ts (2)
6-27: LGTM!The
normalizeMagnetLinkfunction has solid defensive coding: proper type guards, bounded decoding loop (max 2 iterations to handle double-encoding), and appropriate error handling. The case-insensitive regex for magnet: prefix validation is correct.
29-42: LGTM!The
extractMagnetFromTargetURLfunction correctly tries direct normalization first, then gracefully falls back to URL parsing for query parameter extraction. Usingwindow.location.originas the base URL for relative paths is a reasonable approach.web/src/routes/add.tsx (4)
32-53: LGTM!The component setup is well-structured: proper hook usage, magnet normalization chain with fallback, and state initialization. The
hasReceivedPayloadref correctly prevents falling back to "waiting" state after consumption.
92-115: LGTM!The standalone PWA detection pattern is correct, and the timeout logic appropriately handles the case where file payloads don't arrive. The 2-second timeout with proper cleanup via
clearTimeoutis reasonable.
136-220: LGTM!The render logic handles all edge cases well: authentication loading, unauthenticated redirect with intent storage, instance loading, no-instances redirect, multi-instance selection, and payload-ready dialog rendering. The flow is clear and comprehensive.
228-263: LGTM!The
InstanceSelectorcomponent is clean and functional. The connection status indicator (green/red dot) provides good visual feedback, and the fallback name (Instance ${instance.id}) handles missing names gracefully.web/src/components/MagnetHandlerBanner.tsx (2)
18-47: LGTM!The component logic is well-structured with:
- Lazy state initialization for dismissed status
- Clean early returns for unsupported browsers and dismissed state
- Proper success/error handling with toast notifications
- Browser-specific guidance displayed on success
The separation between
handleRegisterandhandleDismisskeeps the code readable.
49-64: LGTM!The banner UI is clean and functional. The blue styling appropriately signals an informational/action item, and the layout with icon, message, and action buttons is intuitive.
web/src/lib/protocol-handler.ts (3)
29-32: LGTM!The
canRegisterProtocolHandlerfunction correctly checks both API availability and secure context requirement. The optional chaining onnavigator?.registerProtocolHandleris a good defensive pattern.
55-67: LGTM!The
registerMagnetHandlerfunction correctly composes the handler URL usingwithBasePathand handles the deprecated third argument appropriately with a documented@ts-expect-error. The try-catch ensures graceful failure.
72-89: LGTM!Both localStorage functions have proper try-catch blocks to handle cases where localStorage is unavailable (private browsing mode, storage quota exceeded). The warning log in
dismissProtocolHandlerBannerappropriately signals non-critical failures.
|
Is there something that's holding this off? Would love to see this soon. |
Testing and time really. Been prioritizing other things. Is it something you want to work on? Contributions are very much appreciated 👍 |
|
If it's manual testing with just screenshot proof or something, I can probably help out. I'm not a TS dev so I can't really help out that much on the coding side. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@web/src/lib/launch-queue.ts`:
- Around line 22-25: The emitLaunchQueueEvent function is using
listeners.forEach with a callback that may be interpreted as returning values;
change it to explicitly invoke listeners without returning from the
callback—e.g., iterate with a for...of loop over the listeners or use a forEach
callback block that does not return—to call each listener(event) and set
pendingEvent = event in the emitLaunchQueueEvent function so no callback return
values are possible (refer to emitLaunchQueueEvent, pendingEvent, and
listeners).
🧹 Nitpick comments (2)
web/src/routes/add.tsx (2)
25-31: TheexpectingFilespreprocess logic is correct but could be simplified.The manual string-to-boolean conversion works, but Zod's coercion handles common truthy/falsy string values automatically. However, I notice the current implementation is more explicit about only accepting
"true"/"false"strings, which may be intentional for stricter validation.♻️ Optional: Simpler approach using Zod coerce
- expectingFiles: z.preprocess((value) => { - if (typeof value === "string") { - if (value.toLowerCase() === "true") return true - if (value.toLowerCase() === "false") return false - } - return value - }, z.boolean().optional()), + expectingFiles: z.coerce.boolean().optional(),Note:
z.coerce.boolean()treats any truthy value astrueand falsy values asfalse, which may differ from the current behavior of only accepting exact"true"/"false"strings.
241-245: Consider adding accessible status text for connection indicator.The connection status indicator relies solely on color (green/red dot), which may not be accessible to colorblind users. Consider adding a tooltip or sr-only text.
♻️ Optional: Add accessible status indication
<div className="flex items-center gap-2"> <div className={`h-2 w-2 rounded-full ${instance.connected ? "bg-green-500" : "bg-red-500" - }`} /> + }`} + aria-hidden="true" /> <span>{instance.name || `Instance ${instance.id}`}</span> + <span className="sr-only"> + {instance.connected ? "(connected)" : "(disconnected)"} + </span> </div>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@web/src/routes/add.tsx`:
- Around line 39-58: Detect when a magnetParam or url is present but
normalizeMagnetLink returns null and surface an error instead of silently
redirecting: inside the existing useEffect that runs on [magnet], check the raw
params (magnetParam and url) and if either exists while normalizeMagnetLink
returned null, call the same toast/error routine used in handleLaunchQueueEvent
(or the component’s toast API) to show a user-facing error, set
hasReceivedPayload.current appropriately, and avoid calling setDropPayload;
reference normalizeMagnetLink, magnetParam, url, hasReceivedPayload,
setDropPayload, and the useEffect that initializes payload from magnet to locate
where to add this handling.
🧹 Nitpick comments (1)
web/src/routes/add.tsx (1)
141-150: Move add-intent storage out of the render path.
storeAddIntentis a side effect and will run multiple times in dev Strict Mode. Shifting it to an effect after auth state resolves avoids duplicate writes and keeps render pure.♻️ Suggested refactor
+ const shouldStoreIntent = !authLoading && !isAuthenticated + useEffect(() => { + if (!shouldStoreIntent) return + if (magnet) { + storeAddIntent({ magnet, openAdd: true }) + } else if (expectingFiles || isStandalone) { + storeAddIntent({ hasFiles: true, openAdd: true }) + } + }, [shouldStoreIntent, magnet, expectingFiles, isStandalone]) + if (!isAuthenticated) { - // Store intent if there's a payload OR if we're in PWA mode (potential file-handler launch) - if (magnet) { - storeAddIntent({ magnet, openAdd: true }) - } else if (expectingFiles || isStandalone) { - // In standalone mode without magnet, files may be coming via launchQueue - storeAddIntent({ hasFiles: true, openAdd: true }) - } return <Navigate to="/login" /> }
Are you running docker and want to test this PR? If so you can pull |
|
Test Setup
Test Firefox registerProtocolHandler from dashboard banner
Test Firefox registerProtocolHandler from Settings > Security
Test while logged out - should redirect to login, then back to /add after auth
Test opening invalid file type - should show error toast and redirect home (firefox)
Magnet handler also works from Brave. brave://settings/handlers
PWA Installation
Found 1 Bug
Edit: One more Issue. If you use a malformed torrent file, there's no error message in the PWA app.
|
|
Thanks @NekoShinobi 👍 |
- defer launch queue consume/subscribe until authenticated on /add - show explicit add-torrent failure toast on hard API errors
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.13.1` → `v1.14.0` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.14.0`](https://github.com/autobrr/qui/releases/tag/v1.14.0) [Compare Source](autobrr/qui@v1.13.1...v1.14.0) #### Changelog ##### New Features - [`6f8e6ed`](autobrr/qui@6f8e6ed): feat(api): add torrent field endpoint for select all copy ([#​1477](autobrr/qui#1477)) ([@​jabloink](https://github.com/jabloink)) - [`2d9b4c7`](autobrr/qui@2d9b4c7): feat(automation): trigger external programs automatically via automation rules ([#​1284](autobrr/qui#1284)) ([@​0rkag](https://github.com/0rkag)) - [`32692a4`](autobrr/qui@32692a4): feat(automations): Add the ability to define the move automation with a templated path ([#​1376](autobrr/qui#1376)) ([@​ColinHebert](https://github.com/ColinHebert)) - [`61bbeb1`](autobrr/qui@61bbeb1): feat(automations): add Resume action to Automations ([#​1350](autobrr/qui#1350)) ([@​cy1der](https://github.com/cy1der)) - [`450b98f`](autobrr/qui@450b98f): feat(automations): grouping + release fields ([#​1467](autobrr/qui#1467)) ([@​s0up4200](https://github.com/s0up4200)) - [`18d4a64`](autobrr/qui@18d4a64): feat(automations): match tracker conditions by display name ([#​1420](autobrr/qui#1420)) ([@​s0up4200](https://github.com/s0up4200)) - [`7c67b82`](autobrr/qui@7c67b82): feat(automations): show activity run details ([#​1385](autobrr/qui#1385)) ([@​s0up4200](https://github.com/s0up4200)) - [`177ef4d`](autobrr/qui@177ef4d): feat(crossseed): Multiple hard/reflink dirs ([#​1289](autobrr/qui#1289)) ([@​rybertm](https://github.com/rybertm)) - [`a72b673`](autobrr/qui@a72b673): feat(crossseed): gazelle-only OPS/RED ([#​1436](autobrr/qui#1436)) ([@​s0up4200](https://github.com/s0up4200)) - [`6a29384`](autobrr/qui@6a29384): feat(crossseed): match bit depth ([#​1427](autobrr/qui#1427)) ([@​s0up4200](https://github.com/s0up4200)) - [`c7fd5aa`](autobrr/qui@c7fd5aa): feat(dirscan): add max searchee age filter ([#​1486](autobrr/qui#1486)) ([@​s0up4200](https://github.com/s0up4200)) - [`d595a55`](autobrr/qui@d595a55): feat(documentation): add AI doc actions and llms discoverability ([#​1451](autobrr/qui#1451)) ([@​s0up4200](https://github.com/s0up4200)) - [`562ab3f`](autobrr/qui@562ab3f): feat(metrics): add tracker metrics ([#​1073](autobrr/qui#1073)) ([@​Winter](https://github.com/Winter)) - [`1b9aa9d`](autobrr/qui@1b9aa9d): feat(notifications): add shoutrrr and notifiarr ([#​1371](autobrr/qui#1371)) ([@​s0up4200](https://github.com/s0up4200)) - [`6d1dac7`](autobrr/qui@6d1dac7): feat(pwa): add protocol and file handlers for magnet links and torrent files ([#​783](autobrr/qui#783)) ([@​s0up4200](https://github.com/s0up4200)) - [`42fa501`](autobrr/qui@42fa501): feat(torrents): add unified cross-instance torrent table ([#​1481](autobrr/qui#1481)) ([@​s0up4200](https://github.com/s0up4200)) - [`498eaca`](autobrr/qui@498eaca): feat(ui): show speeds in page title ([#​1292](autobrr/qui#1292)) ([@​NoLife141](https://github.com/NoLife141)) - [`94a506e`](autobrr/qui@94a506e): feat(unregistered): nem talalhato ([#​1483](autobrr/qui#1483)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`8bf366c`](autobrr/qui@8bf366c): feat(web): add logs nav ([#​1458](autobrr/qui#1458)) ([@​s0up4200](https://github.com/s0up4200)) - [`babc88d`](autobrr/qui@babc88d): feat(web): add responsive popover with mobile drawer support ([#​1398](autobrr/qui#1398)) ([@​jabloink](https://github.com/jabloink)) - [`06d341b`](autobrr/qui@06d341b): feat(web): add torrent table selection quick wins ([#​1455](autobrr/qui#1455)) ([@​s0up4200](https://github.com/s0up4200)) - [`56fbbec`](autobrr/qui@56fbbec): feat(web): hide selection column ([#​1460](autobrr/qui#1460)) ([@​s0up4200](https://github.com/s0up4200)) - [`46814aa`](autobrr/qui@46814aa): feat(web): qBittorrent autorun preferences ([#​1430](autobrr/qui#1430)) ([@​s0up4200](https://github.com/s0up4200)) - [`342643e`](autobrr/qui@342643e): feat(web): unify instance settings & qbit options dialog ([#​1257](autobrr/qui#1257)) ([@​0rkag](https://github.com/0rkag)) - [`e634d01`](autobrr/qui@e634d01): feat: add cross-seed blocklist ([#​1391](autobrr/qui#1391)) ([@​s0up4200](https://github.com/s0up4200)) - [`13aaac8`](autobrr/qui@13aaac8): feat: add dry-run workflows ([#​1395](autobrr/qui#1395)) ([@​s0up4200](https://github.com/s0up4200)) - [`f01101d`](autobrr/qui@f01101d): feat: add option to disable built-in authentication ([#​1464](autobrr/qui#1464)) ([@​libussa](https://github.com/libussa)) - [`6d1da50`](autobrr/qui@6d1da50): feat: download individual content files from context menu ([#​1465](autobrr/qui#1465)) ([@​libussa](https://github.com/libussa)) - [`77e9abf`](autobrr/qui@77e9abf): feat: migrate to dodopayments ([#​1407](autobrr/qui#1407)) ([@​s0up4200](https://github.com/s0up4200)) - [`9f6c856`](autobrr/qui@9f6c856): feat: support basic auth for ARR and Torznab ([#​1442](autobrr/qui#1442)) ([@​s0up4200](https://github.com/s0up4200)) ##### Bug Fixes - [`8a06d4b`](autobrr/qui@8a06d4b): fix(api): correct add-torrent OpenAPI param names and add missing fields ([#​1426](autobrr/qui#1426)) ([@​s0up4200](https://github.com/s0up4200)) - [`b9a687c`](autobrr/qui@b9a687c): fix(api): honor explicit basic auth clear from URL userinfo ([@​s0up4200](https://github.com/s0up4200)) - [`948ca67`](autobrr/qui@948ca67): fix(api): tighten CORS/auth routing and base URL joins ([#​1325](autobrr/qui#1325)) ([@​s0up4200](https://github.com/s0up4200)) - [`12bea13`](autobrr/qui@12bea13): fix(automations): improve applied action summaries ([#​1478](autobrr/qui#1478)) ([@​s0up4200](https://github.com/s0up4200)) - [`8fe658b`](autobrr/qui@8fe658b): fix(automations): negate regex match for NotContains/NotEqual operators ([#​1441](autobrr/qui#1441)) ([@​andresatierf](https://github.com/andresatierf)) - [`8a808eb`](autobrr/qui@8a808eb): fix(automations): respect remove-only tag conditions ([#​1444](autobrr/qui#1444)) ([@​s0up4200](https://github.com/s0up4200)) - [`a72715e`](autobrr/qui@a72715e): fix(backups): add failure cooldown and export throttling ([#​1214](autobrr/qui#1214)) ([@​s0up4200](https://github.com/s0up4200)) - [`2e75c14`](autobrr/qui@2e75c14): fix(backups): skip exports missing metadata ([#​1362](autobrr/qui#1362)) ([@​s0up4200](https://github.com/s0up4200)) - [`5658421`](autobrr/qui@5658421): fix(config): update commented log settings in place ([#​1402](autobrr/qui#1402)) ([@​s0up4200](https://github.com/s0up4200)) - [`62c50c0`](autobrr/qui@62c50c0): fix(crossseed): tighten TV title matching ([#​1445](autobrr/qui#1445)) ([@​s0up4200](https://github.com/s0up4200)) - [`e7cc489`](autobrr/qui@e7cc489): fix(dirscan): prevent immediate requeue after cancel ([#​1446](autobrr/qui#1446)) ([@​s0up4200](https://github.com/s0up4200)) - [`36cbfcf`](autobrr/qui@36cbfcf): fix(docs): avoid mdx jsx parse error ([@​s0up4200](https://github.com/s0up4200)) - [`d8d6f62`](autobrr/qui@d8d6f62): fix(filters): stabilize dense sidebar layout ([#​1384](autobrr/qui#1384)) ([@​s0up4200](https://github.com/s0up4200)) - [`b959fc6`](autobrr/qui@b959fc6): fix(orphanscan): NFC-normalize paths to avoid false orphans ([#​1422](autobrr/qui#1422)) ([@​s0up4200](https://github.com/s0up4200)) - [`598e994`](autobrr/qui@598e994): fix(reflink): retry EAGAIN clones ([#​1360](autobrr/qui#1360)) ([@​s0up4200](https://github.com/s0up4200)) - [`aaa5ee0`](autobrr/qui@aaa5ee0): fix(reflinktree): retry transient FICLONE EINVAL and add diagnostics ([#​1487](autobrr/qui#1487)) ([@​s0up4200](https://github.com/s0up4200)) - [`647af31`](autobrr/qui@647af31): fix(rss): enable rules list scrolling ([#​1359](autobrr/qui#1359)) ([@​s0up4200](https://github.com/s0up4200)) - [`c356a6f`](autobrr/qui@c356a6f): fix(sync): Optimize torrent sorting and reference management ([#​1474](autobrr/qui#1474)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`cf4310e`](autobrr/qui@cf4310e): fix(ui): update placeholder text in ArrInstanceForm based on instance type ([#​1375](autobrr/qui#1375)) ([@​pashioya](https://github.com/pashioya)) - [`92b6748`](autobrr/qui@92b6748): fix(web): format IPv6 peer addresses and copy IP without port ([#​1417](autobrr/qui#1417)) ([@​sleepm](https://github.com/sleepm)) - [`25039bc`](autobrr/qui@25039bc): fix(web): handle SSO session expiry behind Cloudflare Access and other proxies ([#​1438](autobrr/qui#1438)) ([@​nitrobass24](https://github.com/nitrobass24)) - [`77fe310`](autobrr/qui@77fe310): fix(web): prevent category submenu re-render ([#​1357](autobrr/qui#1357)) ([@​jabloink](https://github.com/jabloink)) - [`a42ab1e`](autobrr/qui@a42ab1e): fix(web): raise instance preferences max value from 999 to 99999 ([#​1311](autobrr/qui#1311)) ([@​s0up4200](https://github.com/s0up4200)) - [`540168c`](autobrr/qui@540168c): fix(web): raise virtualization threshold ([#​1355](autobrr/qui#1355)) ([@​jabloink](https://github.com/jabloink)) - [`8547dc6`](autobrr/qui@8547dc6): fix(web): remove column filters when column is hidden ([#​1418](autobrr/qui#1418)) ([@​jabloink](https://github.com/jabloink)) - [`6b09b8d`](autobrr/qui@6b09b8d): fix(web): remove panel size bounds ([@​s0up4200](https://github.com/s0up4200)) - [`db4cdc4`](autobrr/qui@db4cdc4): fix(web): show piece size in torrent details ([#​1365](autobrr/qui#1365)) ([@​s0up4200](https://github.com/s0up4200)) - [`1f94a06`](autobrr/qui@1f94a06): fix(web): use absolute for scroll-to-top on desktop ([#​1419](autobrr/qui#1419)) ([@​jabloink](https://github.com/jabloink)) - [`e31fe3a`](autobrr/qui@e31fe3a): fix: detect tracker health support after qBit upgrade ([#​909](autobrr/qui#909)) ([@​s0up4200](https://github.com/s0up4200)) - [`52f01da`](autobrr/qui@52f01da): fix: disable update indicators when update checks are off ([#​1364](autobrr/qui#1364)) ([@​s0up4200](https://github.com/s0up4200)) - [`f7e3fed`](autobrr/qui@f7e3fed): fix: normalize DD+ and DDP file keys ([#​1456](autobrr/qui#1456)) ([@​s0up4200](https://github.com/s0up4200)) ##### Other Changes - [`d914301`](autobrr/qui@d914301): chore(ci): fire Blacksmith (my wallet screamed) ([#​1408](autobrr/qui#1408)) ([@​s0up4200](https://github.com/s0up4200)) - [`b43327d`](autobrr/qui@b43327d): chore(deps): bump the golang group with 2 updates ([#​1378](autobrr/qui#1378)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`57747bd`](autobrr/qui@57747bd): chore(deps): bump the npm group across 1 directory with 27 updates ([#​1379](autobrr/qui#1379)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`a43850d`](autobrr/qui@a43850d): chore(docs): add BIMI SVG logo ([@​s0up4200](https://github.com/s0up4200)) - [`914bede`](autobrr/qui@914bede): chore(funding): add Patreon to FUNDING.yml ([@​s0up4200](https://github.com/s0up4200)) - [`8b76f1e`](autobrr/qui@8b76f1e): docs(automations): clarify tag matching examples ([#​1457](autobrr/qui#1457)) ([@​s0up4200](https://github.com/s0up4200)) - [`2994054`](autobrr/qui@2994054): docs(readme): restore concise README ([#​1452](autobrr/qui#1452)) ([@​s0up4200](https://github.com/s0up4200)) - [`51237d4`](autobrr/qui@51237d4): docs: Add configuration reference ([#​1440](autobrr/qui#1440)) ([@​s0up4200](https://github.com/s0up4200)) - [`741462c`](autobrr/qui@741462c): docs: add Windows installation guide ([#​1463](autobrr/qui#1463)) ([@​soggy-cr0uton](https://github.com/soggy-cr0uton)) - [`6a11430`](autobrr/qui@6a11430): docs: clarify autobrr filter + apply troubleshooting ([#​1459](autobrr/qui#1459)) ([@​s0up4200](https://github.com/s0up4200)) - [`5a2edc2`](autobrr/qui@5a2edc2): docs: update 2 documentation files ([#​1454](autobrr/qui#1454)) ([@​s0up4200](https://github.com/s0up4200)) - [`139ada9`](autobrr/qui@139ada9): docs: update contributing.md ([#​1470](autobrr/qui#1470)) ([@​s0up4200](https://github.com/s0up4200)) - [`3909aa1`](autobrr/qui@3909aa1): docs: update docs/features/automations.md ([#​1447](autobrr/qui#1447)) ([@​s0up4200](https://github.com/s0up4200)) - [`5dc57ca`](autobrr/qui@5dc57ca): docs: update intro.md ([#​1453](autobrr/qui#1453)) ([@​s0up4200](https://github.com/s0up4200)) - [`5d9e986`](autobrr/qui@5d9e986): perf(web): memoize useDateTimeFormatters ([#​1403](autobrr/qui#1403)) ([@​jabloink](https://github.com/jabloink)) **Full Changelog**: <autobrr/qui@v1.13.1...v1.14.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.14.0` - `docker pull ghcr.io/autobrr/qui:latest` #### What to do next? - Join our [Discord server](https://discord.autobrr.com/qui) Thank you for using qui! </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/4154 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>




Summary
magnet:links - clicking magnet links opens qui when installed as PWA.torrentfiles - opening torrent files from Finder/file manager opens quiregisterProtocolHandler()fallback for Firefox magnet link supportlaunch_handler.client_mode: "navigate-existing"to reuse existing window instead of opening new ones/addroute with instance selection and AddTorrentDialog integrationCloses #329
Browser Support
Magnet links:
registerProtocolHandler()- user registers from dashboard banner or SettingsTorrent files:
Test plan
Summary by CodeRabbit
New Features
UI
Utilities
PWA
Bug Fixes
Documentation