From c20dbba4c9b4ca88018107950c3c6af9128f57d0 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 23 Jul 2024 14:51:02 +0800 Subject: [PATCH] feat: change folder to view Signed-off-by: Innei --- src/renderer/src/constants/tabs.tsx | 9 +++ src/renderer/src/database/db.ts | 3 +- src/renderer/src/database/db_schema.ts | 5 ++ .../src/modules/feed-column/category.tsx | 57 ++++++++++++++-- .../src/modules/feed-column/index.tsx | 2 +- src/renderer/src/services/entry.ts | 8 ++- src/renderer/src/services/subscription.ts | 9 ++- src/renderer/src/store/entry/store.ts | 17 +++-- src/renderer/src/store/subscription/hooks.ts | 4 +- src/renderer/src/store/subscription/store.ts | 68 ++++++++++++++++--- 10 files changed, 151 insertions(+), 31 deletions(-) diff --git a/src/renderer/src/constants/tabs.tsx b/src/renderer/src/constants/tabs.tsx index 5d04ac0dda..f465ad21d5 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 108168a875..f8ee999b30 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 8918143d91..b9f653d0ce 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 64ebc635d9..978d2e4668 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 70cb6fe7d0..097769bf3e 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 cf43b68cdd..0a9c690dff 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 3cd55c5ea1..29bd71e732 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 8498766cb9..4530c0846f 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 1bfcb7dd64..910d4ed6c4 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()