diff --git a/src/locales/de-DE/settings.json b/src/locales/de-DE/settings.json index 3b6e562d8..d9f7b3ddb 100644 --- a/src/locales/de-DE/settings.json +++ b/src/locales/de-DE/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Favicon-Dienst", "favicon_service_desc": "Dienst zum Abrufen von Website-Symbolen auswählen", + "favicon_service_none": "Deaktiviert", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/en-US/settings.json b/src/locales/en-US/settings.json index 509d573ba..acff52b63 100644 --- a/src/locales/en-US/settings.json +++ b/src/locales/en-US/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Favicon Service", "favicon_service_desc": "Choose the service for fetching website icons", + "favicon_service_none": "Disabled", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/ja-JP/settings.json b/src/locales/ja-JP/settings.json index b18950e97..f212a11a1 100644 --- a/src/locales/ja-JP/settings.json +++ b/src/locales/ja-JP/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Favicon サービス", "favicon_service_desc": "ウェブサイトアイコンの取得サービスを選択", + "favicon_service_none": "無効", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/ko-KR/settings.json b/src/locales/ko-KR/settings.json index cef9fc167..ab94b9b1c 100644 --- a/src/locales/ko-KR/settings.json +++ b/src/locales/ko-KR/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "파비콘 서비스", "favicon_service_desc": "웹사이트 아이콘을 가져올 서비스를 선택하세요", + "favicon_service_none": "비활성화", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/pt-BR/settings.json b/src/locales/pt-BR/settings.json index 9f4004db9..104625aff 100644 --- a/src/locales/pt-BR/settings.json +++ b/src/locales/pt-BR/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Serviço de favicon", "favicon_service_desc": "Escolha o serviço para buscar os ícones dos sites", + "favicon_service_none": "Desativado", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/ru-RU/settings.json b/src/locales/ru-RU/settings.json index 156dbb7d4..1cbd5eb2d 100644 --- a/src/locales/ru-RU/settings.json +++ b/src/locales/ru-RU/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Сервис Favicon", "favicon_service_desc": "Выберите сервис для получения значков сайтов", + "favicon_service_none": "Выключено", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/tr-TR/settings.json b/src/locales/tr-TR/settings.json index 10fe021da..73a396c8e 100644 --- a/src/locales/tr-TR/settings.json +++ b/src/locales/tr-TR/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Favicon Hizmeti", "favicon_service_desc": "Web sitesi simgelerini almak için hizmeti seçin", + "favicon_service_none": "Devre Dışı", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/vi-VN/settings.json b/src/locales/vi-VN/settings.json index 44c521d36..bb4f0d8da 100644 --- a/src/locales/vi-VN/settings.json +++ b/src/locales/vi-VN/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "Dịch vụ Favicon", "favicon_service_desc": "Chọn dịch vụ để lấy biểu tượng trang web", + "favicon_service_none": "Đã tắt", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/zh-CN/settings.json b/src/locales/zh-CN/settings.json index af451511b..660aa1650 100644 --- a/src/locales/zh-CN/settings.json +++ b/src/locales/zh-CN/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "图标服务", "favicon_service_desc": "选择获取网站图标的服务", + "favicon_service_none": "禁用", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/locales/zh-TW/settings.json b/src/locales/zh-TW/settings.json index 361f57576..edbd54d54 100644 --- a/src/locales/zh-TW/settings.json +++ b/src/locales/zh-TW/settings.json @@ -124,6 +124,7 @@ }, "favicon_service": "圖示服務", "favicon_service_desc": "選擇取得網站圖示的服務", + "favicon_service_none": "停用", "favicon_service_scriptcat": "ScriptCat", "favicon_service_google": "Google", "favicon_service_duckduckgo": "DuckDuckGo", diff --git a/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx b/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx index dcfe1a62d..5c20a558e 100644 --- a/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx +++ b/src/pages/options/routes/Setting/sections/InterfaceSection.test.tsx @@ -34,6 +34,13 @@ describe("界面分区-图标服务", () => { await screen.findByText("扩展图标徽标"); expect(screen.getAllByText("扩展图标徽标")).toHaveLength(1); }); + + // 图标服务可整体关闭:选中 none 时下拉框需回显「禁用」,否则用户无从选择这一档 + it("图标服务为 none 时下拉框回显禁用", async () => { + get.mockImplementation((key: string) => Promise.resolve(key === "favicon_service" ? "none" : "")); + render( () => {}} />); + expect(await screen.findByText("禁用")).toBeInTheDocument(); + }); }); describe("界面分区-popup 布局", () => { diff --git a/src/pages/options/routes/Setting/sections/InterfaceSection.tsx b/src/pages/options/routes/Setting/sections/InterfaceSection.tsx index 544612dda..0288ff09d 100644 --- a/src/pages/options/routes/Setting/sections/InterfaceSection.tsx +++ b/src/pages/options/routes/Setting/sections/InterfaceSection.tsx @@ -77,6 +77,7 @@ export function InterfaceSection({ register }: { register: (id: string) => (el: + {t("settings:favicon_service_none")} {t("settings:favicon_service_scriptcat")} {t("settings:favicon_service_google")} {t("settings:favicon_service_duckduckgo")} diff --git a/src/pages/options/routes/SubscribeList/components.test.tsx b/src/pages/options/routes/SubscribeList/components.test.tsx new file mode 100644 index 000000000..0cbf390a1 --- /dev/null +++ b/src/pages/options/routes/SubscribeList/components.test.tsx @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, waitFor } from "@testing-library/react"; + +const { get } = vi.hoisted(() => ({ get: vi.fn() })); +vi.mock("@App/pages/store/global", async () => { + const { createGlobalStoreMock } = await import("@Tests/mocks/pageStores.ts"); + return createGlobalStoreMock({ systemConfig: { get } }); +}); + +vi.mock("@App/pages/store/features/subscribe", () => ({ + requestCheckSubscribeUpdate: vi.fn(() => Promise.resolve(false)), +})); + +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { TooltipProvider } from "@App/pages/components/ui/tooltip"; +import { PermissionFavicons } from "./components"; + +beforeAll(() => { + initTestLanguage("zh-CN"); +}); + +afterEach(() => { + cleanup(); + get.mockReset(); +}); + +const renderFavicons = () => + render( + + + + ); + +describe("订阅 @connect 域名图标", () => { + it("图标服务可用时以站点 favicon 呈现", async () => { + get.mockResolvedValue("scriptcat"); + renderFavicons(); + + const img = await screen.findByAltText("example.com"); + expect(img).toHaveAttribute("src", "https://example.com/favicon.ico"); + }); + + it("图标服务禁用时不向站点请求 favicon", async () => { + get.mockResolvedValue("none"); + const { container } = renderFavicons(); + + // 配置读取是异步的:等到读取完成后仍不能出现 ,否则请求早已发出 + await waitFor(() => expect(get).toHaveBeenCalledWith("favicon_service")); + expect(container.querySelector("img")).toBeNull(); + }); +}); diff --git a/src/pages/options/routes/SubscribeList/components.tsx b/src/pages/options/routes/SubscribeList/components.tsx index 061864825..f5cd66570 100644 --- a/src/pages/options/routes/SubscribeList/components.tsx +++ b/src/pages/options/routes/SubscribeList/components.tsx @@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next"; import { cn } from "@App/pkg/utils/cn"; import { notify } from "@App/pages/components/ui/toast"; import { NameAvatar } from "@App/pages/components/NameAvatar"; +import { useSystemConfig } from "@App/pages/options/hooks/useSystemConfig"; import { Globe, RefreshCw, Rss, Trash2, CircleArrowUp, Check, Loader2, Link } from "lucide-react"; // ========== SubscribeIcon ========== @@ -45,12 +46,12 @@ SubscribeEnableSwitch.displayName = "SubscribeEnableSwitch"; // ========== PermissionFavicons ========== // @connect 域名以站点 favicon 呈现,加载失败回退到地球图标 -function DomainFavicon({ domain }: { domain: string }) { +function DomainFavicon({ domain, showIcon }: { domain: string; showIcon: boolean }) { const [imgError, setImgError] = useState(false); return ( - {imgError ? ( + {!showIcon || imgError ? ( ) : ( { + const [faviconService] = useSystemConfig("favicon_service"); + // 配置读取完成前一律先显示占位: 一旦挂载请求就已发出,关闭图标获取后再撤下已经晚了 + const showIcon = !!faviconService && faviconService !== "none"; if (!connect || connect.length === 0) return {"-"}; const visible = connect.slice(0, maxShow); const extra = connect.length - maxShow; return (
{visible.map((domain) => ( - + ))} {extra > 0 && {`+${extra}`}}
diff --git a/src/pages/store/favicons.test.ts b/src/pages/store/favicons.test.ts index 75c04442b..ce157a52f 100644 --- a/src/pages/store/favicons.test.ts +++ b/src/pages/store/favicons.test.ts @@ -4,7 +4,9 @@ import { parseFaviconsNew, fetchIconByService, fetchIconByDomain, + loadScriptFavicons, } from "@App/pages/store/favicons"; +import type { Script } from "@App/app/repo/scripts"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; describe("extractFaviconsDomain", () => { @@ -172,9 +174,38 @@ describe("fetchIconByService", () => { ]); }); + it("none 服务不应返回任何图标地址", async () => { + const result = await fetchIconByService("example.com", "none"); + expect(result).toEqual([]); + expect(fetch).not.toHaveBeenCalled(); + }); + // local 服务的具体行为已在 fetchIconByDomain 测试中充分覆盖 }); +describe("loadScriptFavicons", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("图标服务设为 none 时不产出图标,也不发起任何请求", async () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const scripts = [ + { uuid: "uuid-1", metadata: { match: ["https://example.com/*"] } }, + { uuid: "uuid-2", metadata: { match: ["https://another.com/*"] } }, + ] as unknown as Script[]; + + const chunks = []; + for await (const chunk of loadScriptFavicons(scripts, "none")) { + chunks.push(chunk); + } + + expect(chunks).toEqual([]); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); + describe("fetchIconByDomain", () => { afterEach(() => { vi.restoreAllMocks(); diff --git a/src/pages/store/favicons.ts b/src/pages/store/favicons.ts index 95c599695..229a60290 100644 --- a/src/pages/store/favicons.ts +++ b/src/pages/store/favicons.ts @@ -187,6 +187,10 @@ export async function fetchIconByDomain(domain: string): Promise { */ export async function fetchIconByService(domain: string, service: FaviconService): Promise { switch (service) { + case "none": + // 用户关闭了图标获取,不产生任何外部请求 + return []; + case "scriptcat": /** * ScriptCat 图标服务 @@ -382,6 +386,8 @@ type TFaviconStack = { chunkResults: FavIconResult[]; pendingCount: number }; // 处理favicon加载,以批次方式处理 export const loadScriptFavicons = async function* (scripts: Script[], service: FaviconService = "scriptcat") { + // 关闭图标获取时直接结束:不读写 favicon 缓存,也不发起任何请求 + if (service === "none") return; const stack: TFaviconStack[] = []; const asyncWaiter: { promise?: any; resolve?: any } = {}; const createPromise = () => { diff --git a/src/pkg/config/config.ts b/src/pkg/config/config.ts index 026ee2290..fb50f668e 100644 --- a/src/pkg/config/config.ts +++ b/src/pkg/config/config.ts @@ -37,7 +37,8 @@ export const DEFAULT_CLOUD_SYNC_STATE: CloudSyncState = { counts: { total: 0, overwrite: 0, conflict: 0, failed: 0 }, }; -export type FaviconService = "scriptcat" | "google" | "duckduckgo" | "icon-horse" | "local"; +// "none" 表示彻底关闭网站图标获取:不向任何图标服务或目标站点发起请求 +export type FaviconService = "none" | "scriptcat" | "google" | "duckduckgo" | "icon-horse" | "local"; export type CATFileStorage = { filesystem: FileSystemType;