Ver Fonte

added small button to open wallpaper locally

UwU há 2 meses atrás
pai
commit
b3c7659f11

+ 63 - 22
src/main/services/library.service.ts

@@ -5,7 +5,7 @@ import * as path from 'path'
 import * as fs from 'fs'
 import { randomUUID } from 'crypto'
 import { cleanupPlaylists } from './playlist.service'
-import { getWorkshopTimesUpdated } from './workshop.service'
+import { getWorkshopTimesUpdated, getWorkshopTags } from './workshop.service'
 import { findAndMutate } from '../utils/collections'
 import { isSteamRunning, getItemDownloadStatus, getSubscribedItems } from './steam.service'
 
@@ -187,6 +187,27 @@ export function normalizeRating(raw?: string): ContentRating {
   }
 }
 
+// Derived from the item's Steam Workshop tags for wallpapers that never finished
+// downloading (no local project.json), so failed downloads still land in the
+// right type/rating filters instead of "uncategorized".
+function deriveTypeFromTags(tags: string[]): WallpaperType | undefined {
+  const match = tags.find((t) => ['Video', 'Web', 'Application', 'Scene'].includes(t))
+  return match ? normalizeType(match) : undefined
+}
+
+function deriveRatingFromTags(tags: string[]): ContentRating | undefined {
+  const match = tags.find((t) => ['Everyone', 'Questionable', 'Mature'].includes(t))
+  return match ? normalizeRating(match) : undefined
+}
+
+async function safeGetWorkshopTags(ids: string[]): Promise<Map<string, string[]>> {
+  try {
+    return await getWorkshopTags(ids)
+  } catch {
+    return new Map()
+  }
+}
+
 export function getDirSize(dirPath: string): number {
   let total = 0
   try {
@@ -242,24 +263,29 @@ function buildMeta(
 function buildIncompleteMeta(
   workshopId: string,
   localPath: string,
-  existing?: WallpaperMeta
+  existing?: WallpaperMeta,
+  workshopTags?: string[]
 ): WallpaperMeta {
   const now = Date.now()
   const { downloading, failed } = isSteamRunning()
     ? getItemDownloadStatus(BigInt(workshopId))
     : { downloading: true, failed: false }
+  const tags = workshopTags
+    ? [...new Set([...(existing?.tags ?? []), ...workshopTags])]
+    : (existing?.tags ?? [])
   return {
     id: workshopId,
     title: existing?.title ?? `Wallpaper ${workshopId}`,
-    type: existing?.type ?? 'scene',
-    contentRating: existing?.contentRating ?? 'uncategorized',
+    type: (workshopTags && deriveTypeFromTags(workshopTags)) ?? existing?.type ?? 'scene',
+    contentRating:
+      (workshopTags && deriveRatingFromTags(workshopTags)) ?? existing?.contentRating ?? 'uncategorized',
     localPath,
     createdAt: existing?.createdAt ?? now,
     updatedAt: now,
     subscribed: true,
     appliedCount: existing?.appliedCount ?? 0,
     source: 'workshop',
-    tags: existing?.tags ?? [],
+    tags,
     categories: existing?.categories ?? [],
     downloading,
     downloadFailed: failed
@@ -273,7 +299,8 @@ export async function importWallpaperById(workshopId: string): Promise<Wallpaper
   const existing = getWallpaper(workshopId) ?? undefined
   const pj = readProjectJson(localPath)
   if (!pj) {
-    const meta = buildIncompleteMeta(workshopId, localPath, existing)
+    const tags = await safeGetWorkshopTags([workshopId])
+    const meta = buildIncompleteMeta(workshopId, localPath, existing, tags.get(workshopId))
     upsertWallpaper(meta)
     return meta
   }
@@ -308,20 +335,7 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
     }
   }
 
-  const trackIncomplete = (id: string, localPath: string): void => {
-    const existing = wallpapers[id]
-    const meta = buildIncompleteMeta(id, localPath, existing)
-    if (!existing) {
-      wallpapers[id] = meta
-      imported++
-    } else if (existing.downloading !== meta.downloading || existing.downloadFailed !== meta.downloadFailed) {
-      wallpapers[id] = { ...existing, downloading: meta.downloading, downloadFailed: meta.downloadFailed }
-      skipped++
-    } else {
-      skipped++
-    }
-  }
-
+  const incomplete: { id: string; localPath: string }[] = []
   const toRebuild: { entry: fs.Dirent; localPath: string; pj: ProjectJson }[] = []
   const cacheHits: string[] = []
   for (const entry of entries) {
@@ -331,7 +345,7 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
     const pj = readProjectJson(localPath)
 
     if (!pj) {
-      trackIncomplete(entry.name, localPath)
+      incomplete.push({ id: entry.name, localPath })
       continue
     }
 
@@ -349,7 +363,34 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
   // at all (the download failed before writing anything, or hasn't started)
   for (const id of subscribedIds) {
     if (onDisk.has(id)) continue
-    trackIncomplete(id, path.join(workshopPath, id))
+    incomplete.push({ id, localPath: path.join(workshopPath, id) })
+  }
+
+  const incompleteTags = await safeGetWorkshopTags(incomplete.map((i) => i.id))
+  for (const { id, localPath } of incomplete) {
+    const existing = wallpapers[id]
+    const meta = buildIncompleteMeta(id, localPath, existing, incompleteTags.get(id))
+    if (!existing) {
+      wallpapers[id] = meta
+      imported++
+    } else if (
+      existing.downloading !== meta.downloading ||
+      existing.downloadFailed !== meta.downloadFailed ||
+      existing.type !== meta.type ||
+      existing.contentRating !== meta.contentRating
+    ) {
+      wallpapers[id] = {
+        ...existing,
+        downloading: meta.downloading,
+        downloadFailed: meta.downloadFailed,
+        type: meta.type,
+        contentRating: meta.contentRating,
+        tags: meta.tags
+      }
+      skipped++
+    } else {
+      skipped++
+    }
   }
 
   const timesUpdated = await safeGetWorkshopTimesUpdated([

+ 1 - 1
src/main/services/steam.service.ts

@@ -55,7 +55,7 @@ function getUgcFfi(): UgcFfi {
     const libPath = path.join(
       path.dirname(require.resolve('steamworks.js')),
       'dist', 'linux64', 'libsteam_api.so'
-    )
+    ).replace(/app\.asar(?!\.unpacked)/, 'app.asar.unpacked')
     const steamLib = koffi.load(libPath)
     const ugcPtrType = koffi.pointer(koffi.opaque('ISteamUGC'))
     const getSteamUGC = steamLib.func('SteamAPI_SteamUGC_v020', ugcPtrType, [])

+ 15 - 0
src/main/services/workshop.service.ts

@@ -70,6 +70,21 @@ export async function getWorkshopTimesUpdated(
   return map
 }
 
+// Steam tags an item's type ("Video", "Scene", ...) and age rating ("Everyone", ...)
+// alongside its genre/resolution tags, so this works even for items that never
+// finished downloading and have no local project.json to read that from.
+export async function getWorkshopTags(publishedFileIds: string[]): Promise<Map<string, string[]>> {
+  const map = new Map<string, string[]>()
+  if (publishedFileIds.length === 0) return map
+
+  const client = getClient()
+  const result = await client.workshop.getItems(publishedFileIds.map((id) => BigInt(id)))
+  for (const item of result.items) {
+    if (item) map.set(item.publishedFileId.toString(), item.tags ?? [])
+  }
+  return map
+}
+
 function transformItem(
   item: NonNullable<Awaited<ReturnType<ReturnType<typeof getClient>['workshop']['getItem']>>>,
   subscribedSet: Set<string>

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

@@ -1,5 +1,5 @@
 import { useState, useEffect } from 'react'
-import { Play, Loader2, Check, X, ThumbsUp, Trash2, ExternalLink, Download, Archive, AlertTriangle, RotateCw, Plus } from 'lucide-react'
+import { Play, Loader2, Check, X, ThumbsUp, Trash2, ExternalLink, Download, Archive, AlertTriangle, RotateCw, Plus, FolderOpen } from 'lucide-react'
 import type { WallpaperMeta } from '@shared/types'
 import clsx from 'clsx'
 import { getPreviewSrc } from '../../utils/preview'
@@ -146,6 +146,12 @@ export default function WallpaperCard({
     }
   }
 
+  async function handleOpenLocally(e: React.MouseEvent) {
+    e.stopPropagation()
+    if (!wallpaper.localPath) return
+    await window.electronAPI.shell.openPath(wallpaper.localPath)
+  }
+
   const previewSrc = getPreviewSrc(wallpaper)
   const compact = previewSize !== 'big'
 
@@ -333,6 +339,18 @@ export default function WallpaperCard({
             <ExternalLink size={11} />
             {!compact && 'Steam'}
           </button>
+          <button
+            onClick={handleOpenLocally}
+            disabled={!wallpaper.localPath}
+            title={wallpaper.localPath ? 'Open wallpaper folder' : 'Local files not found'}
+            className={clsx(
+              'flex items-center gap-1 rounded bg-white/5 text-xs text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-200 disabled:cursor-not-allowed disabled:opacity-50',
+              compact ? 'p-1.5' : 'px-2 py-1'
+            )}
+          >
+            <FolderOpen size={11} />
+            {!compact && 'Open'}
+          </button>
         </div>
       </div>