From 3622c34780b7278c98d159421c397cff94a9bb09 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Sat, 26 Oct 2024 02:56:27 +0300 Subject: [PATCH] feat: selection preps --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 47 +++++++++++ frontend/src/App.tsx | 25 +++--- .../GameScreenshotsListItem.tsx | 3 +- frontend/src/components/Icons/Icons.tsx | 17 ++++ .../ScreenshotImg/ScreenshotImg.tsx | 42 +++++++--- .../ScreenshotsCarouselModal.tsx | 23 +++--- frontend/src/components/ui/checkbox.tsx | 28 +++++++ .../pages/Screenshots/ScreenshotsGamePage.tsx | 82 ++++++++++++------- frontend/tsconfig.json | 3 + frontend/tsconfig.node.json | 3 + frontend/vite.config.ts | 1 + 12 files changed, 213 insertions(+), 62 deletions(-) create mode 100644 frontend/src/components/Icons/Icons.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx diff --git a/frontend/package.json b/frontend/package.json index b11ff55..45b965e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@fontsource-variable/nunito": "^5.1.0", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e9e841a..f92711f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-alert-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -502,6 +505,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.2': + resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collapsible@1.1.1': resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} peerDependencies: @@ -736,6 +752,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -2738,6 +2763,22 @@ snapshots: '@types/react': 18.3.11 '@types/react-dom': 18.3.1 + '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + '@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2953,6 +2994,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.11 + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.11)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.0 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d4b3281..ea2a2b8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import { GamesHomePage } from './pages/Games/GamesHomePage' import ScreenshotsGamePage from './pages/Screenshots/ScreenshotsGamePage' import { ScreenshotsHome } from './pages/Screenshots/ScreenshotsHomePage' import { GameInfoPage } from './pages/Games/GameInfoPage' +import { TooltipProvider } from '@ui/tooltip' function App() { const [queryClient] = useState(() => new QueryClient()) @@ -21,18 +22,20 @@ function App() { -
- -
- - } /> - } /> - } /> - } /> - } /> - + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + +
-
+ diff --git a/frontend/src/components/GameScreenshotsListItem/GameScreenshotsListItem.tsx b/frontend/src/components/GameScreenshotsListItem/GameScreenshotsListItem.tsx index 55de00a..80dc0dc 100644 --- a/frontend/src/components/GameScreenshotsListItem/GameScreenshotsListItem.tsx +++ b/frontend/src/components/GameScreenshotsListItem/GameScreenshotsListItem.tsx @@ -6,6 +6,7 @@ import { Link } from 'react-router-dom' import { NativeOpen } from '$app' import { ScreenshotImg } from '@/components/ScreenshotImg/ScreenshotImg' import { useMemo } from 'react' +import { OpenFolderIcon } from '../Icons/Icons' export function GameScreenshotsListItem({ className, @@ -43,7 +44,7 @@ export function GameScreenshotsListItem({ View All ({coll.totalCount})
diff --git a/frontend/src/components/Icons/Icons.tsx b/frontend/src/components/Icons/Icons.tsx new file mode 100644 index 0000000..6f5bfa8 --- /dev/null +++ b/frontend/src/components/Icons/Icons.tsx @@ -0,0 +1,17 @@ +import { + FaAngleLeft, + FaAngleRight, + FaArrowUpRightFromSquare, + FaRegFolderOpen, + FaTrash, +} from 'react-icons/fa6' + +export const DeleteIcon = FaTrash + +export const OpenFolderIcon = FaRegFolderOpen + +export const ChevronLeftIcon = FaAngleLeft + +export const ChevronRightIcon = FaAngleRight + +export const ExternalLinkIcon = FaArrowUpRightFromSquare diff --git a/frontend/src/components/ScreenshotImg/ScreenshotImg.tsx b/frontend/src/components/ScreenshotImg/ScreenshotImg.tsx index 33ca019..3555dd6 100644 --- a/frontend/src/components/ScreenshotImg/ScreenshotImg.tsx +++ b/frontend/src/components/ScreenshotImg/ScreenshotImg.tsx @@ -1,21 +1,39 @@ import { HtmlProps } from '@/common/types' import { screenshots } from '$models' import { cn } from '@/common/utils' +import { Checkbox } from '@ui/checkbox' export function ScreenshotImg({ className, screenshot, - load = true, + selectable = false, + selected = false, + onToggleSelect, ...rest -}: Omit, 'src'> & { screenshot: screenshots.ScreenshotEntry; load?: boolean }) { - return load ? ( - {screenshot.name} - ) : ( -
- ) +}: Omit, 'src'> & { + screenshot: screenshots.ScreenshotEntry + load?: boolean + selectable?: boolean + selected?: boolean + onToggleSelect?(value: boolean): void +}) { + const img = {screenshot.name} + + if (selectable) { + return ( +
+ {img} + { + console.log('toggle select', selected) + onToggleSelect?.(!selected) + }} + className="absolute top-1 right-1" + /> +
+ ) + } + + return
{img}
} diff --git a/frontend/src/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal.tsx b/frontend/src/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal.tsx index 8861078..205b8d0 100644 --- a/frontend/src/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal.tsx +++ b/frontend/src/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal.tsx @@ -5,7 +5,6 @@ import { DialogContent, DialogTitle } from '@/components/ui/dialog' import { ScreenshotImg } from '../ScreenshotImg/ScreenshotImg' import { useCallback, useEffect, useMemo, useState } from 'react' import { Button, ButtonProps } from '../ui/button' -import { FaAngleLeft, FaAngleRight } from 'react-icons/fa6' import { ManageScreenshot, NativeOpen } from '$app' import { AlertDialog, @@ -17,10 +16,14 @@ import { AlertDialogHeader, AlertDialogTitle, } from '../ui/alert-dialog' -import { FaTrash } from 'react-icons/fa6' -import { FaArrowUpRightFromSquare } from 'react-icons/fa6' -import { FaRegFolderOpen } from 'react-icons/fa6' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' +import { + ChevronLeftIcon, + ChevronRightIcon, + DeleteIcon, + ExternalLinkIcon, + OpenFolderIcon, +} from '../Icons/Icons' export function ScreenshotsCarouselModal( props: HtmlProps<'div'> & { @@ -80,17 +83,17 @@ export function ScreenshotsCarouselModal( const actions = [ { label: 'Open Containing Folder', - icon: , + icon: , onClick: () => NativeOpen(screenshots[idx].dir), }, { label: 'Open in Default App', - icon: , + icon: , onClick: () => NativeOpen(screenshots[idx].path), }, { label: 'Delete', - icon: , + icon: , onClick: () => setConfirmDelete(true), variant: 'destructive', }, @@ -108,7 +111,7 @@ export function ScreenshotsCarouselModal( onClick={prevScreenshot} aria-keyshortcuts="ArrowLeft" > - +
{visible.map((scr, i) => ( @@ -126,7 +129,7 @@ export function ScreenshotsCarouselModal( onClick={nextScreenshot} aria-keyshortcuts="ArrowRight" > - +
@@ -163,7 +166,7 @@ export function ScreenshotsCarouselModal( Cancel - + Delete diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..f831532 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/frontend/src/pages/Screenshots/ScreenshotsGamePage.tsx b/frontend/src/pages/Screenshots/ScreenshotsGamePage.tsx index d18607b..ce06453 100644 --- a/frontend/src/pages/Screenshots/ScreenshotsGamePage.tsx +++ b/frontend/src/pages/Screenshots/ScreenshotsGamePage.tsx @@ -2,7 +2,6 @@ import { Link, useParams } from 'react-router-dom' import { NativeOpen } from '$app' import { LoadingContainer } from '@/components/Loader/LoadingContainer' import { Button } from '@/components/ui/button' -import { FaAngleLeft } from 'react-icons/fa6' import { ScreenshotImg } from '@/components/ScreenshotImg/ScreenshotImg' import { useCallback, useEffect, useState } from 'react' import { ScreenshotsCarouselModal } from '@/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal' @@ -11,11 +10,26 @@ import { useGameScreenshots } from '@/common/hooks/useScreenshots' import { FixedSizeGrid } from 'react-window' import { useStateRef } from '@/common/hooks/useStateRef' import { useScreenshotsModal } from '@/components/ScreenshotsCarouselModal/useScreenshotsModal' +import { screenshots } from '$models' +import { ChevronLeftIcon, OpenFolderIcon } from '@/components/Icons/Icons' +import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip' function ScreenshotsGamePage() { const thumbSize = 256 + 8 const { gameId } = useParams() const { screenshots, isPending, refetch } = useGameScreenshots(gameId!) + const [selected, setSelected] = useState([]) + const setSelectedValue = useCallback( + (file: screenshots.ScreenshotEntry) => () => { + setSelected((selected) => { + if (selected.includes(file.path)) { + return selected.filter((s) => s !== file.path) + } + return [...selected, file.path] + }) + }, + [], + ) const [dir] = screenshots.screenshotCollections ?? [{ screenshots: [] }] const { modalIndex, @@ -58,29 +72,36 @@ function ScreenshotsGamePage() { } }, [colCount, getColCount, gridRef, setGridRef, thumbSize]) - function Cell({ - columnIndex, - rowIndex, - style, - }: { - columnIndex: number - rowIndex: number - style: React.CSSProperties - }) { - const i = rowIndex * colCount + columnIndex - const file = dir.screenshots[i] - if (!file) return null - return ( -
- openScreenshotsModal(i, dir.screenshots)} - /> -
- ) - } + const Cell = useCallback( + function Cell({ + columnIndex, + rowIndex, + style, + }: { + columnIndex: number + rowIndex: number + style: React.CSSProperties + }) { + const i = rowIndex * colCount + columnIndex + const file = dir.screenshots[i] + const isSelected = Boolean(file) && selected.includes(file.path) + if (!file) return null + return ( +
+ openScreenshotsModal(i, dir.screenshots)} + /> +
+ ) + }, + [colCount, dir.screenshots, openScreenshotsModal, selected, setSelectedValue, thumbSize], + ) const handleKeyUp = useCallback( (e: React.KeyboardEvent) => { @@ -102,7 +123,7 @@ function ScreenshotsGamePage() {
@@ -112,9 +133,14 @@ function ScreenshotsGamePage() { Screenshots for {dir.gameName}
- + + + + + Open containing folder +
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ae27c9..6a7eb3e 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -12,6 +12,9 @@ "@/*": [ "./src/*" ], + "@ui/*": [ + "./src/components/ui/*" + ], "$app": [ "./wailsjs/go/main/App" ], diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 93ffcb3..0e4982a 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -9,6 +9,9 @@ "@/*": [ "./src/*" ], + "@ui/*": [ + "./src/components/ui/*" + ], "$app": [ "./wailsjs/go/main/App" ], diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2f55628..7e7112a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, './src'), + '@ui': path.resolve(__dirname, './src/components/ui'), $app: path.resolve(__dirname, './wailsjs/go/main/App'), $main: path.resolve(__dirname, './wailsjs/go/main'), $runtime: path.resolve(__dirname, './wailsjs/runtime'),