From 2ed3eae425560f6633e6eb65b20690a959d8d201 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 12 Nov 2025 21:00:29 +0800 Subject: [PATCH] feat(subscription): add unsubscribe and rename category functionality - Introduced CategoryUnsubscribeDialogContent for unsubscribing from feeds within a category. - Added RenameCategoryForm for renaming categories. - Updated FeedCategory to include options for unsubscribing and renaming categories in the context menu. - Enhanced localization files to support new dialog messages for unsubscribe and rename actions. Signed-off-by: Innei --- .../CategoryUnsubscribeDialogContent.tsx | 65 ++++++ .../subscription-column/FeedCategory.tsx | 199 ++---------------- .../RenameCategoryForm.tsx | 110 ++++++++++ .../subscription-column/SortedFeedItems.tsx | 84 ++++++++ locales/app/en.json | 30 ++- locales/app/ja.json | 30 ++- locales/app/zh-CN.json | 30 ++- locales/app/zh-TW.json | 30 ++- 8 files changed, 374 insertions(+), 204 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/subscription-column/CategoryUnsubscribeDialogContent.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/subscription-column/RenameCategoryForm.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/subscription-column/SortedFeedItems.tsx diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryUnsubscribeDialogContent.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryUnsubscribeDialogContent.tsx new file mode 100644 index 000000000..1833ef478 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryUnsubscribeDialogContent.tsx @@ -0,0 +1,65 @@ +import { Button } from "@follow/components/ui/button/index.js" +import type { FeedViewType } from "@follow/constants" +import { getCategoryFeedIds } from "@follow/store/subscription/getter" +import { subscriptionSyncService } from "@follow/store/subscription/store" +import { useMutation } from "@tanstack/react-query" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" + +import { createErrorToaster } from "~/lib/error-parser" + +import { useCurrentModal } from "../../components/ui/modal/stacked/hooks" + +export function CategoryUnsubscribeDialogContent({ + category, + view, +}: { + category: string + view: FeedViewType +}) { + const { t } = useTranslation() + const feedIds = useMemo(() => getCategoryFeedIds(category, view), [category, view]) + const count = feedIds.length + + const unsubscribeMutation = useMutation({ + mutationFn: async (ids: string[]) => { + if (ids.length === 0) return + await subscriptionSyncService.unsubscribe(ids) + }, + onError: createErrorToaster(t("sidebar.category_unsubscribe_dialog.error")), + onSuccess: () => { + toast.success(t("sidebar.category_unsubscribe_dialog.success", { count, category })) + }, + }) + + const { dismiss } = useCurrentModal() + + return ( +
+

+ {t("sidebar.category_unsubscribe_dialog.description", { + category, + count, + })} +

+ +
+ + +
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx index 3722666e7..b82d5c0f1 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx @@ -3,11 +3,9 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" import { MotionButtonBase } from "@follow/components/ui/button/index.js" import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js" -import { ShrinkingFocusBorder } from "@follow/components/ui/shrinking-focus-border/index.js" import type { FeedViewType } from "@follow/constants" import { getViewList } from "@follow/constants" -import { useInputComposition, useRefValue } from "@follow/hooks" -import { useFeedStore } from "@follow/store/feed/store" +import { useRefValue } from "@follow/hooks" import { useOwnedListByView } from "@follow/store/list/hooks" import { useSubscriptionByFeedId, @@ -17,15 +15,13 @@ import { subscriptionActions, subscriptionSyncService } from "@follow/store/subs import { getDefaultCategory } from "@follow/store/subscription/utils" import { useSortedIdsByUnread, useUnreadByIds } from "@follow/store/unread/hooks" import { unreadSyncService } from "@follow/store/unread/store" -import { nextFrame, stopPropagation } from "@follow/utils/dom" -import { cn, sortByAlphabet } from "@follow/utils/utils" +import { stopPropagation } from "@follow/utils/dom" +import { cn } from "@follow/utils/utils" import { useMutation } from "@tanstack/react-query" import { AnimatePresence, m } from "motion/react" -import type { FC } from "react" import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next" -import { toast } from "sonner" -import { useEventCallback, useOnClickOutside } from "usehooks-ts" +import { useEventCallback } from "usehooks-ts" import type { MenuItemInput } from "~/atoms/context-menu" import { MenuItemSeparator, MenuItemText, useShowContextMenu } from "~/atoms/context-menu" @@ -35,14 +31,13 @@ import { useAddFeedToFeedList } from "~/hooks/biz/useFeedActions" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useContextMenu } from "~/hooks/common/useContextMenu" -import { createErrorToaster } from "~/lib/error-parser" -import { getPreferredTitle } from "~/store/feed/hooks" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { ListCreationModalContent } from "../settings/tabs/lists/modals" -import { useFeedListSortSelector } from "./atom" import { CategoryRemoveDialogContent } from "./CategoryRemoveDialogContent" -import { FeedItemAutoHideUnread } from "./FeedItem" +import { CategoryUnsubscribeDialogContent } from "./CategoryUnsubscribeDialogContent" +import { RenameCategoryForm } from "./RenameCategoryForm" +import { SortedFeedItems } from "./SortedFeedItems" import { feedColumnStyles } from "./styles" import { UnreadNumber } from "./UnreadNumber" @@ -256,17 +251,31 @@ function FeedCategoryImpl({ }, }), new MenuItemText({ - label: t("sidebar.feed_column.context_menu.delete_category"), + label: t("sidebar.feed_column.context_menu.ungroup_category"), hide: !folderName || isAutoGroupedCategory, click: () => { present({ - title: t("sidebar.feed_column.context_menu.delete_category_confirmation", { + title: t("sidebar.feed_column.context_menu.ungroup_category_confirmation", { folderName, }), content: () => , }) }, }), + new MenuItemText({ + label: t("sidebar.feed_column.context_menu.unsubscribe_category"), + hide: !folderName || isAutoGroupedCategory, + click: () => { + present({ + title: t("sidebar.category_unsubscribe_dialog.title", { + folderName, + }), + content: () => ( + + ), + }) + }, + }), ], e, ) @@ -385,165 +394,3 @@ export const FeedCategoryAutoHideUnread = memo(function FeedCategoryAutoHideUnre } return }) - -const RenameCategoryForm: FC<{ - currentCategory: string - view: FeedViewType - onFinished: () => void -}> = ({ currentCategory, view, onFinished }) => { - const navigate = useNavigateEntry() - const { t } = useTranslation() - const renameMutation = useMutation({ - mutationFn: async ({ - lastCategory, - newCategory, - }: { - lastCategory: string - newCategory: string - }) => subscriptionSyncService.renameCategory({ lastCategory, newCategory, view }), - onMutate({ lastCategory, newCategory }) { - const routeParams = getRouteParams() - - if (routeParams.folderName === lastCategory) { - navigate({ - folderName: newCategory, - }) - } - - onFinished() - }, - onError: createErrorToaster(t("sidebar.feed_column.context_menu.rename_category_error")), - onSuccess: () => { - toast.success(t("sidebar.feed_column.context_menu.rename_category_success")) - }, - }) - const formRef = useRef(null) - const [isFocused, setIsFocused] = useState(false) - - useOnClickOutside( - formRef as React.RefObject, - () => { - onFinished() - }, - "mousedown", - ) - const inputRef = useRef(null) - useEffect(() => { - nextFrame(() => { - inputRef.current?.focus() - setIsFocused(true) - }) - }, []) - const compositionInputProps = useInputComposition({ - onKeyDown: (e) => { - if (e.key === "Escape") { - onFinished() - } - }, - }) - return ( -
- -
{ - e.preventDefault() - - return renameMutation.mutateAsync({ - lastCategory: currentCategory!, - newCategory: e.currentTarget.category.value, - }) - }} - > - setIsFocused(true)} - onBlur={() => setIsFocused(false)} - /> - - - - -
- ) -} - -type SortListProps = { - ids: string[] - view: FeedViewType - showCollapse: boolean -} -const SortedFeedItems = memo((props: SortListProps) => { - const by = useFeedListSortSelector((s) => s.by) - switch (by) { - case "count": { - return - } - case "alphabetical": { - return - } - - default: { - return - } - } -}) - -const SortByAlphabeticalList = (props: SortListProps) => { - const { ids, showCollapse, view } = props - const isDesc = useFeedListSortSelector((s) => s.order === "desc") - const sortedFeedList = useFeedStore( - useCallback( - (state) => { - const res = ids.sort((a, b) => { - const feedTitleA = getPreferredTitle(state.feeds[a]) || "" - const feedTitleB = getPreferredTitle(state.feeds[b]) || "" - return sortByAlphabet(feedTitleA, feedTitleB) - }) - - if (isDesc) { - return res - } - return res.reverse() - }, - [ids, isDesc], - ), - ) - return ( - - {sortedFeedList.map((feedId) => ( - - ))} - - ) -} -const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => { - const isDesc = useFeedListSortSelector((s) => s.order === "desc") - const sortByUnreadFeedList = useSortedIdsByUnread(ids, isDesc) - - return ( - - {sortByUnreadFeedList.map((feedId) => ( - - ))} - - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/RenameCategoryForm.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/RenameCategoryForm.tsx new file mode 100644 index 000000000..2daeb2dd0 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/RenameCategoryForm.tsx @@ -0,0 +1,110 @@ +import { MotionButtonBase } from "@follow/components/ui/button/index.js" +import { ShrinkingFocusBorder } from "@follow/components/ui/shrinking-focus-border/index.js" +import type { FeedViewType } from "@follow/constants" +import { useInputComposition } from "@follow/hooks" +import { subscriptionSyncService } from "@follow/store/subscription/store" +import { nextFrame } from "@follow/utils/dom" +import { useMutation } from "@tanstack/react-query" +import { useEffect, useRef, useState } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" +import { useOnClickOutside } from "usehooks-ts" + +import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" +import { getRouteParams } from "~/hooks/biz/useRouteParams" +import { createErrorToaster } from "~/lib/error-parser" + +export const RenameCategoryForm = ({ + currentCategory, + view, + onFinished, +}: { + currentCategory: string + view: FeedViewType + onFinished: () => void +}) => { + const navigate = useNavigateEntry() + const { t } = useTranslation() + const renameMutation = useMutation({ + mutationFn: async ({ + lastCategory, + newCategory, + }: { + lastCategory: string + newCategory: string + }) => subscriptionSyncService.renameCategory({ lastCategory, newCategory, view }), + onMutate({ lastCategory, newCategory }) { + const routeParams = getRouteParams() + + if (routeParams.folderName === lastCategory) { + navigate({ + folderName: newCategory, + }) + } + + onFinished() + }, + onError: createErrorToaster(t("sidebar.feed_column.context_menu.rename_category_error")), + onSuccess: () => { + toast.success(t("sidebar.feed_column.context_menu.rename_category_success")) + }, + }) + const formRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + + useOnClickOutside( + formRef as React.RefObject, + () => { + onFinished() + }, + "mousedown", + ) + const inputRef = useRef(null) + useEffect(() => { + nextFrame(() => { + inputRef.current?.focus() + setIsFocused(true) + }) + }, []) + const compositionInputProps = useInputComposition({ + onKeyDown: (e) => { + if (e.key === "Escape") { + onFinished() + } + }, + }) + return ( +
+ +
{ + e.preventDefault() + + return renameMutation.mutateAsync({ + lastCategory: currentCategory!, + newCategory: e.currentTarget.category.value, + }) + }} + > + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + /> + + + + +
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/SortedFeedItems.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/SortedFeedItems.tsx new file mode 100644 index 000000000..56a7426e8 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/SortedFeedItems.tsx @@ -0,0 +1,84 @@ +import type { FeedViewType } from "@follow/constants" +import { useFeedStore } from "@follow/store/feed/store" +import { useSortedIdsByUnread } from "@follow/store/unread/hooks" +import { sortByAlphabet } from "@follow/utils/utils" +import { Fragment, memo, useCallback } from "react" + +import { getPreferredTitle } from "~/store/feed/hooks" + +import { useFeedListSortSelector } from "./atom" +import { FeedItemAutoHideUnread } from "./FeedItem" + +type SortListProps = { + ids: string[] + view: FeedViewType + showCollapse: boolean +} + +export const SortedFeedItems = memo((props: SortListProps) => { + const by = useFeedListSortSelector((s) => s.by) + switch (by) { + case "count": { + return + } + case "alphabetical": { + return + } + + default: { + return + } + } +}) + +const SortByAlphabeticalList = (props: SortListProps) => { + const { ids, showCollapse, view } = props + const isDesc = useFeedListSortSelector((s) => s.order === "desc") + const sortedFeedList = useFeedStore( + useCallback( + (state) => { + const res = ids.sort((a, b) => { + const feedTitleA = getPreferredTitle(state.feeds[a]) || "" + const feedTitleB = getPreferredTitle(state.feeds[b]) || "" + return sortByAlphabet(feedTitleA, feedTitleB) + }) + + if (isDesc) { + return res + } + return res.reverse() + }, + [ids, isDesc], + ), + ) + return ( + + {sortedFeedList.map((feedId) => ( + + ))} + + ) +} + +const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => { + const isDesc = useFeedListSortSelector((s) => s.order === "desc") + const sortByUnreadFeedList = useSortedIdsByUnread(ids, isDesc) + + return ( + + {sortByUnreadFeedList.map((feedId) => ( + + ))} + + ) +} diff --git a/locales/app/en.json b/locales/app/en.json index 9201af85b..a76e33a5b 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -353,11 +353,26 @@ "sidebar.add_more_feeds": "Add more feeds", "sidebar.already_on_discover_page": "You're already on the Discover page! Add the content you want to follow to the panel on the right.", "sidebar.category_remove_dialog.cancel": "Cancel", - "sidebar.category_remove_dialog.continue": "Continue", - "sidebar.category_remove_dialog.description": "This operation will delete your category, but the feeds it contains will be retained and grouped by website.", - "sidebar.category_remove_dialog.error": "Failed to remove category", - "sidebar.category_remove_dialog.success": "Category removed successfully", - "sidebar.category_remove_dialog.title": "Remove Category", + "sidebar.category_remove_dialog.continue": "Ungroup", + "sidebar.category_remove_dialog.description": "Feeds in this category will be ungrouped and moved back to their default grouping.", + "sidebar.category_remove_dialog.error": "Failed to ungroup category", + "sidebar.category_remove_dialog.success": "Category ungrouped successfully", + "sidebar.category_remove_dialog.title": "Ungroup Category", + "sidebar.category_unsubscribe_dialog.cancel": "Cancel", + "sidebar.category_unsubscribe_dialog.confirm": "Unsubscribe {{count}} feeds", + "sidebar.category_unsubscribe_dialog.confirm_one": "Unsubscribe 1 feed", + "sidebar.category_unsubscribe_dialog.confirm_other": "Unsubscribe {{count}} feeds", + "sidebar.category_unsubscribe_dialog.confirm_zero": "Unsubscribe 0 feeds", + "sidebar.category_unsubscribe_dialog.description": "You are about to unsubscribe from {{count}} feeds in {{category}}. This action cannot be undone.", + "sidebar.category_unsubscribe_dialog.description_one": "You are about to unsubscribe from the only feed in {{category}}. This action cannot be undone.", + "sidebar.category_unsubscribe_dialog.description_other": "You are about to unsubscribe from {{count}} feeds in {{category}}. This action cannot be undone.", + "sidebar.category_unsubscribe_dialog.description_zero": "There are no feeds in {{category}} to unsubscribe.", + "sidebar.category_unsubscribe_dialog.error": "Failed to unsubscribe from category", + "sidebar.category_unsubscribe_dialog.success": "Unsubscribed from {{count}} feeds in {{category}}.", + "sidebar.category_unsubscribe_dialog.success_one": "Unsubscribed from 1 feed in {{category}}.", + "sidebar.category_unsubscribe_dialog.success_other": "Unsubscribed from {{count}} feeds in {{category}}.", + "sidebar.category_unsubscribe_dialog.success_zero": "No feeds were unsubscribed in {{category}}.", + "sidebar.category_unsubscribe_dialog.title": "Unsubscribe {{folderName}}", "sidebar.feed_actions.claim": "Claim", "sidebar.feed_actions.claim_feed": "Claim Feed", "sidebar.feed_actions.copy_email_address": "Copy Email Address", @@ -393,8 +408,6 @@ "sidebar.feed_column.context_menu.add_feeds_to_list": "Add Feeds to List", "sidebar.feed_column.context_menu.change_to_other_view": "Switch to Another View", "sidebar.feed_column.context_menu.create_category": "New Category", - "sidebar.feed_column.context_menu.delete_category": "Delete Category", - "sidebar.feed_column.context_menu.delete_category_confirmation": "Delete Category {{folderName}}?", "sidebar.feed_column.context_menu.mark_as_read": "Mark as Read", "sidebar.feed_column.context_menu.new_category_modal.category_name": "Category Name", "sidebar.feed_column.context_menu.new_category_modal.create": "Create", @@ -402,6 +415,9 @@ "sidebar.feed_column.context_menu.rename_category_error": "Failed to rename category", "sidebar.feed_column.context_menu.rename_category_success": "Category renamed successfully", "sidebar.feed_column.context_menu.title": "Move to New Category", + "sidebar.feed_column.context_menu.ungroup_category": "Ungroup Category", + "sidebar.feed_column.context_menu.ungroup_category_confirmation": "Ungroup category {{folderName}}?", + "sidebar.feed_column.context_menu.unsubscribe_category": "Unsubscribe All in Category", "sidebar.select_sort_method": "Select a sort method", "sidebar.timeline_tabs.customize": "Customize View Tabs...", "sidebar.timeline_tabs.hide_tab": "Hide this view", diff --git a/locales/app/ja.json b/locales/app/ja.json index ec3c670b1..3b306ee0d 100644 --- a/locales/app/ja.json +++ b/locales/app/ja.json @@ -349,11 +349,26 @@ "sidebar.add_more_feeds": "さらにフィードを追加", "sidebar.already_on_discover_page": "すでに 発見 ページにいます!フォローしたいコンテンツを右側のパネルに追加してください。", "sidebar.category_remove_dialog.cancel": "キャンセル", - "sidebar.category_remove_dialog.continue": "続行", - "sidebar.category_remove_dialog.description": "この操作によりカテゴリーが削除されますが、含まれているフィードは保持され、ウェブサイトごとにグループ化されます。", - "sidebar.category_remove_dialog.error": "カテゴリーの削除に失敗しました", - "sidebar.category_remove_dialog.success": "カテゴリーの削除に成功しました", - "sidebar.category_remove_dialog.title": "カテゴリーを削除", + "sidebar.category_remove_dialog.continue": "グループ解除", + "sidebar.category_remove_dialog.description": "この操作によりカテゴリーのグループが解除され、含まれるフィードは既定のグループに戻ります。", + "sidebar.category_remove_dialog.error": "カテゴリーのグループ解除に失敗しました", + "sidebar.category_remove_dialog.success": "カテゴリーのグループ解除に成功しました", + "sidebar.category_remove_dialog.title": "カテゴリーのグループ解除", + "sidebar.category_unsubscribe_dialog.cancel": "キャンセル", + "sidebar.category_unsubscribe_dialog.confirm": "{{count}} 件のフィードを解除", + "sidebar.category_unsubscribe_dialog.confirm_one": "1 件のフィードを解除", + "sidebar.category_unsubscribe_dialog.confirm_other": "{{count}} 件のフィードを解除", + "sidebar.category_unsubscribe_dialog.confirm_zero": "0 件のフィードを解除", + "sidebar.category_unsubscribe_dialog.description": "この操作によりカテゴリー「{{category}}」内の {{count}} 件のフィードの購読を解除します。この操作は元に戻せません。", + "sidebar.category_unsubscribe_dialog.description_one": "この操作によりカテゴリー「{{category}}」内の 1 件のフィードの購読を解除します。この操作は元に戻せません。", + "sidebar.category_unsubscribe_dialog.description_other": "この操作によりカテゴリー「{{category}}」内の {{count}} 件のフィードの購読を解除します。この操作は元に戻せません。", + "sidebar.category_unsubscribe_dialog.description_zero": "カテゴリー「{{category}}」には解除できるフィードがありません。", + "sidebar.category_unsubscribe_dialog.error": "カテゴリーの購読解除に失敗しました", + "sidebar.category_unsubscribe_dialog.success": "カテゴリー「{{category}}」内の {{count}} 件のフィードを解除しました。", + "sidebar.category_unsubscribe_dialog.success_one": "カテゴリー「{{category}}」内の 1 件のフィードを解除しました。", + "sidebar.category_unsubscribe_dialog.success_other": "カテゴリー「{{category}}」内の {{count}} 件のフィードを解除しました。", + "sidebar.category_unsubscribe_dialog.success_zero": "カテゴリー「{{category}}」で解除されたフィードはありません。", + "sidebar.category_unsubscribe_dialog.title": "「{{folderName}}」の購読を解除", "sidebar.feed_actions.claim": "クレーム", "sidebar.feed_actions.claim_feed": "フィードをクレーム", "sidebar.feed_actions.copy_email_address": "Email アドレスをコピー", @@ -389,8 +404,6 @@ "sidebar.feed_column.context_menu.add_feeds_to_list": "フィードをリストに追加", "sidebar.feed_column.context_menu.change_to_other_view": "他のビューに切り替え", "sidebar.feed_column.context_menu.create_category": "新規カテゴリーを作成", - "sidebar.feed_column.context_menu.delete_category": "カテゴリーを削除", - "sidebar.feed_column.context_menu.delete_category_confirmation": "カテゴリー{{folderName}} を削除しますか?", "sidebar.feed_column.context_menu.mark_as_read": "既読にする", "sidebar.feed_column.context_menu.new_category_modal.category_name": "カテゴリー名", "sidebar.feed_column.context_menu.new_category_modal.create": "作成", @@ -398,6 +411,9 @@ "sidebar.feed_column.context_menu.rename_category_error": "カテゴリーのリネームに失敗しました", "sidebar.feed_column.context_menu.rename_category_success": "カテゴリーのリネームに成功しました", "sidebar.feed_column.context_menu.title": "新規カテゴリーに移動する", + "sidebar.feed_column.context_menu.ungroup_category": "カテゴリーのグループ解除", + "sidebar.feed_column.context_menu.ungroup_category_confirmation": "カテゴリー「{{folderName}}」のグループを解除しますか?", + "sidebar.feed_column.context_menu.unsubscribe_category": "カテゴリー内の購読をすべて解除", "sidebar.select_sort_method": "並べ替え方法を選択", "sidebar.timeline_tabs.customize": "ビュータブをカスタマイズ...", "sidebar.timeline_tabs.hide_tab": "このビューを非表示にする", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index 8b7a274cc..2ef89e38b 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -352,11 +352,26 @@ "sidebar.add_more_feeds": "添加订阅源", "sidebar.already_on_discover_page": "你已位于发现页面!请将想要订阅的内容添加到右侧面板。", "sidebar.category_remove_dialog.cancel": "取消", - "sidebar.category_remove_dialog.continue": "继续", - "sidebar.category_remove_dialog.description": "正在删除分类,操作后仍保留订阅源并按网站域名分组。", - "sidebar.category_remove_dialog.error": "删除分类失败", - "sidebar.category_remove_dialog.success": "分类删除成功", - "sidebar.category_remove_dialog.title": "删除分类", + "sidebar.category_remove_dialog.continue": "取消分组", + "sidebar.category_remove_dialog.description": "此操作会取消当前分类,将其中的订阅恢复为默认分组。", + "sidebar.category_remove_dialog.error": "取消分类分组失败", + "sidebar.category_remove_dialog.success": "分类取消分组成功", + "sidebar.category_remove_dialog.title": "取消分类分组", + "sidebar.category_unsubscribe_dialog.cancel": "取消", + "sidebar.category_unsubscribe_dialog.confirm": "取消订阅 {{count}} 个源", + "sidebar.category_unsubscribe_dialog.confirm_one": "取消订阅 1 个源", + "sidebar.category_unsubscribe_dialog.confirm_other": "取消订阅 {{count}} 个源", + "sidebar.category_unsubscribe_dialog.confirm_zero": "取消订阅 0 个源", + "sidebar.category_unsubscribe_dialog.description": "此操作将取消订阅分类「{{category}}」中的 {{count}} 个源,且无法撤销。", + "sidebar.category_unsubscribe_dialog.description_one": "此操作将取消订阅分类「{{category}}」中的 1 个源,且无法撤销。", + "sidebar.category_unsubscribe_dialog.description_other": "此操作将取消订阅分类「{{category}}」中的 {{count}} 个源,且无法撤销。", + "sidebar.category_unsubscribe_dialog.description_zero": "分类「{{category}}」中没有可取消订阅的源。", + "sidebar.category_unsubscribe_dialog.error": "取消分类订阅失败", + "sidebar.category_unsubscribe_dialog.success": "已取消订阅分类「{{category}}」中的 {{count}} 个源。", + "sidebar.category_unsubscribe_dialog.success_one": "已取消订阅分类「{{category}}」中的 1 个源。", + "sidebar.category_unsubscribe_dialog.success_other": "已取消订阅分类「{{category}}」中的 {{count}} 个源。", + "sidebar.category_unsubscribe_dialog.success_zero": "分类「{{category}}」中没有源被取消订阅。", + "sidebar.category_unsubscribe_dialog.title": "取消订阅 {{folderName}}", "sidebar.feed_actions.claim": "认证", "sidebar.feed_actions.claim_feed": "认证订阅", "sidebar.feed_actions.copy_email_address": "复制邮件地址", @@ -392,8 +407,6 @@ "sidebar.feed_column.context_menu.add_feeds_to_list": "添加订阅源到列表", "sidebar.feed_column.context_menu.change_to_other_view": "更改为其他视图", "sidebar.feed_column.context_menu.create_category": "新分类", - "sidebar.feed_column.context_menu.delete_category": "删除分类", - "sidebar.feed_column.context_menu.delete_category_confirmation": "删除分类 {{folderName}}?", "sidebar.feed_column.context_menu.mark_as_read": "标记为已读", "sidebar.feed_column.context_menu.new_category_modal.category_name": "分类名称", "sidebar.feed_column.context_menu.new_category_modal.create": "创建", @@ -401,6 +414,9 @@ "sidebar.feed_column.context_menu.rename_category_error": "重命名分类失败", "sidebar.feed_column.context_menu.rename_category_success": "分类重命名成功", "sidebar.feed_column.context_menu.title": "移动至新分类", + "sidebar.feed_column.context_menu.ungroup_category": "取消分类分组", + "sidebar.feed_column.context_menu.ungroup_category_confirmation": "取消分类「{{folderName}}」的分组?", + "sidebar.feed_column.context_menu.unsubscribe_category": "取消分类内所有订阅", "sidebar.select_sort_method": "选择排序方法", "sidebar.timeline_tabs.customize": "自定义视图标签...", "sidebar.timeline_tabs.hide_tab": "隐藏此视图", diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json index a9f5e301b..49c230959 100644 --- a/locales/app/zh-TW.json +++ b/locales/app/zh-TW.json @@ -350,11 +350,26 @@ "sidebar.add_more_feeds": "新增 RSS 摘要", "sidebar.already_on_discover_page": "你已位於探索頁面!請將想要訂閱的內容加入右側面板。", "sidebar.category_remove_dialog.cancel": "取消", - "sidebar.category_remove_dialog.continue": "繼續", - "sidebar.category_remove_dialog.description": "正在刪除類別,但仍保留 RSS 摘要並按網站分組。", - "sidebar.category_remove_dialog.error": "刪除類別失敗", - "sidebar.category_remove_dialog.success": "類別刪除成功", - "sidebar.category_remove_dialog.title": "移除類別", + "sidebar.category_remove_dialog.continue": "取消分組", + "sidebar.category_remove_dialog.description": "此操作會取消分類分組,並將其中的訂閱恢復為預設分組。", + "sidebar.category_remove_dialog.error": "取消分類分組失敗", + "sidebar.category_remove_dialog.success": "分類取消分組成功", + "sidebar.category_remove_dialog.title": "取消分類分組", + "sidebar.category_unsubscribe_dialog.cancel": "取消", + "sidebar.category_unsubscribe_dialog.confirm": "取消訂閱 {{count}} 個來源", + "sidebar.category_unsubscribe_dialog.confirm_one": "取消訂閱 1 個來源", + "sidebar.category_unsubscribe_dialog.confirm_other": "取消訂閱 {{count}} 個來源", + "sidebar.category_unsubscribe_dialog.confirm_zero": "取消訂閱 0 個來源", + "sidebar.category_unsubscribe_dialog.description": "此操作將取消訂閱分類「{{category}}」中的 {{count}} 個來源,且無法復原。", + "sidebar.category_unsubscribe_dialog.description_one": "此操作將取消訂閱分類「{{category}}」中的 1 個來源,且無法復原。", + "sidebar.category_unsubscribe_dialog.description_other": "此操作將取消訂閱分類「{{category}}」中的 {{count}} 個來源,且無法復原。", + "sidebar.category_unsubscribe_dialog.description_zero": "分類「{{category}}」中沒有可取消訂閱的來源。", + "sidebar.category_unsubscribe_dialog.error": "取消分類訂閱失敗", + "sidebar.category_unsubscribe_dialog.success": "已取消訂閱分類「{{category}}」中的 {{count}} 個來源。", + "sidebar.category_unsubscribe_dialog.success_one": "已取消訂閱分類「{{category}}」中的 1 個來源。", + "sidebar.category_unsubscribe_dialog.success_other": "已取消訂閱分類「{{category}}」中的 {{count}} 個來源。", + "sidebar.category_unsubscribe_dialog.success_zero": "分類「{{category}}」中沒有來源被取消訂閱。", + "sidebar.category_unsubscribe_dialog.title": "取消訂閱 {{folderName}}", "sidebar.feed_actions.claim": "認領", "sidebar.feed_actions.claim_feed": "認領RSS 摘要", "sidebar.feed_actions.copy_email_address": "複製電子信箱", @@ -390,8 +405,6 @@ "sidebar.feed_column.context_menu.add_feeds_to_list": "新增 RSS 摘要到列表", "sidebar.feed_column.context_menu.change_to_other_view": "切換到其他視圖", "sidebar.feed_column.context_menu.create_category": "新增分類", - "sidebar.feed_column.context_menu.delete_category": "刪除分類", - "sidebar.feed_column.context_menu.delete_category_confirmation": "刪除分類 {{folderName}}?", "sidebar.feed_column.context_menu.mark_as_read": "標記為已讀", "sidebar.feed_column.context_menu.new_category_modal.category_name": "分類名稱", "sidebar.feed_column.context_menu.new_category_modal.create": "創建", @@ -399,6 +412,9 @@ "sidebar.feed_column.context_menu.rename_category_error": "重命名分類失敗", "sidebar.feed_column.context_menu.rename_category_success": "分類重新命名成功", "sidebar.feed_column.context_menu.title": "移動到新分類", + "sidebar.feed_column.context_menu.ungroup_category": "取消分類分組", + "sidebar.feed_column.context_menu.ungroup_category_confirmation": "取消分類「{{folderName}}」的分組?", + "sidebar.feed_column.context_menu.unsubscribe_category": "取消分類內所有訂閱", "sidebar.select_sort_method": "選擇排序方式", "signin.continue_with": "透過 {{provider}} 登入", "signin.sign_in_to": "登入",