Przeglądaj źródła

added detailsidebar features according to linux-wallpaperegine-kde

UwU 2 miesięcy temu
rodzic
commit
399aeda33a

+ 13 - 1
src/main/ipc/lwe.handlers.ts

@@ -55,7 +55,19 @@ export function registerLweHandlers(win: BrowserWindow): void {
 
   ipcMain.handle(
     IpcChannels.LWE_HOTSWAP_SETTINGS,
-    (_e, options: { disabledObjects?: string[]; enabledObjects?: string[]; volume?: number; xray?: boolean }) => {
+    (
+      _e,
+      options: {
+        disabledObjects?: string[]
+        enabledObjects?: string[]
+        volume?: number
+        xray?: boolean
+        scaling?: string
+        zoom?: number
+        disableParallax?: boolean
+        cornerColor?: string
+      }
+    ) => {
       return { ok: hotswapLweSettings(options) }
     }
   )

+ 43 - 10
src/main/services/lwe.service.ts

@@ -619,6 +619,10 @@ function buildLweArgs(
     disabledObjects?: string[]
     enabledObjects?: string[]
     propertyOverrides?: Record<string, string>
+    scalingMode?: string
+    zoom?: number
+    disableParallax?: boolean
+    cornerColor?: string
   }
 ): string[] {
   const args: string[] = []
@@ -635,6 +639,10 @@ function buildLweArgs(
 
   if (options.fps) args.push('--fps', String(options.fps))
   if (options.volume !== undefined) args.push('--volume', String(options.volume))
+  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')
+  if (options.cornerColor) args.push('--corner-color', options.cornerColor)
   if ((options.disabledObjects?.length || options.enabledObjects?.length) && supportsObjectFlags()) {
     for (const id of options.disabledObjects ?? []) args.push('--disable-object', id)
     for (const id of options.enabledObjects ?? []) args.push('--enable-object', id)
@@ -667,21 +675,29 @@ function hotReloadLwe(wallpaperPath: string): boolean {
 }
 
 /**
- * Pushes layer/volume/xray changes to an already-running instance via the extended
- * key=value control file, without touching the background path. Layers, volume and
- * xray are independent in the protocol: a volume-only request never reloads the
- * project, and a layers-only request doesn't need to resend the path. disabledObjects/
- * enabledObjects being present (even as empty arrays) is what tells the engine to
- * replace its override lists - omit both to leave layers untouched. xray toggles the
- * "xray" scene effect's reveal spot between following the mouse (off) and covering the
- * whole masked area (on) - it's a live-only setting with no launch-time equivalent, so
- * it has to be pushed here even right after a fresh launch.
+ * Pushes layer/volume/xray/scaling/zoom/parallax changes to an already-running instance via the
+ * extended key=value control file, without touching the background path. Each field is independent
+ * in the protocol: a volume-only request never reloads the project, and a layers-only request
+ * doesn't need to resend the path. disabledObjects/enabledObjects being present (even as empty
+ * arrays) is what tells the engine to replace its override lists - omit both to leave layers
+ * untouched. xray toggles the "xray" scene effect's reveal spot between following the mouse (off)
+ * and covering the whole masked area (on) - it's a live-only setting with no launch-time
+ * equivalent, so it has to be pushed here even right after a fresh launch. scaling/zoom apply to
+ * every screen currently rendering (the engine doesn't scope these to a single screen), matching
+ * how volume/xray are global rather than per-screen here. disableParallax is a straight passthrough
+ * to the engine's --disable-parallax setting, which every parallax-capable scene object reads live.
+ * cornerColor is a hex "RRGGBB"/"RRGGBBAA" string, only visible where the engine's clamp mode is
+ * border (the default).
  */
 export function hotswapLweSettings(options: {
   disabledObjects?: string[]
   enabledObjects?: string[]
   volume?: number
   xray?: boolean
+  scaling?: string
+  zoom?: number
+  disableParallax?: boolean
+  cornerColor?: string
 }): boolean {
   if (!activeProcess || activeProcess.exitCode !== null) return false
 
@@ -693,6 +709,10 @@ export function hotswapLweSettings(options: {
   }
   if (options.volume !== undefined) lines.push(`volume=${options.volume}`)
   if (options.xray !== undefined) lines.push(`xray=${options.xray ? 'on' : 'off'}`)
+  if (options.scaling !== undefined) lines.push(`scaling=${options.scaling}`)
+  if (options.zoom !== undefined) lines.push(`zoom=${options.zoom}`)
+  if (options.disableParallax !== undefined) lines.push(`disable-parallax=${options.disableParallax ? 'on' : 'off'}`)
+  if (options.cornerColor !== undefined) lines.push(`corner-color=${options.cornerColor}`)
   if (lines.length === 0) return false
 
   try {
@@ -717,13 +737,26 @@ export function launchLweAsync(
     enabledObjects?: string[]
     propertyOverrides?: Record<string, string>
     xrayFullReveal?: boolean
+    scalingMode?: string
+    zoom?: number
+    disableParallax?: boolean
+    cornerColor?: string
   } = {}
 ): Promise<void> {
   // Hot-reload if already running. xray has no launch flag and the running instance
   // keeps its own xray state across a background swap, so it needs an explicit push
-  // even when the new wallpaper wants it off.
+  // even when the new wallpaper wants it off. Scaling/zoom/parallax/cornerColor do have launch
+  // flags, but a hot-reload doesn't restart the process (so launch args never get re-read) -
+  // the running instance keeps whatever the previous wallpaper set, so they need the same
+  // explicit push, defaulting to "no override" when the new wallpaper doesn't set them.
   if (isLweRunning() && hotReloadLwe(wallpaperPath)) {
     if (options.xrayFullReveal !== undefined) hotswapLweSettings({ xray: options.xrayFullReveal })
+    hotswapLweSettings({
+      scaling: options.scalingMode ?? 'default',
+      zoom: options.zoom ?? 1,
+      disableParallax: options.disableParallax ?? false,
+      cornerColor: options.cornerColor ?? '000000'
+    })
     return Promise.resolve()
   }
 

+ 6 - 1
src/main/services/wallpaper.service.ts

@@ -224,7 +224,12 @@ export async function applyWallpaperMeta(
       propertyOverrides: wallpaper.propertyOverrides,
       // Explicit even when off: xray state lives on the running LWE process, not per-wallpaper,
       // so a hot-reload into a wallpaper with xray off must actively clear a previous wallpaper's "on".
-      xrayFullReveal: wallpaper.xrayFullReveal ?? false
+      xrayFullReveal: wallpaper.xrayFullReveal ?? false,
+      scalingMode: wallpaper.scalingMode,
+      zoom: wallpaper.zoom,
+      // Explicit even when off, same reasoning as xrayFullReveal above.
+      disableParallax: wallpaper.disableParallax ?? false,
+      cornerColor: wallpaper.cornerColor
     })
     return wallpaper.localPath
   }

+ 4 - 0
src/preload/index.ts

@@ -233,6 +233,10 @@ const api = {
       enabledObjects?: string[]
       volume?: number
       xray?: boolean
+      scaling?: string
+      zoom?: number
+      disableParallax?: boolean
+      cornerColor?: string
     }): Promise<{ ok: boolean }> =>
       ipcRenderer.invoke(IpcChannels.LWE_HOTSWAP_SETTINGS, options),
     listProperties: (wallpaperPath: string): Promise<LweProperty[]> =>

+ 172 - 3
src/renderer/src/components/common/DetailSidebar.tsx

@@ -23,12 +23,25 @@ import {
   ToggleLeft,
   ToggleRight,
   SlidersHorizontal,
-  ScanEye
+  ScanEye,
+  Maximize,
+  ZoomIn,
+  ZoomOut,
+  Move3d,
+  Palette
 } from 'lucide-react'
 import clsx from 'clsx'
 import { openWorkshopPage, openProfilePage, isWorkshopId } from '../../utils/steam'
 import { WE_TYPES, WE_AGE_RATINGS, WE_RESOLUTION_GROUPS } from '../../constants/weFilters'
-import type { LweSceneObject, LweProperty, WallpaperMeta } from '@shared/types'
+import type { LweSceneObject, LweProperty, WallpaperMeta, ScalingMode } from '@shared/types'
+
+const SCALING_MODE_OPTIONS: { value: ScalingMode; label: string }[] = [
+  { value: 'default', label: 'Default' },
+  { value: 'stretch', label: 'Stretch' },
+  { value: 'fill', label: 'Fill (crop to fit)' },
+  { value: 'fit', label: 'Fit (letterbox)' },
+  { value: 'center', label: 'Center (native size)' }
+]
 
 const TYPE_TAGS = new Set(WE_TYPES.map((i) => i.tag))
 const AGE_TAGS = new Set(WE_AGE_RATINGS.map((i) => i.tag))
@@ -327,7 +340,11 @@ export default function DetailSidebar({
         disabledObjects: patch.disabledObjects,
         enabledObjects: patch.enabledObjects,
         volume: patch.volumeOverride,
-        xray: patch.xrayFullReveal
+        xray: patch.xrayFullReveal,
+        scaling: patch.scalingMode,
+        zoom: patch.zoom,
+        disableParallax: patch.disableParallax,
+        cornerColor: patch.cornerColor
       })
       if (!ok) {
         await window.electronAPI.lwe.stop()
@@ -396,6 +413,58 @@ export default function DetailSidebar({
     }
   }
 
+  const [isCommittingScaling, setIsCommittingScaling] = useState(false)
+  const scalingMode = libraryMeta?.scalingMode ?? 'default'
+
+  async function commitScaling(mode: ScalingMode) {
+    if (isCommittingScaling || mode === scalingMode) return
+    setIsCommittingScaling(true)
+    try {
+      await persistAndMaybeRelaunch({ scalingMode: mode })
+    } finally {
+      setIsCommittingScaling(false)
+    }
+  }
+
+  const [zoomDraft, setZoomDraft] = useState<number | null>(null)
+  const [isCommittingZoom, setIsCommittingZoom] = useState(false)
+  const zoomPercent = zoomDraft ?? Math.round((libraryMeta?.zoom ?? 1) * 100)
+
+  async function commitZoom(percent: number) {
+    setIsCommittingZoom(true)
+    try {
+      await persistAndMaybeRelaunch({ zoom: percent / 100 })
+    } finally {
+      setZoomDraft(null)
+      setIsCommittingZoom(false)
+    }
+  }
+
+  const [isCommittingCornerColor, setIsCommittingCornerColor] = useState(false)
+  const cornerColor = libraryMeta?.cornerColor ?? '#000000'
+
+  async function commitCornerColor(color: string) {
+    setIsCommittingCornerColor(true)
+    try {
+      await persistAndMaybeRelaunch({ cornerColor: color })
+    } finally {
+      setIsCommittingCornerColor(false)
+    }
+  }
+
+  const [isCommittingParallax, setIsCommittingParallax] = useState(false)
+  const disableParallax = libraryMeta?.disableParallax ?? false
+
+  async function toggleParallax() {
+    if (isCommittingParallax) return
+    setIsCommittingParallax(true)
+    try {
+      await persistAndMaybeRelaunch({ disableParallax: !disableParallax })
+    } finally {
+      setIsCommittingParallax(false)
+    }
+  }
+
   const typeTag = tags.find((t) => TYPE_TAGS.has(t))
   const ageTag = tags.find((t) => AGE_TAGS.has(t))
   const resolutionTags = tags.filter((t) => RESOLUTION_TAGS.has(t))
@@ -552,6 +621,85 @@ export default function DetailSidebar({
           </div>
         )}
 
+        {localPath && lweInstalled && libraryMeta?.type && libraryMeta.type !== 'application' && (
+          <div className="border-t border-white/5 pt-3">
+            <div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
+              <Maximize size={12} />
+              <span className="flex-1 text-left">Scaling</span>
+              {isActive && (
+                <span className="rounded-full bg-green-600/30 px-1.5 normal-case text-green-300">
+                  live
+                </span>
+              )}
+              {isCommittingScaling && <Loader2 size={11} className="animate-spin" />}
+            </div>
+            <select
+              value={scalingMode}
+              disabled={isCommittingScaling}
+              onChange={(e) => commitScaling(e.target.value as ScalingMode)}
+              className="w-full rounded bg-white/5 px-1.5 py-1 text-xs text-gray-300 outline-none disabled:opacity-50 [&>option]:bg-[#1a1a1a]"
+            >
+              {SCALING_MODE_OPTIONS.map((opt) => (
+                <option key={opt.value} value={opt.value}>
+                  {opt.label}
+                </option>
+              ))}
+            </select>
+          </div>
+        )}
+
+        {localPath && lweInstalled && libraryMeta?.type && libraryMeta.type !== 'application' && (
+          <div className="border-t border-white/5 pt-3">
+            <div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
+              {zoomPercent < 100 ? <ZoomOut size={12} /> : <ZoomIn size={12} />}
+              <span className="flex-1 text-left">Zoom</span>
+              {isActive && (
+                <span className="rounded-full bg-green-600/30 px-1.5 normal-case text-green-300">
+                  live
+                </span>
+              )}
+              {isCommittingZoom && <Loader2 size={11} className="animate-spin" />}
+              <span className="normal-case text-gray-400">{zoomPercent}%</span>
+            </div>
+            <input
+              type="range"
+              min={25}
+              max={300}
+              step={1}
+              value={zoomPercent}
+              disabled={isCommittingZoom}
+              onChange={(e) => setZoomDraft(Number(e.target.value))}
+              onMouseUp={(e) => commitZoom(Number(e.currentTarget.value))}
+              onTouchEnd={(e) => commitZoom(Number(e.currentTarget.value))}
+              onKeyUp={(e) => commitZoom(Number(e.currentTarget.value))}
+              className="w-full accent-indigo-500 disabled:opacity-50"
+            />
+          </div>
+        )}
+
+        {localPath && lweInstalled && libraryMeta?.type && libraryMeta.type !== 'application' && (
+          <div className="border-t border-white/5 pt-3">
+            <label className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
+              <Palette size={12} />
+              <span className="flex-1 text-left">Corner color</span>
+              {isActive && (
+                <span className="rounded-full bg-green-600/30 px-1.5 normal-case text-green-300">
+                  live
+                </span>
+              )}
+              {isCommittingCornerColor && <Loader2 size={11} className="animate-spin" />}
+              <input
+                type="color"
+                value={cornerColor}
+                disabled={isCommittingCornerColor}
+                onChange={(e) => commitCornerColor(e.target.value)}
+                title="Color shown outside the wallpaper's bounds (letterboxing / zoomed-out scaling)"
+                className="h-5 w-8 shrink-0 cursor-pointer rounded border border-white/10 bg-transparent p-0 disabled:opacity-50"
+              />
+            </label>
+          </div>
+        )}
+
         {localPath && lweInstalled && libraryMeta?.type === 'video' && (
           <div className="border-t border-white/5 pt-3">
             <div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
@@ -602,6 +750,27 @@ export default function DetailSidebar({
                 <ToggleLeft size={14} className="text-gray-600" />
               )}
             </button>
+            <button
+              onClick={toggleParallax}
+              disabled={isCommittingParallax}
+              title="Force-disable the mouse parallax effect on this wallpaper's layers"
+              className="mt-1.5 flex w-full items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500 hover:text-gray-300 disabled:opacity-40"
+            >
+              <Move3d size={12} />
+              <span className="flex-1 text-left">Disable parallax</span>
+              {isActive && (
+                <span className="rounded-full bg-green-600/30 px-1.5 normal-case text-green-300">
+                  live
+                </span>
+              )}
+              {isCommittingParallax ? (
+                <Loader2 size={14} className="animate-spin text-gray-500" />
+              ) : disableParallax ? (
+                <ToggleRight size={14} className="text-indigo-400" />
+              ) : (
+                <ToggleLeft size={14} className="text-gray-600" />
+              )}
+            </button>
           </div>
         )}
 

+ 14 - 0
src/shared/types.ts

@@ -1,5 +1,8 @@
 export type WallpaperType = 'scene' | 'video' | 'web' | 'application'
 
+/** Matches linux-wallpaperengine's --scaling choices */
+export type ScalingMode = 'default' | 'stretch' | 'fit' | 'fill' | 'center'
+
 export type WallpaperBackend =
   | 'swww'
   | 'swaybg'
@@ -48,6 +51,17 @@ export interface WallpaperMeta {
   volumeOverride?: number
   /** Forces the "xray" scene effect's reveal spot to cover the whole masked area instead of following the mouse */
   xrayFullReveal?: boolean
+  /** How the wallpaper is scaled to fit the screen, applied via linux-wallpaperengine --scaling */
+  scalingMode?: ScalingMode
+  /** Manual zoom layered on top of scalingMode via linux-wallpaperengine --zoom (1 = no extra zoom) */
+  zoom?: number
+  /** Force-disables the scene's mouse parallax effect via linux-wallpaperengine --disable-parallax */
+  disableParallax?: boolean
+  /**
+   * Color shown outside the wallpaper's bounds (Center/Fit letterboxing, zoomed-out scaling) as a
+   * "RRGGBB"/"RRGGBBAA" hex string, applied via linux-wallpaperengine --corner-color. Default: black.
+   */
+  cornerColor?: string
   backedUp?: boolean
   /** Set by a "check unavailable" scan: true if the item has been removed from the Steam Workshop */
   unavailable?: boolean