Bläddra i källkod

fixed few small bugs, added setting to chose on which wallpaper to play audio

UwU 1 månad sedan
förälder
incheckning
c45a3a9e1c

+ 18 - 2
src/main/ipc/config.handlers.ts

@@ -30,7 +30,11 @@ import {
   getAutostartPlaylistId,
   setAutostartPlaylistId,
   getKillLweOnQuit,
-  setKillLweOnQuit
+  setKillLweOnQuit,
+  getAudioScreen,
+  setAudioScreen,
+  getAmbientVolume,
+  setAmbientVolume
 } from '../services/config.service'
 import { getDefaultWorkshopPath } from '../utils/paths'
 import { DEFAULT_LWE_REPO } from '@shared/constants'
@@ -59,7 +63,9 @@ export function registerConfigHandlers(): void {
     autostartEnabled: getAutostartEnabled(),
     autostartMinimized: getAutostartMinimized(),
     autostartPlaylistId: getAutostartPlaylistId(),
-    killLweOnQuit: getKillLweOnQuit()
+    killLweOnQuit: getKillLweOnQuit(),
+    audioScreen: getAudioScreen(),
+    ambientVolume: getAmbientVolume()
   }))
 
   ipcMain.handle(IpcChannels.CONFIG_GET_AUTOSTART_SUPPORTED, () => isAutostartSupported())
@@ -115,6 +121,16 @@ export function registerConfigHandlers(): void {
     return { ok: true }
   })
 
+  ipcMain.handle(IpcChannels.CONFIG_SET_AUDIO_SCREEN, (_e, screen: string | null) => {
+    setAudioScreen(screen)
+    return { ok: true }
+  })
+
+  ipcMain.handle(IpcChannels.CONFIG_SET_AMBIENT_VOLUME, (_e, volume: number | null) => {
+    setAmbientVolume(volume)
+    return { ok: true }
+  })
+
   ipcMain.handle(IpcChannels.CONFIG_SET_LWE_REPO, (_e, url: string | null, branch: string | null) => {
     setLweRepo(url, branch)
     return { ok: true }

+ 7 - 0
src/main/ipc/lwe.handlers.ts

@@ -2,6 +2,7 @@ import { ipcMain, type BrowserWindow } from 'electron'
 import { IpcChannels } from '@shared/ipc-channels'
 import { getLweStatus, detectDistro, installLweDeps, installLwe, uninstallLwe, launchLweAsync, stopLwe, isLweRunning, killAllLweProcesses, listLweObjects, hotswapLweSettings, listLweProperties } from '../services/lwe.service'
 import { invalidateEnvCache } from '../services/wallpaper.service'
+import { getConnectedScreens } from '../utils/platform'
 
 export function registerLweHandlers(win: BrowserWindow): void {
   ipcMain.handle(IpcChannels.LWE_STATUS, () => {
@@ -68,12 +69,18 @@ export function registerLweHandlers(win: BrowserWindow): void {
         cornerColor?: string
         speed?: number
         propertyOverrides?: Record<string, string>
+        audioScreen?: string
+        ambientVolume?: number
       }
     ) => {
       return { ok: hotswapLweSettings(options) }
     }
   )
 
+  ipcMain.handle(IpcChannels.LWE_LIST_SCREENS, () => {
+    return getConnectedScreens()
+  })
+
   ipcMain.handle(IpcChannels.LWE_LIST_PROPERTIES, async (_e, wallpaperPath: string) => {
     // Same reasoning as list-objects: some wallpapers have properties linux-wallpaperengine
     // itself can't parse, treat that as "no properties" rather than a hard error.

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

@@ -19,6 +19,8 @@ interface AppConfig {
   killLweOnQuit: boolean
   recommendedFpsEnabled: boolean
   recommendedWebFpsEnabled: boolean
+  audioScreen: string | null
+  ambientVolume: number | null
 }
 
 const store = new Store<AppConfig>({
@@ -37,7 +39,9 @@ const store = new Store<AppConfig>({
     autostartEnabled: false,
     autostartMinimized: false,
     autostartPlaylistId: null,
-    killLweOnQuit: false
+    killLweOnQuit: false,
+    audioScreen: null,
+    ambientVolume: null
   }
 })
 
@@ -138,6 +142,24 @@ export function setRecommendedWebFpsEnabled(enabled: boolean): void {
   store.set('recommendedWebFpsEnabled', enabled)
 }
 
+/** Screen name (matches --screen-root/--audio-screen) that alone produces audio; null = every screen can. */
+export function getAudioScreen(): string | null {
+  return store.get('audioScreen')
+}
+
+export function setAudioScreen(screen: string | null): void {
+  store.set('audioScreen', screen)
+}
+
+/** 0-128, applied to non-video (scene sound + web) backgrounds instead of the per-wallpaper volume; null = same volume. */
+export function getAmbientVolume(): number | null {
+  return store.get('ambientVolume')
+}
+
+export function setAmbientVolume(volume: number | null): void {
+  store.set('ambientVolume', volume)
+}
+
 /** Custom linux-wallpaperengine repo (git URL or local path); null = official repo. */
 export function getLweRepoUrl(): string | null {
   return store.get('lweRepoUrl')

+ 25 - 4
src/main/services/library.service.ts

@@ -269,7 +269,8 @@ function buildMeta(
   pj: ProjectJson | null,
   existing?: WallpaperMeta,
   workshopTimeUpdated?: number,
-  authorSteamId?: string
+  authorSteamId?: string,
+  liveWorkshopTags?: string[]
 ): WallpaperMeta {
   const now = Date.now()
   const { downloading: _d, downloadFailed: _df, ...existingRest } = existing ?? { appliedCount: 0, categories: [] }
@@ -280,12 +281,17 @@ function buildMeta(
   // it unset here falls back to the "No preview" placeholder, and the next
   // library scan picks up the real file once it exists.
   const previewPath = pj?.preview ? path.join(localPath, pj.preview) : undefined
+  // Steam can flag an item Mature after the fact (moderation, community reports) without the
+  // author ever republishing - project.json keeps saying whatever it said at publish time, so
+  // the live Workshop tags are the only place that catches it.
+  const fromProject = stricterRating(normalizeRating(pj?.contentrating), deriveRatingFromTags(pj?.tags ?? []))
+  const contentRating = stricterRating(fromProject, deriveRatingFromTags(liveWorkshopTags ?? []))
   return {
     ...existingRest,
     id: workshopId,
     title: pj?.title ?? `Wallpaper ${workshopId}`,
     type: normalizeType(pj?.type),
-    contentRating: stricterRating(normalizeRating(pj?.contentrating), deriveRatingFromTags(pj?.tags ?? [])),
+    contentRating,
     description: pj?.description,
     previewLocal: previewPath && fs.existsSync(previewPath) ? previewPath : undefined,
     previewUrl: undefined,
@@ -459,6 +465,10 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
     ...toRebuild.map(({ entry }) => entry.name),
     ...cacheHits
   ])
+  const liveTags = await safeGetWorkshopTags([
+    ...toRebuild.map(({ entry }) => entry.name),
+    ...cacheHits
+  ])
 
   for (const { entry, localPath, pj } of toRebuild) {
     wallpapers[entry.name] = buildMeta(
@@ -467,7 +477,8 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
       pj,
       wallpapers[entry.name] ?? undefined,
       timesUpdated.get(entry.name),
-      authors.get(entry.name)
+      authors.get(entry.name),
+      liveTags.get(entry.name)
     )
     imported++
   }
@@ -476,6 +487,7 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
   for (const id of cacheHits) {
     const seconds = timesUpdated.get(id)
     const authorId = authors.get(id)
+    const live = liveTags.get(id)
 
     // A time_updated bump means the author changed something on the Workshop side
     // (tags, content rating, a republish) - re-read project.json instead of just
@@ -491,7 +503,8 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
           pj,
           wallpapers[id],
           seconds,
-          authorId ?? wallpapers[id].authorSteamId
+          authorId ?? wallpapers[id].authorSteamId,
+          live
         )
         resynced = true
         continue
@@ -505,6 +518,14 @@ export async function scanLibrary(): Promise<{ imported: number; skipped: number
     if (authorId && wallpapers[id].authorSteamId !== authorId) {
       patch.authorSteamId = authorId
     }
+    // Only tighten, never loosen - a failed/empty tag fetch must not walk a rating back down.
+    const liveRating = live ? deriveRatingFromTags(live) : undefined
+    if (liveRating) {
+      const stricter = stricterRating(wallpapers[id].contentRating ?? 'uncategorized', liveRating)
+      if (stricter !== wallpapers[id].contentRating) {
+        patch.contentRating = stricter
+      }
+    }
     if (Object.keys(patch).length > 0) {
       wallpapers[id] = { ...wallpapers[id], ...patch }
       resynced = true

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

@@ -15,7 +15,7 @@ import {
   getXdgRuntimeDir
 } from '../utils/platform'
 import { getWorkshopPath, getLweManifestPath } from '../utils/paths'
-import { getLweRepoUrl, getLweRepoBranch, getLweCmakeArgs } from './config.service'
+import { getLweRepoUrl, getLweRepoBranch, getLweCmakeArgs, getAudioScreen, getAmbientVolume } from './config.service'
 import { DEFAULT_LWE_REPO } from '@shared/constants'
 
 const execFileAsync = promisify(execFile)
@@ -663,6 +663,11 @@ function buildLweArgs(
 
   if (options.fps) args.push('--fps', String(options.fps))
   if (options.volume !== undefined) args.push('--volume', String(options.volume))
+  // Global, not per-wallpaper - pulled from config.service instead of options.
+  const audioScreen = getAudioScreen()
+  if (audioScreen) args.push('--audio-screen', audioScreen)
+  const ambientVolume = getAmbientVolume()
+  if (ambientVolume !== null) args.push('--ambient-volume', String(ambientVolume))
   if (options.scalingMode) args.push('--scaling', options.scalingMode)
   if (options.zoom !== undefined) args.push('--zoom', String(options.zoom))
   if (options.disableParallax) args.push('--disable-parallax')
@@ -774,6 +779,9 @@ export function hotswapLweSettings(options: {
   cornerColor?: string
   speed?: number
   propertyOverrides?: Record<string, string>
+  /** Empty string clears the restriction back to "every screen can produce audio" */
+  audioScreen?: string
+  ambientVolume?: number
 }): boolean {
   const pid = resolveActivePid()
   if (pid === null) return false
@@ -792,6 +800,8 @@ export function hotswapLweSettings(options: {
   if (options.disableParallax !== undefined) lines.push(`disable-parallax=${options.disableParallax ? 'on' : 'off'}`)
   if (options.cornerColor !== undefined) lines.push(`corner-color=${options.cornerColor}`)
   if (options.speed !== undefined) lines.push(`speed=${options.speed}`)
+  if (options.audioScreen !== undefined) lines.push(`audio-screen=${options.audioScreen}`)
+  if (options.ambientVolume !== undefined) lines.push(`ambient-volume=${options.ambientVolume}`)
   for (const [name, value] of Object.entries(options.propertyOverrides ?? {})) {
     lines.push(`property=${name}=${value}`)
   }

+ 12 - 2
src/preload/index.ts

@@ -107,6 +107,8 @@ const api = {
       autostartMinimized: boolean
       autostartPlaylistId: string | null
       killLweOnQuit: boolean
+      audioScreen: string | null
+      ambientVolume: number | null
     }> => ipcRenderer.invoke(IpcChannels.CONFIG_GET),
     setWorkshopPath: (p: string): Promise<{ ok: boolean }> =>
       ipcRenderer.invoke(IpcChannels.CONFIG_SET_WORKSHOP_PATH, p),
@@ -143,7 +145,11 @@ const api = {
     ): Promise<{ ok: boolean }> =>
       ipcRenderer.invoke(IpcChannels.CONFIG_SET_AUTOSTART, enabled, minimized, playlistId),
     getAutostartSupported: (): Promise<boolean> =>
-      ipcRenderer.invoke(IpcChannels.CONFIG_GET_AUTOSTART_SUPPORTED)
+      ipcRenderer.invoke(IpcChannels.CONFIG_GET_AUTOSTART_SUPPORTED),
+    setAudioScreen: (screen: string | null): Promise<{ ok: boolean }> =>
+      ipcRenderer.invoke(IpcChannels.CONFIG_SET_AUDIO_SCREEN, screen),
+    setAmbientVolume: (volume: number | null): Promise<{ ok: boolean }> =>
+      ipcRenderer.invoke(IpcChannels.CONFIG_SET_AMBIENT_VOLUME, volume)
   },
 
   playlist: {
@@ -242,10 +248,14 @@ const api = {
       cornerColor?: string
       speed?: number
       propertyOverrides?: Record<string, string>
+      audioScreen?: string
+      ambientVolume?: number
     }): Promise<{ ok: boolean }> =>
       ipcRenderer.invoke(IpcChannels.LWE_HOTSWAP_SETTINGS, options),
     listProperties: (wallpaperPath: string): Promise<LweProperty[]> =>
-      ipcRenderer.invoke(IpcChannels.LWE_LIST_PROPERTIES, wallpaperPath)
+      ipcRenderer.invoke(IpcChannels.LWE_LIST_PROPERTIES, wallpaperPath),
+    listScreens: (): Promise<string[]> =>
+      ipcRenderer.invoke(IpcChannels.LWE_LIST_SCREENS)
   },
 
   backup: {

+ 84 - 0
src/renderer/src/components/settings/SettingsView.tsx

@@ -59,6 +59,11 @@ export default function SettingsView() {
   const [autostartPlaylistId, setAutostartPlaylistId] = useState<string | null>(null)
   const [playlists, setPlaylists] = useState<{ id: string; title: string }[]>([])
 
+  const [screens, setScreens] = useState<string[]>([])
+  const [audioScreen, setAudioScreen] = useState<string | null>(null)
+  const [ambientVolume, setAmbientVolume] = useState<string>('')
+  const [ambientVolumeSaved, setAmbientVolumeSaved] = useState(false)
+
   useEffect(() => {
     window.electronAPI.config.get().then((cfg) => {
       setWorkshopPath(cfg.workshopPath ?? cfg.defaultWorkshopPath)
@@ -77,11 +82,14 @@ export default function SettingsView() {
       setAutostartEnabledState(cfg.autostartEnabled)
       setAutostartMinimized(cfg.autostartMinimized)
       setAutostartPlaylistId(cfg.autostartPlaylistId)
+      setAudioScreen(cfg.audioScreen)
+      setAmbientVolume(cfg.ambientVolume != null ? String(cfg.ambientVolume) : '')
     })
     window.electronAPI.lwe.status().then(setLweStatus)
     window.electronAPI.lwe.detectDistro().then(setDistro)
     window.electronAPI.desktopIcons.getEnabled().then(setDesktopIconsEnabled)
     window.electronAPI.playlist.getAll().then((all) => setPlaylists(all.map((p) => ({ id: p.id, title: p.title }))))
+    window.electronAPI.lwe.listScreens().then(setScreens)
   }, [])
 
   // The event subscription itself lives in App.tsx (see lwe-install-progress query above) so it
@@ -256,6 +264,24 @@ export default function SettingsView() {
     await window.electronAPI.config.setKillLweOnQuit(newVal)
   }
 
+  async function handleAudioScreenChange(screen: string | null) {
+    setAudioScreen(screen)
+    await window.electronAPI.config.setAudioScreen(screen)
+    // Push live so a currently running instance updates without a relaunch
+    await window.electronAPI.lwe.hotswapSettings({ audioScreen: screen ?? '' })
+  }
+
+  async function handleSaveAmbientVolume() {
+    const parsed = ambientVolume.trim() === '' ? null : parseInt(ambientVolume, 10)
+    if (parsed !== null && (isNaN(parsed) || parsed < 0 || parsed > 128)) return
+    await window.electronAPI.config.setAmbientVolume(parsed)
+    if (parsed !== null) {
+      await window.electronAPI.lwe.hotswapSettings({ ambientVolume: parsed })
+    }
+    setAmbientVolumeSaved(true)
+    setTimeout(() => setAmbientVolumeSaved(false), 2000)
+  }
+
   async function saveAutostart(patch: {
     enabled?: boolean
     minimized?: boolean
@@ -491,6 +517,64 @@ export default function SettingsView() {
           </div>
         </div>
 
+        <div>
+          <label className="block text-xs font-medium uppercase tracking-wide text-gray-500">
+            Audio
+          </label>
+          <p className="mt-1 text-xs text-gray-600">
+            Global audio behavior for all wallpapers, across every monitor.
+          </p>
+          <div className="mt-3 space-y-4">
+            <div>
+              <label className="block text-xs text-gray-400 mb-1">Play sound only on</label>
+              <p className="text-xs text-gray-600 mb-2">
+                Restricts sound to a single monitor so wallpapers on other screens stay silent,
+                even if they have their own sound.
+              </p>
+              <select
+                value={audioScreen ?? ''}
+                onChange={(e) => handleAudioScreenChange(e.target.value || null)}
+                className="w-56 rounded-lg bg-white/5 px-3 py-2 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-indigo-500 [&>option]:bg-[#1a1a1a] [&>option]:text-gray-200"
+              >
+                <option value="">All screens (default)</option>
+                {screens.map((s) => (
+                  <option key={s} value={s}>
+                    {s}
+                  </option>
+                ))}
+              </select>
+            </div>
+            <div>
+              <label className="block text-xs text-gray-400 mb-1">Ambient wallpaper volume</label>
+              <p className="text-xs text-gray-600 mb-2">
+                Separate volume (0-128) for non-video wallpapers (scene sound effects/music and
+                web wallpapers), instead of each wallpaper's own volume. Video wallpapers are
+                unaffected. Leave empty to use each wallpaper's normal volume. Web wallpapers only
+                support 0 (muted) vs. any other value (normal volume).
+              </p>
+              <div className="flex gap-2 items-center">
+                <input
+                  type="number"
+                  min={0}
+                  max={128}
+                  placeholder="same as wallpaper volume"
+                  value={ambientVolume}
+                  onChange={(e) => { setAmbientVolume(e.target.value); setAmbientVolumeSaved(false) }}
+                  onKeyDown={(e) => e.key === 'Enter' && handleSaveAmbientVolume()}
+                  className="w-44 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={handleSaveAmbientVolume}
+                  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} />
+                  {ambientVolumeSaved ? 'Saved!' : 'Save'}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+
         <div>
           <label className="block text-xs font-medium uppercase tracking-wide text-gray-500">
             Wallpaper Engine Config

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

@@ -57,6 +57,8 @@ export const IpcChannels = {
   CONFIG_SET_LWE_CMAKE_ARGS: 'config:set-lwe-cmake-args',
   CONFIG_SET_BACKUP_PATH: 'config:set-backup-path',
   CONFIG_SET_AUTO_UNSUBSCRIBE: 'config:set-auto-unsubscribe',
+  CONFIG_SET_AUDIO_SCREEN: 'config:set-audio-screen',
+  CONFIG_SET_AMBIENT_VOLUME: 'config:set-ambient-volume',
   CONFIG_PICK_FOLDER: 'config:pick-folder',
   CONFIG_PICK_FILE: 'config:pick-file',
   CONFIG_IMPORT_WE: 'config:import-we',
@@ -111,6 +113,7 @@ export const IpcChannels = {
   LWE_LIST_OBJECTS: 'lwe:list-objects',
   LWE_HOTSWAP_SETTINGS: 'lwe:hotswap-settings',
   LWE_LIST_PROPERTIES: 'lwe:list-properties',
+  LWE_LIST_SCREENS: 'lwe:list-screens',
 
   // Desktop icons overlay
   DESKTOP_ICONS_SET_ENABLED: 'desktop-icons:set-enabled',