parent
81fc943c9e
commit
e40c2ebece
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -11,3 +11,8 @@ export const dbSchemaV2 = {
|
|||
...dbSchemaV1,
|
||||
subscriptions: "&id, userId, feedId",
|
||||
}
|
||||
|
||||
export const dbSchemaV3 = {
|
||||
...dbSchemaV1,
|
||||
subscriptions: "&id, userId, &feedId",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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[]) {
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue