瀏覽代碼

added fps

UwU 4 月之前
父節點
當前提交
6d123cad57

+ 9 - 1
src/main/ipc/config.handlers.ts

@@ -4,6 +4,8 @@ import {
   getConfiguredWorkshopPath,
   setConfiguredWorkshopPath,
   isWorkshopPathConfigured,
+  getDefaultFps,
+  setDefaultFps,
   importWEConfigFile,
   createFreshConfig
 } from '../services/config.service'
@@ -15,9 +17,15 @@ export function registerConfigHandlers(): void {
   ipcMain.handle(IpcChannels.CONFIG_GET, () => ({
     workshopPath: getConfiguredWorkshopPath(),
     defaultWorkshopPath: getDefaultWorkshopPath(),
-    isConfigured: isWorkshopPathConfigured()
+    isConfigured: isWorkshopPathConfigured(),
+    defaultFps: getDefaultFps()
   }))
 
+  ipcMain.handle(IpcChannels.CONFIG_SET_DEFAULT_FPS, (_e, fps: number | null) => {
+    setDefaultFps(fps)
+    return { ok: true }
+  })
+
   ipcMain.handle(IpcChannels.CONFIG_SET_WORKSHOP_PATH, (_e, newPath: string) => {
     setConfiguredWorkshopPath(newPath)
     restartWatcher()

+ 5 - 0
src/main/ipc/library.handlers.ts

@@ -3,6 +3,7 @@ import { IpcChannels } from '@shared/ipc-channels'
 import * as library from '../services/library.service'
 import type { LibraryFilters, WallpaperMeta } from '@shared/types'
 
+
 export function registerLibraryHandlers(): void {
   ipcMain.handle(IpcChannels.LIBRARY_GET_ALL, (_e, filters?: LibraryFilters) => {
     return library.getAllWallpapers(filters)
@@ -47,4 +48,8 @@ export function registerLibraryHandlers(): void {
   ipcMain.handle(IpcChannels.LIBRARY_DISTINCT_TAGS, () => {
     return library.getDistinctTags()
   })
+
+  ipcMain.handle(IpcChannels.LIBRARY_RESET_FPS_OVERRIDES, () => {
+    return library.resetAllFpsOverrides()
+  })
 }

+ 3 - 1
src/main/ipc/wallpaper.handlers.ts

@@ -3,6 +3,7 @@ import { IpcChannels } from '@shared/ipc-channels'
 import { applyWallpaper, detectEnvironment } from '../services/wallpaper.service'
 import { getWallpaper, updateWallpaper } from '../services/library.service'
 import { getLweStatus, launchLweAsync } from '../services/lwe.service'
+import { getDefaultFps } from '../services/config.service'
 import type { ApplyWallpaperOptions } from '@shared/types'
 import Store from 'electron-store'
 import * as os from 'os'
@@ -34,7 +35,8 @@ export function registerWallpaperHandlers(): void {
 
     // Use linux-wallpaperengine for animated wallpapers on Linux if available
     if (isAnimated && os.platform() === 'linux' && lwe.installed) {
-      await launchLweAsync(wallpaper.localPath)
+      const fps = wallpaper.fpsOverride ?? getDefaultFps() ?? undefined
+      await launchLweAsync(wallpaper.localPath, { fps })
 
       store.set('activeWallpaperId', options.wallpaperId)
       updateWallpaper(options.wallpaperId, {

+ 11 - 1
src/main/services/config.service.ts

@@ -6,12 +6,14 @@ import { WE_APP_ID } from '@shared/constants'
 
 interface AppConfig {
   workshopPath: string | null
+  defaultFps: number | null
 }
 
 const store = new Store<AppConfig>({
   name: 'config',
   defaults: {
-    workshopPath: null
+    workshopPath: null,
+    defaultFps: null
   }
 })
 
@@ -27,6 +29,14 @@ export function isWorkshopPathConfigured(): boolean {
   return store.get('workshopPath') !== null
 }
 
+export function getDefaultFps(): number | null {
+  return store.get('defaultFps')
+}
+
+export function setDefaultFps(fps: number | null): void {
+  store.set('defaultFps', fps)
+}
+
 /** Path where we store our copy of config.json */
 export function getWeConfigPath(): string {
   return path.join(app.getPath('userData'), 'we-config.json')

+ 13 - 0
src/main/services/library.service.ts

@@ -97,6 +97,19 @@ export function deleteWallpaper(id: string): void {
   store.set('wallpapers', wallpapers)
 }
 
+export function resetAllFpsOverrides(): { count: number } {
+  const wallpapers = store.get('wallpapers')
+  let count = 0
+  for (const id of Object.keys(wallpapers)) {
+    if (wallpapers[id].fpsOverride !== undefined) {
+      delete wallpapers[id].fpsOverride
+      count++
+    }
+  }
+  store.set('wallpapers', wallpapers)
+  return { count }
+}
+
 export function addTag(wallpaperId: string, tagName: string): void {
   const wallpaper = getWallpaper(wallpaperId)
   if (!wallpaper) return

+ 2 - 1
src/main/services/lwe.service.ts

@@ -17,7 +17,8 @@ const BUILD_DIR = path.join(os.tmpdir(), 'lwe-build')
 
 /** Common paths where linux-wallpaperengine may be installed outside $PATH. */
 const LWE_SEARCH_PATHS = [
-  '/home/uwu/projects/linux-wallpaperengine-fork/build/output/linux-wallpaperengine',
+  '/home/uwu/projects/private/linux-wallpaperengine/build/output/linux-wallpaperengine',
+  '/home/uwu/projects/linux-wallpaperengine/build/output/linux-wallpaperengine',
   '/usr/local/bin/linux-wallpaperengine',
   '/usr/local/linux-wallpaperengine',
   '/usr/bin/linux-wallpaperengine',

+ 6 - 2
src/preload/index.ts

@@ -62,14 +62,18 @@ const api = {
     scan: (): Promise<{ imported: number; skipped: number; removed: number }> =>
       ipcRenderer.invoke(IpcChannels.LIBRARY_SCAN),
     distinctTags: (): Promise<string[]> =>
-      ipcRenderer.invoke(IpcChannels.LIBRARY_DISTINCT_TAGS)
+      ipcRenderer.invoke(IpcChannels.LIBRARY_DISTINCT_TAGS),
+    resetFpsOverrides: (): Promise<{ count: number }> =>
+      ipcRenderer.invoke(IpcChannels.LIBRARY_RESET_FPS_OVERRIDES)
   },
 
   config: {
-    get: (): Promise<{ workshopPath: string | null; defaultWorkshopPath: string; isConfigured: boolean }> =>
+    get: (): Promise<{ workshopPath: string | null; defaultWorkshopPath: string; isConfigured: boolean; defaultFps: number | null }> =>
       ipcRenderer.invoke(IpcChannels.CONFIG_GET),
     setWorkshopPath: (p: string): Promise<{ ok: boolean }> =>
       ipcRenderer.invoke(IpcChannels.CONFIG_SET_WORKSHOP_PATH, p),
+    setDefaultFps: (fps: number | null): Promise<{ ok: boolean }> =>
+      ipcRenderer.invoke(IpcChannels.CONFIG_SET_DEFAULT_FPS, fps),
     pickFolder: (): Promise<string | null> =>
       ipcRenderer.invoke(IpcChannels.CONFIG_PICK_FOLDER),
     pickFile: (): Promise<string | null> =>

+ 25 - 0
src/renderer/src/components/library/WallpaperCard.tsx

@@ -3,6 +3,7 @@ import { Play, Loader2, Check, ThumbsUp, Trash2, ExternalLink, Download } from '
 import type { WallpaperMeta } from '@shared/types'
 import clsx from 'clsx'
 
+
 interface WallpaperCardProps {
   wallpaper: WallpaperMeta
   selected?: boolean
@@ -30,6 +31,7 @@ export default function WallpaperCard({
   const [isLiking, setIsLiking] = useState(false)
   const [unsubState, setUnsubState] = useState<'idle' | 'confirm' | 'pending'>('idle')
   const [error, setError] = useState<string | null>(null)
+  const [fpsInput, setFpsInput] = useState(wallpaper.fpsOverride != null ? String(wallpaper.fpsOverride) : '')
 
   async function handleApply(e: React.MouseEvent) {
     e.stopPropagation()
@@ -59,6 +61,15 @@ export default function WallpaperCard({
     }
   }
 
+  function handleFpsBlur() {
+    const trimmed = fpsInput.trim()
+    const parsed = trimmed === '' ? undefined : parseInt(trimmed, 10)
+    if (parsed !== undefined && (isNaN(parsed) || parsed < 1)) return
+    const current = wallpaper.fpsOverride
+    if ((parsed ?? undefined) === current) return
+    window.electronAPI.library.update(wallpaper.id, { fpsOverride: parsed })
+  }
+
   async function handleUnsubscribeClick(e: React.MouseEvent) {
     e.stopPropagation()
     if (unsubState === 'idle') { setUnsubState('confirm'); return }
@@ -140,6 +151,20 @@ export default function WallpaperCard({
             ))}
           </div>
         )}
+        <div className="mt-2 flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
+          <span className="text-xs text-gray-500">FPS:</span>
+          <input
+            type="number"
+            min={1}
+            max={360}
+            placeholder="default"
+            value={fpsInput}
+            onChange={(e) => setFpsInput(e.target.value)}
+            onBlur={handleFpsBlur}
+            onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur() } }}
+            className="w-16 rounded bg-white/5 px-1.5 py-0.5 text-xs text-gray-300 outline-none focus:ring-1 focus:ring-indigo-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+          />
+        </div>
         {error && <p className="mt-1 text-xs text-red-400">{error}</p>}
         <div className="mt-2 flex items-center gap-2">
           <button

+ 93 - 1
src/renderer/src/components/settings/SettingsView.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect } from 'react'
-import { FolderOpen, Save, Upload, Download, CheckCircle, XCircle, Loader2, Package, Trash2, Monitor } from 'lucide-react'
+import { FolderOpen, Save, Upload, Download, CheckCircle, XCircle, Loader2, Package, Trash2, Monitor, RotateCcw } from 'lucide-react'
 import type { LweStatus, LweInstallProgress, LinuxDistro } from '../../../../shared/types'
 
 const DISTRO_LABELS: Record<LinuxDistro, string> = {
@@ -13,6 +13,10 @@ export default function SettingsView() {
   const [workshopPath, setWorkshopPath] = useState('')
   const [saved, setSaved] = useState(false)
   const [importStatus, setImportStatus] = useState<string | null>(null)
+  const [defaultFps, setDefaultFps] = useState<string>('')
+  const [fpsSaved, setFpsSaved] = useState(false)
+  const [resetFpsConfirm, setResetFpsConfirm] = useState(false)
+  const [resetFpsMsg, setResetFpsMsg] = useState<string | null>(null)
 
   // linux-wallpaperengine state
   const [lweStatus, setLweStatus] = useState<LweStatus | null>(null)
@@ -29,6 +33,7 @@ export default function SettingsView() {
   useEffect(() => {
     window.electronAPI.config.get().then((cfg) => {
       setWorkshopPath(cfg.workshopPath ?? cfg.defaultWorkshopPath)
+      setDefaultFps(cfg.defaultFps != null ? String(cfg.defaultFps) : '')
     })
     window.electronAPI.lwe.status().then(setLweStatus)
     window.electronAPI.lwe.detectDistro().then(setDistro)
@@ -121,6 +126,22 @@ export default function SettingsView() {
     }
   }
 
+  async function handleSaveDefaultFps() {
+    const parsed = defaultFps.trim() === '' ? null : parseInt(defaultFps, 10)
+    if (parsed !== null && (isNaN(parsed) || parsed < 1)) return
+    await window.electronAPI.config.setDefaultFps(parsed)
+    setFpsSaved(true)
+    setTimeout(() => setFpsSaved(false), 2000)
+  }
+
+  async function handleResetFpsOverrides() {
+    if (!resetFpsConfirm) { setResetFpsConfirm(true); return }
+    setResetFpsConfirm(false)
+    const result = await window.electronAPI.library.resetFpsOverrides()
+    setResetFpsMsg(`Reset FPS for ${result.count} wallpaper${result.count !== 1 ? 's' : ''}.`)
+    setTimeout(() => setResetFpsMsg(null), 3000)
+  }
+
   async function handleDesktopIconsToggle() {
     const newVal = !desktopIconsEnabled
     setDesktopIconsEnabled(newVal)
@@ -170,6 +191,77 @@ export default function SettingsView() {
           </button>
         </div>
 
+        {/* LWE launch options */}
+        <div>
+          <label className="block text-xs font-medium uppercase tracking-wide text-gray-500">
+            Wallpaper Engine Options
+          </label>
+          <p className="mt-1 text-xs text-gray-600">
+            Launch options passed to linux-wallpaperengine.
+          </p>
+          <div className="mt-3 space-y-3">
+            <div>
+              <label className="block text-xs text-gray-400 mb-1">Default FPS limit</label>
+              <p className="text-xs text-gray-600 mb-2">
+                Limits frame rate for all wallpapers. Leave empty for unlimited. Can be overridden per wallpaper.
+              </p>
+              <div className="flex gap-2 items-center">
+                <input
+                  type="number"
+                  min={1}
+                  max={360}
+                  placeholder="unlimited"
+                  value={defaultFps}
+                  onChange={(e) => { setDefaultFps(e.target.value); setFpsSaved(false) }}
+                  onKeyDown={(e) => e.key === 'Enter' && handleSaveDefaultFps()}
+                  className="w-28 rounded-lg bg-white/5 px-3 py-2 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-indigo-500"
+                />
+                <button
+                  onClick={handleSaveDefaultFps}
+                  className="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500"
+                >
+                  <Save size={14} />
+                  {fpsSaved ? 'Saved!' : 'Save'}
+                </button>
+              </div>
+            </div>
+            <div>
+              <label className="block text-xs text-gray-400 mb-1">Reset per-wallpaper FPS</label>
+              <p className="text-xs text-gray-600 mb-2">
+                Remove all per-wallpaper FPS overrides so they use the default.
+              </p>
+              {resetFpsConfirm ? (
+                <div className="flex items-center gap-2">
+                  <span className="text-xs text-gray-400">Are you sure?</span>
+                  <button
+                    onClick={handleResetFpsOverrides}
+                    className="rounded-lg bg-red-600/80 px-3 py-1.5 text-xs text-white hover:bg-red-600"
+                  >
+                    Yes, reset all
+                  </button>
+                  <button
+                    onClick={() => setResetFpsConfirm(false)}
+                    className="rounded-lg bg-white/5 px-3 py-1.5 text-xs text-gray-400 hover:bg-white/10"
+                  >
+                    Cancel
+                  </button>
+                </div>
+              ) : (
+                <button
+                  onClick={handleResetFpsOverrides}
+                  className="flex items-center gap-2 rounded-lg bg-white/5 px-4 py-2 text-sm text-gray-300 hover:bg-white/10"
+                >
+                  <RotateCcw size={14} />
+                  Reset all FPS overrides
+                </button>
+              )}
+              {resetFpsMsg && (
+                <p className="mt-2 text-xs text-gray-400">{resetFpsMsg}</p>
+              )}
+            </div>
+          </div>
+        </div>
+
         {/* Import WE config */}
         <div>
           <label className="block text-xs font-medium uppercase tracking-wide text-gray-500">

+ 2 - 0
src/shared/ipc-channels.ts

@@ -26,6 +26,7 @@ export const IpcChannels = {
   LIBRARY_SEARCH: 'library:search',
   LIBRARY_SCAN: 'library:scan',
   LIBRARY_DISTINCT_TAGS: 'library:distinct-tags',
+  LIBRARY_RESET_FPS_OVERRIDES: 'library:reset-fps-overrides',
 
   // Folders
   FOLDERS_GET_ALL: 'folders:get-all',
@@ -45,6 +46,7 @@ export const IpcChannels = {
   // Configuration
   CONFIG_GET: 'config:get',
   CONFIG_SET_WORKSHOP_PATH: 'config:set-workshop-path',
+  CONFIG_SET_DEFAULT_FPS: 'config:set-default-fps',
   CONFIG_PICK_FOLDER: 'config:pick-folder',
   CONFIG_PICK_FILE: 'config:pick-file',
   CONFIG_IMPORT_WE: 'config:import-we',

+ 1 - 0
src/shared/types.ts

@@ -43,6 +43,7 @@ export interface WallpaperMeta {
   tags: string[]
   categories: string[]
   downloading?: boolean
+  fpsOverride?: number
 }
 
 export interface WorkshopItem {