diff --git a/apps/renderer/package.json b/apps/renderer/package.json index ba27eeb76e..2b5c47169e 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -49,6 +49,8 @@ "dexie-export-import": "^4.1.2", "dnum": "^2.14.0", "electron-log": "5.2.0", + "embla-carousel-react": "8.3.1", + "embla-carousel-wheel-gestures": "8.0.1", "firebase": "10.14.1", "foxact": "0.2.41", "framer-motion": "11.11.11", @@ -97,7 +99,6 @@ "remark-rehype": "11.1.1", "shiki": "1.22.2", "sonner": "1.5.0", - "swiper": "11.1.14", "tldts": "6.1.58", "unified": "11.0.5", "unist-util-visit-parents": "^6.0.1", @@ -114,6 +115,7 @@ "@follow/components": "workspace:*", "@follow/constants": "workspace:*", "@follow/hooks": "workspace:*", + "@follow/logger": "workspace:*", "@follow/models": "workspace:*", "@follow/shared": "workspace:*", "@follow/types": "workspace:*", diff --git a/apps/renderer/src/components/ui/media/SwipeMedia.tsx b/apps/renderer/src/components/ui/media/SwipeMedia.tsx index a891abb000..7e3024e1ed 100644 --- a/apps/renderer/src/components/ui/media/SwipeMedia.tsx +++ b/apps/renderer/src/components/ui/media/SwipeMedia.tsx @@ -1,34 +1,25 @@ -import "swiper/css" -import "swiper/css/navigation" -import "swiper/css/scrollbar" - import type { MediaModel } from "@follow/shared/hono" import { cn } from "@follow/utils/utils" -import { useHover } from "@use-gesture/react" +import useEmblaCarousel from "embla-carousel-react" +import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures" import { uniqBy } from "lodash-es" -import { useRef, useState } from "react" -import { Mousewheel, Navigation, Scrollbar } from "swiper/modules" -import { Swiper, SwiperSlide } from "swiper/react" +import { useCallback, useRef } from "react" import { Media } from "~/components/ui/media" -import styles from "./index.module.css" - const defaultProxySize = { width: 600, height: 0, } + export function SwipeMedia({ media, - uniqueKey, className, imgClassName, onPreview, proxySize = defaultProxySize, - forceSwiper, }: { media?: MediaModel[] | null - uniqueKey?: string className?: string imgClassName?: string onPreview?: (media: MediaModel[], index?: number) => void @@ -36,53 +27,37 @@ export function SwipeMedia({ width: number height: number } - forceSwiper?: boolean }) { const uniqMedia = uniqBy(media, "url") const hoverRef = useRef(null) - const [enableSwipe, setEnableSwipe] = useState(!!forceSwiper) - useHover( - (event) => { - if (event.active) { - setEnableSwipe(event.active) - } - }, - { - target: hoverRef, - enabled: !forceSwiper, - }, - ) + + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [WheelGesturesPlugin()]) + + const scrollPrev = useCallback(() => { + if (emblaApi) emblaApi.scrollPrev() + }, [emblaApi]) + + const scrollNext = useCallback(() => { + if (emblaApi) emblaApi.scrollNext() + }, [emblaApi]) if (!media) return null + return (
- {enableSwipe && (uniqMedia?.length || 0) > 1 ? ( - <> - + {uniqMedia?.length ? ( +
+
{uniqMedia?.slice(0, 5).map((med, i) => ( - +
- +
))} - -
e.stopPropagation()} - > -
-
e.stopPropagation()} - > - -
- - ) : uniqMedia?.length >= 1 ? ( - { - e.stopPropagation() - onPreview?.(uniqMedia) - }} - cacheDimensions={uniqMedia[0].type === "photo"} - className="size-full rounded-none object-cover sm:transition-transform sm:duration-300 sm:ease-in-out sm:group-hover:scale-105" - alt="cover" - src={uniqMedia[0].url} - type={uniqMedia[0].type} - previewImageUrl={uniqMedia[0].preview_image_url} - loading="lazy" - proxy={proxySize} - showFallback={true} - height={uniqMedia[0].height} - width={uniqMedia[0].width} - blurhash={uniqMedia[0].blurhash} - fitContent - /> + {emblaApi?.canScrollPrev() && ( + + )} + {emblaApi?.canScrollNext() && ( + + )} +
) : (
diff --git a/apps/renderer/src/components/ui/media/VideoPlayer.tsx b/apps/renderer/src/components/ui/media/VideoPlayer.tsx index b4a9a7e8a4..4f77023b32 100644 --- a/apps/renderer/src/components/ui/media/VideoPlayer.tsx +++ b/apps/renderer/src/components/ui/media/VideoPlayer.tsx @@ -378,7 +378,7 @@ const PlayProgressBar = () => { }) return ( = ({ media, initialIndex = 0, children }) => { + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, startIndex: initialIndex }, [ + WheelGesturesPlugin(), + ]) const [currentMedia, setCurrentMedia] = useState(media[initialIndex]) - const [currentSlideIndex, setCurrentSlideIndex] = useState(initialIndex) - const swiperRef = useRef(null) + // This only to delay show + const [currentSlideIndex, setCurrentSlideIndex] = useState(initialIndex) const [showActions, setShowActions] = useState(false) useEffect(() => { @@ -128,6 +130,31 @@ export const PreviewMediaContent: FC<{ return () => clearTimeout(timer) }, []) + useEffect(() => { + if (emblaApi) { + emblaApi.on("select", () => { + const realIndex = emblaApi.selectedScrollSnap() + setCurrentMedia(media[realIndex]) + setCurrentSlideIndex(realIndex) + }) + } + }, [emblaApi, media]) + + const { ref } = useCurrentModal() + + // Keyboard + useEffect(() => { + if (!emblaApi) return + const $container = ref.current + if (!$container) return + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowLeft") emblaApi?.scrollPrev() + if (e.key === "ArrowRight") emblaApi?.scrollNext() + } + $container.addEventListener("keydown", handleKeyDown) + return () => $container.removeEventListener("keydown", handleKeyDown) + }, [emblaApi, ref]) + if (media.length === 0) return null if (media.length === 1) { const src = media[0].url @@ -163,30 +190,44 @@ export const PreviewMediaContent: FC<{ const isVideo = currentMedia.type === "video" return ( - { - setCurrentMedia(media[realIndex]) - setCurrentSlideIndex(realIndex) - }} - modules={[Mousewheel, Keyboard]} - className="h-full w-auto" - > +
+
+ {media.map((med) => ( +
+ {med.type === "video" ? ( + e.stopPropagation()} + /> + ) : ( + + )} +
+ ))} +
+ {showActions && (
swiperRef.current?.swiper.slidePrev()} + whileTap={{ transform: "translate3d(0, 0, 0) scale(0.9)" }} + onClick={() => emblaApi?.scrollPrev()} type="button" className="center absolute left-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900" > @@ -194,10 +235,11 @@ export const PreviewMediaContent: FC<{ swiperRef.current?.swiper.slideNext()} + whileTap={{ transform: "translate3d(0, 0, 0) scale(0.9)" }} + onClick={() => emblaApi?.scrollNext()} type="button" className="center absolute right-2 top-1/2 z-[99] size-8 -translate-y-1/2 rounded-full border border-white/20 bg-neutral-900/80 text-white backdrop-blur duration-200 hover:bg-neutral-900" > @@ -229,7 +271,7 @@ export const PreviewMediaContent: FC<{ .map((_, index) => (
)} - - {media.map((med, index) => ( - - {med.type === "video" ? ( - e.stopPropagation()} - /> - ) : ( - - )} - - ))} -
+
) } diff --git a/apps/renderer/src/components/ui/modal/stacked/modal.tsx b/apps/renderer/src/components/ui/modal/stacked/modal.tsx index a454ac5bf3..66dc1f0f13 100644 --- a/apps/renderer/src/components/ui/modal/stacked/modal.tsx +++ b/apps/renderer/src/components/ui/modal/stacked/modal.tsx @@ -136,7 +136,7 @@ export const ModalInternal = memo( const { noticeModal, animateController } = useModalAnimate(!!isTop) const getIndex = useEventCallback(() => index) - const modalContentRef = useRef(null) + const [modalContentRef, setModalContentRef] = useState(null) const ModalProps: ModalActionsInternal = useMemo( () => ({ dismiss: close, @@ -159,9 +159,9 @@ export const ModalInternal = memo( const ModalContextProps = useMemo( () => ({ ...ModalProps, - ref: modalContentRef, + ref: { current: modalContentRef }, }), - [ModalProps], + [ModalProps, modalContentRef], ) const [edgeElementRef, setEdgeElementRef] = useState(null) @@ -239,6 +239,7 @@ export const ModalInternal = memo( {Overlay} {title} {Overlay} { if (ELECTRON) log(...args) diff --git a/apps/renderer/src/modules/entry-column/Items/picture-item.tsx b/apps/renderer/src/modules/entry-column/Items/picture-item.tsx index a05ee65fa7..50f3d7de02 100644 --- a/apps/renderer/src/modules/entry-column/Items/picture-item.tsx +++ b/apps/renderer/src/modules/entry-column/Items/picture-item.tsx @@ -50,7 +50,6 @@ export function PictureItem({ entryId, entryPreview, translation }: UniversalIte isActive && "rounded-b-none", )} imgClassName="object-cover" - uniqueKey={entryId} onPreview={(media, i) => { previewMedia(media, i) }} @@ -127,7 +126,6 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({ className={cn("w-full shrink-0 grow rounded-md", isActive && "rounded-b-none")} proxySize={proxySize} imgClassName="object-cover" - uniqueKey={entryId} onPreview={previewMedia} /> diff --git a/packages/logger/electron.ts b/packages/logger/electron.ts new file mode 100644 index 0000000000..c53fb89f00 --- /dev/null +++ b/packages/logger/electron.ts @@ -0,0 +1 @@ +export { initialize, log } from "electron-log" diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000000..0869f31c4b --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,14 @@ +{ + "name": "@follow/logger", + "version": "0.0.1", + "exports": { + ".": { + "types": "./electron.ts", + "web": "./web.ts", + "default": "./electron.ts" + } + }, + "dependencies": { + "electron-log": "5.2.2" + } +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000000..343febd5f4 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.extend.json", + "compilerOptions": { + "noEmit": true, + "baseUrl": ".", + "jsx": "preserve", + "declaration": true, + "paths": { + "@pkg": ["../../package.json"] + } + }, + "include": ["**/*"] +} diff --git a/packages/logger/web.ts b/packages/logger/web.ts new file mode 100644 index 0000000000..1d0c9d1d7e --- /dev/null +++ b/packages/logger/web.ts @@ -0,0 +1,6 @@ +export const log = (...args: any[]) => { + // eslint-disable-next-line no-console + console.log(...args) +} + +export const initialize = () => {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1aec1c304..c50d03ca65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,6 +453,12 @@ importers: electron-log: specifier: 5.2.0 version: 5.2.0 + embla-carousel-react: + specifier: 8.3.1 + version: 8.3.1(react@18.3.1) + embla-carousel-wheel-gestures: + specifier: 8.0.1 + version: 8.0.1(embla-carousel@8.3.1) firebase: specifier: 10.14.1 version: 10.14.1 @@ -597,9 +603,6 @@ importers: sonner: specifier: 1.5.0 version: 1.5.0(patch_hash=6s3tquyt5wnkqaogymn3mkivuq)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - swiper: - specifier: 11.1.14 - version: 11.1.14 tldts: specifier: 6.1.58 version: 6.1.58 @@ -643,6 +646,9 @@ importers: '@follow/hooks': specifier: workspace:* version: link:../../packages/hooks + '@follow/logger': + specifier: workspace:* + version: link:../../packages/logger '@follow/models': specifier: workspace:* version: link:../../packages/models @@ -1032,6 +1038,12 @@ importers: specifier: 3.1.0 version: 3.1.0(react@18.3.1) + packages/logger: + dependencies: + electron-log: + specifier: 5.2.2 + version: 5.2.2 + packages/models: dependencies: '@auth/core': @@ -6080,6 +6092,10 @@ packages: resolution: {integrity: sha512-VjLkvaLmbP3AOGOh5Fob9M8bFU0mmeSAb5G2EoTBx+kQLf2XA/0byzjsVGBTHhikbT+m1AB27NEQUv9wX9nM8w==} engines: {node: '>= 14'} + electron-log@5.2.2: + resolution: {integrity: sha512-fgvx6srjIHDowJD8WAAjoAXmiTyOz6JnGQoxOtk1mXw7o4S+HutuPHLCsk24xTXqWZgy4uO63NbedG+oEvldLw==} + engines: {node: '>= 14'} + electron-packager-languages@0.5.0: resolution: {integrity: sha512-ryJsVXgHq0+7eZpJ+YSUQNYUnH4yPq2J4gXtmP9HEq8N6PHtygLEmohKMm4VrwI5qTir4HRCxy+O1UNo8mbwgg==} engines: {node: '>6.0.0'} @@ -6125,6 +6141,25 @@ packages: elliptic@6.5.7: resolution: {integrity: sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==} + embla-carousel-react@8.3.1: + resolution: {integrity: sha512-gBY0zM+2ASvKFwRpTIOn2SLifFqOKKap9R/y0iCpJWS3bc8OHVEn2gAThGYl2uq0N+hu9aBiswffL++OYZOmDQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.3.1: + resolution: {integrity: sha512-Js6rTTINNGnUGPu7l5kTcheoSbEnP5Ak2iX0G9uOoI8okTNLMzuWlEIpYFd1WP0Sq82FFcLkKM2oiO6jcElZ/Q==} + peerDependencies: + embla-carousel: 8.3.1 + + embla-carousel-wheel-gestures@8.0.1: + resolution: {integrity: sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==} + engines: {node: '>=10'} + peerDependencies: + embla-carousel: ^8.0.0 || ~8.0.0-rc03 + + embla-carousel@8.3.1: + resolution: {integrity: sha512-DutFjtEO586XptDn4cwvBJwsR/8fMa4jUk5Jk2g+/elKgu8mdn0Z2sx33g4JskvbLc1/6P8Xg4QlfELGJFcP5A==} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -9916,10 +9951,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - swiper@11.1.14: - resolution: {integrity: sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==} - engines: {node: '>= 4.7.0'} - synckit@0.6.2: resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} engines: {node: '>=12.20'} @@ -10617,6 +10648,10 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + wheel-gestures@2.2.48: + resolution: {integrity: sha512-f+Gy33Oa5Z14XY9679Zze+7VFhbsQfBFXodnU2x589l4kxGM9L5Y8zETTmcMR5pWOPQyRv4Z0lNax6xCO0NSlA==} + engines: {node: '>=18'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -16831,6 +16866,8 @@ snapshots: electron-log@5.2.0: {} + electron-log@5.2.2: {} + electron-packager-languages@0.5.0: dependencies: rimraf: 2.7.1 @@ -16925,6 +16962,23 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + embla-carousel-react@8.3.1(react@18.3.1): + dependencies: + embla-carousel: 8.3.1 + embla-carousel-reactive-utils: 8.3.1(embla-carousel@8.3.1) + react: 18.3.1 + + embla-carousel-reactive-utils@8.3.1(embla-carousel@8.3.1): + dependencies: + embla-carousel: 8.3.1 + + embla-carousel-wheel-gestures@8.0.1(embla-carousel@8.3.1): + dependencies: + embla-carousel: 8.3.1 + wheel-gestures: 2.2.48 + + embla-carousel@8.3.1: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -21424,8 +21478,6 @@ snapshots: csso: 5.0.5 picocolors: 1.1.0 - swiper@11.1.14: {} - synckit@0.6.2: dependencies: tslib: 2.7.0 @@ -22163,6 +22215,8 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 + wheel-gestures@2.2.48: {} + which@1.3.1: dependencies: isexe: 2.0.0 diff --git a/vite.config.ts b/vite.config.ts index e7e1e0df05..f716690da7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -65,6 +65,12 @@ export default ({ mode }) => { ignored: ["**/dist/**", "**/out/**", "**/public/**", ".git/**"], }, }, + resolve: { + alias: { + ...viteRenderBaseConfig.resolve?.alias, + "@follow/logger": resolve(__dirname, "./packages/logger/web.ts"), + }, + }, plugins: [ ...((viteRenderBaseConfig.plugins ?? []) as any), mode !== "development" && @@ -143,8 +149,6 @@ export default ({ mode }) => { ["shiki", "@shikijs/transformers"], ["@sentry/react", "@openpanel/web"], ["zod", "react-hook-form", "@hookform/resolvers"], - - ["swiper"], ]), createPlatformSpecificImportPlugin(false),