From c46c90b5efcf6d3d5b5f1e6014a2994ea19b24fd Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 7 Jun 2024 19:48:07 +0800 Subject: [PATCH] feat: redesign image preview and optimize some ux/ui, add modal rc (#47) * feat: optimize some ux Signed-off-by: Innei * fix: align Signed-off-by: Innei * fix: github login button Signed-off-by: Innei * fix: align Signed-off-by: Innei * feat: dialog init Signed-off-by: Innei * feat: image swiper preview Signed-off-by: Innei * feat: open in new window Signed-off-by: Innei * feat: optimize spring Signed-off-by: Innei * fix: sidebar bar tooltip bug Signed-off-by: Innei * fix: lint Signed-off-by: Innei * fix: remark image Signed-off-by: Innei * fix: unread reset after re-fetch Signed-off-by: Innei * fix: layout and vibrancy Signed-off-by: Innei * feat(add): default view Signed-off-by: Innei * fix: follow mutation Signed-off-by: Innei * feat: preview Signed-off-by: Innei * fix: update Signed-off-by: Innei --------- Signed-off-by: Innei --- eslint.config.mjs | 8 + postcss.config.cjs | 3 +- src/main/tipc.ts | 3 + src/renderer/src/App.tsx | 6 +- src/renderer/src/components/discover/form.tsx | 38 +-- .../src/components/entry-column/index.tsx | 44 ++-- .../components/entry-column/picture-item.tsx | 38 ++- .../entry-column/social-media-item.tsx | 1 + .../src/components/entry-content/index.tsx | 2 +- .../src/components/entry-content/share.tsx | 4 +- .../src/components/feed-column/index.tsx | 76 +++--- src/renderer/src/components/icons/empty.tsx | 34 +++ src/renderer/src/components/swipe-images.tsx | 103 -------- .../src/components/ui/background/index.ts | 1 + .../src/components/ui/background/vibrancy.tsx | 25 ++ .../src/components/ui/button/index.tsx | 14 +- .../src/components/ui/constants/spring.ts | 32 +++ .../src/components/ui/{ => dialog}/dialog.tsx | 39 +-- .../src/components/ui/dialog/index.ts | 1 + .../src/components/ui/divider/Divider.tsx | 51 ++++ .../src/components/ui/divider/index.ts | 1 + src/renderer/src/components/ui/image.tsx | 78 +++--- .../src/components/ui/image/hooks.tsx | 24 ++ .../src/components/ui/image/index.module.css | 20 ++ .../src/components/ui/image/preview-image.tsx | 94 ++++++++ .../src/components/ui/image/swipe-images.tsx | 117 +++++++++ src/renderer/src/components/ui/modal/index.ts | 1 + .../components/ui/modal/stacked/context.tsx | 23 ++ .../ui/modal/stacked/declarative-modal.tsx | 58 +++++ .../components/ui/modal/stacked/helper.tsx | 9 + .../src/components/ui/modal/stacked/hooks.tsx | 54 +++++ .../src/components/ui/modal/stacked/index.ts | 5 + .../src/components/ui/modal/stacked/modal.tsx | 223 ++++++++++++++++++ .../components/ui/modal/stacked/provider.tsx | 33 +++ .../src/components/ui/modal/stacked/types.tsx | 19 ++ src/renderer/src/lib/dom.ts | 6 + src/renderer/src/lib/parse-html.ts | 6 +- src/renderer/src/main.tsx | 7 +- src/renderer/src/models/types.ts | 1 - src/renderer/src/pages/(external)/login.tsx | 2 +- .../(main)/(context)/(nonhome)/layout.tsx | 13 +- src/renderer/src/pages/(main)/layout.tsx | 13 +- src/renderer/src/pages/add/index.tsx | 7 +- src/renderer/src/pages/settings/layout.tsx | 5 +- src/renderer/src/providers/root-providers.tsx | 2 + src/renderer/src/router.tsx | 25 +- src/renderer/src/store/subscription.ts | 6 +- src/renderer/src/store/unread.ts | 28 ++- src/renderer/src/styles/base.css | 12 - src/renderer/src/styles/tailwind-extend.css | 4 + 50 files changed, 1113 insertions(+), 306 deletions(-) create mode 100644 src/renderer/src/components/icons/empty.tsx delete mode 100644 src/renderer/src/components/swipe-images.tsx create mode 100644 src/renderer/src/components/ui/background/index.ts create mode 100644 src/renderer/src/components/ui/background/vibrancy.tsx create mode 100644 src/renderer/src/components/ui/constants/spring.ts rename src/renderer/src/components/ui/{ => dialog}/dialog.tsx (72%) create mode 100644 src/renderer/src/components/ui/dialog/index.ts create mode 100644 src/renderer/src/components/ui/divider/Divider.tsx create mode 100644 src/renderer/src/components/ui/divider/index.ts create mode 100644 src/renderer/src/components/ui/image/hooks.tsx create mode 100644 src/renderer/src/components/ui/image/index.module.css create mode 100644 src/renderer/src/components/ui/image/preview-image.tsx create mode 100644 src/renderer/src/components/ui/image/swipe-images.tsx create mode 100644 src/renderer/src/components/ui/modal/index.ts create mode 100644 src/renderer/src/components/ui/modal/stacked/context.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/declarative-modal.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/helper.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/hooks.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/index.ts create mode 100644 src/renderer/src/components/ui/modal/stacked/modal.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/provider.tsx create mode 100644 src/renderer/src/components/ui/modal/stacked/types.tsx create mode 100644 src/renderer/src/lib/dom.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index fa04862ad..45d4cb650 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,6 +22,14 @@ export default defineConfig( "unicorn/prefer-module": "off", "@typescript-eslint/no-floating-promises": "off", }, + settings: { + tailwindcss: { + callees: ["cn"], + whitelist: [ + "center", + ], + }, + }, }, { files: GLOB_TS_SRC, diff --git a/postcss.config.cjs b/postcss.config.cjs index 08a01d4d1..fe5e4a5c7 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,5 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, + "tailwindcss": {}, + "tailwindcss/nesting": {}, }, } diff --git a/src/main/tipc.ts b/src/main/tipc.ts index 956cbb114..9278618cb 100644 --- a/src/main/tipc.ts +++ b/src/main/tipc.ts @@ -106,6 +106,9 @@ export const router = { handlers.invalidateQuery.send(input) }), + /** + * @deprecated + */ previewImage: t.procedure .input<{ realUrl: string diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4f18c5fa3..dc8af5633 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -3,6 +3,7 @@ import { useEffect } from "react" import { Outlet } from "react-router-dom" import { useDark } from "./hooks/useDark" +import { RootProviders } from "./providers/root-providers" import { handlers } from "./tipc" function App() { @@ -25,8 +26,9 @@ function App() { aria-hidden /> )} - - + + + ) } diff --git a/src/renderer/src/components/discover/form.tsx b/src/renderer/src/components/discover/form.tsx index 57e7c9bcb..095acd813 100644 --- a/src/renderer/src/components/discover/form.tsx +++ b/src/renderer/src/components/discover/form.tsx @@ -17,6 +17,8 @@ import { import { Image } from "@renderer/components/ui/image" import { Input } from "@renderer/components/ui/input" import { apiClient } from "@renderer/lib/api-fetch" +import { FeedViewType } from "@renderer/lib/enum" +import { useFeedStore } from "@renderer/store" import { DEEPLINK_SCHEME } from "@shared/constants" import { openElectronWindow } from "@shared/electron" import { useMutation } from "@tanstack/react-query" @@ -183,20 +185,28 @@ export function DiscoverForm({ type }: { type: string }) { ) : ( diff --git a/src/renderer/src/components/entry-column/index.tsx b/src/renderer/src/components/entry-column/index.tsx index 223cd45ea..5d55c1b03 100644 --- a/src/renderer/src/components/entry-column/index.tsx +++ b/src/renderer/src/components/entry-column/index.tsx @@ -1,4 +1,4 @@ -import { Button, HeaderActionButton } from "@renderer/components/ui/button" +import { ActionButton, Button } from "@renderer/components/ui/button" import { Popover, PopoverClose, @@ -29,6 +29,7 @@ import { Virtuoso, VirtuosoGrid } from "react-virtuoso" import { useEventCallback } from "usehooks-ts" import { useShallow } from "zustand/react/shallow" +import { EmptyIcon } from "../icons/empty" import { ArticleItem } from "./article-item" import { AudioItem } from "./audio-item" import { EntryItemWrapper } from "./item-wrapper" @@ -149,17 +150,21 @@ export function EntryColumn() { return (
setActiveEntry?.(null)} + onClick={() => setActiveEntry(null)} > - {activeList?.view && views[activeList.view].gridMode ? ( - - ) : ( - - )} + {virtuosoOptions.totalCount === 0 ? ( + + ) : activeList?.view && views[activeList.view].gridMode ? + ( + + ) : + ( + + )}
) } @@ -208,7 +213,7 @@ const ListHeader: FC = () => {
- setUnreadOnly(!unreadOnly)} > @@ -217,12 +222,12 @@ const ListHeader: FC = () => { ) : ( )} - + - {}} tooltip="Mark All as Read"> + {}} tooltip="Mark All as Read"> - +
Mark all as read?
@@ -274,15 +279,20 @@ const EmptyList = (props, ref) => { initial={{ opacity: 0.01, y: 100 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0.01, y: -100 }} - className="-mt-10 flex h-full flex-col items-center justify-center gap-2 text-zinc-400" + className="-mt-20 flex h-full flex-col items-center justify-center gap-2 text-zinc-400" {...props} ref={ref} > - {unreadOnly && ( + {unreadOnly ? ( <> - + Zero Unread + ) : ( +
+ + Zero Items +
)} ) diff --git a/src/renderer/src/components/entry-column/picture-item.tsx b/src/renderer/src/components/entry-column/picture-item.tsx index b60876b4c..e92da5bce 100644 --- a/src/renderer/src/components/entry-column/picture-item.tsx +++ b/src/renderer/src/components/entry-column/picture-item.tsx @@ -1,28 +1,42 @@ import { FeedIcon } from "@renderer/components/feed-icon" -import { SwipeImages } from "@renderer/components/swipe-images" +import { SwipeImages } from "@renderer/components/ui/image/swipe-images" import { ReactVirtuosoItemPlaceholder } from "@renderer/components/ui/placeholder" import dayjs from "@renderer/lib/dayjs" import { cn } from "@renderer/lib/utils" import { useEntry } from "@renderer/store/entry" +import { usePreviewImages } from "../ui/image/hooks" import type { UniversalItemProps } from "./types" export function PictureItem({ entryId, entryPreview }: UniversalItemProps) { const entry = useEntry(entryId) || entryPreview + const previewImage = usePreviewImages() if (!entry) return return (
- ) } diff --git a/src/renderer/src/components/entry-column/social-media-item.tsx b/src/renderer/src/components/entry-column/social-media-item.tsx index 556617a9d..77228d31c 100644 --- a/src/renderer/src/components/entry-column/social-media-item.tsx +++ b/src/renderer/src/components/entry-column/social-media-item.tsx @@ -39,6 +39,7 @@ export function SocialMediaItem({ entryId, entryPreview }: UniversalItemProps) {
{entry.entries.images?.map((image) => ( diff --git a/src/renderer/src/components/entry-content/share.tsx b/src/renderer/src/components/entry-content/share.tsx index 7ec64dfe7..357388013 100644 --- a/src/renderer/src/components/entry-content/share.tsx +++ b/src/renderer/src/components/entry-content/share.tsx @@ -1,4 +1,4 @@ -import { HeaderActionButton } from "@renderer/components/ui/button" +import { ActionButton } from "@renderer/components/ui/button" import { TooltipProvider, } from "@renderer/components/ui/tooltip" @@ -26,7 +26,7 @@ export function EntryShare({ {items .filter((item) => !item.disabled) .map((item) => ( - setActiveList({ level: levels.view, id: active, name: views[active].name, view: active, - preventNavigate: true, })} >
- + - + - + - +
{views.map((item, index) => ( - - - - - {item.name} - + { + setActive(index) + setActiveList?.({ + level: "view", + id: index, + name: views[index].name, + view: index, + }) + e.stopPropagation() + }} + > + {item.icon} + ))}
@@ -154,6 +142,6 @@ export function FeedColumn() { ))}
-
+ ) } diff --git a/src/renderer/src/components/icons/empty.tsx b/src/renderer/src/components/icons/empty.tsx new file mode 100644 index 000000000..8bfe0be4e --- /dev/null +++ b/src/renderer/src/components/icons/empty.tsx @@ -0,0 +1,34 @@ +/* eslint-disable @stylistic/no-tabs */ +import { cn } from "@renderer/lib/utils" + +export const EmptyIcon: Component = ({ className }) => ( + + + + + + + +) diff --git a/src/renderer/src/components/swipe-images.tsx b/src/renderer/src/components/swipe-images.tsx deleted file mode 100644 index 93a44dc0d..000000000 --- a/src/renderer/src/components/swipe-images.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import "swiper/css" -import "swiper/css/navigation" -import "swiper/css/scrollbar" - -import { Image } from "@renderer/components/ui/image" -import { cn } from "@renderer/lib/utils" -import { Mousewheel, Navigation, Scrollbar } from "swiper/modules" -import { Swiper, SwiperSlide } from "swiper/react" - -export function SwipeImages({ - images, - uniqueKey, - className, - imgClassName, -}: { - images?: string[] | null - uniqueKey?: string - className?: string - imgClassName?: string -}) { - return ( -
- {(images?.length || 0) > 1 ? - ( - <> - - {images?.slice(0, 5).map((image) => ( - - cover - - ))} - -
e.stopPropagation()} - > - -
-
e.stopPropagation()} - > - -
- - ) : - images?.length === 1 ? - ( - cover - ) : - ( -
-
-
-
-
- )} -
- ) -} diff --git a/src/renderer/src/components/ui/background/index.ts b/src/renderer/src/components/ui/background/index.ts new file mode 100644 index 000000000..11474f523 --- /dev/null +++ b/src/renderer/src/components/ui/background/index.ts @@ -0,0 +1 @@ +export * from "./vibrancy" diff --git a/src/renderer/src/components/ui/background/vibrancy.tsx b/src/renderer/src/components/ui/background/vibrancy.tsx new file mode 100644 index 000000000..2ae19980a --- /dev/null +++ b/src/renderer/src/components/ui/background/vibrancy.tsx @@ -0,0 +1,25 @@ +import { useDark } from "@renderer/hooks/useDark" +import { cn } from "@renderer/lib/utils" +import { useMediaQuery } from "usehooks-ts" + +export const Vibrancy: Component< + React.DetailedHTMLProps, HTMLDivElement> +> = ({ className, children, ...rest }) => { + const canVibrancy = + window.electron && window.electron.process.platform === "darwin" + + const systemDark = useMediaQuery("(prefers-color-scheme: dark)") + const { isDark } = useDark() + return ( +
+ {children} +
+ ) +} diff --git a/src/renderer/src/components/ui/button/index.tsx b/src/renderer/src/components/ui/button/index.tsx index 6ffbe7546..4b10d5ca2 100644 --- a/src/renderer/src/components/ui/button/index.tsx +++ b/src/renderer/src/components/ui/button/index.tsx @@ -1,4 +1,5 @@ import { Slot } from "@radix-ui/react-slot" +import { stopPropagation } from "@renderer/lib/dom" import { cn } from "@renderer/lib/utils" import type { VariantProps } from "class-variance-authority" import type { HTMLMotionProps } from "framer-motion" @@ -42,24 +43,26 @@ Button.displayName = "Button" // BIZ buttons -interface HeaderActionButtonProps { - onClick?: () => void +interface ActionButtonProps { + onClick?: React.MouseEventHandler icon?: React.ReactNode | React.FC tooltip: string tooltipSide?: "top" | "bottom" active?: boolean } -export const HeaderActionButton = React.forwardRef< +export const ActionButton = React.forwardRef< HTMLButtonElement, - ComponentType + ComponentType >( ( { icon, onClick, tooltip, className, tooltipSide, children, active }, ref, ) => ( - +