summaryrefslogtreecommitdiff
path: root/apps/web/app/reader
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 00:10:09 -0800
committerFuwn <[email protected]>2026-02-08 00:10:09 -0800
commitf2baa5a45fa9e5b8c3d0aa0ecbb0113c5166747b (patch)
treeb74ff019343b6f571e5b09f826069831e8bf563d /apps/web/app/reader
parentfeat: display folders above ungrouped feeds in sidebar, add delete-all-custom... (diff)
downloadasa.news-f2baa5a45fa9e5b8c3d0aa0ecbb0113c5166747b.tar.xz
asa.news-f2baa5a45fa9e5b8c3d0aa0ecbb0113c5166747b.zip
feat: add appearance option to toggle folders above/below ungrouped feeds in sidebar
Diffstat (limited to 'apps/web/app/reader')
-rw-r--r--apps/web/app/reader/_components/sidebar-content.tsx206
-rw-r--r--apps/web/app/reader/settings/_components/appearance-settings.tsx21
2 files changed, 128 insertions, 99 deletions
diff --git a/apps/web/app/reader/_components/sidebar-content.tsx b/apps/web/app/reader/_components/sidebar-content.tsx
index 844f8b3..401d203 100644
--- a/apps/web/app/reader/_components/sidebar-content.tsx
+++ b/apps/web/app/reader/_components/sidebar-content.tsx
@@ -96,6 +96,9 @@ export function SidebarContent() {
const focusedSidebarIndex = useUserInterfaceStore(
(state) => state.focusedSidebarIndex
)
+ const showFoldersAboveFeeds = useUserInterfaceStore(
+ (state) => state.showFoldersAboveFeeds
+ )
function closeSidebarOnMobile() {
if (typeof window !== "undefined" && window.innerWidth < 768) {
toggleSidebar()
@@ -214,59 +217,107 @@ export function SidebarContent() {
</div>
)}
- {folders.map((folder) => {
- const isExpanded = expandedFolderIdentifiers.includes(
- folder.folderIdentifier
- )
- const folderSubscriptions = subscriptions.filter(
- (subscription) =>
- subscription.folderIdentifier === folder.folderIdentifier
- )
- const folderUnreadCount = getFolderUnreadCount(
- folder.folderIdentifier
- )
+ {(showFoldersAboveFeeds
+ ? ["folders", "ungrouped"]
+ : ["ungrouped", "folders"]
+ ).map((section) =>
+ section === "folders"
+ ? folders.map((folder) => {
+ const isExpanded = expandedFolderIdentifiers.includes(
+ folder.folderIdentifier
+ )
+ const folderSubscriptions = subscriptions.filter(
+ (subscription) =>
+ subscription.folderIdentifier === folder.folderIdentifier
+ )
+ const folderUnreadCount = getFolderUnreadCount(
+ folder.folderIdentifier
+ )
- const folderNavIndex = navIndex++
+ const folderNavIndex = navIndex++
- return (
- <div key={folder.folderIdentifier} className="mt-2">
- <div
- data-sidebar-nav-item
- {...(folderUnreadCount > 0 ? { "data-has-unreads": "" } : {})}
- className={classNames(
- "flex w-full items-center gap-1 px-2 py-1",
- sidebarFocusClass(focusedPanel, focusedSidebarIndex, folderNavIndex)
- )}
- >
- <button
- type="button"
- aria-expanded={isExpanded}
- onClick={() =>
- toggleFolderExpansion(folder.folderIdentifier)
- }
- className="shrink-0 px-0.5 text-text-secondary transition-colors hover:text-text-primary"
- >
- {isExpanded ? "\u25BE" : "\u25B8"}
- </button>
- <Link
- href={`/reader?folder=${folder.folderIdentifier}`}
- onClick={closeSidebarOnMobile}
- className={classNames(
- "flex flex-1 items-center gap-2 truncate text-text-secondary transition-colors hover:text-text-primary",
- activeFolderIdentifier === folder.folderIdentifier &&
- "text-text-primary"
- )}
- >
- {folder.iconUrl && (
- <img src={folder.iconUrl} alt="" width={16} height={16} className="shrink-0" loading="lazy" />
- )}
- <span className="truncate">{folder.name}</span>
- </Link>
- <UnreadBadge count={folderUnreadCount} />
- </div>
- {isExpanded && (
- <div className="space-y-0.5">
- {folderSubscriptions.map((subscription) => (
+ return (
+ <div key={folder.folderIdentifier} className="mt-2">
+ <div
+ data-sidebar-nav-item
+ {...(folderUnreadCount > 0 ? { "data-has-unreads": "" } : {})}
+ className={classNames(
+ "flex w-full items-center gap-1 px-2 py-1",
+ sidebarFocusClass(focusedPanel, focusedSidebarIndex, folderNavIndex)
+ )}
+ >
+ <button
+ type="button"
+ aria-expanded={isExpanded}
+ onClick={() =>
+ toggleFolderExpansion(folder.folderIdentifier)
+ }
+ className="shrink-0 px-0.5 text-text-secondary transition-colors hover:text-text-primary"
+ >
+ {isExpanded ? "\u25BE" : "\u25B8"}
+ </button>
+ <Link
+ href={`/reader?folder=${folder.folderIdentifier}`}
+ onClick={closeSidebarOnMobile}
+ className={classNames(
+ "flex flex-1 items-center gap-2 truncate text-text-secondary transition-colors hover:text-text-primary",
+ activeFolderIdentifier === folder.folderIdentifier &&
+ "text-text-primary"
+ )}
+ >
+ {folder.iconUrl && (
+ <img src={folder.iconUrl} alt="" width={16} height={16} className="shrink-0" loading="lazy" />
+ )}
+ <span className="truncate">{folder.name}</span>
+ </Link>
+ <UnreadBadge count={folderUnreadCount} />
+ </div>
+ {isExpanded && (
+ <div className="space-y-0.5">
+ {folderSubscriptions.map((subscription) => (
+ <Link
+ key={subscription.subscriptionIdentifier}
+ href={`/reader?feed=${subscription.feedIdentifier}`}
+ data-sidebar-nav-item
+ {...((unreadCounts?.[subscription.feedIdentifier] ?? 0) > 0 ? { "data-has-unreads": "" } : {})}
+ onClick={closeSidebarOnMobile}
+ className={classNames(
+ NAVIGATION_LINK_CLASS,
+ "flex items-center truncate pl-6 text-[0.85em]",
+ activeFeedIdentifier ===
+ subscription.feedIdentifier && ACTIVE_LINK_CLASS,
+ sidebarFocusClass(focusedPanel, focusedSidebarIndex, navIndex++)
+ )}
+ >
+ {showFeedFavicons && (
+ <FeedFavicon feedUrl={subscription.feedUrl} />
+ )}
+ <span className={classNames("truncate", showFeedFavicons && "ml-2")}>
+ {displayNameForSubscription(subscription)}
+ </span>
+ {subscription.feedType === "podcast" && (
+ <span className="ml-1 shrink-0 text-text-dim" title="podcast">&#9835;</span>
+ )}
+ {subscription.consecutiveFailures > 0 && (
+ <span className="ml-1 shrink-0 text-status-warning" title={subscription.lastFetchError ?? "feed error"}>
+ [!]
+ </span>
+ )}
+ <UnreadBadge
+ count={
+ unreadCounts?.[subscription.feedIdentifier] ?? 0
+ }
+ />
+ </Link>
+ ))}
+ </div>
+ )}
+ </div>
+ )
+ })
+ : ungroupedSubscriptions.length > 0 && (
+ <div key="ungrouped" className="mt-3 space-y-0.5">
+ {ungroupedSubscriptions.map((subscription) => (
<Link
key={subscription.subscriptionIdentifier}
href={`/reader?feed=${subscription.feedIdentifier}`}
@@ -275,9 +326,9 @@ export function SidebarContent() {
onClick={closeSidebarOnMobile}
className={classNames(
NAVIGATION_LINK_CLASS,
- "flex items-center truncate pl-6 text-[0.85em]",
- activeFeedIdentifier ===
- subscription.feedIdentifier && ACTIVE_LINK_CLASS,
+ "flex items-center truncate pl-4 text-[0.85em]",
+ activeFeedIdentifier === subscription.feedIdentifier &&
+ ACTIVE_LINK_CLASS,
sidebarFocusClass(focusedPanel, focusedSidebarIndex, navIndex++)
)}
>
@@ -296,55 +347,12 @@ export function SidebarContent() {
</span>
)}
<UnreadBadge
- count={
- unreadCounts?.[subscription.feedIdentifier] ?? 0
- }
+ count={unreadCounts?.[subscription.feedIdentifier] ?? 0}
/>
</Link>
))}
</div>
- )}
- </div>
- )
- })}
-
- {ungroupedSubscriptions.length > 0 && (
- <div className="mt-3 space-y-0.5">
- {ungroupedSubscriptions.map((subscription) => (
- <Link
- key={subscription.subscriptionIdentifier}
- href={`/reader?feed=${subscription.feedIdentifier}`}
- data-sidebar-nav-item
- {...((unreadCounts?.[subscription.feedIdentifier] ?? 0) > 0 ? { "data-has-unreads": "" } : {})}
- onClick={closeSidebarOnMobile}
- className={classNames(
- NAVIGATION_LINK_CLASS,
- "flex items-center truncate pl-4 text-[0.85em]",
- activeFeedIdentifier === subscription.feedIdentifier &&
- ACTIVE_LINK_CLASS,
- sidebarFocusClass(focusedPanel, focusedSidebarIndex, navIndex++)
- )}
- >
- {showFeedFavicons && (
- <FeedFavicon feedUrl={subscription.feedUrl} />
- )}
- <span className={classNames("truncate", showFeedFavicons && "ml-2")}>
- {displayNameForSubscription(subscription)}
- </span>
- {subscription.feedType === "podcast" && (
- <span className="ml-1 shrink-0 text-text-dim" title="podcast">&#9835;</span>
- )}
- {subscription.consecutiveFailures > 0 && (
- <span className="ml-1 shrink-0 text-status-warning" title={subscription.lastFetchError ?? "feed error"}>
- [!]
- </span>
- )}
- <UnreadBadge
- count={unreadCounts?.[subscription.feedIdentifier] ?? 0}
- />
- </Link>
- ))}
- </div>
+ )
)}
<div className="mt-3 space-y-0.5">
diff --git a/apps/web/app/reader/settings/_components/appearance-settings.tsx b/apps/web/app/reader/settings/_components/appearance-settings.tsx
index 6c04f00..aaf499a 100644
--- a/apps/web/app/reader/settings/_components/appearance-settings.tsx
+++ b/apps/web/app/reader/settings/_components/appearance-settings.tsx
@@ -49,6 +49,12 @@ export function AppearanceSettings() {
const setShowReadingTime = useUserInterfaceStore(
(state) => state.setShowReadingTime
)
+ const showFoldersAboveFeeds = useUserInterfaceStore(
+ (state) => state.showFoldersAboveFeeds
+ )
+ const setShowFoldersAboveFeeds = useUserInterfaceStore(
+ (state) => state.setShowFoldersAboveFeeds
+ )
return (
<div className="px-4 py-3">
<div className="mb-6">
@@ -188,6 +194,21 @@ export function AppearanceSettings() {
<span>show entry images</span>
</label>
</div>
+ <div className="mb-6">
+ <h3 className="mb-2 text-text-primary">sidebar ordering</h3>
+ <p className="mb-3 text-text-dim">
+ display folders above ungrouped feeds in the sidebar
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showFoldersAboveFeeds}
+ onChange={(event) => setShowFoldersAboveFeeds(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show folders above feeds</span>
+ </label>
+ </div>
<div>
<h3 className="mb-2 text-text-primary">reading time</h3>
<p className="mb-3 text-text-dim">