Explorar el Código

playlist also has sidebar now, playlist always plays on autostart

UwU hace 1 mes
padre
commit
79c7eb215b

+ 3 - 1
src/main/services/playlist-player.service.ts

@@ -7,6 +7,7 @@ import { getWallpaper, updateWallpaper } from './library.service'
 import { applyWallpaperMeta } from './wallpaper.service'
 import { setActiveWallpaperId } from './wallpaper-state.service'
 import { getLweStatus } from './lwe.service'
+import { getAutostartPlaylistId } from './config.service'
 
 interface PlaybackStore {
   activePlaylistId: string | null
@@ -277,7 +278,8 @@ export function initPlaylistPlayer(window: BrowserWindow): boolean {
   if (!getLweStatus().installed) return false
 
   const wasPlaying = store.get('isPlaying')
+  const isConfiguredAutostart = activeId === getAutostartPlaylistId()
   startPlaylist(activeId)
-  if (!wasPlaying) pausePlaylist()
+  if (!wasPlaying && !isConfiguredAutostart) pausePlaylist()
   return true
 }

+ 9 - 3
src/renderer/src/components/playlist/PlaylistItemRow.tsx

@@ -17,6 +17,7 @@ interface PlaylistItemRowProps {
   onRemove: () => void
   onUpdate: (patch: { volume?: number; durationSec?: number }) => void
   onContextMenu: (e: React.MouseEvent) => void
+  onOpenDetail: () => void
   defaultDurationSec: number
   defaultVolume: number
 }
@@ -34,6 +35,7 @@ export default function PlaylistItemRow({
   onRemove,
   onUpdate,
   onContextMenu,
+  onOpenDetail,
   defaultDurationSec,
   defaultVolume
 }: PlaylistItemRowProps) {
@@ -83,14 +85,18 @@ export default function PlaylistItemRow({
             )}
           </div>
         </button>
-        <div className="min-w-0 flex-1">
-          <p className="truncate text-sm text-gray-200" title={wallpaper?.title}>
+        <button
+          onClick={onOpenDetail}
+          title="View details"
+          className="min-w-0 flex-1 text-left"
+        >
+          <p className="truncate text-sm text-gray-200 hover:text-white" title={wallpaper?.title}>
             {wallpaper?.title ?? item.wallpaperId}
           </p>
           <p className="text-xs text-gray-600">
             {item.durationSec ?? defaultDurationSec}s · Vol {item.volume ?? defaultVolume}
           </p>
-        </div>
+        </button>
         <button
           onClick={() => setExpanded((v) => !v)}
           title="Per-wallpaper overrides"

+ 65 - 0
src/renderer/src/components/playlist/PlaylistsView.tsx

@@ -27,10 +27,12 @@ import clsx from 'clsx'
 import type { Playlist, PlaylistPlaybackState, PlaylistSettings } from '@shared/types'
 import PlaylistItemRow from './PlaylistItemRow'
 import WallpaperPickerModal from './WallpaperPickerModal'
+import DetailSidebar from '../common/DetailSidebar'
 import { useClickOutside } from '../../hooks/useClickOutside'
 import { useClampedPosition, useFlipSide } from '../../hooks/useContextMenuPosition'
 import { openWorkshopPage, isWorkshopId } from '../../utils/steam'
 import { useToast } from '../common/Toast'
+import { getPreviewSrc } from '../../utils/preview'
 
 const SORT_OPTIONS: { value: PlaylistSettings['sortBy']; label: string }[] = [
   { value: 'manual', label: 'Manual order' },
@@ -54,6 +56,7 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
   const [dragIndex, setDragIndex] = useState<number | null>(null)
   const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
   const [error, setError] = useState<string | null>(null)
+  const [detailId, setDetailId] = useState<string | null>(null)
 
   const [ctxMenu, setCtxMenu] = useState<{
     x: number
@@ -101,6 +104,18 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
     queryFn: () => window.electronAPI.playlist.getState()
   })
 
+  const { data: lweStatus } = useQuery({
+    queryKey: ['lwe-status'],
+    queryFn: () => window.electronAPI.lwe.status()
+  })
+
+  const { data: votedIds } = useQuery({
+    queryKey: ['steam-voted-ids'],
+    queryFn: () => window.electronAPI.steam.getVotedIds(),
+    staleTime: Infinity
+  })
+  const votedSet = useMemo(() => new Set(votedIds ?? []), [votedIds])
+
   useEffect(() => {
     return window.electronAPI.on.playlistStateChanged((state: PlaylistPlaybackState) => {
       queryClient.setQueryData(['playlist-playback-state'], state)
@@ -193,6 +208,21 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
     refetchFolders()
   }
 
+  async function unsubscribeWallpaper(wallpaperId: string) {
+    await window.electronAPI.steam.unsubscribe(wallpaperId)
+    queryClient.invalidateQueries({ queryKey: ['library-all'] })
+    refetchFolders()
+  }
+
+  async function handleApplyDetail(wallpaperId: string) {
+    try {
+      await window.electronAPI.wallpaper.apply({ wallpaperId })
+      queryClient.invalidateQueries({ queryKey: ['library-all'] })
+    } catch (err) {
+      showToast((err as Error).message)
+    }
+  }
+
   async function ctxVote(up: boolean) {
     if (!ctxMenu) return
     const wallpaperId = ctxMenu.wallpaperId
@@ -445,6 +475,7 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
         ))}
       </div>
 
+      <div className="flex flex-1 overflow-hidden">
       <div className="flex flex-1 flex-col overflow-hidden">
         {!activePlaylist ? (
           <div className="flex h-full items-center justify-center text-gray-600">
@@ -605,6 +636,7 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
                   onRemove={() => handleRemoveItem(item.wallpaperId)}
                   onUpdate={(patch) => handleUpdateItem(item.wallpaperId, patch)}
                   onContextMenu={(e) => openItemCtxMenu(e, item.wallpaperId)}
+                  onOpenDetail={() => setDetailId(item.wallpaperId)}
                   defaultDurationSec={activePlaylist.settings.defaultDurationSec}
                   defaultVolume={activePlaylist.settings.defaultVolume}
                 />
@@ -614,6 +646,39 @@ export default function PlaylistsView({ onBrowseCreator }: PlaylistsViewProps) {
         )}
       </div>
 
+      {detailId && wallpaperById.get(detailId) && (
+        <DetailSidebar
+          id={detailId}
+          fallbackTitle={wallpaperById.get(detailId)!.title}
+          fallbackPreviewUrl={getPreviewSrc(wallpaperById.get(detailId))}
+          fallbackTags={wallpaperById.get(detailId)!.tags}
+          fallbackAuthorSteamId={wallpaperById.get(detailId)!.authorSteamId}
+          localFileSize={wallpaperById.get(detailId)!.fileSize}
+          isSubscribed={wallpaperById.get(detailId)!.subscribed}
+          isLiked={votedSet.has(detailId)}
+          canPlay={
+            !!wallpaperById.get(detailId)!.localPath &&
+            !wallpaperById.get(detailId)!.downloading &&
+            !wallpaperById.get(detailId)!.downloadFailed
+          }
+          lweInstalled={lweStatus?.installed ?? false}
+          onClose={() => setDetailId(null)}
+          onSubscribe={async () => {
+            await window.electronAPI.steam.subscribe(detailId)
+            queryClient.invalidateQueries({ queryKey: ['library-all'] })
+          }}
+          onUnsubscribe={() => unsubscribeWallpaper(detailId)}
+          onLiked={() => {
+            queryClient.setQueryData<string[]>(['steam-voted-ids'], (old) =>
+              old ? [...old, detailId] : [detailId]
+            )
+          }}
+          onPlay={() => handleApplyDetail(detailId)}
+          onBrowseCreator={onBrowseCreator}
+        />
+      )}
+      </div>
+
       {pickerOpen && activePlaylist && (
         <WallpaperPickerModal
           excludeIds={existingIds}

+ 102 - 3
src/renderer/src/components/workshop/WorkshopBrowser.tsx

@@ -12,7 +12,10 @@ import {
   Play,
   Download,
   EyeOff,
-  Eye
+  Eye,
+  Folder,
+  FolderInput,
+  Trash2
 } from 'lucide-react'
 import WorkshopCard from './WorkshopCard'
 import PreviewSizeToggle from '../common/PreviewSizeToggle'
@@ -30,7 +33,7 @@ import {
 } from '../../constants/weFilters'
 import { usePreviewSize, previewGridStyle } from '../../hooks/usePreviewSize'
 import { useClickOutside } from '../../hooks/useClickOutside'
-import { useClampedPosition } from '../../hooks/useContextMenuPosition'
+import { useClampedPosition, useFlipSide } from '../../hooks/useContextMenuPosition'
 import { useSubscriptionQueue } from '../../hooks/useSubscriptionQueue'
 import { toggle } from '../../utils/array'
 import { forEachIgnoringErrors } from '../../utils/async'
@@ -149,6 +152,7 @@ interface CtxMenu {
   x: number
   y: number
   ids: string[]
+  showFolderSub: boolean
 }
 
 interface Marquee {
@@ -184,6 +188,7 @@ export default function WorkshopBrowser({
   const gridRef = useRef<HTMLDivElement>(null)
   const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null)
   const ctxRef = useRef<HTMLDivElement>(null)
+  const folderSubRef = useRef<HTMLDivElement>(null)
   const [page, setPage] = useState(1)
   const [pageSize, setPageSize] = useState(50)
   const [previewSize, setPreviewSize] = usePreviewSize()
@@ -192,6 +197,7 @@ export default function WorkshopBrowser({
 
   useClickOutside(ctxRef, () => setCtxMenu(null), !!ctxMenu)
   const ctxPos = useClampedPosition(ctxRef, ctxMenu)
+  const folderSubSide = useFlipSide(folderSubRef, !!ctxMenu?.showFolderSub)
 
   useEffect(() => {
     saveFilters(filters)
@@ -255,7 +261,7 @@ export default function WorkshopBrowser({
       if (!selection.has(itemId)) {
         setSelection(new Set(ids))
       }
-      setCtxMenu({ x: e.clientX, y: e.clientY, ids })
+      setCtxMenu({ x: e.clientX, y: e.clientY, ids, showFolderSub: false })
     },
     [selection]
   )
@@ -356,6 +362,22 @@ export default function WorkshopBrowser({
       ),
     [libraryWallpapers]
   )
+  const libraryIdSet = useMemo(
+    () => new Set(libraryWallpapers.map((w) => w.id)),
+    [libraryWallpapers]
+  )
+
+  const { data: folders = [], refetch: refetchFolders } = useQuery({
+    queryKey: ['folders'],
+    queryFn: () => window.electronAPI.folders.getAll()
+  })
+  const folderOfItem = useMemo(() => {
+    const map = new Map<string, string>()
+    for (const f of folders) {
+      for (const id of f.items) map.set(id, f.id)
+    }
+    return map
+  }, [folders])
 
   const { data: lweStatus } = useQuery({
     queryKey: ['lwe-status'],
@@ -384,6 +406,28 @@ export default function WorkshopBrowser({
     closeCtxMenu()
   }
 
+  async function ctxMoveToFolder(folderId: string) {
+    if (!ctxMenu) return
+    const ids = ctxMenu.ids.filter((id) => libraryIdSet.has(id))
+    closeCtxMenu()
+    if (ids.length === 0) return
+    await window.electronAPI.folders.addItems(folderId, ids)
+    refetchFolders()
+  }
+
+  async function ctxRemoveFromFolder() {
+    if (!ctxMenu) return
+    const ids = ctxMenu.ids
+    closeCtxMenu()
+    for (const folder of folders) {
+      const overlap = ids.filter((id) => folder.items.includes(id))
+      if (overlap.length > 0) {
+        await window.electronAPI.folders.removeItems(folder.id, overlap)
+      }
+    }
+    refetchFolders()
+  }
+
   async function ctxToggleIgnoreCreator() {
     if (!ctxMenu || ctxMenu.ids.length !== 1) return
     const item = allItems.find((i) => i.publishedFileId === ctxMenu.ids[0])
@@ -584,6 +628,15 @@ export default function WorkshopBrowser({
       }
     : null
 
+  const ctxLibraryIds = ctxMenu ? ctxMenu.ids.filter((id) => libraryIdSet.has(id)) : []
+  const ctxCurrentFolderIds = ctxMenu
+    ? new Set(ctxMenu.ids.map((id) => folderOfItem.get(id)).filter((id): id is string => !!id))
+    : new Set<string>()
+  const ctxHasFolder = ctxCurrentFolderIds.size > 0
+  const ctxMoveTargets = folders.filter(
+    (f) => !(ctxCurrentFolderIds.size === 1 && ctxCurrentFolderIds.has(f.id))
+  )
+
   const detailItem = detailId ? allItems.find((i) => i.publishedFileId === detailId) : undefined
   const detailSubscribeState = detailId ? subscribeEntries.get(detailId)?.state : undefined
   const detailSubscribed = detailItem
@@ -1047,6 +1100,52 @@ export default function WorkshopBrowser({
               <User size={12} /> Browse wallpapers from this creator
             </button>
           )}
+          {ctxLibraryIds.length > 0 && (
+            <>
+              <div className="my-1 border-t border-white/5" />
+              <div className="relative">
+                <button
+                  onClick={() =>
+                    setCtxMenu((prev) => (prev ? { ...prev, showFolderSub: !prev.showFolderSub } : null))
+                  }
+                  className="flex w-full items-center gap-2 px-3 py-1.5 text-gray-300 hover:bg-white/5"
+                >
+                  <FolderInput size={12} /> Move to folder
+                  <ChevronRight size={12} className="ml-auto" />
+                </button>
+                {ctxMenu.showFolderSub && (
+                  <div
+                    ref={folderSubRef}
+                    className={clsx(
+                      'absolute top-0 min-w-[160px] rounded-lg border border-white/10 bg-[#1a1a1a] py-1 shadow-xl',
+                      folderSubSide === 'left' ? 'right-full mr-1' : 'left-full ml-1'
+                    )}
+                  >
+                    {ctxMoveTargets.length === 0 && (
+                      <div className="px-3 py-1.5 text-gray-500">No other folders</div>
+                    )}
+                    {ctxMoveTargets.map((f) => (
+                      <button
+                        key={f.id}
+                        onClick={() => ctxMoveToFolder(f.id)}
+                        className="flex w-full items-center gap-2 px-3 py-1.5 text-gray-300 hover:bg-white/5"
+                      >
+                        <Folder size={12} /> {f.title}
+                      </button>
+                    ))}
+                  </div>
+                )}
+              </div>
+              {ctxHasFolder && (
+                <button
+                  onClick={ctxRemoveFromFolder}
+                  className="flex w-full items-center gap-2 px-3 py-1.5 text-red-400 hover:bg-white/5"
+                >
+                  <Trash2 size={12} /> Remove from folder
+                </button>
+              )}
+            </>
+          )}
           {ctxMenu.ids.length === 1 && (() => {
             const ctxItem = allItems.find((i) => i.publishedFileId === ctxMenu.ids[0])
             const ctxCreatorIgnored = ctxItem ? ignoredCreatorSet.has(ctxItem.creatorSteamId) : false