Skip to content

Commit 5dc93af

Browse files
committed
feat: sortable feed list
Signed-off-by: Innei <i@innei.in>
1 parent fdd2623 commit 5dc93af

File tree

6 files changed

+438
-88
lines changed

6 files changed

+438
-88
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@radix-ui/react-context-menu": "2.2.1",
4949
"@radix-ui/react-dialog": "1.1.1",
5050
"@radix-ui/react-dropdown-menu": "2.1.1",
51+
"@radix-ui/react-hover-card": "1.1.1",
5152
"@radix-ui/react-label": "2.1.0",
5253
"@radix-ui/react-popover": "1.1.1",
5354
"@radix-ui/react-radio-group": "1.2.0",

pnpm-lock.yaml

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,75 @@
11
import { cn } from "@renderer/lib/utils"
2+
import type { Target } from "framer-motion"
23
import { AnimatePresence, m } from "framer-motion"
34
import { useEffect, useState } from "react"
45

5-
export const IconScaleTransition = ({
6-
icon1,
7-
icon2,
8-
status,
9-
10-
className,
11-
icon1ClassName,
12-
icon2ClassName,
13-
}: {
14-
status: "init" | "done"
6+
type TransitionType = {
7+
initial: Target | boolean
8+
animate: Target
9+
exit: Target
10+
}
1511

12+
type IconTransitionProps = {
1613
icon1: string
17-
icon1ClassName?: string
1814
icon2: string
19-
icon2ClassName?: string
15+
status: "init" | "done"
2016
className?: string
21-
}) => {
22-
const [isMount, isMounted] = useState(false)
23-
useEffect(() => {
24-
isMounted(true)
25-
return () => {
26-
isMounted(false)
17+
icon1ClassName?: string
18+
icon2ClassName?: string
19+
}
20+
21+
const createIconTransition =
22+
(transitionType: TransitionType) =>
23+
({
24+
icon1,
25+
icon2,
26+
status,
27+
className,
28+
icon1ClassName,
29+
icon2ClassName,
30+
}: IconTransitionProps) => {
31+
const [isMount, setIsMounted] = useState(false)
32+
useEffect(() => {
33+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
34+
setIsMounted(true)
35+
return () => setIsMounted(false)
36+
}, [])
37+
38+
const initial = isMount ? transitionType.initial : true
39+
const { animate } = transitionType
40+
const { exit } = transitionType
41+
42+
return (
43+
<AnimatePresence mode="popLayout">
44+
{status === "init" ? (
45+
<m.i
46+
className={cn(icon1ClassName, className, icon1)}
47+
key="1"
48+
initial={initial}
49+
animate={animate}
50+
exit={exit}
51+
/>
52+
) : (
53+
<m.i
54+
className={cn(icon2ClassName, className, icon2)}
55+
key="2"
56+
initial={initial}
57+
animate={animate}
58+
exit={exit}
59+
/>
60+
)}
61+
</AnimatePresence>
62+
)
2763
}
28-
}, [])
2964

30-
const initial = isMount ? { scale: 0 } : true
31-
return (
32-
<AnimatePresence mode="popLayout">
33-
{status === "init" ? (
34-
<m.i
35-
className={cn(icon1ClassName, className, icon1)}
36-
key="1"
37-
initial={initial}
38-
animate={{ scale: 1 }}
39-
exit={{ scale: 0 }}
40-
/>
41-
) : (
42-
<m.i
43-
className={cn(icon2ClassName, className, icon2)}
44-
key="2"
45-
initial={initial}
46-
animate={{ scale: 1 }}
47-
exit={{ scale: 0 }}
48-
/>
49-
)}
50-
</AnimatePresence>
51-
)
52-
}
65+
export const IconScaleTransition = createIconTransition({
66+
initial: { scale: 0 },
67+
animate: { scale: 1 },
68+
exit: { scale: 0 },
69+
})
70+
71+
export const IconOpacityTransition = createIconTransition({
72+
initial: { opacity: 0 },
73+
animate: { opacity: 1 },
74+
exit: { opacity: 0 },
75+
})

src/renderer/src/lib/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,21 @@ export const getViewFromRoute = (route: RSSHubRoute) => {
180180
}
181181
return null
182182
}
183+
184+
export const sortByAlphabet = (a: string, b: string) => {
185+
const isALetter = /^[a-z]/i.test(a)
186+
const isBLetter = /^[a-z]/i.test(b)
187+
188+
if (isALetter && !isBLetter) {
189+
return -1
190+
}
191+
if (!isALetter && isBLetter) {
192+
return 1
193+
}
194+
195+
if (isALetter && isBLetter) {
196+
return a.localeCompare(b)
197+
}
198+
199+
return a.localeCompare(b, "zh-CN")
200+
}

src/renderer/src/modules/feed-column/category.tsx

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { useInputComposition } from "@renderer/hooks/common"
1010
import { stopPropagation } from "@renderer/lib/dom"
1111
import type { FeedViewType } from "@renderer/lib/enum"
1212
import { showNativeMenu } from "@renderer/lib/native-menu"
13-
import { cn } from "@renderer/lib/utils"
13+
import { cn, sortByAlphabet } from "@renderer/lib/utils"
14+
import { useFeedStore } from "@renderer/store/feed"
1415
import {
1516
subscriptionActions,
1617
useSubscriptionByFeedId,
@@ -23,6 +24,7 @@ import { Fragment, memo, useEffect, useRef, useState } from "react"
2324
import { useOnClickOutside } from "usehooks-ts"
2425

2526
import { useModalStack } from "../../components/ui/modal/stacked/hooks"
27+
import { useFeedListSortSelector } from "./atom"
2628
import { CategoryRemoveDialogContent } from "./category-remove-dialog"
2729
import { FeedItem } from "./item"
2830
import { UnreadNumber } from "./unread-number"
@@ -261,15 +263,12 @@ function FeedCategoryImpl({
261263
opacity: 0.01,
262264
}}
263265
>
264-
{sortByUnreadFeedList.map((feedId) => (
265-
<FeedItem
266-
showUnreadCount={showUnreadCount}
267-
key={feedId}
268-
feedId={feedId}
269-
view={view}
270-
className={showCollapse ? "pl-6" : "pl-2.5"}
271-
/>
272-
))}
266+
<SortedFeedItems
267+
ids={ids}
268+
showCollapse={showCollapse as boolean}
269+
view={view as FeedViewType}
270+
showUnreadCount={showUnreadCount}
271+
/>
273272
</m.div>
274273
)}
275274
</AnimatePresence>
@@ -353,3 +352,81 @@ const RenameCategoryForm: FC<{
353352
</form>
354353
)
355354
}
355+
356+
type SortListProps = {
357+
ids: string[]
358+
showUnreadCount?: boolean
359+
view: FeedViewType
360+
showCollapse: boolean
361+
}
362+
const SortedFeedItems = (props: SortListProps) => {
363+
const by = useFeedListSortSelector((s) => s.by)
364+
switch (by) {
365+
case "count": {
366+
return <SortByUnreadList {...props} />
367+
}
368+
case "alphabetical": {
369+
return <SortByAlphabeticalList {...props} />
370+
}
371+
372+
default: {
373+
return <SortByUnreadList {...props} />
374+
}
375+
}
376+
}
377+
378+
const SortByAlphabeticalList = (props: SortListProps) => {
379+
const { ids, showUnreadCount, showCollapse, view } = props
380+
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
381+
const sortedFeedList = useFeedStore((state) => {
382+
const res = ids.sort((a, b) => {
383+
const feedTitleA = state.feeds[a]?.title || ""
384+
const feedTitleB = state.feeds[b]?.title || ""
385+
return sortByAlphabet(feedTitleA, feedTitleB)
386+
})
387+
388+
if (isDesc) {
389+
return res
390+
}
391+
return res.reverse()
392+
})
393+
return (
394+
<Fragment>
395+
{sortedFeedList.map((feedId) => (
396+
<FeedItem
397+
showUnreadCount={showUnreadCount}
398+
key={feedId}
399+
feedId={feedId}
400+
view={view}
401+
className={showCollapse ? "pl-6" : "pl-2.5"}
402+
/>
403+
))}
404+
</Fragment>
405+
)
406+
}
407+
const SortByUnreadList = ({
408+
ids,
409+
showUnreadCount,
410+
showCollapse,
411+
view,
412+
}: SortListProps) => {
413+
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
414+
const sortByUnreadFeedList = useFeedUnreadStore((state) => {
415+
const res = ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0))
416+
return isDesc ? res : res.reverse()
417+
})
418+
419+
return (
420+
<Fragment>
421+
{sortByUnreadFeedList.map((feedId) => (
422+
<FeedItem
423+
showUnreadCount={showUnreadCount}
424+
key={feedId}
425+
feedId={feedId}
426+
view={view}
427+
className={showCollapse ? "pl-6" : "pl-2.5"}
428+
/>
429+
))}
430+
</Fragment>
431+
)
432+
}

0 commit comments

Comments
 (0)