Skip to content

Commit

Permalink
feat: sortable feed list
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Aug 23, 2024
1 parent fdd2623 commit 5dc93af
Show file tree
Hide file tree
Showing 6 changed files with 438 additions and 88 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@radix-ui/react-context-menu": "2.2.1",
"@radix-ui/react-dialog": "1.1.1",
"@radix-ui/react-dropdown-menu": "2.1.1",
"@radix-ui/react-hover-card": "1.1.1",
"@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-radio-group": "1.2.0",
Expand Down
33 changes: 33 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 65 additions & 42 deletions src/renderer/src/components/ux/transition/icon.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,75 @@
import { cn } from "@renderer/lib/utils"
import type { Target } from "framer-motion"
import { AnimatePresence, m } from "framer-motion"
import { useEffect, useState } from "react"

export const IconScaleTransition = ({
icon1,
icon2,
status,

className,
icon1ClassName,
icon2ClassName,
}: {
status: "init" | "done"
type TransitionType = {
initial: Target | boolean
animate: Target
exit: Target
}

type IconTransitionProps = {
icon1: string
icon1ClassName?: string
icon2: string
icon2ClassName?: string
status: "init" | "done"
className?: string
}) => {
const [isMount, isMounted] = useState(false)
useEffect(() => {
isMounted(true)
return () => {
isMounted(false)
icon1ClassName?: string
icon2ClassName?: string
}

const createIconTransition =
(transitionType: TransitionType) =>
({
icon1,
icon2,
status,
className,
icon1ClassName,
icon2ClassName,
}: IconTransitionProps) => {
const [isMount, setIsMounted] = useState(false)
useEffect(() => {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setIsMounted(true)
return () => setIsMounted(false)
}, [])

const initial = isMount ? transitionType.initial : true
const { animate } = transitionType
const { exit } = transitionType

return (
<AnimatePresence mode="popLayout">
{status === "init" ? (
<m.i
className={cn(icon1ClassName, className, icon1)}
key="1"
initial={initial}
animate={animate}
exit={exit}
/>
) : (
<m.i
className={cn(icon2ClassName, className, icon2)}
key="2"
initial={initial}
animate={animate}
exit={exit}
/>
)}
</AnimatePresence>
)
}
}, [])

const initial = isMount ? { scale: 0 } : true
return (
<AnimatePresence mode="popLayout">
{status === "init" ? (
<m.i
className={cn(icon1ClassName, className, icon1)}
key="1"
initial={initial}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
) : (
<m.i
className={cn(icon2ClassName, className, icon2)}
key="2"
initial={initial}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
)}
</AnimatePresence>
)
}
export const IconScaleTransition = createIconTransition({
initial: { scale: 0 },
animate: { scale: 1 },
exit: { scale: 0 },
})

export const IconOpacityTransition = createIconTransition({
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
})
18 changes: 18 additions & 0 deletions src/renderer/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,21 @@ export const getViewFromRoute = (route: RSSHubRoute) => {
}
return null
}

export const sortByAlphabet = (a: string, b: string) => {
const isALetter = /^[a-z]/i.test(a)
const isBLetter = /^[a-z]/i.test(b)

if (isALetter && !isBLetter) {
return -1
}
if (!isALetter && isBLetter) {
return 1
}

if (isALetter && isBLetter) {
return a.localeCompare(b)
}

return a.localeCompare(b, "zh-CN")
}
97 changes: 87 additions & 10 deletions src/renderer/src/modules/feed-column/category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { useInputComposition } from "@renderer/hooks/common"
import { 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 { cn, sortByAlphabet } from "@renderer/lib/utils"
import { useFeedStore } from "@renderer/store/feed"
import {
subscriptionActions,
useSubscriptionByFeedId,
Expand All @@ -23,6 +24,7 @@ import { Fragment, memo, useEffect, useRef, useState } from "react"
import { useOnClickOutside } from "usehooks-ts"

import { useModalStack } from "../../components/ui/modal/stacked/hooks"
import { useFeedListSortSelector } from "./atom"
import { CategoryRemoveDialogContent } from "./category-remove-dialog"
import { FeedItem } from "./item"
import { UnreadNumber } from "./unread-number"
Expand Down Expand Up @@ -261,15 +263,12 @@ function FeedCategoryImpl({
opacity: 0.01,
}}
>
{sortByUnreadFeedList.map((feedId) => (
<FeedItem
showUnreadCount={showUnreadCount}
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
<SortedFeedItems
ids={ids}
showCollapse={showCollapse as boolean}
view={view as FeedViewType}
showUnreadCount={showUnreadCount}
/>
</m.div>
)}
</AnimatePresence>
Expand Down Expand Up @@ -353,3 +352,81 @@ const RenameCategoryForm: FC<{
</form>
)
}

type SortListProps = {
ids: string[]
showUnreadCount?: boolean
view: FeedViewType
showCollapse: boolean
}
const SortedFeedItems = (props: SortListProps) => {
const by = useFeedListSortSelector((s) => s.by)
switch (by) {
case "count": {
return <SortByUnreadList {...props} />
}
case "alphabetical": {
return <SortByAlphabeticalList {...props} />
}

default: {
return <SortByUnreadList {...props} />
}
}
}

const SortByAlphabeticalList = (props: SortListProps) => {
const { ids, showUnreadCount, showCollapse, view } = props
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
const sortedFeedList = useFeedStore((state) => {
const res = ids.sort((a, b) => {
const feedTitleA = state.feeds[a]?.title || ""
const feedTitleB = state.feeds[b]?.title || ""
return sortByAlphabet(feedTitleA, feedTitleB)
})

if (isDesc) {
return res
}
return res.reverse()
})
return (
<Fragment>
{sortedFeedList.map((feedId) => (
<FeedItem
showUnreadCount={showUnreadCount}
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}
const SortByUnreadList = ({
ids,
showUnreadCount,
showCollapse,
view,
}: SortListProps) => {
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
const sortByUnreadFeedList = useFeedUnreadStore((state) => {
const res = ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0))
return isDesc ? res : res.reverse()
})

return (
<Fragment>
{sortByUnreadFeedList.map((feedId) => (
<FeedItem
showUnreadCount={showUnreadCount}
key={feedId}
feedId={feedId}
view={view}
className={showCollapse ? "pl-6" : "pl-2.5"}
/>
))}
</Fragment>
)
}
Loading

0 comments on commit 5dc93af

Please sign in to comment.