diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx index 073e5b15..4005cc2e 100644 --- a/frontend/src/components/ChannelPage.tsx +++ b/frontend/src/components/ChannelPage.tsx @@ -120,7 +120,9 @@ export default function ChannelPage({ qc.invalidateQueries({ queryKey: ["feed"] }); qc.invalidateQueries({ queryKey: ["blocked-channels"] }); }, - onError: (err) => notifyYouTubeActionError(err, t("channels.notify.blockFailed")), + // No onError: block/unblock is a local DB op (not a YouTube write, so no 403 "connect" case), + // and its 400/409/422/500 are already surfaced by the global error modal (see api.ts req) — + // a caller toast here would just double it. }); const subscribe = useMutation({ diff --git a/frontend/src/components/DownloadCenter.tsx b/frontend/src/components/DownloadCenter.tsx index 057ae59b..8e3f91ac 100644 --- a/frontend/src/components/DownloadCenter.tsx +++ b/frontend/src/components/DownloadCenter.tsx @@ -681,7 +681,8 @@ export default function DownloadCenter() { ? api.cancelDownload(id) : api.deleteDownload(id), onSuccess: invalidate, - onError: () => notify({ level: "error", message: t("downloads.actionFailed") }), + // No onError: a failed job action returns 400/409/422/500, already surfaced by the global + // error modal (api.ts req) with the backend's specific message — a caller toast would double it. }); // Remove a shared-with-me item from your list (only your grant; the owner's file is untouched). diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index f0a5d04f..58604c9d 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -533,14 +533,15 @@ function PlexWatchSync() { } }; - const onFail = () => notify({ level: "error", message: t("settings.plexSync.failed") }); + // No onError on these: they're Plex, not YouTube (notifyYouTubeActionError would wrongly say + // "connect YouTube"), and their 409/500 are already surfaced by the global error modal — a caller + // toast would double it. Actual import failures come back as 200 {error}, handled by announce. const toggle = useMutation({ mutationFn: (next: boolean) => api.plexWatchSetLink(next), onSuccess: (res) => { qc.setQueryData(["plex-watch-link"], res); announce(res.import); }, - onError: onFail, }); const reimport = useMutation({ mutationFn: () => api.plexWatchImport(), @@ -548,7 +549,6 @@ function PlexWatchSync() { qc.setQueryData(["plex-watch-link"], res); announce(res.import); }, - onError: onFail, }); const busy = toggle.isPending || reimport.isPending; diff --git a/frontend/src/components/Toaster.tsx b/frontend/src/components/Toaster.tsx index bf0e3c54..02a6be86 100644 --- a/frontend/src/components/Toaster.tsx +++ b/frontend/src/components/Toaster.tsx @@ -16,17 +16,23 @@ export default function Toaster() { const toasts = useSyncExternalStore(subscribe, getActiveToasts, getActiveToasts); return ( -
+ // Stable, always-mounted polite live region — a polite region must PRE-EXIST in the DOM for a + // screen reader to announce content later inserted into it (a region created together with its + // first message is unreliably announced). Non-error toasts render inside here and are announced + // politely; error/fatal toasts additionally carry role="alert", which announces assertively on + // insertion regardless. +
{toasts.map((toast) => { const { icon: Icon, color, bar } = LEVEL_STYLE[toast.level]; const urgent = toast.level === "error" || toast.level === "fatal"; return (
diff --git a/frontend/src/i18n/locales/en/channels.json b/frontend/src/i18n/locales/en/channels.json index 07d1a6e7..bfedd362 100644 --- a/frontend/src/i18n/locales/en/channels.json +++ b/frontend/src/i18n/locales/en/channels.json @@ -119,7 +119,6 @@ "syncFailed": "Subscription sync failed", "unsubscribed": "Unsubscribed from {{name}} on YouTube", "unsubscribeFailed": "Unsubscribe failed", - "blockFailed": "Couldn't update block state", "fullHistoryRequested": "Full history requested for {{count}} channels", "fullHistoryFailed": "Couldn't request full history", "needYouTube": "Connect your YouTube account to do this.", diff --git a/frontend/src/i18n/locales/en/downloads.json b/frontend/src/i18n/locales/en/downloads.json index 74b96bf9..896007fd 100644 --- a/frontend/src/i18n/locales/en/downloads.json +++ b/frontend/src/i18n/locales/en/downloads.json @@ -197,6 +197,5 @@ "source_not_ready": "The source download isn't ready to edit.", "generic": "That edit couldn't be created." } - }, - "actionFailed": "Download action failed" + } } diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 15f31bd1..0ce4a0ea 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -14,8 +14,7 @@ "imported": "Imported from Plex: {{watched}} watched, {{in_progress}} in progress.", "lastSync": "Last import: {{when}}", "never": "Not imported yet", - "importing": "Importing your Plex watch history…", - "failed": "Plex watch sync failed" + "importing": "Importing your Plex watch history…" }, "save": { "saving": "Saving…", diff --git a/frontend/src/i18n/locales/hu/channels.json b/frontend/src/i18n/locales/hu/channels.json index efe120eb..5a236e6e 100644 --- a/frontend/src/i18n/locales/hu/channels.json +++ b/frontend/src/i18n/locales/hu/channels.json @@ -119,7 +119,6 @@ "syncFailed": "A feliratkozások szinkronizálása sikertelen", "unsubscribed": "Leiratkozva a(z) {{name}} csatornáról a YouTube-on", "unsubscribeFailed": "A leiratkozás sikertelen", - "blockFailed": "A tiltás állapotát nem sikerült frissíteni", "fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához", "fullHistoryFailed": "Nem sikerült teljes előzményt kérni", "needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod.", diff --git a/frontend/src/i18n/locales/hu/downloads.json b/frontend/src/i18n/locales/hu/downloads.json index ac7df9a3..61cf56bc 100644 --- a/frontend/src/i18n/locales/hu/downloads.json +++ b/frontend/src/i18n/locales/hu/downloads.json @@ -197,6 +197,5 @@ "source_not_ready": "A forrásletöltés még nem szerkeszthető.", "generic": "Ezt a vágást nem sikerült létrehozni." } - }, - "actionFailed": "A letöltési művelet sikertelen" + } } diff --git a/frontend/src/i18n/locales/hu/settings.json b/frontend/src/i18n/locales/hu/settings.json index 854b5de3..582a787f 100644 --- a/frontend/src/i18n/locales/hu/settings.json +++ b/frontend/src/i18n/locales/hu/settings.json @@ -14,8 +14,7 @@ "imported": "Importálva a Plexből: {{watched}} megnézve, {{in_progress}} folyamatban.", "lastSync": "Utolsó import: {{when}}", "never": "Még nincs importálva", - "importing": "Plex-előzmény importálása…", - "failed": "A Plex-megtekintés szinkron sikertelen" + "importing": "Plex-előzmény importálása…" }, "save": { "saving": "Mentés…",