v0.4.1: Add app icons, RSSHub integration & reorder features#2
v0.4.1: Add app icons, RSSHub integration & reorder features#2devohmycode merged 1 commit intomasterfrom
Conversation
Update app icons (desktop & Android adaptive launcher) and add RSSHub integration and UI improvements. Adds src-tauri Android adaptive icon XML and background color, plus many updated icon assets. Introduces a new ExpandingPanel component and a rsshubService with Settings and AddFeed UI to configure/detect an RSSHub instance. Implements drag-and-drop reordering for Favorites / Read Later in FeedPanel, wires reorder handlers in App, and exposes feed rename input in SourcePanel. Improves TTS error handling/UI in ReaderPanel and adds debug logging for ElevenLabs TTS calls in the Tauri backend.
📝 WalkthroughWalkthroughThis PR introduces RSSHub integration with route detection and instance management, implements drag-and-drop reordering for favorites and read-later items, enables feed renaming, enhances TTS with verbose logging and error handling including text truncation for ElevenLabs, and adds a new expanding panel UI component for modal interactions. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
Review Summary by QodoAdd RSSHub integration, drag-and-drop reordering, and ExpandingPanel component
WalkthroughsDescription• Add RSSHub integration for automatic RSS feed detection from websites • Implement drag-and-drop reordering for Favorites and Read Later lists • Create ExpandingPanel component for About/Help modal with app info • Add feed rename functionality and improve TTS error handling • Enhance ElevenLabs TTS with character limit handling and debug logging Diagramflowchart LR
A["User Input"] -->|detectRSSHubRoute| B["RSSHub Service"]
B -->|rsshubMatch| C["AddFeedModal"]
C -->|Suggestion| D["Feed Added"]
E["Favorites/ReadLater"] -->|Drag-Drop| F["FeedPanel Reorder"]
F -->|onReorderItems| G["Store Persistence"]
H["About Button"] -->|Click| I["ExpandingPanel"]
I -->|Display| J["App Info & Shortcuts"]
K["TTS Request"] -->|Truncate| L["ElevenLabs Service"]
L -->|Debug Logs| M["Tauri Backend"]
File Changes1. src/services/rsshubService.ts
|
Code Review by Qodo
1. UI shows raw ttsError
|
| className={`reader-tool-btn tts ${ttsStatus !== 'idle' ? 'active' : ''} ${ttsError ? 'error' : ''}`} | ||
| title={ttsError ? `Erreur: ${ttsError}` : ttsStatus === 'playing' ? 'Pause' : ttsStatus === 'paused' ? 'Reprendre' : 'Écouter'} | ||
| onClick={handleTts} | ||
| > | ||
| {ttsStatus === 'playing' ? '⏸' : '▶'} | ||
| {ttsError ? '⚠' : ttsStatus === 'playing' ? '⏸' : '▶'} | ||
| </button> |
There was a problem hiding this comment.
1. Ui shows raw ttserror 📘 Rule violation ⛨ Security
The UI tooltip displays the raw ttsError string, which can expose internal/third-party error details directly to end users. This can leak implementation details (e.g., HTTP status/error bodies) and should be replaced with a generic user-facing message while keeping details in internal logs.
Agent Prompt
## Issue description
The UI currently displays raw TTS error details (`ttsError`) to end users (tooltip). This can expose internal/provider details and violates secure error handling.
## Issue Context
`ttsError` is set from `err.message` coming from the TTS invocation path, and backend errors can include HTTP status and response bodies.
## Fix Focus Areas
- src/components/ReaderPanel.tsx[330-335]
- src/components/ReaderPanel.tsx[837-842]
- src-tauri/src/lib.rs[496-499]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const getFavoritesOrder = useCallback((): string[] => { | ||
| return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []); | ||
| }, []); | ||
|
|
||
| const getReadLaterOrder = useCallback((): string[] => { | ||
| return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []); | ||
| }, []); | ||
|
|
||
| const reorderFavorites = useCallback((orderedIds: string[]) => { | ||
| saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds); | ||
| }, []); | ||
|
|
||
| const reorderReadLater = useCallback((orderedIds: string[]) => { | ||
| saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds); | ||
| }, []); |
There was a problem hiding this comment.
2. Reorder doesn’t re-render 🐞 Bug ✓ Correctness
Saving the new Favorites/Read Later order only writes to localStorage; no React state is updated, so the UI may not reflect a drag-and-drop reorder until some unrelated re-render occurs.
Agent Prompt
## Issue description
Reordering only writes to localStorage, which doesn’t trigger React re-renders. After a drop, the UI may not update to reflect the new order.
## Issue Context
Favorites/Read Later ordering is read via `getFavoritesOrder/getReadLaterOrder()` but the reorder functions do not update any React state.
## Fix Focus Areas
- src/hooks/useFeedStore.ts[534-585]
- src/App.tsx[121-158]
- src/App.tsx[282-285]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const handleDrop = useCallback((e: React.DragEvent, targetId: string) => { | ||
| e.preventDefault(); | ||
| const sourceId = e.dataTransfer.getData('text/plain'); | ||
| if (!sourceId || sourceId === targetId || !onReorderItems) return; | ||
|
|
||
| const ids = items.map(i => i.id); | ||
| const fromIdx = ids.indexOf(sourceId); | ||
| const toIdx = ids.indexOf(targetId); | ||
| if (fromIdx === -1 || toIdx === -1) return; | ||
|
|
||
| ids.splice(fromIdx, 1); | ||
| ids.splice(toIdx, 0, sourceId); | ||
| onReorderItems(ids); | ||
|
|
There was a problem hiding this comment.
3. Reorder index wrong 🐞 Bug ✓ Correctness
The drag-and-drop reorder algorithm inserts at the original target index after removing the source item, which shifts indices; dragging an item downward can place it one position off (often after the intended target).
Agent Prompt
## Issue description
Downward drag-and-drop reorder uses an unadjusted target index after removing the source element, leading to off-by-one placement.
## Issue Context
`ids.splice(fromIdx, 1)` shifts indices; `toIdx` must be adjusted when `fromIdx < toIdx`.
## Fix Focus Areas
- src/components/FeedPanel.tsx[153-169]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| useEffect(() => { | ||
| if (isOpen) { | ||
| setPhase('mounting'); | ||
| document.addEventListener('keydown', handleKeyDown); | ||
|
|
||
| const t1 = setTimeout(() => setPhase('expanding'), 30); | ||
| const t2 = setTimeout(() => setPhase('content-in'), 900); | ||
|
|
||
| return () => { | ||
| clearTimeout(t1); | ||
| clearTimeout(t2); | ||
| }; | ||
| } else { | ||
| if (phase === 'hidden') return; | ||
|
|
||
| setPhase('content-out'); | ||
|
|
||
| const t1 = setTimeout(() => setPhase('closing'), 350); | ||
| const t2 = setTimeout(() => { | ||
| setPhase('hidden'); | ||
| document.body.style.overflow = ''; | ||
| }, 1350); | ||
|
|
||
| document.removeEventListener('keydown', handleKeyDown); | ||
|
|
There was a problem hiding this comment.
4. Expandingpanel listener leak 🐞 Bug ⛯ Reliability
ExpandingPanel adds a global keydown listener when opening, but its open-branch cleanup doesn’t remove it; if the component unmounts while open (or if handler identity changes), Escape listeners can leak and fire unexpectedly.
Agent Prompt
## Issue description
A document-level keydown listener is added when the panel opens, but not removed in the open-branch cleanup, so it can leak on unmount.
## Issue Context
The app conditionally unmounts panel trees when collapsed, so unmount-while-open can occur.
## Fix Focus Areas
- src/components/ExpandingPanel.tsx[39-70]
- src/App.tsx[311-316]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| export function getRSSHubInstance(): string { | ||
| return localStorage.getItem(STORAGE_KEY) || DEFAULT_INSTANCE; | ||
| } | ||
|
|
||
| export function setRSSHubInstance(url: string) { | ||
| const trimmed = url.trim().replace(/\/+$/, ''); | ||
| localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE); | ||
| } |
There was a problem hiding this comment.
5. Rsshub storage can throw 🐞 Bug ⛯ Reliability
rsshubService reads/writes localStorage without try/catch; since isRSSHubUrl() can be called during render, a localStorage SecurityError/Quota error can crash the UI in restricted environments.
Agent Prompt
## Issue description
Unguarded localStorage access can throw during render paths, crashing the UI.
## Issue Context
Other store code uses guarded access helpers; rsshubService should match that pattern.
## Fix Focus Areas
- src/services/rsshubService.ts[1-12]
- src/hooks/useFeedStore.ts[40-67]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/App.tsx (1)
122-158:⚠️ Potential issue | 🟠 Major
itemsmemo won't recalculate after a drag-and-drop reorder.
getFavoritesOrder()/getReadLaterOrder()are called inside this memo, butreorderFavorites/reorderReadLater(inuseFeedStore) only write tolocalStoragewithout updating any React state. Thereforestore's reference never changes due to a reorder, the memo's deps remain unchanged, and this computeditemslist is not re-sorted until the next unrelated state change (e.g., marking an item as read). IfFeedPanelre-derives its display from theitemsprop on each render, the reordering will visually revert.Root cause and fix are in
useFeedStore— see the comment there.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/App.tsx` around lines 122 - 158, The computed items memo (items) relies on getFavoritesOrder/getReadLaterOrder but reorderFavorites/reorderReadLater only write to localStorage in useFeedStore, so the store reference never changes and the memo never recalculates after a drag; fix by making reorderFavorites and reorderReadLater update React state inside useFeedStore (e.g., store an orders state or a version/timestamp in the store and setState when reordering) and still persist to localStorage, so getFavoritesOrder/getReadLaterOrder return the updated in-memory order and items' useMemo sees a changed dependency and re-sorts immediately.
🧹 Nitpick comments (2)
src/components/SettingsModal.tsx (1)
535-555: Persist RSSHub instance on blur/debounce, not every keystroke.Saving on each change can temporarily store invalid partial URLs while the user is typing. Consider persisting on blur or after a debounce.
♻️ Example adjustment
value={rsshubInstance} -onChange={(e) => { - setRsshubInstance(e.target.value); - setRSSHubInstanceConfig(e.target.value); -}} +onChange={(e) => setRsshubInstance(e.target.value)} +onBlur={() => setRSSHubInstanceConfig(rsshubInstance)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsModal.tsx` around lines 535 - 555, The input currently calls setRsshubInstance and persists on every keystroke via setRSSHubInstanceConfig in the onChange handler, which can save invalid partial URLs; change this so onChange only updates local state (setRsshubInstance) and persist the value on blur (input id "rsshub-instance" onBlur -> call setRSSHubInstanceConfig) or implement a debounced persister (e.g., use a debounced callback or useEffect that calls setRSSHubInstanceConfig(rsshubInstance) after a short delay) so only completed values are saved; ensure you update the JSX handlers around rsshubInstance, setRsshubInstance and setRSSHubInstanceConfig accordingly.src-tauri/src/lib.rs (1)
474-507: Consider gating verbose ElevenLabs logs.These eprintln! statements are helpful during debugging but can be noisy in release builds and may expose error payloads. Consider guarding them behind a debug flag or feature.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src-tauri/src/lib.rs` around lines 474 - 507, Replace the raw eprintln! calls in the ElevenLabs request/response block (the logging lines around the client.post/send awaiting, the status/error body prints, and the "Audio received" print near STANDARD.encode(&bytes)) with gated logging: either use cfg!(debug_assertions) or a Cargo feature (e.g., feature "verbose_elevenlabs") to conditionally emit those messages, or switch to the log crate (debug! / trace!) and enable that level only in debug/verbose builds; ensure you update the ElevenLabs request error mapping and any sensitive error prints to only log when the gate is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/AddFeedModal.tsx`:
- Around line 324-341: When the RSSHub suggestion button is clicked it sets the
input and name but does not clear the prior search state, leaving a stale
dropdown; update the onClick handler inside the rsshubMatch block (the
motion.button that calls setInput and setName) to also invoke the app’s
search-reset function — e.g. call the existing search-clearing handler
(resetSearch, clearSearchResults, setSearchOpen(false), or whatever the project
uses) immediately after setInput/setName so prior results and any open dropdown
are cleared when applying the suggestion.
In `@src/components/ExpandingPanel.tsx`:
- Around line 110-116: The ExpandingPanel component declares role="dialog" with
aria-modal="true" but lacks a focus trap; update the component to trap focus
while open by either adding two hidden focusable sentinel elements (start/end
sentinels inside the element referenced by panelRef) that on focus move focus to
the first/last focusable element, or by enhancing the existing handleKeyDown to
intercept Tab and Shift+Tab and cycle focus within the panel's focusable
elements; ensure the logic runs only when the panel is open, uses panelRef to
query tabbable elements, and restores focus when the panel closes.
- Around line 39-50: The cleanup inside the useEffect's isOpen branch currently
clears timeouts but doesn't remove the keydown listener, causing a leak; update
the cleanup returned from that branch in ExpandingPanel's useEffect to call
document.removeEventListener('keydown', handleKeyDown) (alongside
clearTimeout(t1) and clearTimeout(t2)) so the listener is removed when the panel
unmounts or closes; reference the useEffect that sets setPhase('mounting'), adds
document.addEventListener('keydown', handleKeyDown), and creates t1/t2 timeouts
to locate where to add the removeEventListener.
- Around line 57-60: The close timer resets body scroll but opening never locks
it; fix by saving and restoring the body's overflow: in the ExpandingPanel
component add a ref (e.g. prevBodyOverflowRef) to store
document.body.style.overflow before opening, set document.body.style.overflow =
'hidden' when you transition the panel to the open/visible phase (where you call
setPhase('visible') or in the open timeout), and in the closing timeout (where
t2 is created and you setPhase('hidden')) restore document.body.style.overflow =
prevBodyOverflowRef.current instead of setting it to ''. This ensures scroll is
locked on open and restored exactly on close.
In `@src/components/FeedPanel.tsx`:
- Around line 153-165: The handleDrop handler shifts indices when removing the
dragged item and currently inserts at toIdx which causes an off-by-one when
dragging downward; update handleDrop (used with items, ids, fromIdx, toIdx, and
onReorderItems) to decrement the insertion index when fromIdx < toIdx (e.g.,
compute adjustedTo = fromIdx < toIdx ? toIdx - 1 : toIdx) before calling
ids.splice and then call onReorderItems(ids) so the moved item ends up in the
intended position.
In `@src/components/SourcePanel.tsx`:
- Around line 286-299: The onKeyDown/onBlur handlers double-call onRenameFeed
because Enter triggers onRenameFeed then unmount triggers onBlur which sees the
old renameFeedInput and calls onRenameFeed again; modify the handlers in
SourcePanel so that onKeyDown still commits (when e.key === 'Enter' and
renameFeedInput.value.trim()) by calling onRenameFeed(feed.id,
renameFeedInput.value.trim()) and then setRenameFeedInput(null), but change
onBlur to only cancel (call setRenameFeedInput(null) without invoking
onRenameFeed) or alternatively implement a ref flag (e.g., commitInProgressRef)
checked by onBlur to avoid calling onRenameFeed twice; update references to
renameFeedInput, onRenameFeed, setRenameFeedInput, feed.id and feed.name
accordingly.
- Line 657: The displayed hard-coded version string in SourcePanel (the <div
className="panel-about-version"> v0.4.0) is out of sync; import the package.json
version and use it instead so it stays current. Update SourcePanel.tsx to import
the version (e.g., import { version } from '../../package.json') and replace the
hard-coded string in the JSX (the element with className "panel-about-version")
to render the imported version variable.
In `@src/hooks/useFeedStore.ts`:
- Around line 536-550: The reorderFavorites and reorderReadLater functions only
call saveToStorage so they don't update React state and the hook never
re-renders; change the hook to hold favoritesOrder and readLaterOrder in
useState (initialize from loadFromStorage in the hook), update those state
setters inside reorderFavorites/reorderReadLater (and still persist via
saveToStorage), and update getFavoritesOrder/getReadLaterOrder to return the
state values instead of calling loadFromStorage each time so that invoking
reorderFavorites/reorderReadLater triggers a re-render and the UI reflects the
new order immediately.
- Around line 281-286: The renameFeed function currently updates only
name/feedName via setFeeds and setItems; update it to also set updated_at (e.g.,
Date.now() or new Date().toISOString()) on the changed feed objects and on any
item-level objects that need the timestamp, and after state updates call the
sync callbacks (onItemsChanged and/or onFeedAdded or whatever feed-sync callback
exists) so the change will propagate to Supabase/remote providers; locate
renameFeed, setFeeds, setItems, updated_at, and the onItemsChanged/onFeedAdded
handlers and ensure the timestamp and callback invocation are included.
In `@src/services/rsshubService.ts`:
- Around line 8-11: The setRSSHubInstance function currently persists any
trimmed string, which can result in invalid RSSHub links; update
setRSSHubInstance to validate the incoming url: trim and remove trailing
slashes, then attempt to parse it (e.g. via new URL(...) or an equivalent URL
validation) and ensure the protocol is http or https; if validation fails or the
value is empty, store DEFAULT_INSTANCE instead of the invalid value; keep
STORAGE_KEY and localStorage.setItem as the persistence mechanism and ensure the
saved value is the validated/trusted instance string.
---
Outside diff comments:
In `@src/App.tsx`:
- Around line 122-158: The computed items memo (items) relies on
getFavoritesOrder/getReadLaterOrder but reorderFavorites/reorderReadLater only
write to localStorage in useFeedStore, so the store reference never changes and
the memo never recalculates after a drag; fix by making reorderFavorites and
reorderReadLater update React state inside useFeedStore (e.g., store an orders
state or a version/timestamp in the store and setState when reordering) and
still persist to localStorage, so getFavoritesOrder/getReadLaterOrder return the
updated in-memory order and items' useMemo sees a changed dependency and
re-sorts immediately.
---
Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 474-507: Replace the raw eprintln! calls in the ElevenLabs
request/response block (the logging lines around the client.post/send awaiting,
the status/error body prints, and the "Audio received" print near
STANDARD.encode(&bytes)) with gated logging: either use cfg!(debug_assertions)
or a Cargo feature (e.g., feature "verbose_elevenlabs") to conditionally emit
those messages, or switch to the log crate (debug! / trace!) and enable that
level only in debug/verbose builds; ensure you update the ElevenLabs request
error mapping and any sensitive error prints to only log when the gate is
enabled.
In `@src/components/SettingsModal.tsx`:
- Around line 535-555: The input currently calls setRsshubInstance and persists
on every keystroke via setRSSHubInstanceConfig in the onChange handler, which
can save invalid partial URLs; change this so onChange only updates local state
(setRsshubInstance) and persist the value on blur (input id "rsshub-instance"
onBlur -> call setRSSHubInstanceConfig) or implement a debounced persister
(e.g., use a debounced callback or useEffect that calls
setRSSHubInstanceConfig(rsshubInstance) after a short delay) so only completed
values are saved; ensure you update the JSX handlers around rsshubInstance,
setRsshubInstance and setRSSHubInstanceConfig accordingly.
| {rsshubMatch && !resolved.shorthand && ( | ||
| <motion.button | ||
| type="button" | ||
| className="rsshub-suggestion" | ||
| initial={{ opacity: 0, y: -4 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| transition={{ duration: 0.15 }} | ||
| onClick={() => { | ||
| setInput(rsshubMatch.rsshubUrl); | ||
| if (!name.trim()) setName(rsshubMatch.label); | ||
| }} | ||
| > | ||
| <svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"> | ||
| <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/> | ||
| </svg> | ||
| <span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span> | ||
| </motion.button> | ||
| )} |
There was a problem hiding this comment.
Clear search state when applying the RSSHub suggestion.
Clicking the suggestion updates the input but leaves prior search results/search state intact, which can leave a stale dropdown open.
🛠️ Proposed fix
onClick={() => {
setInput(rsshubMatch.rsshubUrl);
if (!name.trim()) setName(rsshubMatch.label);
+ setSearchResults([]);
+ setIsSearching(false);
+ setSearchQuery('');
}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {rsshubMatch && !resolved.shorthand && ( | |
| <motion.button | |
| type="button" | |
| className="rsshub-suggestion" | |
| initial={{ opacity: 0, y: -4 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.15 }} | |
| onClick={() => { | |
| setInput(rsshubMatch.rsshubUrl); | |
| if (!name.trim()) setName(rsshubMatch.label); | |
| }} | |
| > | |
| <svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"> | |
| <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/> | |
| </svg> | |
| <span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span> | |
| </motion.button> | |
| )} | |
| {rsshubMatch && !resolved.shorthand && ( | |
| <motion.button | |
| type="button" | |
| className="rsshub-suggestion" | |
| initial={{ opacity: 0, y: -4 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.15 }} | |
| onClick={() => { | |
| setInput(rsshubMatch.rsshubUrl); | |
| if (!name.trim()) setName(rsshubMatch.label); | |
| setSearchResults([]); | |
| setIsSearching(false); | |
| setSearchQuery(''); | |
| }} | |
| > | |
| <svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"> | |
| <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/> | |
| </svg> | |
| <span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span> | |
| </motion.button> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/AddFeedModal.tsx` around lines 324 - 341, When the RSSHub
suggestion button is clicked it sets the input and name but does not clear the
prior search state, leaving a stale dropdown; update the onClick handler inside
the rsshubMatch block (the motion.button that calls setInput and setName) to
also invoke the app’s search-reset function — e.g. call the existing
search-clearing handler (resetSearch, clearSearchResults, setSearchOpen(false),
or whatever the project uses) immediately after setInput/setName so prior
results and any open dropdown are cleared when applying the suggestion.
| useEffect(() => { | ||
| if (isOpen) { | ||
| setPhase('mounting'); | ||
| document.addEventListener('keydown', handleKeyDown); | ||
|
|
||
| const t1 = setTimeout(() => setPhase('expanding'), 30); | ||
| const t2 = setTimeout(() => setPhase('content-in'), 900); | ||
|
|
||
| return () => { | ||
| clearTimeout(t1); | ||
| clearTimeout(t2); | ||
| }; |
There was a problem hiding this comment.
Event listener leak when the panel unmounts while open.
The cleanup returned from the isOpen branch only clears the timeouts. If the component is unmounted while isOpen is true (e.g., a parent conditional removes it), document.removeEventListener('keydown', handleKeyDown) is never called, leaving a dangling listener that will invoke onClose after unmount.
🔒 Proposed fix — add removeEventListener to the cleanup of the open branch
return () => {
clearTimeout(t1);
clearTimeout(t2);
+ document.removeEventListener('keydown', handleKeyDown);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (isOpen) { | |
| setPhase('mounting'); | |
| document.addEventListener('keydown', handleKeyDown); | |
| const t1 = setTimeout(() => setPhase('expanding'), 30); | |
| const t2 = setTimeout(() => setPhase('content-in'), 900); | |
| return () => { | |
| clearTimeout(t1); | |
| clearTimeout(t2); | |
| }; | |
| useEffect(() => { | |
| if (isOpen) { | |
| setPhase('mounting'); | |
| document.addEventListener('keydown', handleKeyDown); | |
| const t1 = setTimeout(() => setPhase('expanding'), 30); | |
| const t2 = setTimeout(() => setPhase('content-in'), 900); | |
| return () => { | |
| clearTimeout(t1); | |
| clearTimeout(t2); | |
| document.removeEventListener('keydown', handleKeyDown); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ExpandingPanel.tsx` around lines 39 - 50, The cleanup inside
the useEffect's isOpen branch currently clears timeouts but doesn't remove the
keydown listener, causing a leak; update the cleanup returned from that branch
in ExpandingPanel's useEffect to call document.removeEventListener('keydown',
handleKeyDown) (alongside clearTimeout(t1) and clearTimeout(t2)) so the listener
is removed when the panel unmounts or closes; reference the useEffect that sets
setPhase('mounting'), adds document.addEventListener('keydown', handleKeyDown),
and creates t1/t2 timeouts to locate where to add the removeEventListener.
| const t2 = setTimeout(() => { | ||
| setPhase('hidden'); | ||
| document.body.style.overflow = ''; | ||
| }, 1350); |
There was a problem hiding this comment.
document.body.style.overflow = '' is a no-op — the corresponding overflow = 'hidden' on open is missing.
The closing timeout resets body overflow, but nowhere in the opening path is document.body.style.overflow = 'hidden' (or 'clip') set. Either the setter was forgotten — meaning body scroll is never locked when the panel is open — or this reset line is dead code and should be removed.
♻️ Option A — lock body scroll on open, restore on close (typical modal pattern)
if (isOpen) {
setPhase('mounting');
document.addEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'hidden';
const t1 = setTimeout(() => setPhase('expanding'), 30);
const t2 = setTimeout(() => setPhase('content-in'), 900);
return () => {
clearTimeout(t1);
clearTimeout(t2);
document.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = '';
};Then remove the document.body.style.overflow = '' line from the closing timeout (line 59).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ExpandingPanel.tsx` around lines 57 - 60, The close timer
resets body scroll but opening never locks it; fix by saving and restoring the
body's overflow: in the ExpandingPanel component add a ref (e.g.
prevBodyOverflowRef) to store document.body.style.overflow before opening, set
document.body.style.overflow = 'hidden' when you transition the panel to the
open/visible phase (where you call setPhase('visible') or in the open timeout),
and in the closing timeout (where t2 is created and you setPhase('hidden'))
restore document.body.style.overflow = prevBodyOverflowRef.current instead of
setting it to ''. This ensures scroll is locked on open and restored exactly on
close.
| <div | ||
| ref={panelRef} | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-label={title || 'Panneau'} | ||
| className="expanding-panel" | ||
| style={{ clipPath, transition: clipTransition }} |
There was a problem hiding this comment.
aria-modal="true" dialog lacks a focus trap.
Declaring aria-modal="true" signals to assistive technologies that interaction is restricted to this dialog, but there is no Tab-key interception or sentinel elements to keep focus inside the panel. Keyboard-only and screen-reader users can Tab out of the dialog while it is open.
Add focus sentinels (hidden focusable elements at the start and end of the panel) that redirect focus back in, or intercept Tab/Shift+Tab in the handleKeyDown callback.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ExpandingPanel.tsx` around lines 110 - 116, The ExpandingPanel
component declares role="dialog" with aria-modal="true" but lacks a focus trap;
update the component to trap focus while open by either adding two hidden
focusable sentinel elements (start/end sentinels inside the element referenced
by panelRef) that on focus move focus to the first/last focusable element, or by
enhancing the existing handleKeyDown to intercept Tab and Shift+Tab and cycle
focus within the panel's focusable elements; ensure the logic runs only when the
panel is open, uses panelRef to query tabbable elements, and restores focus when
the panel closes.
| const handleDrop = useCallback((e: React.DragEvent, targetId: string) => { | ||
| e.preventDefault(); | ||
| const sourceId = e.dataTransfer.getData('text/plain'); | ||
| if (!sourceId || sourceId === targetId || !onReorderItems) return; | ||
|
|
||
| const ids = items.map(i => i.id); | ||
| const fromIdx = ids.indexOf(sourceId); | ||
| const toIdx = ids.indexOf(targetId); | ||
| if (fromIdx === -1 || toIdx === -1) return; | ||
|
|
||
| ids.splice(fromIdx, 1); | ||
| ids.splice(toIdx, 0, sourceId); | ||
| onReorderItems(ids); |
There was a problem hiding this comment.
Fix off‑by‑one when dropping an item below its original position.
When dragging downward, removing the source shifts indices; inserting at the original target index places the item after the target (despite the drop‑over border at the top). Adjust the insert index when fromIdx < toIdx.
🐛 Proposed fix
const fromIdx = ids.indexOf(sourceId);
const toIdx = ids.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;
ids.splice(fromIdx, 1);
-ids.splice(toIdx, 0, sourceId);
+const insertIdx = fromIdx < toIdx ? toIdx - 1 : toIdx;
+ids.splice(insertIdx, 0, sourceId);
onReorderItems(ids);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/FeedPanel.tsx` around lines 153 - 165, The handleDrop handler
shifts indices when removing the dragged item and currently inserts at toIdx
which causes an off-by-one when dragging downward; update handleDrop (used with
items, ids, fromIdx, toIdx, and onReorderItems) to decrement the insertion index
when fromIdx < toIdx (e.g., compute adjustedTo = fromIdx < toIdx ? toIdx - 1 :
toIdx) before calling ids.splice and then call onReorderItems(ids) so the moved
item ends up in the intended position.
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter' && renameFeedInput.value.trim()) { | ||
| onRenameFeed(feed.id, renameFeedInput.value.trim()); | ||
| setRenameFeedInput(null); | ||
| } else if (e.key === 'Escape') { | ||
| setRenameFeedInput(null); | ||
| } | ||
| }} | ||
| onBlur={() => { | ||
| if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) { | ||
| onRenameFeed(feed.id, renameFeedInput.value.trim()); | ||
| } | ||
| setRenameFeedInput(null); | ||
| }} |
There was a problem hiding this comment.
Double onRenameFeed call when Enter is pressed.
When Enter is pressed, onKeyDown calls onRenameFeed and schedules setRenameFeedInput(null). React then re-renders, unmounts the input, and fires a synthetic blur before removal. The onBlur closure captures the pre-update renameFeedInput (still non-null), the guard condition value.trim() !== feed.name is true, and onRenameFeed is called a second time. While idempotent today, it is fragile — any future sync callback on rename will fire twice.
Use a ref flag to guard against the double-fire, or simply remove the rename logic from onBlur and commit only on Enter:
🐛 Proposed fix — commit only via Enter, onBlur cancels silently
onKeyDown={(e) => {
if (e.key === 'Enter' && renameFeedInput.value.trim()) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
setRenameFeedInput(null);
} else if (e.key === 'Escape') {
setRenameFeedInput(null);
}
}}
- onBlur={() => {
- if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) {
- onRenameFeed(feed.id, renameFeedInput.value.trim());
- }
- setRenameFeedInput(null);
- }}
+ onBlur={() => setRenameFeedInput(null)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' && renameFeedInput.value.trim()) { | |
| onRenameFeed(feed.id, renameFeedInput.value.trim()); | |
| setRenameFeedInput(null); | |
| } else if (e.key === 'Escape') { | |
| setRenameFeedInput(null); | |
| } | |
| }} | |
| onBlur={() => { | |
| if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) { | |
| onRenameFeed(feed.id, renameFeedInput.value.trim()); | |
| } | |
| setRenameFeedInput(null); | |
| }} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' && renameFeedInput.value.trim()) { | |
| onRenameFeed(feed.id, renameFeedInput.value.trim()); | |
| setRenameFeedInput(null); | |
| } else if (e.key === 'Escape') { | |
| setRenameFeedInput(null); | |
| } | |
| }} | |
| onBlur={() => setRenameFeedInput(null)} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SourcePanel.tsx` around lines 286 - 299, The onKeyDown/onBlur
handlers double-call onRenameFeed because Enter triggers onRenameFeed then
unmount triggers onBlur which sees the old renameFeedInput and calls
onRenameFeed again; modify the handlers in SourcePanel so that onKeyDown still
commits (when e.key === 'Enter' and renameFeedInput.value.trim()) by calling
onRenameFeed(feed.id, renameFeedInput.value.trim()) and then
setRenameFeedInput(null), but change onBlur to only cancel (call
setRenameFeedInput(null) without invoking onRenameFeed) or alternatively
implement a ref flag (e.g., commitInProgressRef) checked by onBlur to avoid
calling onRenameFeed twice; update references to renameFeedInput, onRenameFeed,
setRenameFeedInput, feed.id and feed.name accordingly.
| <div className="panel-about-hero"> | ||
| <div className="panel-about-logo">◈</div> | ||
| <div className="panel-about-appname">SuperFlux</div> | ||
| <div className="panel-about-version">v0.4.0</div> |
There was a problem hiding this comment.
About panel version v0.4.0 doesn't match the PR's v0.4.1.
Consider importing the version from package.json to keep it in sync automatically:
♻️ Proposed fix — pull version from package.json
At the top of the file:
import { version } from '../../package.json';Then in the JSX:
- <div className="panel-about-version">v0.4.0</div>
+ <div className="panel-about-version">v{version}</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="panel-about-version">v0.4.0</div> | |
| import { version } from '../../package.json'; | |
| // ... (other code) | |
| <div className="panel-about-version">v{version}</div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SourcePanel.tsx` at line 657, The displayed hard-coded version
string in SourcePanel (the <div className="panel-about-version"> v0.4.0) is out
of sync; import the package.json version and use it instead so it stays current.
Update SourcePanel.tsx to import the version (e.g., import { version } from
'../../package.json') and replace the hard-coded string in the JSX (the element
with className "panel-about-version") to render the imported version variable.
| // Rename a feed | ||
| const renameFeed = useCallback((feedId: string, newName: string) => { | ||
| if (!newName.trim()) return; | ||
| setFeeds(prev => prev.map(f => f.id === feedId ? { ...f, name: newName.trim() } : f)); | ||
| setItems(prev => prev.map(i => i.feedId === feedId ? { ...i, feedName: newName.trim() } : i)); | ||
| }, []); |
There was a problem hiding this comment.
renameFeed omits updated_at bump and sync notification.
The feed and its items are updated in place but updated_at is not refreshed, so any sync logic relying on that field to detect changes will miss the rename. Additionally, no onItemsChanged / onFeedAdded callback is triggered, meaning the rename is silently local-only and will not propagate to Supabase or a connected provider.
♻️ Proposed fix
const renameFeed = useCallback((feedId: string, newName: string) => {
if (!newName.trim()) return;
- setFeeds(prev => prev.map(f => f.id === feedId ? { ...f, name: newName.trim() } : f));
- setItems(prev => prev.map(i => i.feedId === feedId ? { ...i, feedName: newName.trim() } : i));
+ const now = new Date().toISOString();
+ setFeeds(prev => prev.map(f =>
+ f.id === feedId ? { ...f, name: newName.trim(), updated_at: now } : f
+ ));
+ setItems(prev => {
+ const next = prev.map(i =>
+ i.feedId === feedId ? { ...i, feedName: newName.trim(), updated_at: now } : i
+ );
+ const changed = next.filter((item, idx) => item !== prev[idx]);
+ if (changed.length > 0) queueMicrotask(() => cbRef.current?.onItemsChanged?.(changed));
+ return next;
+ });
}, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useFeedStore.ts` around lines 281 - 286, The renameFeed function
currently updates only name/feedName via setFeeds and setItems; update it to
also set updated_at (e.g., Date.now() or new Date().toISOString()) on the
changed feed objects and on any item-level objects that need the timestamp, and
after state updates call the sync callbacks (onItemsChanged and/or onFeedAdded
or whatever feed-sync callback exists) so the change will propagate to
Supabase/remote providers; locate renameFeed, setFeeds, setItems, updated_at,
and the onItemsChanged/onFeedAdded handlers and ensure the timestamp and
callback invocation are included.
| const getFavoritesOrder = useCallback((): string[] => { | ||
| return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []); | ||
| }, []); | ||
|
|
||
| const getReadLaterOrder = useCallback((): string[] => { | ||
| return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []); | ||
| }, []); | ||
|
|
||
| const reorderFavorites = useCallback((orderedIds: string[]) => { | ||
| saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds); | ||
| }, []); | ||
|
|
||
| const reorderReadLater = useCallback((orderedIds: string[]) => { | ||
| saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds); | ||
| }, []); |
There was a problem hiding this comment.
reorderFavorites / reorderReadLater never update React state — order changes are invisible until the next unrelated re-render.
Both functions only call saveToStorage and return. Because no setState is invoked, useFeedStore does not re-render, the store reference in App.tsx does not change, and the items useMemo (which calls getFavoritesOrder / getReadLaterOrder) is never re-evaluated after a drag-and-drop. The new order is applied the next time any other state change happens to trigger a re-render — until then, the displayed order can visually revert.
Lift the orders into useState so that a reorder triggers a re-render:
🐛 Proposed fix — store orders in React state
+ const [favoritesOrder, setFavoritesOrder] = useState<string[]>(() =>
+ loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []),
+ );
+ const [readLaterOrder, setReadLaterOrder] = useState<string[]>(() =>
+ loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []),
+ );
const getFavoritesOrder = useCallback((): string[] => {
- return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
+ return favoritesOrder;
- }, []);
+ }, [favoritesOrder]);
const getReadLaterOrder = useCallback((): string[] => {
- return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
+ return readLaterOrder;
- }, []);
+ }, [readLaterOrder]);
const reorderFavorites = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
+ setFavoritesOrder(orderedIds);
}, []);
const reorderReadLater = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
+ setReadLaterOrder(orderedIds);
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getFavoritesOrder = useCallback((): string[] => { | |
| return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []); | |
| }, []); | |
| const getReadLaterOrder = useCallback((): string[] => { | |
| return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []); | |
| }, []); | |
| const reorderFavorites = useCallback((orderedIds: string[]) => { | |
| saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds); | |
| }, []); | |
| const reorderReadLater = useCallback((orderedIds: string[]) => { | |
| saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds); | |
| }, []); | |
| const [favoritesOrder, setFavoritesOrder] = useState<string[]>(() => | |
| loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []), | |
| ); | |
| const [readLaterOrder, setReadLaterOrder] = useState<string[]>(() => | |
| loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []), | |
| ); | |
| const getFavoritesOrder = useCallback((): string[] => { | |
| return favoritesOrder; | |
| }, [favoritesOrder]); | |
| const getReadLaterOrder = useCallback((): string[] => { | |
| return readLaterOrder; | |
| }, [readLaterOrder]); | |
| const reorderFavorites = useCallback((orderedIds: string[]) => { | |
| saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds); | |
| setFavoritesOrder(orderedIds); | |
| }, []); | |
| const reorderReadLater = useCallback((orderedIds: string[]) => { | |
| saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds); | |
| setReadLaterOrder(orderedIds); | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useFeedStore.ts` around lines 536 - 550, The reorderFavorites and
reorderReadLater functions only call saveToStorage so they don't update React
state and the hook never re-renders; change the hook to hold favoritesOrder and
readLaterOrder in useState (initialize from loadFromStorage in the hook), update
those state setters inside reorderFavorites/reorderReadLater (and still persist
via saveToStorage), and update getFavoritesOrder/getReadLaterOrder to return the
state values instead of calling loadFromStorage each time so that invoking
reorderFavorites/reorderReadLater triggers a re-render and the UI reflects the
new order immediately.
| export function setRSSHubInstance(url: string) { | ||
| const trimmed = url.trim().replace(/\/+$/, ''); | ||
| localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE); | ||
| } |
There was a problem hiding this comment.
Validate RSSHub instance URLs before persisting.
Persisting invalid values (missing scheme, malformed URL) can generate unusable RSSHub links downstream. Consider validating before saving and falling back to the default.
🛡️ Proposed fix
export function setRSSHubInstance(url: string) {
const trimmed = url.trim().replace(/\/+$/, '');
- localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
+ if (!trimmed) {
+ localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
+ return;
+ }
+ try {
+ new URL(trimmed);
+ localStorage.setItem(STORAGE_KEY, trimmed);
+ } catch {
+ localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function setRSSHubInstance(url: string) { | |
| const trimmed = url.trim().replace(/\/+$/, ''); | |
| localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE); | |
| } | |
| export function setRSSHubInstance(url: string) { | |
| const trimmed = url.trim().replace(/\/+$/, ''); | |
| if (!trimmed) { | |
| localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE); | |
| return; | |
| } | |
| try { | |
| new URL(trimmed); | |
| localStorage.setItem(STORAGE_KEY, trimmed); | |
| } catch { | |
| localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/rsshubService.ts` around lines 8 - 11, The setRSSHubInstance
function currently persists any trimmed string, which can result in invalid
RSSHub links; update setRSSHubInstance to validate the incoming url: trim and
remove trailing slashes, then attempt to parse it (e.g. via new URL(...) or an
equivalent URL validation) and ensure the protocol is http or https; if
validation fails or the value is empty, store DEFAULT_INSTANCE instead of the
invalid value; keep STORAGE_KEY and localStorage.setItem as the persistence
mechanism and ensure the saved value is the validated/trusted instance string.
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes