From f841d2ada2fdd3a02bc8683f5c0cce480feb8de1 Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 5 Jun 2025 13:27:09 +0800 Subject: [PATCH] feat: init --- .cursor/rules/builder-storage.mdc | 128 + .cursor/rules/color.mdc | 191 + .env.template | 7 + .github/workflows/build.yml | 41 + .gitignore | 20 + .prettierrc.mjs | 5 + .vscode/settings.json | 39 + README.md | 119 + apps/web/.gitignore | 6 + apps/web/index.html | 399 + apps/web/package.json | 95 + apps/web/public/android-chrome-192x192.png | Bin 0 -> 7509 bytes apps/web/public/android-chrome-512x512.png | Bin 0 -> 25695 bytes apps/web/public/apple-touch-icon.png | Bin 0 -> 6832 bytes apps/web/public/favicon-16x16.png | Bin 0 -> 547 bytes apps/web/public/favicon-32x32.png | Bin 0 -> 1062 bytes apps/web/public/favicon-48x48.png | Bin 0 -> 1614 bytes apps/web/public/favicon.ico | Bin 0 -> 1062 bytes apps/web/public/site.webmanifest | 19 + apps/web/scripts/build.ts | 16 + apps/web/scripts/dev.ts | 18 + apps/web/scripts/precheck.ts | 35 + apps/web/scripts/pull-remote.ts | 49 + apps/web/src/App.tsx | 18 + apps/web/src/assets/fonts/GeistVF.woff2 | Bin 0 -> 56800 bytes apps/web/src/atoms/app.ts | 9 + apps/web/src/atoms/context-menu.ts | 195 + apps/web/src/atoms/route.ts | 43 + apps/web/src/atoms/viewport.ts | 38 + .../src/components/common/ErrorElement.tsx | 77 + .../common/LoadRemixAsyncComponent.tsx | 53 + apps/web/src/components/common/NotFound.tsx | 25 + .../components/common/PassiveFragmenet.tsx | 7 + .../components/common/ProviderComposer.tsx | 10 + apps/web/src/components/ui/button/Button.tsx | 135 + .../src/components/ui/button/MotionButton.tsx | 25 + apps/web/src/components/ui/button/index.ts | 2 + apps/web/src/components/ui/checkbox.tsx | 51 + .../ui/context-menu/context-menu.tsx | 202 + .../src/components/ui/context-menu/index.ts | 1 + apps/web/src/components/ui/dropdown-menu.tsx | 205 + apps/web/src/components/ui/loading.tsx | 19 + .../components/ui/photo-viewer/ExifPanel.tsx | 909 ++ .../ui/photo-viewer/GalleryThumbnail.tsx | 98 + .../components/ui/photo-viewer/LivePhoto.tsx | 316 + .../ui/photo-viewer/LoadingIndicator.tsx | 114 + .../ui/photo-viewer/PhotoViewer.css | 87 + .../ui/photo-viewer/PhotoViewer.tsx | 401 + .../ui/photo-viewer/ProgressiveImage.tsx | 321 + .../components/ui/photo-viewer/SharePanel.tsx | 289 + .../src/components/ui/photo-viewer/index.ts | 5 + apps/web/src/components/ui/portal/index.tsx | 16 + .../web/src/components/ui/portal/provider.tsx | 15 + .../components/ui/scroll-areas/ScrollArea.tsx | 180 + .../web/src/components/ui/scroll-areas/ctx.ts | 5 + .../src/components/ui/scroll-areas/hooks.ts | 9 + apps/web/src/components/ui/sonner.tsx | 9 + apps/web/src/components/ui/tooltip/index.tsx | 51 + apps/web/src/components/ui/tooltip/styles.ts | 10 + .../ui/typography/EllipsisWithTooltip.tsx | 101 + .../web/src/components/ui/typography/index.ts | 1 + apps/web/src/core/README.md | 153 + apps/web/src/core/builder/builder.ts | 346 + apps/web/src/core/builder/index.ts | 5 + apps/web/src/core/cli.ts | 180 + apps/web/src/core/constants/index.ts | 15 + apps/web/src/core/image/blurhash.ts | 76 + apps/web/src/core/image/exif.ts | 144 + apps/web/src/core/image/processor.ts | 101 + apps/web/src/core/image/thumbnail.ts | 121 + apps/web/src/core/index.ts | 61 + apps/web/src/core/logger/index.ts | 24 + apps/web/src/core/manifest/manager.ts | 99 + apps/web/src/core/photo/info-extractor.ts | 117 + apps/web/src/core/photo/processor.ts | 248 + apps/web/src/core/runAsWorker.ts | 291 + apps/web/src/core/s3/client.ts | 27 + apps/web/src/core/s3/operations.ts | 10 + apps/web/src/core/storage/adapters.ts | 79 + apps/web/src/core/storage/factory.ts | 42 + apps/web/src/core/storage/index.ts | 16 + apps/web/src/core/storage/interfaces.ts | 69 + apps/web/src/core/storage/manager.ts | 83 + apps/web/src/core/storage/providers/README.md | 157 + .../core/storage/providers/github-provider.ts | 278 + .../src/core/storage/providers/s3-provider.ts | 194 + apps/web/src/core/types/photo.ts | 48 + apps/web/src/core/worker/cluster-pool.ts | 462 + apps/web/src/core/worker/pool.ts | 71 + apps/web/src/data/photos.ts | 37 + apps/web/src/framer-lazy-feature.ts | 1 + apps/web/src/global.d.ts | 55 + apps/web/src/hooks/common/index.ts | 4 + apps/web/src/hooks/common/useDark.ts | 73 + .../src/hooks/common/useInputComposition.ts | 98 + apps/web/src/hooks/common/useIsOnline.ts | 20 + apps/web/src/hooks/common/usePrevious.ts | 9 + apps/web/src/hooks/common/useRefValue.ts | 14 + apps/web/src/hooks/common/useTitle.ts | 14 + apps/web/src/hooks/useMobile.ts | 10 + apps/web/src/hooks/usePhotoViewer.ts | 121 + apps/web/src/hooks/useTypeScriptCallback.ts | 6 + apps/web/src/hooks/useViewport.ts | 21 + apps/web/src/icons/index.tsx | 107 + apps/web/src/lib/blob-url-manager.ts | 109 + apps/web/src/lib/cn.ts | 44 + apps/web/src/lib/defineQuery.ts | 188 + apps/web/src/lib/dev.tsx | 59 + apps/web/src/lib/device-viewport.ts | 13 + apps/web/src/lib/dom.ts | 23 + apps/web/src/lib/feature.ts | 5 + apps/web/src/lib/heic-converter.ts | 170 + apps/web/src/lib/image-loader-manager.ts | 441 + apps/web/src/lib/image-utils.ts | 79 + apps/web/src/lib/jotai.ts | 39 + apps/web/src/lib/lru-cache.ts | 170 + apps/web/src/lib/ns.ts | 11 + apps/web/src/lib/query-client.ts | 22 + apps/web/src/lib/route-builder.ts | 204 + apps/web/src/lib/scroller.ts | 83 + apps/web/src/lib/spring.ts | 75 + apps/web/src/lib/video-converter.ts | 473 + apps/web/src/main.tsx | 18 + apps/web/src/modules/gallery/ActionGroup.tsx | 153 + apps/web/src/modules/gallery/Masonic.tsx | 242 + .../gallery/MasonryHeaderMasonryItem.tsx | 62 + apps/web/src/modules/gallery/MasonryRoot.tsx | 195 + .../src/modules/gallery/PhotoMasonryItem.tsx | 423 + apps/web/src/modules/gallery/photo.module.css | 16 + apps/web/src/pages/(debug)/blurhash.tsx | 39 + apps/web/src/pages/(debug)/webgl-preview.tsx | 39 + apps/web/src/pages/(main)/[photoId]/index.tsx | 23 + apps/web/src/pages/(main)/layout.tsx | 107 + .../src/providers/context-menu-provider.tsx | 150 + apps/web/src/providers/event-provider.tsx | 43 + apps/web/src/providers/root-providers.tsx | 38 + apps/web/src/providers/setting-sync.tsx | 7 + .../src/providers/stable-router-provider.tsx | 50 + apps/web/src/router.tsx | 22 + apps/web/src/store/.gitkeep | 0 apps/web/src/styles/index.css | 1 + apps/web/src/styles/tailwind.css | 177 + apps/web/src/types/photo.ts | 23 + apps/web/src/vite-env.d.ts | 1 + apps/web/tsconfig.json | 45 + apps/web/vercel.json | 1 + apps/web/vite.config.ts | 61 + builder.config.example.json | 6 + builder.config.ts | 138 + config.example.json | 21 + config/site.config.ts | 1 + docs/font-extraction.md | 165 + env.ts | 21 + eslint.config.mjs | 46 + package.json | 56 + packages/webgl-viewer/bump.config.ts | 9 + packages/webgl-viewer/package.json | 34 + packages/webgl-viewer/readme.md | 290 + packages/webgl-viewer/src/DebugInfo.tsx | 134 + .../webgl-viewer/src/WebGLImageViewer.tsx | 176 + .../src/WebGLImageViewerEngine.ts | 1192 ++ packages/webgl-viewer/src/constants.ts | 81 + packages/webgl-viewer/src/index.ts | 12 + packages/webgl-viewer/src/interface.ts | 70 + packages/webgl-viewer/src/shaders.ts | 64 + plugins/eslint-recursive-sort.js | 60 + plugins/og-image-plugin.ts | 98 + plugins/vite/deps.ts | 47 + pnpm-lock.yaml | 9674 +++++++++++++++++ pnpm-workspace.yaml | 3 + renovate.json | 5 + scripts/build-update-remote-repo.sh | 13 + scripts/cleanup-og-images.ts | 57 + scripts/extract-font-glyphs.ts | 398 + scripts/generate-favicon.ts | 147 + scripts/generate-og-image.ts | 394 + scripts/photo-loader.ts | 53 + scripts/preinstall.sh | 2 + scripts/svg-text-renderer.ts | 640 ++ scripts/test-svg-text.ts | 167 + tsconfig.json | 24 + 181 files changed, 27788 insertions(+) create mode 100644 .cursor/rules/builder-storage.mdc create mode 100644 .cursor/rules/color.mdc create mode 100644 .env.template create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .prettierrc.mjs create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 apps/web/.gitignore create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/public/android-chrome-192x192.png create mode 100644 apps/web/public/android-chrome-512x512.png create mode 100644 apps/web/public/apple-touch-icon.png create mode 100644 apps/web/public/favicon-16x16.png create mode 100644 apps/web/public/favicon-32x32.png create mode 100644 apps/web/public/favicon-48x48.png create mode 100644 apps/web/public/favicon.ico create mode 100644 apps/web/public/site.webmanifest create mode 100644 apps/web/scripts/build.ts create mode 100644 apps/web/scripts/dev.ts create mode 100644 apps/web/scripts/precheck.ts create mode 100644 apps/web/scripts/pull-remote.ts create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/assets/fonts/GeistVF.woff2 create mode 100644 apps/web/src/atoms/app.ts create mode 100644 apps/web/src/atoms/context-menu.ts create mode 100644 apps/web/src/atoms/route.ts create mode 100644 apps/web/src/atoms/viewport.ts create mode 100644 apps/web/src/components/common/ErrorElement.tsx create mode 100644 apps/web/src/components/common/LoadRemixAsyncComponent.tsx create mode 100644 apps/web/src/components/common/NotFound.tsx create mode 100644 apps/web/src/components/common/PassiveFragmenet.tsx create mode 100644 apps/web/src/components/common/ProviderComposer.tsx create mode 100644 apps/web/src/components/ui/button/Button.tsx create mode 100644 apps/web/src/components/ui/button/MotionButton.tsx create mode 100644 apps/web/src/components/ui/button/index.ts create mode 100644 apps/web/src/components/ui/checkbox.tsx create mode 100644 apps/web/src/components/ui/context-menu/context-menu.tsx create mode 100644 apps/web/src/components/ui/context-menu/index.ts create mode 100644 apps/web/src/components/ui/dropdown-menu.tsx create mode 100644 apps/web/src/components/ui/loading.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/ExifPanel.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/LivePhoto.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/LoadingIndicator.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/PhotoViewer.css create mode 100644 apps/web/src/components/ui/photo-viewer/PhotoViewer.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/ProgressiveImage.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/SharePanel.tsx create mode 100644 apps/web/src/components/ui/photo-viewer/index.ts create mode 100644 apps/web/src/components/ui/portal/index.tsx create mode 100644 apps/web/src/components/ui/portal/provider.tsx create mode 100644 apps/web/src/components/ui/scroll-areas/ScrollArea.tsx create mode 100644 apps/web/src/components/ui/scroll-areas/ctx.ts create mode 100644 apps/web/src/components/ui/scroll-areas/hooks.ts create mode 100644 apps/web/src/components/ui/sonner.tsx create mode 100644 apps/web/src/components/ui/tooltip/index.tsx create mode 100644 apps/web/src/components/ui/tooltip/styles.ts create mode 100644 apps/web/src/components/ui/typography/EllipsisWithTooltip.tsx create mode 100644 apps/web/src/components/ui/typography/index.ts create mode 100644 apps/web/src/core/README.md create mode 100644 apps/web/src/core/builder/builder.ts create mode 100644 apps/web/src/core/builder/index.ts create mode 100644 apps/web/src/core/cli.ts create mode 100644 apps/web/src/core/constants/index.ts create mode 100644 apps/web/src/core/image/blurhash.ts create mode 100644 apps/web/src/core/image/exif.ts create mode 100644 apps/web/src/core/image/processor.ts create mode 100644 apps/web/src/core/image/thumbnail.ts create mode 100644 apps/web/src/core/index.ts create mode 100644 apps/web/src/core/logger/index.ts create mode 100644 apps/web/src/core/manifest/manager.ts create mode 100644 apps/web/src/core/photo/info-extractor.ts create mode 100644 apps/web/src/core/photo/processor.ts create mode 100644 apps/web/src/core/runAsWorker.ts create mode 100644 apps/web/src/core/s3/client.ts create mode 100644 apps/web/src/core/s3/operations.ts create mode 100644 apps/web/src/core/storage/adapters.ts create mode 100644 apps/web/src/core/storage/factory.ts create mode 100644 apps/web/src/core/storage/index.ts create mode 100644 apps/web/src/core/storage/interfaces.ts create mode 100644 apps/web/src/core/storage/manager.ts create mode 100644 apps/web/src/core/storage/providers/README.md create mode 100644 apps/web/src/core/storage/providers/github-provider.ts create mode 100644 apps/web/src/core/storage/providers/s3-provider.ts create mode 100644 apps/web/src/core/types/photo.ts create mode 100644 apps/web/src/core/worker/cluster-pool.ts create mode 100644 apps/web/src/core/worker/pool.ts create mode 100644 apps/web/src/data/photos.ts create mode 100644 apps/web/src/framer-lazy-feature.ts create mode 100644 apps/web/src/global.d.ts create mode 100644 apps/web/src/hooks/common/index.ts create mode 100644 apps/web/src/hooks/common/useDark.ts create mode 100644 apps/web/src/hooks/common/useInputComposition.ts create mode 100644 apps/web/src/hooks/common/useIsOnline.ts create mode 100644 apps/web/src/hooks/common/usePrevious.ts create mode 100644 apps/web/src/hooks/common/useRefValue.ts create mode 100644 apps/web/src/hooks/common/useTitle.ts create mode 100644 apps/web/src/hooks/useMobile.ts create mode 100644 apps/web/src/hooks/usePhotoViewer.ts create mode 100644 apps/web/src/hooks/useTypeScriptCallback.ts create mode 100644 apps/web/src/hooks/useViewport.ts create mode 100644 apps/web/src/icons/index.tsx create mode 100644 apps/web/src/lib/blob-url-manager.ts create mode 100644 apps/web/src/lib/cn.ts create mode 100644 apps/web/src/lib/defineQuery.ts create mode 100644 apps/web/src/lib/dev.tsx create mode 100644 apps/web/src/lib/device-viewport.ts create mode 100644 apps/web/src/lib/dom.ts create mode 100644 apps/web/src/lib/feature.ts create mode 100644 apps/web/src/lib/heic-converter.ts create mode 100644 apps/web/src/lib/image-loader-manager.ts create mode 100644 apps/web/src/lib/image-utils.ts create mode 100644 apps/web/src/lib/jotai.ts create mode 100644 apps/web/src/lib/lru-cache.ts create mode 100644 apps/web/src/lib/ns.ts create mode 100644 apps/web/src/lib/query-client.ts create mode 100644 apps/web/src/lib/route-builder.ts create mode 100644 apps/web/src/lib/scroller.ts create mode 100644 apps/web/src/lib/spring.ts create mode 100644 apps/web/src/lib/video-converter.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/modules/gallery/ActionGroup.tsx create mode 100644 apps/web/src/modules/gallery/Masonic.tsx create mode 100644 apps/web/src/modules/gallery/MasonryHeaderMasonryItem.tsx create mode 100644 apps/web/src/modules/gallery/MasonryRoot.tsx create mode 100644 apps/web/src/modules/gallery/PhotoMasonryItem.tsx create mode 100644 apps/web/src/modules/gallery/photo.module.css create mode 100644 apps/web/src/pages/(debug)/blurhash.tsx create mode 100644 apps/web/src/pages/(debug)/webgl-preview.tsx create mode 100644 apps/web/src/pages/(main)/[photoId]/index.tsx create mode 100644 apps/web/src/pages/(main)/layout.tsx create mode 100644 apps/web/src/providers/context-menu-provider.tsx create mode 100644 apps/web/src/providers/event-provider.tsx create mode 100644 apps/web/src/providers/root-providers.tsx create mode 100644 apps/web/src/providers/setting-sync.tsx create mode 100644 apps/web/src/providers/stable-router-provider.tsx create mode 100644 apps/web/src/router.tsx create mode 100644 apps/web/src/store/.gitkeep create mode 100644 apps/web/src/styles/index.css create mode 100644 apps/web/src/styles/tailwind.css create mode 100644 apps/web/src/types/photo.ts create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vercel.json create mode 100644 apps/web/vite.config.ts create mode 100644 builder.config.example.json create mode 100644 builder.config.ts create mode 100644 config.example.json create mode 100644 config/site.config.ts create mode 100644 docs/font-extraction.md create mode 100644 env.ts create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 packages/webgl-viewer/bump.config.ts create mode 100644 packages/webgl-viewer/package.json create mode 100644 packages/webgl-viewer/readme.md create mode 100644 packages/webgl-viewer/src/DebugInfo.tsx create mode 100644 packages/webgl-viewer/src/WebGLImageViewer.tsx create mode 100644 packages/webgl-viewer/src/WebGLImageViewerEngine.ts create mode 100644 packages/webgl-viewer/src/constants.ts create mode 100644 packages/webgl-viewer/src/index.ts create mode 100644 packages/webgl-viewer/src/interface.ts create mode 100644 packages/webgl-viewer/src/shaders.ts create mode 100644 plugins/eslint-recursive-sort.js create mode 100644 plugins/og-image-plugin.ts create mode 100644 plugins/vite/deps.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 scripts/build-update-remote-repo.sh create mode 100644 scripts/cleanup-og-images.ts create mode 100644 scripts/extract-font-glyphs.ts create mode 100644 scripts/generate-favicon.ts create mode 100644 scripts/generate-og-image.ts create mode 100644 scripts/photo-loader.ts create mode 100644 scripts/preinstall.sh create mode 100644 scripts/svg-text-renderer.ts create mode 100644 scripts/test-svg-text.ts create mode 100644 tsconfig.json diff --git a/.cursor/rules/builder-storage.mdc b/.cursor/rules/builder-storage.mdc new file mode 100644 index 00000000..eef9c7b5 --- /dev/null +++ b/.cursor/rules/builder-storage.mdc @@ -0,0 +1,128 @@ +--- +description: +globs: src/core/**/*.ts +alwaysApply: false +--- +# 存储抽象层 + +本模块使用适配器模式提供了统一的存储接口,支持多种存储提供商。目前已实现 S3 存储提供商,后续可以方便地扩展其他存储服务。 + +## 架构设计 + +- **`interfaces.ts`**: 定义通用的存储接口和数据结构 +- **`providers/`**: 具体的存储提供商实现 + - **`s3-provider.ts`**: S3 存储提供商实现 +- **`factory.ts`**: 存储提供商工厂,负责创建具体的存储实例 +- **`manager.ts`**: 存储管理器,提供统一的存储操作接口 +- **`adapters.ts`**: 适配器函数,保持与原有 API 的兼容性 + +## 使用方式 + +### 推荐方式:使用 StorageManager + +```typescript +import { defaultStorageManager, StorageManager } from '@/core/storage' + +// 使用默认存储管理器(基于环境变量配置) +const buffer = await defaultStorageManager.getFile('path/to/image.jpg') +const images = await defaultStorageManager.listImages() +const allFiles = await defaultStorageManager.listAllFiles() +const publicUrl = defaultStorageManager.generatePublicUrl('path/to/image.jpg') +const livePhotos = await defaultStorageManager.detectLivePhotos() + +// 或者创建自定义配置的存储管理器 +const customManager = new StorageManager({ + provider: 's3', + bucket: 'my-bucket', + region: 'us-east-1', + endpoint: 'https://s3.amazonaws.com', + prefix: 'photos/', + customDomain: 'https://cdn.example.com' +}) +``` + +### 兼容性方式:使用原有 API + +```typescript +// 这些函数仍然可用,但标记为 deprecated +import { + getImageFromS3, + listImagesFromS3, + listAllFilesFromS3, + detectLivePhotos, + generateS3Url +} from '@/core/s3/operations' + +const buffer = await getImageFromS3('path/to/image.jpg') +const images = await listImagesFromS3() +``` + +### 直接使用存储提供商 + +```typescript +import { S3StorageProvider } from '@/core/storage' + +const s3Provider = new S3StorageProvider({ + provider: 's3', + bucket: 'my-bucket', + region: 'us-east-1', + // ... 其他配置 +}) + +const buffer = await s3Provider.getFile('path/to/image.jpg') +``` + +## 扩展新的存储提供商 + +1. 实现 `StorageProvider` 接口: + +```typescript +import { StorageProvider, StorageObject } from '@/core/storage/interfaces' + +export class MyStorageProvider implements StorageProvider { + async getFile(key: string, logger?: Logger['s3']): Promise { + // 实现文件获取逻辑 + } + + async listImages(): Promise { + // 实现图片列表逻辑 + } + + // ... 实现其他接口方法 +} +``` + +2. 在工厂类中注册新的提供商: + +```typescript +// 在 factory.ts 中添加新的 case +case 'my-storage': + return new MyStorageProvider(config) +``` + +3. 更新 `StorageConfig` 接口的 `provider` 类型。 + +## 优势 + +1. **解耦**: 业务逻辑与具体存储实现分离 +2. **可扩展**: 轻松添加新的存储提供商 +3. **统一接口**: 所有存储操作使用相同的 API +4. **向后兼容**: 保持原有 API 的兼容性 +5. **类型安全**: 完整的 TypeScript 类型支持 +6. **配置灵活**: 支持多种配置方式 + +## 迁移指南 + +现有代码可以继续使用原有的 S3 函数,无需立即修改。建议在新代码中使用 `StorageManager` API,并逐步迁移现有代码。 + +旧代码: +```typescript +import { getImageFromS3 } from '@/core/s3/operations' +const buffer = await getImageFromS3('key') +``` + +新代码: +```typescript +import { defaultStorageManager } from '@/core/storage' +const buffer = await defaultStorageManager.getFile('key') +``` \ No newline at end of file diff --git a/.cursor/rules/color.mdc b/.cursor/rules/color.mdc new file mode 100644 index 00000000..c7411dec --- /dev/null +++ b/.cursor/rules/color.mdc @@ -0,0 +1,191 @@ +--- +description: +globs: +alwaysApply: false +--- +# UIKit Colors for Tailwind CSS + +You should use @https://github.com/Innei/apple-uikit-colors/blob/main/packages/uikit-colors/macos.ts TailwindCSS atom classname. + +## System Colors +red +orange +yellow +green +mint +teal +cyan +blue +indigo +purple +pink +brown +gray + +## Fill Colors +fill +fill-secondary +fill-tertiary +fill-quaternary +fill-quinary +fill-vibrant +fill-vibrant-secondary +fill-vibrant-tertiary +fill-vibrant-quaternary +fill-vibrant-quinary + +## Text Colors +text +text-secondary +text-tertiary +text-quaternary +text-quinary +text-vibrant +text-vibrant-secondary +text-vibrant-tertiary +text-vibrant-quaternary +text-vibrant-quinary + +## Material Colors +material-ultra-thick +material-thick +material-medium +material-thin +material-ultra-thin +material-opaque + +## Control Colors +control-enabled +control-disabled + +## Interface Colors +menu +popover +titlebar +sidebar +selection-focused +selection-focused-fill +selection-unfocused +selection-unfocused-fill +header-view +tooltip +under-window-background + + +## Applied Colors +All above tailwind atom will match this colors. + +```css +@media (prefers-color-scheme: light) { + html { + --color-red: 255 69 58; + --color-orange: 255 149 0; + --color-yellow: 255 204 0; + --color-green: 40 205 65; + --color-mint: 0 199 190; + --color-teal: 89 173 196; + --color-cyan: 85 190 240; + --color-blue: 0 122 255; + --color-indigo: 88 86 214; + --color-purple: 175 82 222; + --color-pink: 255 45 85; + --color-brown: 162 132 94; + --color-gray: 142 142 147; + --color-fill: 0 0 0 / 0.1; + --color-fillSecondary: 0 0 0 / 0.08; + --color-fillTertiary: 0 0 0 / 0.05; + --color-fillQuaternary: 0 0 0 / 0.03; + --color-fillQuinary: 0 0 0 / 0.02; + --color-fillVibrant: 217 217 217; + --color-fillVibrantSecondary: 230 230 230; + --color-fillVibrantTertiary: 242 242 242; + --color-fillVibrantQuaternary: 247 247 247; + --color-fillVibrantQuinary: 251 251 251; + --color-text: 0 0 0 / 0.85; + --color-textSecondary: 0 0 0 / 0.5; + --color-textTertiary: 0 0 0 / 0.25; + --color-textQuaternary: 0 0 0 / 0.1; + --color-textQuinary: 0 0 0 / 0.05; + --color-textVibrant: 76 76 76; + --color-textVibrantSecondary: 128 128 128; + --color-textVibrantTertiary: 191 191 191; + --color-textVibrantQuaternary: 230 230 230; + --color-textVibrantQuinary: 242 242 242; + --color-materialUltraThick: 246 246 246 / 0.84; + --color-materialThick: 246 246 246 / 0.72; + --color-materialMedium: 246 246 246 / 0.6; + --color-materialThin: 246 246 246 / 0.48; + --color-materialUltraThin: 246 246 246 / 0.36; + --color-materialOpaque: 246 246 246; + --color-controlEnabled: 251 251 251; + --color-controlDisabled: 243 243 243; + --color-menu: 40 40 40 / 0.58; + --color-popover: 0 0 0 / 0.28; + --color-titlebar: 234 234 234 / 0.8; + --color-sidebar: 234 234 234 / 0.84; + --color-selectionFocused: 10 130 255 / 0.75; + --color-selectionFocusedFill: 10 130 255; + --color-selectionUnfocused: 0 0 0 / 0.1; + --color-selectionUnfocusedFill: 246 246 246 / 0.84; + --color-headerView: 255 255 255 / 0.8; + --color-tooltip: 246 246 246 / 0.6; + --color-underWindowBackground: 246 246 246 / 0.84; + } +} +@media (prefers-color-scheme: dark) { + html { + --color-red: 255 69 58; + --color-orange: 255 159 10; + --color-yellow: 255 214 10; + --color-green: 50 215 75; + --color-mint: 106 196 220; + --color-teal: 106 196 220; + --color-cyan: 90 200 245; + --color-blue: 10 132 255; + --color-indigo: 94 92 230; + --color-purple: 191 90 242; + --color-pink: 255 55 95; + --color-brown: 172 142 104; + --color-gray: 152 152 157; + --color-fill: 255 255 255 / 0.1; + --color-fillSecondary: 255 255 255 / 0.08; + --color-fillTertiary: 255 255 255 / 0.05; + --color-fillQuaternary: 255 255 255 / 0.03; + --color-fillQuinary: 255 255 255 / 0.02; + --color-fillVibrant: 36 36 36; + --color-fillVibrantSecondary: 20 20 20; + --color-fillVibrantTertiary: 13 13 13; + --color-fillVibrantQuaternary: 9 9 9; + --color-fillVibrantQuinary: 7 7 7; + --color-text: 255 255 255 / 0.85; + --color-textSecondary: 255 255 255 / 0.5; + --color-textTertiary: 255 255 255 / 0.25; + --color-textQuaternary: 255 255 255 / 0.1; + --color-textQuinary: 255 255 255 / 0.05; + --color-textVibrant: 229 229 229; + --color-textVibrantSecondary: 124 124 124; + --color-textVibrantTertiary: 65 65 65; + --color-textVibrantQuaternary: 35 35 35; + --color-textVibrantQuinary: 17 17 17; + --color-materialUltraThick: 40 40 40 / 0.84; + --color-materialThick: 40 40 40 / 0.72; + --color-materialMedium: 40 40 40 / 0.6; + --color-materialThin: 40 40 40 / 0.48; + --color-materialUltraThin: 40 40 40 / 0.36; + --color-materialOpaque: 40 40 40; + --color-controlEnabled: 255 255 255 / 0.2; + --color-controlDisabled: 255 255 255 / 0.1; + --color-menu: 246 246 246 / 0.72; + --color-popover: 246 246 246 / 0.6; + --color-titlebar: 60 60 60 / 0.8; + --color-sidebar: 0 0 0 / 0.45; + --color-selectionFocused: 10 130 255 / 0.75; + --color-selectionFocusedFill: 10 130 255; + --color-selectionUnfocused: 255 255 255 / 0.1; + --color-selectionUnfocusedFill: 40 40 40 / 0.65; + --color-headerView: 30 30 30 / 0.8; + --color-tooltip: 0 0 0 / 0.35; + --color-underWindowBackground: 0 0 0 / 0.45; + } +} +``` diff --git a/.env.template b/.env.template new file mode 100644 index 00000000..e7c25568 --- /dev/null +++ b/.env.template @@ -0,0 +1,7 @@ +S3_REGION=us-east-1 +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_BUCKET_NAME=images +S3_PREFIX= +S3_ENDPOINT= +S3_CUSTOM_DOMAIN= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..f0bb0f97 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Build + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [lts/*] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Cache pnpm modules + uses: actions/cache@v4 + env: + cache-name: cache-pnpm-modules + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}- + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: true + - run: pnpm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b90e6a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +public/og-image-*.png +test-svg-font-rendering.png +test-traditional-font-rendering.png +thomas-x2d-xcd-25v-1.jpg +thomas-x2d-xcd-25v-1.webp + +config.json +.env +builder.config.json + +apps/web/assets-git +apps/web/public/thumbnails +apps/web/src/data/photos-manifest.json +.vercel diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 00000000..0f454b1f --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,5 @@ +import { factory } from '@innei/prettier' + +export default factory({ + importSort: false, +}) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f1821159 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,39 @@ +{ + "editor.formatOnSave": true, + "[javascript][javascriptreact][typescript][typescriptreact][json][jsonc]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + // If you do not want to autofix some rules on save + // You can put this in your user settings or workspace settings + "eslint.codeActionsOnSave.rules": [ + "!unused-imports/no-unused-imports", + "*" + ], + // If you want to silent stylistic rules + // You can put this in your user settings or workspace settings + "eslint.rules.customizations": [ + { + "rule": "@stylistic/*", + "severity": "off", + "fixable": true + }, + { + "rule": "antfu/consistent-list-newline", + "severity": "off" + }, + { + "rule": "hyoban/jsx-attribute-spacing", + "severity": "off" + }, + { + "rule": "simple-import-sort/*", + "severity": "off" + }, + { + "rule": "unused-imports/no-unused-imports", + "severity": "off" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..764a88ea --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Photo Gallery Site + +⚠️警告:此项目多数代码都由 Claude 4 生成,请谨慎使用。 + +一个现代化的照片画廊网站,支持从 S3 存储自动同步照片,具有瀑布流布局、EXIF 信息展示、缩略图生成等功能。 + +Preview: https://gallery.innei.in + +## 特点 + +- 高性能 WebGL 图像渲染器 +- HEIC/HEIF 格式支持 +- 支持缩略图生成 +- 支持 EXIF 信息展示 +- 瀑布流布局 +- 支持富士胶片模拟信息读取 + +## 环境配置 + +创建 `.env` 文件并配置以下环境变量: + +```env +# S3 配置 +S3_REGION=us-east-1 +S3_ACCESS_KEY_ID=your_access_key_id +S3_SECRET_ACCESS_KEY=your_secret_access_key +S3_ENDPOINT=https://s3.us-east-1.amazonaws.com +S3_BUCKET_NAME=your_bucket_name +S3_PREFIX=photos/ +S3_CUSTOM_DOMAIN=your_custom_domain.com +``` + +## Photo Gallery Builder + +基于适配器模式重构的照片库构建器,提供灵活的存储抽象和可配置的构建选项。 + +### 配置文件 + +在项目根目录的 `builder.config.ts` 中可以配置构建器的各种选项: + +```typescript +export const builderConfig: BuilderConfig = { + storage: { + provider: 's3', + bucket: 'my-bucket', + region: 'us-east-1', + // ... 其他存储配置 + }, + + options: { + defaultConcurrency: 8, // 默认并发数 + maxPhotos: 5000, // 最大照片数量限制 + enableLivePhotoDetection: true, // 启用 Live Photo 检测 + showProgress: true, // 显示进度 + showDetailedStats: true, // 显示详细统计 + }, + + logging: { + verbose: true, // 详细日志 + level: 'debug', // 日志级别 + outputToFile: false, // 是否输出到文件 + }, + + performance: { + worker: { + timeout: 30000, // Worker 超时时间 + }, + memoryLimit: 512, // 内存限制(MB) + enableCache: true, // 启用缓存 + }, +} +``` + +#### 自定义存储提供商 + +如果需要使用其他存储服务(如阿里云 OSS),可以: + +1. 实现新的存储提供商类 +2. 在配置中指定使用新的提供商 + +```typescript +const builder = new PhotoGalleryBuilder({ + storage: { + provider: 'oss', // 假设已经实现了 OSS 提供商 + bucket: 'my-oss-bucket', + // ... OSS 特定配置 + }, +}) +``` + +## 🚀 使用 + +### 开发模式 + +```bash +pnpm dev +``` + +### 构建照片清单 + +```bash +# 增量更新(默认) +pnpm run build:manifest + +# 全量更新 +pnpm run build:manifest -- --force +``` + +### 构建生产版本 + +```bash +pnpm run build +``` + +## License + +2025 © Innei, Released under the MIT License. + +> [Personal Website](https://innei.in/) · GitHub [@Innei](https://github.com/innei/) diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 00000000..22322798 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,6 @@ + +public/og-image-*.png +test-svg-font-rendering.png +test-traditional-font-rendering.png +thomas-x2d-xcd-25v-1.jpg +thomas-x2d-xcd-25v-1.webp diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 00000000..84339923 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,399 @@ + + + + + + Photo Gallery + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ + +
+ + + + + + +
+
+ + +
+

+ Photo Gallery +

+
+

+ Capturing moments, creating memories +

+
+ + +
+ +
+
+
+ + +

+ LOADING +

+
+
+ + + +
+
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 00000000..cddcad0b --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,95 @@ +{ + "name": "@photo-gallery/web", + "type": "module", + "version": "0.0.1", + "packageManager": "pnpm@10.11.1", + "repository": { + "type": "git", + "url": "https://github.com/innei/photo-gallery" + }, + "scripts": { + "analyze": "analyzer=1 vite build", + "build": "tsx scripts/build.ts", + "build:manifest": "tsx src/core/cli.ts", + "dev": "tsx scripts/dev.ts", + "format": "prettier --write \"src/**/*.ts\" ", + "lint": "eslint --fix", + "serve": "vite preview" + }, + "dependencies": { + "@aws-sdk/client-s3": "3.823.0", + "@aws-sdk/s3-request-presigner": "3.823.0", + "@essentials/request-timeout": "1.3.0", + "@headlessui/react": "2.2.4", + "@photo-gallery/webgl-viewer": "workspace:*", + "@radix-ui/react-context-menu": "2.2.15", + "@radix-ui/react-dropdown-menu": "2.1.15", + "@radix-ui/react-popover": "1.1.14", + "@radix-ui/react-scroll-area": "1.2.9", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tooltip": "1.2.7", + "@react-hook/window-size": "3.1.1", + "@remixicon/react": "4.6.0", + "@t3-oss/env-core": "0.13.6", + "@tanstack/react-query": "5.80.2", + "@use-gesture/react": "10.3.1", + "@vercel/analytics": "1.5.0", + "blurhash": "2.0.5", + "clsx": "2.1.1", + "consola": "3.4.2", + "dotenv": "16.5.0", + "es-toolkit": "1.38.0", + "exif-reader": "2.0.2", + "foxact": "0.2.45", + "fuji-recipes": "1.0.2", + "heic-convert": "2.1.0", + "heic-to": "1.1.13", + "immer": "10.1.1", + "jotai": "2.12.5", + "masonic": "4.1.0", + "motion": "12.16.0", + "ofetch": "1.4.1", + "react": "19.1.0", + "react-blurhash": "0.3.0", + "react-dom": "19.1.0", + "react-image-gallery": "1.4.0", + "react-intersection-observer": "9.16.0", + "react-remove-scroll": "2.7.1", + "react-router": "7.6.2", + "react-scan": "0.3.4", + "react-zoom-pan-pinch": "3.7.0", + "sharp": "0.34.2", + "sonner": "2.0.5", + "swiper": "11.2.8", + "tailwind-merge": "3.3.0", + "usehooks-ts": "3.1.1", + "zod": "3.25.51", + "zustand": "5.0.5" + }, + "devDependencies": { + "@egoist/tailwindcss-icons": "1.9.0", + "@iconify-json/mingcute": "1.2.3", + "@tailwindcss/container-queries": "0.1.1", + "@tailwindcss/postcss": "4.1.8", + "@tailwindcss/typography": "0.5.16", + "@tailwindcss/vite": "4.1.8", + "@types/react": "19.1.6", + "@types/react-dom": "19.1.5", + "@vitejs/plugin-react": "^4.5.1", + "babel-plugin-react-compiler": "19.1.0-rc.2", + "code-inspector-plugin": "0.20.12", + "daisyui": "5.0.43", + "execa": "9.6.0", + "postcss": "8.5.4", + "postcss-import": "16.1.0", + "postcss-js": "4.0.1", + "react-compiler-runtime": "19.1.0-rc.2", + "simple-git-hooks": "2.13.0", + "tailwind-scrollbar": "4.0.2", + "tailwind-variants": "1.0.0", + "tailwindcss": "4.1.8", + "tailwindcss-animate": "1.0.7", + "tailwindcss-safe-area": "0.6.0", + "tailwindcss-uikit-colors": "1.0.0-alpha.0" + } +} \ No newline at end of file diff --git a/apps/web/public/android-chrome-192x192.png b/apps/web/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..455a657fd1b867433c4d9e1443727887e492d762 GIT binary patch literal 7509 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 literal 0 HcmV?d00001 diff --git a/apps/web/public/android-chrome-512x512.png b/apps/web/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..4ea8597821fe804ec2380834bd12ccd41a295804 GIT binary patch literal 25695 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

;_T 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 M07*qoM6N<$f{xn+s{jB1 literal 0 HcmV?d00001 diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3abda3d72f0b3997deced0164df852dde4d5d41b GIT binary patch literal 1062 zcmV+>1ljwEP)RaNRSUCApeE78-_gSxsp z6Wk<_3Qpz?4Grk-?uOIp#Hq96n7A^7Z|?kw$Xpzu*>wyI45Y1gI#yIvsLx{If*`yU z(4f`T)fhh?#-(d>n7+G=srgO#Zf(Fhtzf_Z3EF*+;qb1&;dzKQ&obJ*%W!*q$?Dy% z%~^O-Kws+4&Q6S8O`!ecKj!!zqd)un8>XvhSnNN)WcN)c8t?-8uJJ6j;Z=r(B^u8-)=8h6L1|xV`HNM`bvS3kr8}wVbvtC_rwzZ zx^}~oK*E0tWo2c^$;nZphtqjdKtHWgn;n@}OdXH}R=@k!lECVOAKBU2CIRi7W2aXV zXliPL_htg`oq2-8lWX|=`xwsMT0ra-?oI^p`_u$h zA|WWDF(l5MK>YX!lp}*s4(~^-cOPQ!zJ;H{0Yek$L%$~lQmo$$(ZcLaHH} zj=Q?LFgQ4f(a}-3TrRagY1gsvq(Hj&d8!wQUW9#^UPy~}t{2c>%k5wpl~Q6m8GQ|~ z|8rKCQEb0p$8|cHbaI?kc)C8Z6L?a9h0{8*xw-k3i;11o1lvcrbS0;SN~Yne0Bx77 zE~EZ0IM4IXNZ{Ts!6^$Q(6>u)*lf0s)Xgbv6yG$Q=lQT { + const __dirname = path.dirname(fileURLToPath(import.meta.url)) + const workdir = path.resolve(__dirname, '..') + + // 检查是否存在 public/thumbnails 和 src/data/photos-manifest.json + const thumbnailsDir = path.resolve(workdir, 'public', 'thumbnails') + const photosManifestPath = path.resolve( + workdir, + 'src', + 'data', + 'photos-manifest.json', + ) + const isExistThumbnails = existsSync(thumbnailsDir) + const isExistPhotosManifest = existsSync(photosManifestPath) + + const shouldDoBuildOrClone = !isExistThumbnails || !isExistPhotosManifest + + // 检查 builder 配置 + if (shouldDoBuildOrClone) { + if (builderConfig.repo.enable) { + await pullAndLinkRemoteRepo() + } else { + await $({ cwd: workdir, stdio: 'inherit' })`pnpm build:manifest` + } + } +} diff --git a/apps/web/scripts/pull-remote.ts b/apps/web/scripts/pull-remote.ts new file mode 100644 index 00000000..3131060c --- /dev/null +++ b/apps/web/scripts/pull-remote.ts @@ -0,0 +1,49 @@ +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { builderConfig } from '@builder' +import { $ } from 'execa' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const workdir = path.resolve(__dirname, '..') + +export const pullAndLinkRemoteRepo = async () => { + const hasExist = existsSync(path.resolve(workdir, 'assets-git')) + if (!hasExist) { + await $({ + cwd: workdir, + stdio: 'inherit', + })`git clone ${builderConfig.repo.url} assets-git` + } else { + await $({ + cwd: path.resolve(workdir, 'assets-git'), + stdio: 'inherit', + })`git pull --rebase` + } + + // 删除 public/thumbnails 目录,并建立软连接到 assets-git/thumbnails + const thumbnailsDir = path.resolve(workdir, 'public', 'thumbnails') + if (existsSync(thumbnailsDir)) { + await $({ cwd: workdir, stdio: 'inherit' })`rm -rf ${thumbnailsDir}` + } + await $({ + cwd: workdir, + stdio: 'inherit', + })`ln -s ${path.resolve(workdir, 'assets-git', 'thumbnails')} ${thumbnailsDir}` + // 删除src/data/photos-manifest.json,并建立软连接到 assets-git/photos-manifest.json + const photosManifestPath = path.resolve( + workdir, + 'src', + 'data', + 'photos-manifest.json', + ) + if (existsSync(photosManifestPath)) { + await $({ cwd: workdir, stdio: 'inherit' })`rm -rf ${photosManifestPath}` + } + await $({ cwd: workdir, stdio: 'inherit' })`ln -s ${path.resolve( + workdir, + 'assets-git', + 'photos-manifest.json', + )} ${photosManifestPath}` +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 00000000..afe53016 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,18 @@ +import { inject } from '@vercel/analytics' +import { Outlet } from 'react-router' + +import { RootProviders } from './providers/root-providers' + +inject() + +function App() { + return ( + +

+ +
+ + ) +} + +export default App diff --git a/apps/web/src/assets/fonts/GeistVF.woff2 b/apps/web/src/assets/fonts/GeistVF.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9983e92a03de482b5708201694101c330b6ae9b8 GIT binary patch literal 56800 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 W3iUXCl68}}UHOUMf4J;#6`lYgehe7^ literal 0 HcmV?d00001 diff --git a/apps/web/src/atoms/app.ts b/apps/web/src/atoms/app.ts new file mode 100644 index 00000000..8b05d0f4 --- /dev/null +++ b/apps/web/src/atoms/app.ts @@ -0,0 +1,9 @@ +import { atom } from 'jotai' + +export type GallerySortBy = 'date' +export type GallerySortOrder = 'asc' | 'desc' +export const gallerySettingAtom = atom({ + sortBy: 'date' as GallerySortBy, + sortOrder: 'desc' as GallerySortOrder, + selectedTags: [] as string[], +}) diff --git a/apps/web/src/atoms/context-menu.ts b/apps/web/src/atoms/context-menu.ts new file mode 100644 index 00000000..d84635c9 --- /dev/null +++ b/apps/web/src/atoms/context-menu.ts @@ -0,0 +1,195 @@ +import { atom } from 'jotai' +import { useCallback } from 'react' + +import { createAtomHooks } from '~/lib/jotai' + +// Atom + +type ContextMenuState = + | { open: false } + | { + open: true + position: { x: number; y: number } + menuItems: FollowMenuItem[] + // Just for abort callback + // Also can be optimized by using the `atomWithListeners` + abortController: AbortController + } + +export const [ + contextMenuAtom, + useContextMenuState, + useContextMenuValue, + useSetContextMenu, +] = createAtomHooks(atom({ open: false })) + +const useShowWebContextMenu = () => { + const setContextMenu = useSetContextMenu() + + const showWebContextMenu = useCallback( + async ( + menuItems: Array, + e: MouseEvent | React.MouseEvent, + ) => { + const abortController = new AbortController() + const resolvers = Promise.withResolvers() + setContextMenu({ + open: true, + position: { x: e.clientX, y: e.clientY }, + menuItems, + abortController, + }) + + abortController.signal.addEventListener('abort', () => { + resolvers.resolve() + }) + return resolvers.promise + }, + [setContextMenu], + ) + + return showWebContextMenu +} + +// Menu + +export type FollowMenuItem = MenuItemText | MenuItemSeparator + +export type MenuItemInput = MenuItemText | MenuItemSeparator | NilValue + +function filterNullableMenuItems(items: MenuItemInput[]): FollowMenuItem[] { + return items + .filter( + (item) => + item !== null && item !== undefined && item !== false && item !== '', + ) + .filter((item) => !item.hide) + .map((item) => { + if (item instanceof MenuItemSeparator) { + return MENU_ITEM_SEPARATOR + } + + if (item.submenu && item.submenu.length > 0) { + return item.extend({ + submenu: filterNullableMenuItems(item.submenu), + }) + } + + return item + }) +} + +export enum MenuItemType { + Separator, + Action, +} + +export const useShowContextMenu = () => { + const showWebContextMenu = useShowWebContextMenu() + + const showContextMenu = useCallback( + async ( + inputMenu: Array, + e: MouseEvent | React.MouseEvent, + ) => { + const menuItems = filterNullableMenuItems(inputMenu) + e.preventDefault() + e.stopPropagation() + await showWebContextMenu(menuItems, e) + }, + [showWebContextMenu], + ) + + return showContextMenu +} + +export class MenuItemSeparator { + readonly type = MenuItemType.Separator + constructor(public hide = false) {} + static default = new MenuItemSeparator() +} + +const noop = () => void 0 +export type BaseMenuItemTextConfig = { + label: string + click?: () => void + /** only work in web app */ + icon?: React.ReactNode + shortcut?: string + disabled?: boolean + checked?: boolean + supportMultipleSelection?: boolean +} + +export class BaseMenuItemText { + readonly type = MenuItemType.Action + + private __sortedShortcut: string | null = null + + constructor(private configs: BaseMenuItemTextConfig) { + this.__sortedShortcut = this.configs.shortcut || null + } + + public get label() { + return this.configs.label + } + + public get click() { + return this.configs.click?.bind(this.configs) || noop + } + + public get onClick() { + return this.click + } + public get icon() { + return this.configs.icon + } + + public get shortcut() { + return this.__sortedShortcut + } + + public get disabled() { + return this.configs.disabled || false + } + + public get checked() { + return this.configs.checked + } + + public get supportMultipleSelection() { + return this.configs.supportMultipleSelection + } +} + +export type MenuItemTextConfig = BaseMenuItemTextConfig & { + hide?: boolean + submenu?: MenuItemInput[] +} + +export class MenuItemText extends BaseMenuItemText { + protected __submenu: FollowMenuItem[] + constructor(protected config: MenuItemTextConfig) { + super(config) + + this.__submenu = this.config.submenu + ? filterNullableMenuItems(this.config.submenu) + : [] + } + + public get submenu() { + return this.__submenu + } + + public get hide() { + return this.config.hide || false + } + + extend(config: Partial) { + return new MenuItemText({ + ...this.config, + ...config, + }) + } +} +export const MENU_ITEM_SEPARATOR = MenuItemSeparator.default diff --git a/apps/web/src/atoms/route.ts b/apps/web/src/atoms/route.ts new file mode 100644 index 00000000..fe18a458 --- /dev/null +++ b/apps/web/src/atoms/route.ts @@ -0,0 +1,43 @@ +import { atom, useAtomValue } from 'jotai' +import { selectAtom } from 'jotai/utils' +import { useMemo } from 'react' +import type { Location, NavigateFunction, Params } from 'react-router' + +import { createAtomHooks } from '~/lib/jotai' + +interface RouteAtom { + params: Readonly> + searchParams: URLSearchParams + location: Location +} + +export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks( + atom({ + params: {}, + searchParams: new URLSearchParams(), + location: { + pathname: '', + search: '', + hash: '', + state: null, + key: '', + }, + }), +) + +const noop = [] +export const useReadonlyRouteSelector = ( + selector: (route: RouteAtom) => T, + deps: any[] = noop, +): T => + useAtomValue( + useMemo(() => selectAtom(routeAtom, (route) => selector(route)), deps), + ) + +// Vite HMR will create new router instance, but RouterProvider always stable + +const [, , , , navigate, setNavigate] = createAtomHooks( + atom<{ fn: NavigateFunction | null }>({ fn() {} }), +) +const getStableRouterNavigate = () => navigate().fn +export { getStableRouterNavigate, setNavigate } diff --git a/apps/web/src/atoms/viewport.ts b/apps/web/src/atoms/viewport.ts new file mode 100644 index 00000000..f5cd79b9 --- /dev/null +++ b/apps/web/src/atoms/viewport.ts @@ -0,0 +1,38 @@ +import { atom } from 'jotai' + +const { innerWidth: w, innerHeight: h } = window +const sm = w >= 640 +const md = w >= 768 +const lg = w >= 1024 +const xl = w >= 1280 +const _2xl = w >= 1536 + +export const viewportAtom = atom({ + /** + * 640px + */ + sm, + + /** + * 768px + */ + md, + + /** + * 1024px + */ + lg, + + /** + * 1280px + */ + xl, + + /** + * 1536px + */ + '2xl': _2xl, + + h, + w, +}) diff --git a/apps/web/src/components/common/ErrorElement.tsx b/apps/web/src/components/common/ErrorElement.tsx new file mode 100644 index 00000000..7c93af5f --- /dev/null +++ b/apps/web/src/components/common/ErrorElement.tsx @@ -0,0 +1,77 @@ +import { repository } from '@pkg' +import { useEffect, useRef } from 'react' +import { isRouteErrorResponse, useRouteError } from 'react-router' + +import { attachOpenInEditor } from '~/lib/dev' + +import { Button } from '../ui/button' + +export function ErrorElement() { + const error = useRouteError() + const message = isRouteErrorResponse(error) + ? `${error.status} ${error.statusText}` + : error instanceof Error + ? error.message + : JSON.stringify(error) + const stack = error instanceof Error ? error.stack : null + + useEffect(() => { + console.error('Error handled by React Router default ErrorBoundary:', error) + }, [error]) + + const reloadRef = useRef(false) + if ( + message.startsWith('Failed to fetch dynamically imported module') && + window.sessionStorage.getItem('reload') !== '1' + ) { + if (reloadRef.current) return null + window.sessionStorage.setItem('reload', '1') + window.location.reload() + reloadRef.current = true + return null + } + + return ( +
+
+
+ +

+ Sorry, the app has encountered an error +

+
+

{message}

+ {import.meta.env.DEV && stack ? ( +
+ {attachOpenInEditor(stack)} +
+ ) : null} + +

+ The App has a temporary problem, click the button below to try reloading + the app or another solution? +

+ +
+ +
+ +

+ Still having this issue? Please give feedback in Github, thanks! + + Submit Issue + +

+
+
+ ) +} diff --git a/apps/web/src/components/common/LoadRemixAsyncComponent.tsx b/apps/web/src/components/common/LoadRemixAsyncComponent.tsx new file mode 100644 index 00000000..fdbe471b --- /dev/null +++ b/apps/web/src/components/common/LoadRemixAsyncComponent.tsx @@ -0,0 +1,53 @@ +import type { FC, ReactNode } from 'react' +import { createElement, useEffect, useState } from 'react' + +import { LoadingCircle } from '../ui/loading' + +export const LoadRemixAsyncComponent: FC<{ + loader: () => Promise + Header: FC<{ loader: () => any; [key: string]: any }> +}> = ({ loader, Header }) => { + const [loading, setLoading] = useState(true) + + const [Component, setComponent] = useState<{ c: () => ReactNode }>({ + c: () => null, + }) + + useEffect(() => { + let isUnmounted = false + setLoading(true) + loader() + .then((module) => { + if (!module.Component) { + return + } + if (isUnmounted) return + + const { loader } = module + setComponent({ + c: () => ( + <> +
+ + + ), + }) + }) + .finally(() => { + setLoading(false) + }) + return () => { + isUnmounted = true + } + }, [Header, loader]) + + if (loading) { + return ( +
+ +
+ ) + } + + return createElement(Component.c) +} diff --git a/apps/web/src/components/common/NotFound.tsx b/apps/web/src/components/common/NotFound.tsx new file mode 100644 index 00000000..a6ed8163 --- /dev/null +++ b/apps/web/src/components/common/NotFound.tsx @@ -0,0 +1,25 @@ +import { useLocation, useNavigate } from 'react-router' + +import { Button } from '../ui/button' + +export const NotFound = () => { + const location = useLocation() + + const navigate = useNavigate() + return ( +
+
+

+ You have come to a desert of knowledge where there is nothing. +

+

+ Current path: {location.pathname} +

+ +

+ +

+
+
+ ) +} diff --git a/apps/web/src/components/common/PassiveFragmenet.tsx b/apps/web/src/components/common/PassiveFragmenet.tsx new file mode 100644 index 00000000..2cb07b17 --- /dev/null +++ b/apps/web/src/components/common/PassiveFragmenet.tsx @@ -0,0 +1,7 @@ +import type { FC, PropsWithChildren } from 'react' +import { Fragment } from 'react' + +export const PassiveFragment: FC = ({ + children, + ...rest +}) => {children} diff --git a/apps/web/src/components/common/ProviderComposer.tsx b/apps/web/src/components/common/ProviderComposer.tsx new file mode 100644 index 00000000..a88cd51f --- /dev/null +++ b/apps/web/src/components/common/ProviderComposer.tsx @@ -0,0 +1,10 @@ +import type { JSX } from 'react' +import { cloneElement } from 'react' + +export const ProviderComposer: Component<{ + contexts: JSX.Element[] +}> = ({ contexts, children }) => + contexts.reduceRight( + (kids: any, parent: any) => cloneElement(parent, { children: kids }), + children, + ) diff --git a/apps/web/src/components/ui/button/Button.tsx b/apps/web/src/components/ui/button/Button.tsx new file mode 100644 index 00000000..137083e2 --- /dev/null +++ b/apps/web/src/components/ui/button/Button.tsx @@ -0,0 +1,135 @@ +// Tremor Button [v0.2.0] + +import { Slot } from '@radix-ui/react-slot' +import { m } from 'motion/react' +import * as React from 'react' +import type { VariantProps } from 'tailwind-variants' +import { tv } from 'tailwind-variants' + +import { cx, focusRing } from '~/lib/cn' + +const buttonVariants = tv({ + base: [ + 'relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-center font-medium transition-all duration-100 ease-in-out', + 'disabled:pointer-events-none', + focusRing, + ], + variants: { + variant: { + primary: [ + 'border-transparent', + 'text-white dark:text-white', + 'bg-accent dark:bg-accent', + 'hover:bg-accent/90 dark:hover:bg-accent/90', + 'disabled:bg-accent/50 disabled:text-white/70', + 'disabled:dark:bg-accent/30 disabled:dark:text-white/50', + ], + secondary: [ + 'border border-gray-200 dark:border-gray-700', + 'text-gray-700 dark:text-gray-200', + 'bg-gray-50 dark:bg-gray-800', + 'hover:bg-gray-100 dark:hover:bg-gray-750', + 'disabled:bg-gray-50 disabled:text-gray-400', + 'disabled:dark:bg-gray-800 disabled:dark:text-gray-500', + ], + light: [ + 'shadow-none', + 'border-transparent', + 'text-gray-900 dark:text-gray-50', + 'bg-gray-200 dark:bg-gray-900', + 'hover:bg-gray-300/70 dark:hover:bg-gray-800/80', + 'disabled:bg-gray-100 disabled:text-gray-400', + 'disabled:dark:bg-gray-800 disabled:dark:text-gray-600', + ], + ghost: [ + 'shadow-none', + 'border-transparent', + 'text-gray-900 dark:text-gray-50', + 'bg-transparent dark:hover:bg-fill-tertiary', + 'disabled:text-gray-400', + 'disabled:dark:text-gray-600', + ], + destructive: [ + 'text-white', + 'border-transparent', + 'bg-red-600 dark:bg-red-700', + 'hover:bg-red-700 dark:hover:bg-red-600', + 'disabled:bg-red-300 disabled:text-white', + 'disabled:dark:bg-red-950 disabled:dark:text-red-400', + ], + }, + size: { + xs: 'h-6 px-2 text-xs', + sm: 'h-8 px-3 text-sm', + md: 'h-10 px-4 text-sm', + lg: 'h-11 px-8 text-base', + xl: 'h-12 px-8 text-base', + }, + flat: { + true: 'shadow-none', + false: 'shadow-sm', + }, + }, + defaultVariants: { + variant: 'primary', + size: 'sm', + flat: false, + }, +}) + +interface ButtonProps + extends React.ComponentPropsWithoutRef<'button'>, + VariantProps { + asChild?: boolean + isLoading?: boolean + loadingText?: string +} + +const Button = ({ + ref: forwardedRef, + asChild, + isLoading = false, + loadingText, + className, + disabled, + variant, + size, + flat, + children, + ...props +}: ButtonProps & { + ref?: React.RefObject +}) => { + const Component = asChild ? Slot : m.button + return ( + // @ts-expect-error + + {isLoading ? ( + +