Bladeren bron

continue code deduplication

UwU 2 maanden geleden
bovenliggende
commit
f61cfc14ee

+ 12 - 47
src/renderer/src/components/library/LibraryView.tsx

@@ -27,7 +27,11 @@ import PreviewSizeToggle from '../common/PreviewSizeToggle'
 import type { LibraryFilters, WallpaperFolder, LweStatus } from '@shared/types'
 import clsx from 'clsx'
 import { WE_TYPES, WE_AGE_RATINGS } from '../../constants/weFilters'
-import { usePreviewSize, PREVIEW_SIZE_MIN_PX } from '../../hooks/usePreviewSize'
+import { usePreviewSize, previewGridStyle } from '../../hooks/usePreviewSize'
+import { useClickOutside } from '../../hooks/useClickOutside'
+import { toggle } from '../../utils/array'
+import { forEachIgnoringErrors } from '../../utils/async'
+import { openWorkshopPage } from '../../utils/steam'
 
 const STORAGE_KEY = 'we-library-filters'
 
@@ -65,10 +69,6 @@ function loadFilters(): LibraryFilterState {
   }
 }
 
-function toggle(arr: string[], value: string): string[] {
-  return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value]
-}
-
 function Chip({
   label,
   active,
@@ -106,13 +106,7 @@ function TagDropdown({
   const [search, setSearch] = useState('')
   const ref = useRef<HTMLDivElement>(null)
 
-  useEffect(() => {
-    function onOutside(e: MouseEvent) {
-      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
-    }
-    document.addEventListener('mousedown', onOutside)
-    return () => document.removeEventListener('mousedown', onOutside)
-  }, [])
+  useClickOutside(ref, () => setOpen(false))
 
   const filtered = available.filter((t) =>
     t.toLowerCase().includes(search.toLowerCase())
@@ -192,15 +186,7 @@ function FolderMenu({
 }) {
   const ref = useRef<HTMLDivElement>(null)
 
-  useEffect(() => {
-    function onMouseDown(e: MouseEvent) {
-      if (ref.current && !ref.current.contains(e.target as Node)) {
-        onClose()
-      }
-    }
-    document.addEventListener('mousedown', onMouseDown)
-    return () => document.removeEventListener('mousedown', onMouseDown)
-  }, [onClose])
+  useClickOutside(ref, onClose)
 
   return (
     <div
@@ -275,17 +261,7 @@ export default function LibraryView() {
   } | null>(null)
   const ctxMenuRef = useRef<HTMLDivElement>(null)
 
-  // Close wallpaper context menu on outside click (mousedown so right-click on another card works)
-  useEffect(() => {
-    if (!ctxMenu) return
-    function onMouseDown(e: MouseEvent) {
-      if (ctxMenuRef.current && !ctxMenuRef.current.contains(e.target as Node)) {
-        setCtxMenu(null)
-      }
-    }
-    document.addEventListener('mousedown', onMouseDown)
-    return () => document.removeEventListener('mousedown', onMouseDown)
-  }, [ctxMenu])
+  useClickOutside(ctxMenuRef, () => setCtxMenu(null), !!ctxMenu)
 
   useEffect(() => {
     localStorage.setItem(STORAGE_KEY, JSON.stringify(filterState))
@@ -724,9 +700,7 @@ export default function LibraryView() {
 
   async function ctxUnsubscribe() {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      try { await window.electronAPI.steam.unsubscribe(id) } catch {}
-    }
+    await forEachIgnoringErrors(ctxMenu.ids, (id) => window.electronAPI.steam.unsubscribe(id))
     closeCtxMenu()
     queryClient.invalidateQueries({ queryKey: ['library'] })
     refetchFolders()
@@ -734,18 +708,14 @@ export default function LibraryView() {
 
   async function ctxVote(up: boolean) {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      try { await window.electronAPI.steam.vote(id, up) } catch {}
-    }
+    await forEachIgnoringErrors(ctxMenu.ids, (id) => window.electronAPI.steam.vote(id, up))
     queryClient.invalidateQueries({ queryKey: ['steam-voted-ids'] })
     closeCtxMenu()
   }
 
   function ctxOpenInSteam() {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      window.electronAPI.shell.openExternal(`steam://url/CommunityFilePage/${id}`)
-    }
+    for (const id of ctxMenu.ids) openWorkshopPage(id)
     closeCtxMenu()
   }
 
@@ -1169,12 +1139,7 @@ export default function LibraryView() {
             </div>
           )}
 
-          <div
-            className="grid gap-4"
-            style={{
-              gridTemplateColumns: `repeat(auto-fill, minmax(${PREVIEW_SIZE_MIN_PX[previewSize]}px, 1fr))`
-            }}
-          >
+          <div className="grid gap-4" style={previewGridStyle(previewSize)}>
             {wallpapers.map((wallpaper) => (
               <WallpaperCard
                 key={wallpaper.id}

+ 2 - 1
src/renderer/src/components/library/WallpaperCard.tsx

@@ -3,6 +3,7 @@ import { Play, Loader2, Check, ThumbsUp, Trash2, ExternalLink, Download, Archive
 import type { WallpaperMeta } from '@shared/types'
 import clsx from 'clsx'
 import { getPreviewSrc } from '../../utils/preview'
+import { openWorkshopPage } from '../../utils/steam'
 
 interface WallpaperCardProps {
   wallpaper: WallpaperMeta
@@ -262,7 +263,7 @@ export default function WallpaperCard({
           <button
             onClick={(e) => {
               e.stopPropagation()
-              window.electronAPI.shell.openExternal(`steam://url/CommunityFilePage/${wallpaper.id}`)
+              openWorkshopPage(wallpaper.id)
             }}
             title="Open in Steam Workshop"
             className="flex items-center gap-1 rounded bg-white/5 px-2 py-1 text-xs text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-200"

+ 11 - 34
src/renderer/src/components/workshop/WorkshopBrowser.tsx

@@ -13,7 +13,11 @@ import {
   WE_RESOLUTION_GROUPS,
   WE_GENRES
 } from '../../constants/weFilters'
-import { usePreviewSize, PREVIEW_SIZE_MIN_PX } from '../../hooks/usePreviewSize'
+import { usePreviewSize, previewGridStyle } from '../../hooks/usePreviewSize'
+import { useClickOutside } from '../../hooks/useClickOutside'
+import { toggle } from '../../utils/array'
+import { forEachIgnoringErrors } from '../../utils/async'
+import { openWorkshopPage } from '../../utils/steam'
 
 const STORAGE_KEY = 'we-workshop-filters'
 const STORAGE_VERSION = 2
@@ -60,10 +64,6 @@ function saveFilters(state: WorkshopFilterState) {
   localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...state, _v: STORAGE_VERSION }))
 }
 
-function toggle(arr: string[], value: string): string[] {
-  return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value]
-}
-
 function CheckItem({
   label,
   checked,
@@ -146,17 +146,7 @@ export default function WorkshopBrowser() {
   const [pageSize, setPageSize] = useState(50)
   const [previewSize, setPreviewSize] = usePreviewSize()
 
-  // Close context menu on outside click (mousedown so right-click on another card works)
-  useEffect(() => {
-    if (!ctxMenu) return
-    function onMouseDown(e: MouseEvent) {
-      if (ctxRef.current && !ctxRef.current.contains(e.target as Node)) {
-        setCtxMenu(null)
-      }
-    }
-    document.addEventListener('mousedown', onMouseDown)
-    return () => document.removeEventListener('mousedown', onMouseDown)
-  }, [ctxMenu])
+  useClickOutside(ctxRef, () => setCtxMenu(null), !!ctxMenu)
 
   useEffect(() => {
     saveFilters(filters)
@@ -238,33 +228,25 @@ export default function WorkshopBrowser() {
 
   async function ctxSubscribe() {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      try { await window.electronAPI.steam.subscribe(id) } catch {}
-    }
+    await forEachIgnoringErrors(ctxMenu.ids, (id) => window.electronAPI.steam.subscribe(id))
     closeCtxMenu()
   }
 
   async function ctxUnsubscribe() {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      try { await window.electronAPI.steam.unsubscribe(id) } catch {}
-    }
+    await forEachIgnoringErrors(ctxMenu.ids, (id) => window.electronAPI.steam.unsubscribe(id))
     closeCtxMenu()
   }
 
   async function ctxVote(up: boolean) {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      try { await window.electronAPI.steam.vote(id, up) } catch {}
-    }
+    await forEachIgnoringErrors(ctxMenu.ids, (id) => window.electronAPI.steam.vote(id, up))
     closeCtxMenu()
   }
 
   function ctxOpenInSteam() {
     if (!ctxMenu) return
-    for (const id of ctxMenu.ids) {
-      window.electronAPI.shell.openExternal(`steam://url/CommunityFilePage/${id}`)
-    }
+    for (const id of ctxMenu.ids) openWorkshopPage(id)
     closeCtxMenu()
   }
 
@@ -564,12 +546,7 @@ export default function WorkshopBrowser() {
               No results found
             </div>
           )}
-          <div
-            className="grid gap-4"
-            style={{
-              gridTemplateColumns: `repeat(auto-fill, minmax(${PREVIEW_SIZE_MIN_PX[previewSize]}px, 1fr))`
-            }}
-          >
+          <div className="grid gap-4" style={previewGridStyle(previewSize)}>
             {paginatedItems.map((item) => (
               <WorkshopCard
                 key={item.publishedFileId}

+ 16 - 0
src/renderer/src/hooks/useClickOutside.ts

@@ -0,0 +1,16 @@
+import { useEffect, type RefObject } from 'react'
+
+export function useClickOutside(
+  ref: RefObject<HTMLElement | null>,
+  onOutside: () => void,
+  active = true
+): void {
+  useEffect(() => {
+    if (!active) return
+    function onMouseDown(e: MouseEvent): void {
+      if (ref.current && !ref.current.contains(e.target as Node)) onOutside()
+    }
+    document.addEventListener('mousedown', onMouseDown)
+    return () => document.removeEventListener('mousedown', onMouseDown)
+  }, [active, onOutside, ref])
+}

+ 5 - 1
src/renderer/src/hooks/usePreviewSize.ts

@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useState, type CSSProperties } from 'react'
 
 export type PreviewSize = 'small' | 'normal' | 'big'
 
@@ -8,6 +8,10 @@ export const PREVIEW_SIZE_MIN_PX: Record<PreviewSize, number> = {
   big: 280
 }
 
+export function previewGridStyle(size: PreviewSize): CSSProperties {
+  return { gridTemplateColumns: `repeat(auto-fill, minmax(${PREVIEW_SIZE_MIN_PX[size]}px, 1fr))` }
+}
+
 const STORAGE_KEY = 'we-preview-size'
 
 function isPreviewSize(value: unknown): value is PreviewSize {

+ 3 - 0
src/renderer/src/utils/array.ts

@@ -0,0 +1,3 @@
+export function toggle<T>(arr: T[], value: T): T[] {
+  return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value]
+}

+ 10 - 0
src/renderer/src/utils/async.ts

@@ -0,0 +1,10 @@
+export async function forEachIgnoringErrors<T>(
+  items: T[],
+  fn: (item: T) => Promise<unknown>
+): Promise<void> {
+  for (const item of items) {
+    try {
+      await fn(item)
+    } catch {}
+  }
+}

+ 3 - 0
src/renderer/src/utils/steam.ts

@@ -0,0 +1,3 @@
+export function openWorkshopPage(id: string): void {
+  window.electronAPI.shell.openExternal(`steam://url/CommunityFilePage/${id}`)
+}