mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
replace old client with new client from commafeed-ui repository
This commit is contained in:
118
commafeed-client/src/components/sidebar/Tree.tsx
Normal file
118
commafeed-client/src/components/sidebar/Tree.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToCategory, redirectToCategoryDetails, redirectToFeed, redirectToFeedDetails } from "app/slices/redirect"
|
||||
import { collapseTreeCategory } from "app/slices/tree"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Category, Subscription } from "app/types"
|
||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { Loader } from "components/Loader"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import React from "react"
|
||||
import { FaChevronDown, FaChevronRight, FaInbox } from "react-icons/fa"
|
||||
import { TreeNode } from "./TreeNode"
|
||||
import { TreeSearch } from "./TreeSearch"
|
||||
|
||||
const allIcon = <FaInbox size={14} />
|
||||
const expandedIcon = <FaChevronDown size={14} />
|
||||
const collapsedIcon = <FaChevronRight size={14} />
|
||||
|
||||
const errorThreshold = 9
|
||||
export function Tree() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const feedClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) dispatch(redirectToFeedDetails(id))
|
||||
else dispatch(redirectToFeed(id))
|
||||
}
|
||||
const categoryClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) {
|
||||
if (id === Constants.categoryIds.all) return
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
} else {
|
||||
dispatch(redirectToCategory(id))
|
||||
}
|
||||
}
|
||||
const categoryIconClicked = (e: React.MouseEvent, category: Category) => {
|
||||
e.stopPropagation()
|
||||
|
||||
dispatch(
|
||||
collapseTreeCategory({
|
||||
id: +category.id,
|
||||
collapse: category.expanded,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const allCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categoryIds.all}
|
||||
name={t`All`}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
selected={source.type === "category" && source.id === Constants.categoryIds.all}
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level: number = 0) => {
|
||||
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
|
||||
return (
|
||||
<TreeNode
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={categoryUnreadCount(category)}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
hasError={hasError}
|
||||
onClick={categoryClicked}
|
||||
onIconClick={e => categoryIconClicked(e, category)}
|
||||
key={category.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level: number = 0) => (
|
||||
<TreeNode
|
||||
id={String(feed.id)}
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
selected={source.type === "feed" && source.id === String(feed.id)}
|
||||
level={level}
|
||||
hasError={feed.errorCount > errorThreshold}
|
||||
onClick={feedClicked}
|
||||
key={feed.id}
|
||||
/>
|
||||
)
|
||||
|
||||
const recursiveCategoryNode = (category: Category, level: number = 0) => (
|
||||
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
|
||||
{categoryNode(category, level)}
|
||||
{category.expanded && category.children.map(c => recursiveCategoryNode(c, level + 1))}
|
||||
{category.expanded && category.feeds.map(f => feedNode(f, level + 1))}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
if (!root) return <Loader />
|
||||
const feeds = flattenCategoryTree(root).flatMap(c => c.feeds)
|
||||
return (
|
||||
<Stack>
|
||||
<OnDesktop>
|
||||
<TreeSearch feeds={feeds} />
|
||||
</OnDesktop>
|
||||
<Box>
|
||||
{allCategoryNode()}
|
||||
{root.children.map(c => recursiveCategoryNode(c))}
|
||||
{root.feeds.map(f => feedNode(f))}
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
62
commafeed-client/src/components/sidebar/TreeNode.tsx
Normal file
62
commafeed-client/src/components/sidebar/TreeNode.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Box, createStyles, Image } from "@mantine/core"
|
||||
import React, { ReactNode } from "react"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
|
||||
interface TreeNodeProps {
|
||||
id: string
|
||||
name: string
|
||||
icon: ReactNode | string
|
||||
unread: number
|
||||
selected: boolean
|
||||
expanded?: boolean
|
||||
level: number
|
||||
hasError: boolean
|
||||
onClick: (e: React.MouseEvent, id: string) => void
|
||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: TreeNodeProps) => {
|
||||
let backgroundColor = "inherit"
|
||||
if (props.selected) backgroundColor = theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3]
|
||||
|
||||
let color
|
||||
if (props.hasError) color = theme.colors.red[6]
|
||||
else if (theme.colorScheme === "dark") color = props.unread > 0 ? theme.colors.dark[0] : theme.colors.dark[3]
|
||||
else color = props.unread > 0 ? theme.black : theme.colors.gray[6]
|
||||
|
||||
return {
|
||||
node: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
color,
|
||||
backgroundColor,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
|
||||
},
|
||||
},
|
||||
nodeText: {
|
||||
flexGrow: 1,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export function TreeNode(props: TreeNodeProps) {
|
||||
const { classes } = useStyles(props)
|
||||
return (
|
||||
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick && props.onIconClick(e, props.id)}>
|
||||
{typeof props.icon === "string" ? <Image src={props.icon} alt="" width={18} height={18} /> : props.icon}
|
||||
</Box>
|
||||
<Box className={classes.nodeText}>{props.name}</Box>
|
||||
{!props.expanded && (
|
||||
<Box>
|
||||
<UnreadCount unreadCount={props.unread} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
60
commafeed-client/src/components/sidebar/TreeSearch.tsx
Normal file
60
commafeed-client/src/components/sidebar/TreeSearch.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { Box, Center, Image, Kbd, TextInput } from "@mantine/core"
|
||||
import { openSpotlight, SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/slices/redirect"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { Subscription } from "app/types"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { TbSearch } from "react-icons/tb"
|
||||
|
||||
export interface TreeSearchProps {
|
||||
feeds: Subscription[]
|
||||
}
|
||||
export function TreeSearch(props: TreeSearchProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const actions: SpotlightAction[] = props.feeds
|
||||
.sort((f1, f2) => f1.name.localeCompare(f2.name))
|
||||
.map(f => ({
|
||||
title: f.name,
|
||||
icon: <Image src={f.iconUrl} alt="" width={18} height={18} />,
|
||||
onTrigger: () => dispatch(redirectToFeed(f.id)),
|
||||
}))
|
||||
|
||||
const searchIcon = <TbSearch size={18} />
|
||||
const rightSection = (
|
||||
<Center>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Box mx={5}>+</Box>
|
||||
<Kbd>K</Kbd>
|
||||
</Center>
|
||||
)
|
||||
|
||||
// additional keyboard shortcut used by commafeed v1
|
||||
useMousetrap("g u", () => openSpotlight())
|
||||
|
||||
return (
|
||||
<SpotlightProvider
|
||||
actions={actions}
|
||||
searchIcon={searchIcon}
|
||||
searchPlaceholder={t`Search`}
|
||||
shortcut="ctrl+k"
|
||||
nothingFoundMessage={t`Nothing found`}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
icon={searchIcon}
|
||||
rightSectionWidth={100}
|
||||
rightSection={rightSection}
|
||||
styles={{
|
||||
input: { cursor: "pointer" },
|
||||
rightSection: { pointerEvents: "none" },
|
||||
}}
|
||||
onClick={() => openSpotlight()}
|
||||
// prevent focus
|
||||
onFocus={e => e.target.blur()}
|
||||
readOnly
|
||||
/>
|
||||
</SpotlightProvider>
|
||||
)
|
||||
}
|
||||
18
commafeed-client/src/components/sidebar/UnreadCount.tsx
Normal file
18
commafeed-client/src/components/sidebar/UnreadCount.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Badge, createStyles } from "@mantine/core"
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
badge: {
|
||||
width: "3.2rem",
|
||||
// for some reason, mantine Badge has "cursor: 'default'"
|
||||
cursor: "pointer",
|
||||
},
|
||||
}))
|
||||
|
||||
export function UnreadCount(props: { unreadCount: number }) {
|
||||
const { classes } = useStyles()
|
||||
|
||||
if (props.unreadCount <= 0) return null
|
||||
|
||||
const count = props.unreadCount >= 1000 ? "999+" : props.unreadCount
|
||||
return <Badge className={classes.badge}>{count}</Badge>
|
||||
}
|
||||
Reference in New Issue
Block a user