feat: selection preps

This commit is contained in:
2024-10-26 02:56:27 +03:00
parent 8035ec7611
commit 3622c34780
12 changed files with 213 additions and 62 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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() {
<HashRouter basename="/">
<QueryClientProvider client={queryClient}>
<AppContextProvider>
<div id="App" className="min-h-screen flex">
<MainSidebar className="min-w-64 w-64" />
<div className="max-h-screen overflow-y-auto w-full">
<Routes>
<Route path="/" element={<GamesHomePage />} />
<Route path="/games/:gameId" element={<GameInfoPage />} />
<Route path="/games/" element={<GamesHomePage />} />
<Route path="/screenshots/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/screenshots/" element={<ScreenshotsHome />} />
</Routes>
<TooltipProvider>
<div id="App" className="min-h-screen flex">
<MainSidebar className="min-w-64 w-64" />
<div className="max-h-screen overflow-y-auto w-full">
<Routes>
<Route path="/" element={<GamesHomePage />} />
<Route path="/games/:gameId" element={<GameInfoPage />} />
<Route path="/games/" element={<GamesHomePage />} />
<Route path="/screenshots/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/screenshots/" element={<ScreenshotsHome />} />
</Routes>
</div>
</div>
</div>
</TooltipProvider>
</AppContextProvider>
</QueryClientProvider>
</HashRouter>

View File

@@ -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({
<Link to={`/screenshots/${coll.gameId}`}>View All ({coll.totalCount})</Link>
</Button>
<Button variant="outline" onClick={() => NativeOpen(coll.dir)}>
Browse Folder
<OpenFolderIcon />
</Button>
</div>
</div>

View File

@@ -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

View File

@@ -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<HtmlProps<'img'>, 'src'> & { screenshot: screenshots.ScreenshotEntry; load?: boolean }) {
return load ? (
<img
className={cn('rounded-md', rest.onClick && 'cursor-pointer', className)}
src={screenshot.url}
alt={screenshot.name}
{...rest}
/>
) : (
<div className={cn('rounded-md', rest.onClick && 'cursor-pointer', className)} />
)
}: Omit<HtmlProps<'img'>, 'src'> & {
screenshot: screenshots.ScreenshotEntry
load?: boolean
selectable?: boolean
selected?: boolean
onToggleSelect?(value: boolean): void
}) {
const img = <img src={screenshot.url} alt={screenshot.name} {...rest} />
if (selectable) {
return (
<div className={cn('rounded-md relative', rest.onClick && 'cursor-pointer', className)}>
{img}
<Checkbox
checked={selected}
onCheckedChange={() => {
console.log('toggle select', selected)
onToggleSelect?.(!selected)
}}
className="absolute top-1 right-1"
/>
</div>
)
}
return <div className={cn('rounded-md', rest.onClick && 'cursor-pointer', className)}>{img}</div>
}

View File

@@ -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: <FaRegFolderOpen />,
icon: <OpenFolderIcon />,
onClick: () => NativeOpen(screenshots[idx].dir),
},
{
label: 'Open in Default App',
icon: <FaArrowUpRightFromSquare />,
icon: <ExternalLinkIcon />,
onClick: () => NativeOpen(screenshots[idx].path),
},
{
label: 'Delete',
icon: <FaTrash />,
icon: <DeleteIcon />,
onClick: () => setConfirmDelete(true),
variant: 'destructive',
},
@@ -108,7 +111,7 @@ export function ScreenshotsCarouselModal(
onClick={prevScreenshot}
aria-keyshortcuts="ArrowLeft"
>
<FaAngleLeft />
<ChevronLeftIcon />
</Button>
<div className="flex-grow flex place-content-center">
{visible.map((scr, i) => (
@@ -126,7 +129,7 @@ export function ScreenshotsCarouselModal(
onClick={nextScreenshot}
aria-keyshortcuts="ArrowRight"
>
<FaAngleRight />
<ChevronRightIcon />
</Button>
</div>
<div className="flex place-content-center gap-2">
@@ -163,7 +166,7 @@ export function ScreenshotsCarouselModal(
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleDelete}>
<FaTrash />
<DeleteIcon />
Delete
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -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<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -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<string[]>([])
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 (
<div style={{ width: thumbSize, ...style }} className="p-1">
<ScreenshotImg
className="rounded-md"
screenshot={file}
key={file.path}
onClick={() => openScreenshotsModal(i, dir.screenshots)}
/>
</div>
)
}
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 (
<div style={{ width: thumbSize, ...style }} className="p-1">
<ScreenshotImg
className="rounded-md"
selectable
selected={isSelected}
onToggleSelect={setSelectedValue(file)}
screenshot={file}
key={file.path}
onClick={() => openScreenshotsModal(i, dir.screenshots)}
/>
</div>
)
},
[colCount, dir.screenshots, openScreenshotsModal, selected, setSelectedValue, thumbSize],
)
const handleKeyUp = useCallback(
(e: React.KeyboardEvent) => {
@@ -102,7 +123,7 @@ function ScreenshotsGamePage() {
<div>
<Button variant="outline" size="sm" asChild>
<Link to="/screenshots">
<FaAngleLeft />
<ChevronLeftIcon />
Back
</Link>
</Button>
@@ -112,9 +133,14 @@ function ScreenshotsGamePage() {
Screenshots for <span className="text-black dark:text-white">{dir.gameName}</span>
</h1>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
<Tooltip>
<TooltipTrigger>
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
<OpenFolderIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Open containing folder</TooltipContent>
</Tooltip>
</div>
</div>
</div>

View File

@@ -12,6 +12,9 @@
"@/*": [
"./src/*"
],
"@ui/*": [
"./src/components/ui/*"
],
"$app": [
"./wailsjs/go/main/App"
],

View File

@@ -9,6 +9,9 @@
"@/*": [
"./src/*"
],
"@ui/*": [
"./src/components/ui/*"
],
"$app": [
"./wailsjs/go/main/App"
],

View File

@@ -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'),