-
Notifications
You must be signed in to change notification settings - Fork 1
v0.4.1: Add app icons, RSSHub integration & reorder features #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <foreground android:drawable="@mipmap/ic_launcher_foreground"/> | ||
| <background android:drawable="@color/ic_launcher_background"/> | ||
| </adaptive-icon> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <color name="ic_launcher_background">#fff</color> | ||
| </resources> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,164 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Phase = 'hidden' | 'mounting' | 'expanding' | 'content-in' | 'content-out' | 'closing'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface ExpandingPanelProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| title?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| corner?: Corner; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getCornerPercent(corner: Corner) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const x = corner.includes('right') ? '100%' : '0%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const y = corner.includes('bottom') ? '100%' : '0%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { x, y }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function ExpandingPanel({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| children, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| corner = 'top-left', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: ExpandingPanelProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const panelRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [phase, setPhase] = useState<Phase>('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { x, y } = getCornerPercent(corner); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleKeyDown = useCallback( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| (e: KeyboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Escape') onClose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [onClose], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Event listener leak when the panel unmounts while open. The cleanup returned from the 🔒 Proposed fix — add removeEventListener to the cleanup of the open branch return () => {
clearTimeout(t1);
clearTimeout(t2);
+ document.removeEventListener('keydown', handleKeyDown);
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (phase === 'hidden') return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| setPhase('content-out'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const t1 = setTimeout(() => setPhase('closing'), 350); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const t2 = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setPhase('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.body.style.overflow = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 1350); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The closing timeout resets body overflow, but nowhere in the opening path is ♻️ 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 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.removeEventListener('keydown', handleKeyDown); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Expandingpanel listener leak 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(t1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(t2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [isOpen, handleKeyDown]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (phase === 'content-in' && panelRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const focusable = panelRef.current.querySelector<HTMLElement>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| focusable?.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [phase]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (phase === 'hidden') return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isExpanded = phase === 'expanding' || phase === 'content-in'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isContentVisible = phase === 'content-in'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isCollapsing = phase === 'closing'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isContentFadingOut = phase === 'content-out'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clipRadius = isExpanded || isContentFadingOut ? '200%' : '0%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clipPath = `circle(${clipRadius} at ${x} ${y})`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clipTransition = isCollapsing | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'clip-path 800ms cubic-bezier(0.55, 0, 1, 0.45)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'clip-path 1200ms cubic-bezier(0.22, 1, 0.36, 1)'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Overlay */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="expanding-panel-overlay" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: isExpanded || isContentFadingOut ? 1 : 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transition: isExpanded || isContentFadingOut | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'opacity 700ms ease' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'opacity 500ms ease', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Panel */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={panelRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="dialog" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-modal="true" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={title || 'Panneau'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="expanding-panel" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ clipPath, transition: clipTransition }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Declaring Add focus sentinels (hidden focusable elements at the start and end of the panel) that redirect focus back in, or intercept 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Header */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <header | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="expanding-panel-header" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: isContentVisible | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'translateY(0)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : isContentFadingOut | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'translateY(0)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'translateY(-16px)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: isContentVisible ? 1 : isContentFadingOut ? 0 : 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transition: isContentVisible | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'transform 700ms ease-out, opacity 700ms ease-out' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'transform 300ms ease-in, opacity 300ms ease-in', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {title && <h2 className="expanding-panel-title">{title}</h2>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="expanding-panel-close" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Fermer le panneau" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ✕ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </header> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Content */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="expanding-panel-body"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: isContentVisible | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'translateY(0)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : isContentFadingOut | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'translateY(0)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'translateY(32px)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: isContentVisible ? 1 : 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transition: isContentVisible | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'transform 1000ms ease-out, opacity 1000ms ease-out' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'transform 300ms ease-in, opacity 300ms ease-in', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents