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:
parent
f40e81cdca
commit
2ed3eae425
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "このビューを非表示にする",
|
||||
|
|
|
|||
|
|
@ -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": "隐藏此视图",
|
||||
|
|
|
|||
|
|
@ -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": "登入",
|
||||
|
|
|
|||
Loading…
Reference in New Issue