diff --git a/src/renderer/src/constants/tabs.tsx b/src/renderer/src/constants/tabs.tsx
index 5d04ac0dd..f465ad21d 100644
--- a/src/renderer/src/constants/tabs.tsx
+++ b/src/renderer/src/constants/tabs.tsx
@@ -1,9 +1,12 @@
+import { FeedViewType } from "@renderer/lib/enum"
+
export const views = [
{
name: "Articles",
icon: ,
className: "text-orange-600",
translation: "title,description",
+ view: FeedViewType.Articles,
},
{
name: "Social Media",
@@ -11,6 +14,7 @@ export const views = [
className: "text-sky-600",
wideMode: true,
translation: "description",
+ view: FeedViewType.SocialMedia,
},
{
name: "Pictures",
@@ -19,6 +23,7 @@ export const views = [
gridMode: true,
wideMode: true,
translation: "title",
+ view: FeedViewType.Pictures,
},
{
name: "Videos",
@@ -27,19 +32,23 @@ export const views = [
gridMode: true,
wideMode: true,
translation: "title",
+ view: FeedViewType.Videos,
},
{
name: "Audios",
icon: ,
className: "text-purple-600",
translation: "title",
+ view: FeedViewType.Audios,
},
{
name: "Notifications",
icon: ,
className: "text-yellow-600",
translation: "title",
+ view: FeedViewType.Notifications,
},
+
]
export const settingTabs = [
diff --git a/src/renderer/src/database/db.ts b/src/renderer/src/database/db.ts
index 108168a87..f8ee999b3 100644
--- a/src/renderer/src/database/db.ts
+++ b/src/renderer/src/database/db.ts
@@ -2,7 +2,7 @@ import type { Transaction } from "dexie"
import Dexie from "dexie"
import { LOCAL_DB_NAME } from "./constants"
-import { dbSchemaV1, dbSchemaV2 } from "./db_schema"
+import { dbSchemaV1, dbSchemaV2, dbSchemaV3 } from "./db_schema"
import type { DB_Base } from "./schemas/base"
import type { DB_FeedId } from "./schemas/feed"
import type { DBModel } from "./types"
@@ -30,6 +30,7 @@ export class BrowserDB extends Dexie {
this.version(1).stores(dbSchemaV1)
this.version(2).stores(dbSchemaV2)
.upgrade(this.upgradeToV2)
+ this.version(3).stores(dbSchemaV3)
this.entries = this.table("entries")
this.feeds = this.table("feeds")
diff --git a/src/renderer/src/database/db_schema.ts b/src/renderer/src/database/db_schema.ts
index 8918143d9..b9f653d0c 100644
--- a/src/renderer/src/database/db_schema.ts
+++ b/src/renderer/src/database/db_schema.ts
@@ -11,3 +11,8 @@ export const dbSchemaV2 = {
...dbSchemaV1,
subscriptions: "&id, userId, feedId",
}
+
+export const dbSchemaV3 = {
+ ...dbSchemaV1,
+ subscriptions: "&id, userId, &feedId",
+}
diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx
index 64ebc635d..978d2e466 100644
--- a/src/renderer/src/modules/feed-column/category.tsx
+++ b/src/renderer/src/modules/feed-column/category.tsx
@@ -2,14 +2,20 @@ import {
Collapsible,
CollapsibleTrigger,
} from "@renderer/components/ui/collapsible"
-import { ROUTE_FEED_IN_FOLDER } from "@renderer/constants"
+import { LoadingCircle } from "@renderer/components/ui/loading"
+import { ROUTE_FEED_IN_FOLDER, views } from "@renderer/constants"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
-import { stopPropagation } from "@renderer/lib/dom"
+import { nextFrame, stopPropagation } from "@renderer/lib/dom"
+import type { FeedViewType } from "@renderer/lib/enum"
import { showNativeMenu } from "@renderer/lib/native-menu"
import { cn } from "@renderer/lib/utils"
-import { useSubscriptionByFeedId } from "@renderer/store/subscription"
+import {
+ subscriptionActions,
+ useSubscriptionByFeedId,
+} from "@renderer/store/subscription"
import { useFeedUnreadStore } from "@renderer/store/unread"
+import { useMutation } from "@tanstack/react-query"
import { AnimatePresence, m } from "framer-motion"
import { memo, useEffect, useState } from "react"
@@ -71,6 +77,20 @@ function FeedCategoryImpl({
)
const { present } = useModalStack()
+ const { mutateAsync: changeCategoryView, isPending: isChangePending } =
+ useMutation({
+ mutationKey: ["changeCategoryView", folderName, view],
+ mutationFn: async (nextView: FeedViewType) => {
+ if (!folderName) return
+ if (typeof view !== "number") return
+ return subscriptionActions.changeCategoryView(
+ folderName,
+ view,
+ nextView,
+ )
+ },
+ })
+
return (
+ showNativeMenu(
+ views
+ .filter((v) => v.view !== view)
+ .map((v) => ({
+ label: v.name,
+ type: "text",
+ click() {
+ return changeCategoryView(v.view)
+ },
+ })),
+ e,
+ ),
+ )
+ },
+ },
+ {
+ type: "text",
+ label: "Rename category",
click: () => {
present({
title: "Rename Category",
@@ -110,7 +151,7 @@ function FeedCategoryImpl({
},
{
type: "text",
- label: "Delete Category",
+ label: "Delete category",
click: async () => {
present({
@@ -134,7 +175,11 @@ function FeedCategoryImpl({
)}
tabIndex={-1}
>
-
+ {isChangePending ? (
+
+ ) : (
+
+ )}
{
const useUnreadByView = () => {
useAuthQuery(Queries.subscription.byView())
- const idByView = useSubscriptionStore((state) => state.dataIdByView)
+ const idByView = useSubscriptionStore((state) => state.feedIdByView)
const totalUnread = useFeedUnreadStore((state) => {
const unread = {} as Record
diff --git a/src/renderer/src/services/entry.ts b/src/renderer/src/services/entry.ts
index 70cb6fe7d..097769bf3 100644
--- a/src/renderer/src/services/entry.ts
+++ b/src/renderer/src/services/entry.ts
@@ -1,7 +1,5 @@
import { entryModel } from "@renderer/database/models"
-import type {
- EntryModel,
-} from "@renderer/models/types"
+import type { EntryModel } from "@renderer/models/types"
import { BaseService } from "./base"
import { EntryRelatedKey, EntryRelatedService } from "./entry-related"
@@ -29,6 +27,10 @@ class EntryServiceStatic extends BaseService {
async deleteCollection(entryId: string) {
return EntryRelatedService.deleteItem(EntryRelatedKey.COLLECTION, entryId)
}
+
+ async deleteEntries(entryIds: string[]) {
+ await entryModel.table.bulkDelete(entryIds)
+ }
}
export const EntryService = new EntryServiceStatic()
diff --git a/src/renderer/src/services/subscription.ts b/src/renderer/src/services/subscription.ts
index cf43b68cd..0a9c690df 100644
--- a/src/renderer/src/services/subscription.ts
+++ b/src/renderer/src/services/subscription.ts
@@ -12,7 +12,10 @@ class SubscriptionServiceStatic extends BaseService {
override async upsertMany(data: SubscriptionFlatModel[]) {
return this.table.bulkPut(
- data.map((d) => ({ ...d, id: this.uniqueId(d.userId, d.feedId) })),
+ data.map(({ feeds, ...d }: any) => ({
+ ...d,
+ id: this.uniqueId(d.userId, d.feedId),
+ })),
)
}
@@ -26,6 +29,10 @@ class SubscriptionServiceStatic extends BaseService {
private uniqueId(userId: string, feedId: string) {
return `${userId}/${feedId}`
}
+
+ async changeView(feedId: string, view: number) {
+ return this.table.where("feedId").equals(feedId).modify({ view })
+ }
}
export const SubscriptionService = new SubscriptionServiceStatic()
diff --git a/src/renderer/src/store/entry/store.ts b/src/renderer/src/store/entry/store.ts
index 3cd55c5ea..29bd71e73 100644
--- a/src/renderer/src/store/entry/store.ts
+++ b/src/renderer/src/store/entry/store.ts
@@ -42,9 +42,9 @@ class EntryActions {
}
clearByFeedId(feedId: string) {
+ const entryIds = get().entries[feedId]
set((state) =>
produce(state, (draft) => {
- const entryIds = draft.entries[feedId]
if (!entryIds) return
entryIds.forEach((entryId) => {
delete draft.flatMapEntries[entryId]
@@ -53,6 +53,7 @@ class EntryActions {
delete draft.internal_feedId2entryIdSet[feedId]
}),
)
+ runTransactionInScope(() => EntryService.deleteEntries(entryIds))
}
async fetchEntryById(entryId: string) {
@@ -222,12 +223,14 @@ class EntryActions {
}))
// Update database
- runTransactionInScope(() => Promise.all([
- EntryService.upsertMany(entries),
- EntryService.bulkStoreReadStatus(entry2Read),
- EntryService.bulkStoreFeedId(entryFeedMap),
- EntryService.bulkStoreCollection(entryCollection),
- ]))
+ runTransactionInScope(() =>
+ Promise.all([
+ EntryService.upsertMany(entries),
+ EntryService.bulkStoreReadStatus(entry2Read),
+ EntryService.bulkStoreFeedId(entryFeedMap),
+ EntryService.bulkStoreCollection(entryCollection),
+ ]),
+ )
}
hydrate(data: FlatEntryModel[]) {
diff --git a/src/renderer/src/store/subscription/hooks.ts b/src/renderer/src/store/subscription/hooks.ts
index 8498766cb..4530c0846 100644
--- a/src/renderer/src/store/subscription/hooks.ts
+++ b/src/renderer/src/store/subscription/hooks.ts
@@ -8,10 +8,10 @@ import { useSubscriptionStore } from "../subscription"
type FeedId = string
export const useFeedIdByView = (view: FeedViewType) =>
- useSubscriptionStore((state) => state.dataIdByView[view] || [])
+ useSubscriptionStore((state) => state.feedIdByView[view] || [])
export const useSubscriptionByView = (view: FeedViewType) =>
useSubscriptionStore((state) =>
- state.dataIdByView[view].map((id) => state.data[id]),
+ state.feedIdByView[view].map((id) => state.data[id]),
)
export const useSubscriptionByFeedId = (feedId: FeedId) =>
diff --git a/src/renderer/src/store/subscription/store.ts b/src/renderer/src/store/subscription/store.ts
index 1bfcb7dd6..910d4ed6c 100644
--- a/src/renderer/src/store/subscription/store.ts
+++ b/src/renderer/src/store/subscription/store.ts
@@ -26,7 +26,7 @@ interface SubscriptionState {
* Key: FeedViewType
* Value: FeedId[]
*/
- dataIdByView: Record
+ feedIdByView: Record
}
function morphResponseData(data: SubscriptionModel[]): SubscriptionFlatModel[] {
@@ -60,7 +60,7 @@ export const useSubscriptionStore = createZustandStore(
"subscription",
)(() => ({
data: {},
- dataIdByView: { ...emptyDataIdByView },
+ feedIdByView: { ...emptyDataIdByView },
}))
const set = useSubscriptionStore.setState
@@ -78,12 +78,12 @@ class SubscriptionActions {
if (view !== undefined) {
set((state) => ({
...state,
- dataIdByView: { ...state.dataIdByView, [view]: [] },
+ feedIdByView: { ...state.feedIdByView, [view]: [] },
}))
} else {
set((state) => ({
...state,
- dataIdByView: { ...emptyDataIdByView },
+ feedIdByView: { ...emptyDataIdByView },
}))
}
@@ -102,7 +102,7 @@ class SubscriptionActions {
produce(state, (state) => {
subscriptions.forEach((subscription) => {
state.data[subscription.feedId] = omit(subscription, "feeds")
- state.dataIdByView[subscription.view].push(subscription.feedId)
+ state.feedIdByView[subscription.view].push(subscription.feedId)
return state
})
@@ -133,7 +133,7 @@ class SubscriptionActions {
clear() {
set({
data: {},
- dataIdByView: { ...emptyDataIdByView },
+ feedIdByView: { ...emptyDataIdByView },
})
}
@@ -162,8 +162,8 @@ class SubscriptionActions {
subscription.category = null
// The logic for removing Category here is to use domain as the default category name.
parsed.domain &&
- (subscription.defaultCategory = (
- capitalizeFirstLetter(parsed.domain)
+ (subscription.defaultCategory = capitalizeFirstLetter(
+ parsed.domain,
))
}
})
@@ -187,8 +187,8 @@ class SubscriptionActions {
set((state) =>
produce(state, (draft) => {
delete draft.data[feedId]
- for (const view in draft.dataIdByView) {
- const currentViewFeedIds = draft.dataIdByView[view] as string[]
+ for (const view in draft.feedIdByView) {
+ const currentViewFeedIds = draft.feedIdByView[view] as string[]
currentViewFeedIds.splice(currentViewFeedIds.indexOf(feedId), 1)
}
}),
@@ -207,6 +207,54 @@ class SubscriptionActions {
})
.then(() => feed)
}
+
+ async changeCategoryView(
+ category: string,
+ currentView: FeedViewType,
+ changeToView: FeedViewType,
+ ) {
+ const state = get()
+ const folderFeedIds = [] as string[]
+ for (const feedId of state.feedIdByView[currentView]) {
+ const subscription = state.data[feedId]
+ if (!subscription) continue
+ if (
+ subscription.category === category ||
+ subscription.defaultCategory === category
+ ) {
+ folderFeedIds.push(feedId)
+ }
+ }
+ await Promise.all(
+ folderFeedIds.map((feedId) =>
+ apiClient.subscriptions.$patch({
+ json: {
+ feedId,
+ view: changeToView,
+ },
+ }),
+ ),
+ )
+
+ set((state) =>
+ produce(state, (state) => {
+ for (const feedId of folderFeedIds) {
+ const feed = state.data[feedId]
+ if (feed) feed.view = changeToView
+ const currentViewFeedIds = state.feedIdByView[
+ currentView
+ ] as string[]
+ const changeToViewFeedIds = state.feedIdByView[
+ changeToView
+ ] as string[]
+ currentViewFeedIds.splice(currentViewFeedIds.indexOf(feedId), 1)
+ changeToViewFeedIds.push(feedId)
+ }
+ }),
+ )
+
+ await Promise.all(folderFeedIds.map((feedId) => SubscriptionService.changeView(feedId, changeToView)))
+ }
}
export const subscriptionActions = new SubscriptionActions()