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 <tukon479@gmail.com>
This commit is contained in:
Innei 2025-11-12 21:00:29 +08:00
parent f40e81cdca
commit 2ed3eae425
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 374 additions and 204 deletions

View File

@ -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 (
<div className="flex w-[65ch] max-w-full flex-col gap-4">
<p className="text-text">
{t("sidebar.category_unsubscribe_dialog.description", {
category,
count,
})}
</p>
<div className="flex items-center justify-end gap-3">
<Button variant="outline" onClick={dismiss}>
{t("sidebar.category_unsubscribe_dialog.cancel")}
</Button>
<Button
isLoading={unsubscribeMutation.isPending}
disabled={count === 0}
onClick={() =>
unsubscribeMutation.mutateAsync(feedIds).then(() => {
dismiss()
})
}
>
{t("sidebar.category_unsubscribe_dialog.confirm", { count })}
</Button>
</div>
</div>
)
}

View File

@ -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: () => <CategoryRemoveDialogContent category={folderName!} view={view} />,
})
},
}),
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: () => (
<CategoryUnsubscribeDialogContent category={folderName!} view={view} />
),
})
},
}),
],
e,
)
@ -385,165 +394,3 @@ export const FeedCategoryAutoHideUnread = memo(function FeedCategoryAutoHideUnre
}
return <FeedCategoryImpl {...props} />
})
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<HTMLFormElement | null>(null)
const [isFocused, setIsFocused] = useState(false)
useOnClickOutside(
formRef as React.RefObject<HTMLElement>,
() => {
onFinished()
},
"mousedown",
)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
nextFrame(() => {
inputRef.current?.focus()
setIsFocused(true)
})
}, [])
const compositionInputProps = useInputComposition({
onKeyDown: (e) => {
if (e.key === "Escape") {
onFinished()
}
},
})
return (
<div className="relative ml-3 flex h-8 w-full items-center">
<ShrinkingFocusBorder isVisible={isFocused} containerRef={inputRef} persistBorder />
<form
className="flex w-full items-center"
onSubmit={(e) => {
e.preventDefault()
return renameMutation.mutateAsync({
lastCategory: currentCategory!,
newCategory: e.currentTarget.category.value,
})
}}
>
<input
{...compositionInputProps}
ref={inputRef}
name="category"
autoFocus
defaultValue={currentCategory}
className="w-full appearance-none bg-transparent px-2 py-1 caret-accent"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<MotionButtonBase
type="submit"
className="center -mr-1 flex size-5 shrink-0 rounded-lg text-green hover:bg-material-ultra-thick"
>
<i className="i-mgc-check-filled size-3" />
</MotionButtonBase>
</form>
</div>
)
}
type SortListProps = {
ids: string[]
view: FeedViewType
showCollapse: boolean
}
const SortedFeedItems = memo((props: SortListProps) => {
const by = useFeedListSortSelector((s) => s.by)
switch (by) {
case "count": {
return <SortByUnreadList {...props} />
}
case "alphabetical": {
return <SortByAlphabeticalList {...props} />
}
default: {
return <SortByUnreadList {...props} />
}
}
})
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 (
<Fragment>
{sortedFeedList.map((feedId) => (
<FeedItemAutoHideUnread
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}
const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => {
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
const sortByUnreadFeedList = useSortedIdsByUnread(ids, isDesc)
return (
<Fragment>
{sortByUnreadFeedList.map((feedId) => (
<FeedItemAutoHideUnread
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}

View File

@ -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<HTMLFormElement | null>(null)
const [isFocused, setIsFocused] = useState(false)
useOnClickOutside(
formRef as React.RefObject<HTMLElement>,
() => {
onFinished()
},
"mousedown",
)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
nextFrame(() => {
inputRef.current?.focus()
setIsFocused(true)
})
}, [])
const compositionInputProps = useInputComposition({
onKeyDown: (e) => {
if (e.key === "Escape") {
onFinished()
}
},
})
return (
<div className="relative ml-3 flex h-8 w-full items-center">
<ShrinkingFocusBorder isVisible={isFocused} containerRef={inputRef} persistBorder />
<form
ref={formRef}
className="flex w-full items-center"
onSubmit={(e) => {
e.preventDefault()
return renameMutation.mutateAsync({
lastCategory: currentCategory!,
newCategory: e.currentTarget.category.value,
})
}}
>
<input
{...compositionInputProps}
ref={inputRef}
name="category"
autoFocus
defaultValue={currentCategory}
className="w-full appearance-none bg-transparent px-2 py-1 caret-accent"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<MotionButtonBase
type="submit"
className="center -mr-1 flex size-5 shrink-0 rounded-lg text-green hover:bg-material-ultra-thick"
>
<i className="i-mgc-check-filled size-3" />
</MotionButtonBase>
</form>
</div>
)
}

View File

@ -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 <SortByUnreadList {...props} />
}
case "alphabetical": {
return <SortByAlphabeticalList {...props} />
}
default: {
return <SortByUnreadList {...props} />
}
}
})
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 (
<Fragment>
{sortedFeedList.map((feedId) => (
<FeedItemAutoHideUnread
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}
const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => {
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
const sortByUnreadFeedList = useSortedIdsByUnread(ids, isDesc)
return (
<Fragment>
{sortByUnreadFeedList.map((feedId) => (
<FeedItemAutoHideUnread
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}

View File

@ -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",

View File

@ -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": "このビューを非表示にする",

View File

@ -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": "隐藏此视图",

View File

@ -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": "登入",