From 2c2cb46deed8fb794440f1f1384fb34819a16bbb Mon Sep 17 00:00:00 2001 From: Kane Blueriver Date: Fri, 23 Oct 2026 12:00:50 +0800 Subject: [PATCH 1/3] feat: quick download from the RSS article context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Quick Download" POSTs the article's torrent/magnet straight to torrents/add with the server's default parameters — no add-torrent dialog round-trip. It counts as reading like the dialog flow and toasts the same "Added successfully" / error as the dialog's submit. --- src/__tests__/e2e/rss.spec.ts | 20 ++++++++++++++++++++ src/i18n/en/rss.ts | 1 + src/i18n/es/rss.ts | 1 + src/i18n/ja/rss.ts | 1 + src/i18n/ko/rss.ts | 1 + src/i18n/pt/rss.ts | 1 + src/i18n/ru/rss.ts | 1 + src/i18n/zh-TW/rss.ts | 1 + src/i18n/zh/rss.ts | 1 + src/pages/rss/ArticleList.tsx | 12 ++++++++++-- src/pages/rss/index.tsx | 19 +++++++++++++++++++ 11 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/__tests__/e2e/rss.spec.ts b/src/__tests__/e2e/rss.spec.ts index 4ba9562..35a4366 100644 --- a/src/__tests__/e2e/rss.spec.ts +++ b/src/__tests__/e2e/rss.spec.ts @@ -92,6 +92,26 @@ test("clicking an article opens the add dialog with article context", async ({ p await expect.poll(() => addBody).toContain("magnet:?xt=v2"); }); +test("quick download adds the torrent directly without the dialog", async ({ page }) => { + let addBody = ""; + await page.route("**/api/v2/torrents/add", (route) => { + addBody = route.request().postData() ?? ""; + route.fulfill({ status: 200, body: "Ok." }); + }); + + await feedRow(page).click(); + // real right-click: a plain Event has button undefined (fixtures README) + await page.getByText("New Release v2.0").evaluate((el) => { + el.dispatchEvent(new MouseEvent("contextmenu", { button: 2, bubbles: true })); + }); + await page.getByRole("menuitem", { name: "Quick Download" }).click(); + // POSTs the magnet straight away with server-default parameters + await expect.poll(() => addBody).toContain("magnet:?xt=v2"); + // the add-torrent dialog never opens, and the flow reports success + await expect(page.getByRole("dialog")).toBeHidden(); + await expect(page.locator("[data-sonner-toast]").getByText("Added successfully")).toBeVisible(); +}); + test("add feed dialog opens", async ({ page }) => { await page.getByRole("button", { name: /Add Feed/i }).click(); await expect(page.getByRole("heading", { name: /Add Feed/i })).toBeVisible(); diff --git a/src/i18n/en/rss.ts b/src/i18n/en/rss.ts index 667f5db..4342842 100644 --- a/src/i18n/en/rss.ts +++ b/src/i18n/en/rss.ts @@ -46,4 +46,5 @@ export default { "Feed failed to refresh — see the server execution log", Refreshing: "Refreshing…", "{name} failed to refresh": "{name} failed to refresh", + "Quick Download": "Quick Download", } satisfies Dictionary; diff --git a/src/i18n/es/rss.ts b/src/i18n/es/rss.ts index 1bc36c4..a1f0430 100644 --- a/src/i18n/es/rss.ts +++ b/src/i18n/es/rss.ts @@ -46,4 +46,5 @@ export default { "El canal no se pudo actualizar — consulta el registro de ejecución del servidor", Refreshing: "Actualizando…", "{name} failed to refresh": "Error al actualizar {name}", + "Quick Download": "Descarga rápida", } satisfies Dictionary; diff --git a/src/i18n/ja/rss.ts b/src/i18n/ja/rss.ts index bb224b6..33a0fe4 100644 --- a/src/i18n/ja/rss.ts +++ b/src/i18n/ja/rss.ts @@ -46,4 +46,5 @@ export default { "フィードの更新に失敗しました — サーバーの実行ログを確認してください", Refreshing: "更新中…", "{name} failed to refresh": "{name} の更新に失敗しました", + "Quick Download": "クイックダウンロード", } satisfies Dictionary; diff --git a/src/i18n/ko/rss.ts b/src/i18n/ko/rss.ts index 09465f8..3177b57 100644 --- a/src/i18n/ko/rss.ts +++ b/src/i18n/ko/rss.ts @@ -46,4 +46,5 @@ export default { "피드 새로 고침 실패 — 서버 실행 로그를 확인하세요", Refreshing: "새로 고치는 중…", "{name} failed to refresh": "{name} 새로 고침 실패", + "Quick Download": "빠른 다운로드", } satisfies Dictionary; diff --git a/src/i18n/pt/rss.ts b/src/i18n/pt/rss.ts index 9619da3..83c5625 100644 --- a/src/i18n/pt/rss.ts +++ b/src/i18n/pt/rss.ts @@ -46,4 +46,5 @@ export default { "O feed falhou ao atualizar — veja o log de execução do servidor", Refreshing: "Atualizando…", "{name} failed to refresh": "Falha ao atualizar {name}", + "Quick Download": "Download rápido", } satisfies Dictionary; diff --git a/src/i18n/ru/rss.ts b/src/i18n/ru/rss.ts index e7a3d71..bfed541 100644 --- a/src/i18n/ru/rss.ts +++ b/src/i18n/ru/rss.ts @@ -46,4 +46,5 @@ export default { "Не удалось обновить подписку — подробности в журнале сервера", Refreshing: "Обновление…", "{name} failed to refresh": "Не удалось обновить {name}", + "Quick Download": "Быстрая загрузка", } satisfies Dictionary; diff --git a/src/i18n/zh-TW/rss.ts b/src/i18n/zh-TW/rss.ts index 4be83d8..549e853 100644 --- a/src/i18n/zh-TW/rss.ts +++ b/src/i18n/zh-TW/rss.ts @@ -46,4 +46,5 @@ export default { "訂閱源重新整理失敗 — 詳情見伺服器執行記錄", Refreshing: "正在重新整理…", "{name} failed to refresh": "{name} 重新整理失敗", + "Quick Download": "快速下載", } satisfies Dictionary; diff --git a/src/i18n/zh/rss.ts b/src/i18n/zh/rss.ts index 6ba408a..fea74ce 100644 --- a/src/i18n/zh/rss.ts +++ b/src/i18n/zh/rss.ts @@ -45,4 +45,5 @@ export default { "Feed failed to refresh — see the server execution log": "订阅源刷新失败 — 详情见服务端执行日志", Refreshing: "正在刷新…", "{name} failed to refresh": "{name} 刷新失败", + "Quick Download": "快速下载", } satisfies Dictionary; diff --git a/src/pages/rss/ArticleList.tsx b/src/pages/rss/ArticleList.tsx index ce8313f..3ede09a 100644 --- a/src/pages/rss/ArticleList.tsx +++ b/src/pages/rss/ArticleList.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, Download, ExternalLink, RefreshCw, Rss } from "lucide-react"; +import { ChevronRight, Download, ExternalLink, RefreshCw, Rss, Zap } from "lucide-react"; import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Shortcut } from "@/components/Shortcut"; @@ -67,13 +67,16 @@ interface ArticleListProps { /** Click / "Download" menu item → open the add-torrent dialog with article * context */ onDownload: (article: RssArticle) => void; + /** "Quick Download" menu item → add with server defaults, no dialog */ + onQuickDownload: (article: RssArticle) => void; /** Refresh just the selected feed */ onRefresh: (path: string) => void; } /** Right column: article table — sortable by title / date / read status. * Click opens the add dialog (with article context); right-click / - * long-press menu offers download or opening the article's webpage. */ + * long-press menu offers download, quick download (server defaults, no + * dialog) or opening the article's webpage. */ export function ArticleList({ selectedFeed, articles, @@ -82,6 +85,7 @@ export function ArticleList({ selectedArticleId, onSelectArticle, onDownload, + onQuickDownload, onRefresh, }: ArticleListProps) { const { t } = useTranslation(); @@ -235,6 +239,10 @@ export function ArticleList({ {t("Download")} + onQuickDownload(article)}> + + {t("Quick Download")} + {article.link && ( window.open(article.link, "_blank")}> diff --git a/src/pages/rss/index.tsx b/src/pages/rss/index.tsx index e81a6b9..d6e621a 100644 --- a/src/pages/rss/index.tsx +++ b/src/pages/rss/index.tsx @@ -225,6 +225,24 @@ export function RssPage() { }); }; + /** Context-menu "Quick Download": add the article's torrent with the + * server's default parameters — no dialog round-trip. Counts as reading + * like the dialog flow does. */ + const quickDownloadArticle = async (article: RssArticle) => { + try { + await qbtClient.addTorrent({ urls: article.torrentURL || article.link }); + toast.success(t("Added successfully")); + if (selectedFeed && !article.isRead) { + qbtClient + .markRssAsRead(selectedFeed, article.id) + .then(() => qc.invalidateQueries({ queryKey: ["rss-feeds"] })) + .catch(() => {}); + } + } catch (e) { + toast.error(e instanceof Error ? e.message : String(e)); + } + }; + /** * qBT's refreshItem is fire-and-forget — it 200s before the fetch even * starts (and silently for an unknown itemPath), so the real outcome only @@ -524,6 +542,7 @@ export function RssPage() { selectedArticleId={selectedArticleId} onSelectArticle={setSelectedArticleId} onDownload={downloadArticle} + onQuickDownload={quickDownloadArticle} onRefresh={refreshOne} /> From 1c7bd7ee5f768dfc13561bbe4e6aaab135acd31a Mon Sep 17 00:00:00 2001 From: Kane Blueriver Date: Fri, 23 Oct 2026 13:51:55 +0800 Subject: [PATCH 2/3] feat: Shift+Enter quick-downloads the keyboard-selected article Enter keeps opening the prefilled add dialog; Shift+Enter takes the Quick Download path (server defaults, no dialog). The hotkey legend on the RSS page gains the new combination in every locale. --- src/__tests__/e2e/rss.spec.ts | 15 +++++++++++++++ src/i18n/en/rss.ts | 2 +- src/i18n/es/rss.ts | 2 +- src/i18n/ja/rss.ts | 2 +- src/i18n/ko/rss.ts | 2 +- src/i18n/pt/rss.ts | 2 +- src/i18n/ru/rss.ts | 2 +- src/i18n/zh-TW/rss.ts | 2 +- src/i18n/zh/rss.ts | 2 +- src/pages/rss/index.tsx | 9 ++++++++- 10 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/__tests__/e2e/rss.spec.ts b/src/__tests__/e2e/rss.spec.ts index 35a4366..bca454a 100644 --- a/src/__tests__/e2e/rss.spec.ts +++ b/src/__tests__/e2e/rss.spec.ts @@ -112,6 +112,21 @@ test("quick download adds the torrent directly without the dialog", async ({ pag await expect(page.locator("[data-sonner-toast]").getByText("Added successfully")).toBeVisible(); }); +test("Shift+Enter on the selected article quick-downloads it", async ({ page }) => { + let addBody = ""; + await page.route("**/api/v2/torrents/add", (route) => { + addBody = route.request().postData() ?? ""; + route.fulfill({ status: 200, body: "Ok." }); + }); + + await feedRow(page).click(); + await page.keyboard.press("j"); // select the first (unread) article + await page.keyboard.press("Shift+Enter"); + await expect.poll(() => addBody).toContain("magnet:?xt=v2"); + // still no dialog — Shift+Enter is the keyboard path of Quick Download + await expect(page.getByRole("dialog")).toBeHidden(); +}); + test("add feed dialog opens", async ({ page }) => { await page.getByRole("button", { name: /Add Feed/i }).click(); await expect(page.getByRole("heading", { name: /Add Feed/i })).toBeVisible(); diff --git a/src/i18n/en/rss.ts b/src/i18n/en/rss.ts index 4342842..734a438 100644 --- a/src/i18n/en/rss.ts +++ b/src/i18n/en/rss.ts @@ -38,7 +38,7 @@ export default { "RSS is disabled on the server — enable it in Settings → RSS", "Copy Feed URL": "Copy Feed URL", "RSS hotkeys hint": - "j/k select article · Shift+J/K switch feed · Enter download · r refresh · Shift+R refresh all", + "j/k select article · Shift+J/K switch feed · Enter download · Shift+Enter quick download · r refresh · Shift+R refresh all", Title: "Title", Date: "Date", Read: "Read", diff --git a/src/i18n/es/rss.ts b/src/i18n/es/rss.ts index a1f0430..ff5519a 100644 --- a/src/i18n/es/rss.ts +++ b/src/i18n/es/rss.ts @@ -38,7 +38,7 @@ export default { "RSS está desactivado en el servidor — actívalo en Ajustes → RSS", "Copy Feed URL": "Copiar URL del canal", "RSS hotkeys hint": - "j/k elegir artículo · Shift+J/K cambiar fuente · Enter descargar · r actualizar · Shift+R actualizar todo", + "j/k elegir artículo · Shift+J/K cambiar fuente · Enter descargar · Shift+Enter descarga rápida · r actualizar · Shift+R actualizar todo", Title: "Título", Date: "Fecha", Read: "Leído", diff --git a/src/i18n/ja/rss.ts b/src/i18n/ja/rss.ts index 33a0fe4..8e1758e 100644 --- a/src/i18n/ja/rss.ts +++ b/src/i18n/ja/rss.ts @@ -38,7 +38,7 @@ export default { "サーバーで RSS が無効です — 設定 → RSS で有効にしてください", "Copy Feed URL": "フィードURLをコピー", "RSS hotkeys hint": - "j/k 記事を選択 · Shift+J/K フィード切替 · Enter ダウンロード · r 更新 · Shift+R すべて更新", + "j/k 記事を選択 · Shift+J/K フィード切替 · Enter ダウンロード · Shift+Enter クイックダウンロード · r 更新 · Shift+R すべて更新", Title: "タイトル", Date: "日付", Read: "既読", diff --git a/src/i18n/ko/rss.ts b/src/i18n/ko/rss.ts index 3177b57..c2735e9 100644 --- a/src/i18n/ko/rss.ts +++ b/src/i18n/ko/rss.ts @@ -38,7 +38,7 @@ export default { "서버에서 RSS가 비활성되어 있습니다 — 설정 → RSS에서 활성화하세요", "Copy Feed URL": "피드 URL 복사", "RSS hotkeys hint": - "j/k 기사 선택 · Shift+J/K 피드 전환 · Enter 다운로드 · r 새로 고침 · Shift+R 모두 새로 고침", + "j/k 기사 선택 · Shift+J/K 피드 전환 · Enter 다운로드 · Shift+Enter 빠른 다운로드 · r 새로 고침 · Shift+R 모두 새로 고침", Title: "제목", Date: "날짜", Read: "읽음", diff --git a/src/i18n/pt/rss.ts b/src/i18n/pt/rss.ts index 83c5625..b41ad3f 100644 --- a/src/i18n/pt/rss.ts +++ b/src/i18n/pt/rss.ts @@ -38,7 +38,7 @@ export default { "RSS está desativado no servidor — ative em Configurações → RSS", "Copy Feed URL": "Copiar URL do feed", "RSS hotkeys hint": - "j/k escolher artigo · Shift+J/K trocar feed · Enter baixar · r atualizar · Shift+R atualizar tudo", + "j/k escolher artigo · Shift+J/K trocar feed · Enter baixar · Shift+Enter download rápido · r atualizar · Shift+R atualizar tudo", Title: "Título", Date: "Data", Read: "Lido", diff --git a/src/i18n/ru/rss.ts b/src/i18n/ru/rss.ts index bfed541..668a9ac 100644 --- a/src/i18n/ru/rss.ts +++ b/src/i18n/ru/rss.ts @@ -38,7 +38,7 @@ export default { "RSS отключён на сервере — включите в Настройки → RSS", "Copy Feed URL": "Копировать ссылку на ленту", "RSS hotkeys hint": - "j/k выбор статьи · Shift+J/K смена ленты · Enter скачать · r обновить · Shift+R обновить все", + "j/k выбор статьи · Shift+J/K смена ленты · Enter скачать · Shift+Enter быстрая загрузка · r обновить · Shift+R обновить все", Title: "Заголовок", Date: "Дата", Read: "Прочитано", diff --git a/src/i18n/zh-TW/rss.ts b/src/i18n/zh-TW/rss.ts index 549e853..fd34aab 100644 --- a/src/i18n/zh-TW/rss.ts +++ b/src/i18n/zh-TW/rss.ts @@ -38,7 +38,7 @@ export default { "RSS 功能已在服務端關閉,請到 設定 → RSS 中開啟", "Copy Feed URL": "複製 RSS 來源網址", "RSS hotkeys hint": - "j/k 選擇文章 · Shift+J/K 切換訂閱源 · Enter 下載 · r 重新整理 · Shift+R 全部重新整理", + "j/k 選擇文章 · Shift+J/K 切換訂閱源 · Enter 下載 · Shift+Enter 快速下載 · r 重新整理 · Shift+R 全部重新整理", Title: "標題", Date: "日期", Read: "已讀", diff --git a/src/i18n/zh/rss.ts b/src/i18n/zh/rss.ts index fea74ce..4ef28f8 100644 --- a/src/i18n/zh/rss.ts +++ b/src/i18n/zh/rss.ts @@ -38,7 +38,7 @@ export default { "RSS 功能已在服务端关闭,请到 设置 → RSS 中开启", "Copy Feed URL": "复制 RSS 源地址", "RSS hotkeys hint": - "j/k 选择文章 · Shift+J/K 切换订阅源 · Enter 下载 · r 刷新 · Shift+R 全部刷新", + "j/k 选择文章 · Shift+J/K 切换订阅源 · Enter 下载 · Shift+Enter 快速下载 · r 刷新 · Shift+R 全部刷新", Title: "标题", Date: "日期", Read: "已读", diff --git a/src/pages/rss/index.tsx b/src/pages/rss/index.tsx index d6e621a..38ceebf 100644 --- a/src/pages/rss/index.tsx +++ b/src/pages/rss/index.tsx @@ -381,6 +381,7 @@ export function RssPage() { refreshOne, refreshAll, downloadArticle, + quickDownloadArticle, sortedArticles, feedPaths: [] as string[], }); @@ -388,6 +389,7 @@ export function RssPage() { refreshOne, refreshAll, downloadArticle, + quickDownloadArticle, sortedArticles, feedPaths: feedOnly.map((f) => f.path), }; @@ -433,7 +435,12 @@ export function RssPage() { } if (e.key === "Enter") { const article = hotkeyRef.current.sortedArticles.find((a) => a.id === selectedArticleId); - if (article) void hotkeyRef.current.downloadArticle(article); + // Enter opens the prefilled add dialog; Shift+Enter quick-downloads + // with server defaults, no dialog + if (article) + void (e.shiftKey + ? hotkeyRef.current.quickDownloadArticle(article) + : hotkeyRef.current.downloadArticle(article)); return; } if (e.key !== "r" && e.key !== "R") return; From 36d2db3bec365e13f822c4c8204b6faf3d0a7d22 Mon Sep 17 00:00:00 2001 From: Kane Blueriver Date: Fri, 23 Oct 2026 13:55:03 +0800 Subject: [PATCH 3/3] fix: persist the RSS article sort across page switches and reloads The article table's sort was component state, so leaving the RSS page or reloading fell it back to date-desc. It now lives in the persistent ui store (qiubi-ui), right beside the torrent table's sorting. --- src/__tests__/e2e/rss.spec.ts | 25 +++++++++++++++++++++++++ src/pages/rss/index.tsx | 13 +++++++------ src/stores/ui.ts | 8 ++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/__tests__/e2e/rss.spec.ts b/src/__tests__/e2e/rss.spec.ts index bca454a..581a23a 100644 --- a/src/__tests__/e2e/rss.spec.ts +++ b/src/__tests__/e2e/rss.spec.ts @@ -127,6 +127,31 @@ test("Shift+Enter on the selected article quick-downloads it", async ({ page }) await expect(page.getByRole("dialog")).toBeHidden(); }); +test("article sort survives switching pages and reloads", async ({ page }) => { + const options = () => page.locator('[role="option"]'); + await feedRow(page).click(); + await expect(options()).toHaveCount(2); + // default date-desc puts New Release first; sorting by title flips it + await expect(options().first()).toContainText("New Release v2.0"); + await page.getByRole("button", { name: "Title" }).click(); + await expect(options().first()).toContainText("Bug Fix v1.9"); + + // leave the RSS page and come back (hash navigation, no reload) + await page.evaluate(() => { + window.location.hash = "#/"; + }); + await page.evaluate(() => { + window.location.hash = "#/rss"; + }); + await expect(options()).toHaveCount(2); + await expect(options().first()).toContainText("Bug Fix v1.9"); + + // the choice also lives in the persisted ui store (survives reloads) + const stored = await page.evaluate(() => localStorage.getItem("qiubi-ui")); + expect(stored).toContain('"articleSort"'); + expect(stored).toContain('"key":"title"'); +}); + test("add feed dialog opens", async ({ page }) => { await page.getByRole("button", { name: /Add Feed/i }).click(); await expect(page.getByRole("heading", { name: /Add Feed/i })).toBeVisible(); diff --git a/src/pages/rss/index.tsx b/src/pages/rss/index.tsx index 38ceebf..9b0b224 100644 --- a/src/pages/rss/index.tsx +++ b/src/pages/rss/index.tsx @@ -20,8 +20,9 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { pollWithBackoff } from "@/hooks/useMainDataSync"; +import { useUiStore } from "@/stores/ui"; import type { AppPreferences, RssArticle, RssFeed } from "@/types/qbt"; -import { ArticleList, type ArticleSort, sortArticles } from "./ArticleList"; +import { ArticleList, sortArticles } from "./ArticleList"; import { FeedTree, flattenFeeds } from "./FeedTree"; /** Apply fn to the map that CONTAINS the feed/folder at `path` — used for @@ -92,11 +93,11 @@ export function RssPage() { const selected = flat.find((x) => x.path === selectedFeed); const selectedArticles: RssArticle[] = selected?.feed.articles ?? []; /** Display order (date-desc by default; the article table headers can - * re-sort) — the exact order ArticleList renders; keyboard nav walks it */ - const [articleSort, setArticleSort] = useState({ - key: "date", - dir: "desc", - }); + * re-sort) — the exact order ArticleList renders; keyboard nav walks it. + * Lives in the persistent ui store so the choice survives page switches + * and reloads, like the torrent table's sorting. */ + const articleSort = useUiStore((s) => s.articleSort); + const setArticleSort = useUiStore((s) => s.setArticleSort); const sortedArticles = useMemo( () => sortArticles(selectedArticles, articleSort), [selectedArticles, articleSort], diff --git a/src/stores/ui.ts b/src/stores/ui.ts index fd96862..99ac931 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,6 +1,7 @@ import type { SortingState } from "@tanstack/react-table"; import { create } from "zustand"; import { persist } from "zustand/middleware"; +import type { ArticleSort } from "@/pages/rss/ArticleList"; type UiState = { /** View menu: show/hide the filter sidebar */ @@ -15,6 +16,9 @@ type UiState = { columnOrder: string[]; /** Torrent list sorting (header click), persisted across reloads */ torrentSorting: SortingState; + /** RSS article table sorting (header click), persisted across reloads and + * page switches — the same ArticleSort the article table sorts by */ + articleSort: ArticleSort; /** Last status filter chosen in the toolbar; a fresh open without ?status= * in the URL seeds the filter from it (explicit URLs always win) */ lastTorrentStatus: string; @@ -33,6 +37,7 @@ type UiState = { setColumnVisibility: (v: Record) => void; setColumnOrder: (order: string[]) => void; setTorrentSorting: (updater: SortingState | ((prev: SortingState) => SortingState)) => void; + setArticleSort: (sort: ArticleSort) => void; setLastTorrentStatus: (status: string) => void; setDetailPanelOpen: (open: boolean) => void; setDetailHash: (hash: string | null) => void; @@ -47,6 +52,7 @@ export const useUiStore = create()( columnVisibility: {}, columnOrder: [], torrentSorting: [], + articleSort: { key: "date", dir: "desc" }, lastTorrentStatus: "all", detailPanelOpen: false, detailHash: null, @@ -67,6 +73,7 @@ export const useUiStore = create()( set((s) => ({ torrentSorting: typeof updater === "function" ? updater(s.torrentSorting) : updater, })), + setArticleSort: (articleSort) => set({ articleSort }), setLastTorrentStatus: (lastTorrentStatus) => set({ lastTorrentStatus }), setDetailPanelOpen: (detailPanelOpen) => set({ detailPanelOpen }), setDetailHash: (detailHash) => set({ detailHash }), @@ -85,6 +92,7 @@ export const useUiStore = create()( columnVisibility: s.columnVisibility, columnOrder: s.columnOrder, torrentSorting: s.torrentSorting, + articleSort: s.articleSort, lastTorrentStatus: s.lastTorrentStatus, detailPanelOpen: s.detailPanelOpen, }),