feat: change folder to view (#150)

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-07-23 14:56:16 +08:00 committed by GitHub
parent 81fc943c9e
commit e40c2ebece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 151 additions and 31 deletions

View File

@ -1,9 +1,12 @@
import { FeedViewType } from "@renderer/lib/enum"
export const views = [
{
name: "Articles",
icon: <i className="i-mgc-paper-cute-fi" />,
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: <i className="i-mgc-mic-cute-fi" />,
className: "text-purple-600",
translation: "title",
view: FeedViewType.Audios,
},
{
name: "Notifications",
icon: <i className="i-mgc-announcement-cute-fi" />,
className: "text-yellow-600",
translation: "title",
view: FeedViewType.Notifications,
},
]
export const settingTabs = [

View File

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

View File

@ -11,3 +11,8 @@ export const dbSchemaV2 = {
...dbSchemaV1,
subscriptions: "&id, userId, feedId",
}
export const dbSchemaV3 = {
...dbSchemaV1,
subscriptions: "&id, userId, &feedId",
}

View File

@ -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 (
<Collapsible
open={open}
@ -92,7 +112,28 @@ function FeedCategoryImpl({
[
{
type: "text",
label: "Rename Category",
enabled: !!(folderName && typeof view === "number"),
label: "Change to other view",
click() {
nextFrame(() =>
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}
>
<i className="i-mgc-right-cute-fi mr-2 transition-transform" />
{isChangePending ? (
<LoadingCircle size="small" className="mr-2" />
) : (
<i className="i-mgc-right-cute-fi mr-2 transition-transform" />
)}
</CollapsibleTrigger>
<span
className={cn(

View File

@ -51,7 +51,7 @@ const useBackHome = (active: number) => {
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<number, number>

View File

@ -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<EntryModel> {
async deleteCollection(entryId: string) {
return EntryRelatedService.deleteItem(EntryRelatedKey.COLLECTION, entryId)
}
async deleteEntries(entryIds: string[]) {
await entryModel.table.bulkDelete(entryIds)
}
}
export const EntryService = new EntryServiceStatic()

View File

@ -12,7 +12,10 @@ class SubscriptionServiceStatic extends BaseService<SubscriptionModelWithId> {
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<SubscriptionModelWithId> {
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()

View File

@ -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[]) {

View File

@ -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) =>

View File

@ -26,7 +26,7 @@ interface SubscriptionState {
* Key: FeedViewType
* Value: FeedId[]
*/
dataIdByView: Record<FeedViewType, FeedId[]>
feedIdByView: Record<FeedViewType, FeedId[]>
}
function morphResponseData(data: SubscriptionModel[]): SubscriptionFlatModel[] {
@ -60,7 +60,7 @@ export const useSubscriptionStore = createZustandStore<SubscriptionState>(
"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()