diff --git a/cli.sh b/cli.sh index 98274b12..8b756212 100755 --- a/cli.sh +++ b/cli.sh @@ -603,9 +603,21 @@ help | --help | -h) # Force Qt6CT export QT_QPA_PLATFORMTHEME=qt6ct + + # Use GStreamer backend for QtMultimedia + export QT_MEDIA_BACKEND=gstreamer - # Cache this script's PID before exec (for fast PID lookups in future CLI calls) - echo $$ >/tmp/ambxst.pid + # Enable VA-API hardware decoding if available + if gst-inspect-1.0 vaapi &>/dev/null; then + export GST_PLUGIN_FEATURE_RANK=vaapidecodebin:MAX,vaapipostproc:MAX + fi + + # Force OpenGL/EGL for better performance + export GST_GL_API=opengl + export GST_GL_PLATFORM=egl + + # Cache this script's PID before exec (for fast PID lookups in future CLI calls) + echo $$ >/tmp/ambxst.pid # Launch QuickShell with the main shell.qml # If NIXGL_BIN is set (NixOS/Nix setup), use it. Otherwise, just run qs directly. diff --git a/install.sh b/install.sh index 22c36696..bb42c0e8 100755 --- a/install.sh +++ b/install.sh @@ -50,7 +50,6 @@ declare -A BINARY_CHECK=( ["jq"]="jq" ["playerctl"]="playerctl" ["wtype"]="wtype" - ["mpvpaper"]="mpvpaper" ["gradia"]="gradia" ["pipx"]="pipx" ["python-pipx"]="pipx" @@ -117,6 +116,7 @@ install_dependencies() { local PKGS=( kitty tmux fuzzel network-manager-applet blueman + gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pipewire wireplumber easyeffects playerctl qt6-qtbase qt6-qtdeclarative qt6-qtwayland qt6-qtsvg qt6-qttools qt6-qtimageformats qt6-qtmultimedia qt6-qtshadertools @@ -128,7 +128,7 @@ install_dependencies() { tesseract-langpack-chi_sim tesseract-langpack-chi_tra tesseract-langpack-kor tesseract-langpack-lat google-roboto-fonts google-roboto-mono-fonts dejavu-sans-fonts liberation-fonts google-noto-fonts-common google-noto-cjk-fonts google-noto-emoji-fonts - mpvpaper matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl + matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl ) log_info "Installing dependencies..." @@ -165,6 +165,7 @@ install_dependencies() { local PKGS=( kitty tmux fuzzel network-manager-applet blueman + gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pipewire wireplumber pavucontrol easyeffects ffmpeg x264 playerctl qt6-base qt6-declarative qt6-wayland qt6-svg qt6-tools qt6-imageformats qt6-multimedia qt6-shadertools libwebp libavif syntax-highlighting breeze-icons hicolor-icon-theme @@ -175,7 +176,7 @@ install_dependencies() { tesseract-data-chi_sim tesseract-data-chi_tra tesseract-data-kor tesseract-data-lat ttf-roboto ttf-roboto-mono ttf-dejavu ttf-liberation noto-fonts noto-fonts-cjk noto-fonts-emoji ttf-nerd-fonts-symbols - matugen gpu-screen-recorder wl-clip-persist mpvpaper gradia + matugen gpu-screen-recorder wl-clip-persist gradia quickshell ttf-phosphor-icons ttf-league-gothic adw-gtk-theme ) diff --git a/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js b/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js deleted file mode 100644 index 9b87795f..00000000 --- a/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js +++ /dev/null @@ -1,93 +0,0 @@ -.pragma library - -function generate(paletteColors) { - // Safety check - if (!paletteColors || paletteColors.length === 0) { - // Return a passthrough shader if no palette - return `//!HOOK MAIN -//!BIND HOOKED -//!DESC Ambxst Passthrough -void main() { - HOOKED_col = HOOKED_tex(HOOKED_pos); -}`; - } - - let unrolledLogic = ""; - - // Unroll the loop to ensure compatibility with all GLES drivers - for (let i = 0; i < paletteColors.length; i++) { - let color = paletteColors[i]; - - let r = (typeof color.r === 'number' ? color.r : 0.0).toFixed(5); - let g = (typeof color.g === 'number' ? color.g : 0.0).toFixed(5); - let b = (typeof color.b === 'number' ? color.b : 0.0).toFixed(5); - - unrolledLogic += ` - { - vec3 pColor = vec3(${r}, ${g}, ${b}); - vec3 diff = color - pColor; - - // Perceptual weighting (Red: 0.299, Green: 0.587, Blue: 0.114) - // This makes the distance match human perception better than raw Euclidean - vec3 weightedDiff = diff * vec3(0.55, 0.77, 0.34); // Sqrt of standard luma weights roughly - float distSq = dot(weightedDiff, weightedDiff); - - // Track closest color for fallback - if (distSq < minDistSq) { - minDistSq = distSq; - closestColor = pColor; - } - - float weight = exp(-distributionSharpness * distSq); - accumulatedColor += pColor * weight; - totalWeight += weight; - } -`; - } - - return `//!HOOK MAIN -//!BIND HOOKED -//!DESC Ambxst Palette Tint - -// Simple dithering function -float noise_random(vec2 uv) { - return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); -} - -vec4 hook() { - vec4 tex = HOOKED_tex(HOOKED_pos); - vec3 color = tex.rgb; - - // Add slight dithering to input to break banding before quantization - float noise = (noise_random(HOOKED_pos * 100.0 + sin(HOOKED_pos.x)) - 0.5) / 64.0; - color += noise; - - vec3 accumulatedColor = vec3(0.0); - float totalWeight = 0.0; - float minDistSq = 1000.0; - vec3 closestColor = vec3(0.0); - - // Increased sharpness for cleaner separation (was 20.0) - // 40.0 makes it stick tighter to palette colors - float distributionSharpness = 40.0; - - // Unrolled palette comparison - ${unrolledLogic} - - vec3 finalColor; - - // If we have a decent match blend, use it. - // Otherwise snap to closest to avoid "holes" or dark spots. - if (totalWeight > 0.0001) { - finalColor = accumulatedColor / totalWeight; - } else { - finalColor = closestColor; - } - - // Mix in the closest color slightly to reinforce structure if the blend is too muddy - // finalColor = mix(finalColor, closestColor, 0.2); - - return vec4(finalColor, tex.a); -} -`; -} diff --git a/modules/widgets/dashboard/wallpapers/Wallpaper.qml b/modules/widgets/dashboard/wallpapers/Wallpaper.qml index 852359ef..27f1ce3b 100644 --- a/modules/widgets/dashboard/wallpapers/Wallpaper.qml +++ b/modules/widgets/dashboard/wallpapers/Wallpaper.qml @@ -1,11 +1,11 @@ import QtQuick +import QtMultimedia import Quickshell import Quickshell.Wayland import Quickshell.Io import qs.modules.globals import qs.modules.theme import qs.config -import "MpvShaderGenerator.js" as ShaderGenerator PanelWindow { id: wallpaper @@ -28,6 +28,19 @@ PanelWindow { property var wallpaperPaths: [] property var subfolderFilters: [] property var allSubdirs: [] + + // Custom palette loaded from JSON file + property var customPalette: [] + property int customPaletteSize: 0 + + // Default palette (optimizedPalette) as fallback + readonly property var fallbackPalette: optimizedPalette + readonly property int fallbackPaletteSize: optimizedPalette.length + + // Effective palette that will be used in the shader + readonly property var effectivePalette: customPaletteSize > 0 ? customPalette : fallbackPalette + readonly property int effectivePaletteSize: customPaletteSize > 0 ? customPaletteSize : fallbackPaletteSize + property int currentIndex: 0 property string currentWallpaper: initialLoadCompleted && wallpaperPaths.length > 0 ? wallpaperPaths[currentIndex] : "" property bool initialLoadCompleted: false @@ -40,14 +53,18 @@ PanelWindow { property alias tintEnabled: wallpaperAdapter.tintEnabled property int thumbnailsVersion: 0 - // QUICKSHELL-GIT: property string mpvShaderDir: Quickshell.cacheDir + "/mpv_shaders_" + (currentScreenName ? currentScreenName : "ALL") - property string mpvShaderDir: Quickshell.env("HOME") + "/.cache/ambxst/mpv_shaders_" + (currentScreenName ? currentScreenName : "ALL") - property string mpvShaderPath: "" - property bool mpvShaderReady: false - - readonly property var optimizedPalette: ["background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", "cyan", "lightCyan", "magenta", "lightMagenta"] - - // Sync state from the primary wallpaper manager to secondary instances + // Optimized palette color names (used as fallback) + readonly property var optimizedPalette: [ + "background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", + "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", + "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", + "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", + "cyan", "lightCyan", "magenta", "lightMagenta" + ] + + // ------------------------------------------------------------------- + // Bindings to sync state from primary wallpaper manager + // ------------------------------------------------------------------- Binding { target: wallpaper property: "wallpaperPaths" @@ -76,6 +93,9 @@ PanelWindow { when: GlobalStates.wallpaperManager !== null && GlobalStates.wallpaperManager !== wallpaper } + // ------------------------------------------------------------------- + // Color presets + // ------------------------------------------------------------------- property string colorPresetsDir: Quickshell.env("HOME") + "/.config/ambxst/colors" property string officialColorPresetsDir: decodeURIComponent(Qt.resolvedUrl("../../../../assets/colors").toString().replace("file://", "")) onColorPresetsDirChanged: console.log("Color Presets Directory:", colorPresetsDir) @@ -83,7 +103,6 @@ PanelWindow { onColorPresetsChanged: console.log("Color Presets Updated:", colorPresets) property string activeColorPreset: wallpaperConfig.adapter.activeColorPreset || "" - // React to light/dark mode changes property bool isLightMode: Config.theme.lightMode onIsLightModeChanged: { if (activeColorPreset) { @@ -106,19 +125,14 @@ PanelWindow { } function applyColorPreset() { - if (!activeColorPreset) - return; + if (!activeColorPreset) return; var mode = Config.theme.lightMode ? "light.json" : "dark.json"; - var officialFile = officialColorPresetsDir + "/" + activeColorPreset + "/" + mode; var userFile = colorPresetsDir + "/" + activeColorPreset + "/" + mode; - // QUICKSHELL-GIT: var dest = Quickshell.cachePath("colors.json"); var dest = Quickshell.env("HOME") + "/.cache/ambxst/colors.json"; - // Try official first, then user. Use bash conditional. var cmd = "if [ -f '" + officialFile + "' ]; then cp '" + officialFile + "' '" + dest + "'; else cp '" + userFile + "' '" + dest + "'; fi"; - console.log("Applying color preset:", activeColorPreset); applyPresetProcess.command = ["bash", "-c", cmd]; applyPresetProcess.running = true; @@ -126,10 +140,11 @@ PanelWindow { function setColorPreset(name) { wallpaperConfig.adapter.activeColorPreset = name; - // activeColorPreset property will update automatically via binding to adapter } - // Funciones utilitarias para tipos de archivo + // ------------------------------------------------------------------- + // Utility functions for file types + // ------------------------------------------------------------------- function getFileType(path) { var extension = path.toLowerCase().split('.').pop(); if (['jpg', 'jpeg', 'png', 'webp', 'tif', 'tiff', 'bmp'].includes(extension)) { @@ -143,68 +158,39 @@ PanelWindow { } function getThumbnailPath(filePath) { - // Compute relative path from wallpaperDir var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; var relativePath = filePath.replace(basePath, ""); - - // Replace the filename with .jpg extension var pathParts = relativePath.split('/'); var fileName = pathParts.pop(); var thumbnailName = fileName + ".jpg"; var relativeDir = pathParts.join('/'); - - // Build the proxy path - // QUICKSHELL-GIT: var thumbnailPath = Quickshell.cacheDir + "/thumbnails/" + relativeDir + "/" + thumbnailName; - var thumbnailPath = Quickshell.env("HOME") + "/.cache/ambxst" + "/thumbnails/" + relativeDir + "/" + thumbnailName; - return thumbnailPath; + return Quickshell.env("HOME") + "/.cache/ambxst/thumbnails/" + relativeDir + "/" + thumbnailName; } function getDisplaySource(filePath) { var fileType = getFileType(filePath); - - // Para el display (WallpapersTab), siempre usar thumbnails si están disponibles if (fileType === 'video' || fileType === 'image' || fileType === 'gif') { - var thumbnailPath = getThumbnailPath(filePath); - // Verificar si el thumbnail existe (esto es solo para debugging, QML manejará el fallback) - return thumbnailPath; + return getThumbnailPath(filePath); } - - // Fallback al archivo original si no es un tipo soportado return filePath; } function getColorSource(filePath) { var fileType = getFileType(filePath); - - // Para generación de colores: solo videos usan thumbnails if (fileType === 'video') { return getThumbnailPath(filePath); } - - // Imágenes y GIFs usan el archivo original para colores return filePath; } function getLockscreenFramePath(filePath) { - if (!filePath) { - return ""; - } - + if (!filePath) return ""; var fileType = getFileType(filePath); - - // Para imágenes estáticas, usar el archivo original - if (fileType === 'image') { - return filePath; - } - - // Para videos y GIFs, usar el frame cacheado + if (fileType === 'image') return filePath; if (fileType === 'video' || fileType === 'gif') { var fileName = filePath.split('/').pop(); - // QUICKSHELL-GIT: var cachePath = Quickshell.cacheDir + "/lockscreen/" + fileName + ".jpg"; - var cachePath = Quickshell.env("HOME") + "/.cache/ambxst" + "/lockscreen/" + fileName + ".jpg"; - return cachePath; + return Quickshell.env("HOME") + "/.cache/ambxst/lockscreen/" + fileName + ".jpg"; } - return filePath; } @@ -213,15 +199,10 @@ PanelWindow { console.warn("generateLockscreenFrame: empty filePath"); return; } - console.log("Generating lockscreen frame for:", filePath); - var scriptPath = decodeURIComponent(Qt.resolvedUrl("../../../../scripts/lockwall.py").toString().replace("file://", "")); - // QUICKSHELL-GIT: var dataPath = Quickshell.cacheDir; var dataPath = Quickshell.env("HOME") + "/.cache/ambxst"; - lockscreenWallpaperScript.command = ["python3", scriptPath, filePath, dataPath]; - lockscreenWallpaperScript.running = true; } @@ -229,58 +210,92 @@ PanelWindow { var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; var relativePath = filePath.replace(basePath, ""); var parts = relativePath.split("/"); - if (parts.length > 1) { - return parts[0]; - } + if (parts.length > 1) return parts[0]; return ""; } + // ------------------------------------------------------------------- + // Palette loading + // ------------------------------------------------------------------- + function loadCustomPalette(filePath) { + if (!filePath) return; + // Vaciar paleta actual para usar fallback mientras se carga la nueva + customPalette = []; + customPaletteSize = 0; + var palettePath = getPalettePath(filePath); + var xhr = new XMLHttpRequest(); + xhr.open("GET", "file://" + palettePath, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var data = JSON.parse(xhr.responseText); + customPalette = data.colors; + customPaletteSize = data.size; + console.log("Palette loaded:", customPaletteSize, "colors - First:", customPalette[0]); + } catch (e) { + console.warn("Failed to parse palette:", palettePath, e); + fallbackToDefaultPalette(); + } + } else { + console.warn("Palette file not found (status " + xhr.status + "):", palettePath); + fallbackToDefaultPalette(); + } + } + }; + xhr.send(); + } + + function fallbackToDefaultPalette() { + customPalette = []; + customPaletteSize = 0; + } + + function getPalettePath(filePath) { + var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; + var relativePath = filePath.replace(basePath, ""); + return Quickshell.env("HOME") + "/.cache/ambxst/palettes/" + relativePath + ".json"; + } + function scanSubfolders() { - if (!wallpaperDir) - return; - // Explicitly update command with current wallpaperDir + if (!wallpaperDir) return; var cmd = ["find", wallpaperDir, "-mindepth", "1", "-name", ".*", "-prune", "-o", "-type", "d", "-print"]; scanSubfoldersProcess.command = cmd; scanSubfoldersProcess.running = true; } - // Update directory watcher when wallpaperDir changes onWallpaperDirChanged: { - // Skip initial spurious changes before config is loaded - if (!_wallpaperDirInitialized) - return; - - // Only the primary wallpaper manager should handle directory changes - if (GlobalStates.wallpaperManager !== wallpaper) - return; + if (!_wallpaperDirInitialized) return; + if (GlobalStates.wallpaperManager !== wallpaper) return; console.log("Wallpaper directory changed to:", wallpaperDir); usingFallback = false; - - // Clear current lists to reflect change immediately wallpaperPaths = []; subfolderFilters = []; - directoryWatcher.path = wallpaperDir; - // Force update scan command - var cmd = ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; + var cmd = ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; scanWallpapers.command = cmd; scanWallpapers.running = true; - scanSubfolders(); - // Regenerate thumbnails for the new directory (delayed) if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); else delayedThumbnailGen.start(); } - onCurrentWallpaperChanged: - // Matugen se ejecuta manualmente en las funciones de cambio - {} + onCurrentWallpaperChanged: { + // Matugen is executed manually in change functions + } + // ------------------------------------------------------------------- + // Wallpaper control functions + // ------------------------------------------------------------------- function setWallpaper(path, targetScreen = null) { if (GlobalStates.wallpaperManager && GlobalStates.wallpaperManager !== wallpaper) { GlobalStates.wallpaperManager.setWallpaper(path, targetScreen); @@ -292,29 +307,28 @@ PanelWindow { var pathIndex = wallpaperPaths.indexOf(path); if (pathIndex !== -1) { if (targetScreen) { - // If targeting a specific screen, save to perScreenWallpapers instead of currentWall let perScreen = Object.assign({}, wallpaperConfig.adapter.perScreenWallpapers || {}); perScreen[targetScreen] = path; wallpaperConfig.adapter.perScreenWallpapers = perScreen; - - // If this targetScreen is the primary screen, it must update currentWall - // because currentWall is exactly the primary monitor fallback. + let isPrimary = false; if (GlobalStates.wallpaperManager && GlobalStates.wallpaperManager.screen) { isPrimary = (targetScreen === GlobalStates.wallpaperManager.screen.name); } - if (isPrimary || !wallpaperConfig.adapter.currentWall) { currentIndex = pathIndex; wallpaperConfig.adapter.currentWall = path; currentWallpaper = path; + loadCustomPalette(path); + generateLockscreenFrame(path); runMatugenForCurrentWallpaper(); } } else { - // Global fallback target currentIndex = pathIndex; wallpaperConfig.adapter.currentWall = path; currentWallpaper = path; + loadCustomPalette(path); + generateLockscreenFrame(path); runMatugenForCurrentWallpaper(); } generateLockscreenFrame(path); @@ -328,7 +342,6 @@ PanelWindow { GlobalStates.wallpaperManager.clearPerScreenWallpaper(targetScreen); return; } - console.log("Clearing per-screen wallpaper for:", targetScreen); let perScreen = Object.assign({}, wallpaperConfig.adapter.perScreenWallpapers || {}); if (perScreen[targetScreen]) { @@ -342,9 +355,7 @@ PanelWindow { GlobalStates.wallpaperManager.nextWallpaper(); return; } - - if (wallpaperPaths.length === 0) - return; + if (wallpaperPaths.length === 0) return; initialLoadCompleted = true; currentIndex = (currentIndex + 1) % wallpaperPaths.length; currentWallpaper = wallpaperPaths[currentIndex]; @@ -358,9 +369,7 @@ PanelWindow { GlobalStates.wallpaperManager.previousWallpaper(); return; } - - if (wallpaperPaths.length === 0) - return; + if (wallpaperPaths.length === 0) return; initialLoadCompleted = true; currentIndex = currentIndex === 0 ? wallpaperPaths.length - 1 : currentIndex - 1; currentWallpaper = wallpaperPaths[currentIndex]; @@ -374,7 +383,6 @@ PanelWindow { GlobalStates.wallpaperManager.setWallpaperByIndex(index); return; } - if (index >= 0 && index < wallpaperPaths.length) { initialLoadCompleted = true; currentIndex = index; @@ -385,10 +393,8 @@ PanelWindow { } } - // Función para re-ejecutar Matugen con el wallpaper actual function setMatugenScheme(scheme) { wallpaperConfig.adapter.matugenScheme = scheme; - if (wallpaperConfig.adapter.activeColorPreset) { console.log("Switching to Matugen scheme, clearing preset"); wallpaperConfig.adapter.activeColorPreset = ""; @@ -397,294 +403,61 @@ PanelWindow { } } - // property string mpvSocket: "/tmp/ambxst_mpv_socket" - property string mpvSocket: "/tmp/ambxst_mpv_socket_" + (currentScreenName ? currentScreenName : "ALL") - function runMatugenForCurrentWallpaper() { if (activeColorPreset) { console.log("Skipping Matugen because color preset is active:", activeColorPreset); return; } - if (currentWallpaper && initialLoadCompleted) { console.log("Running Matugen for current wallpaper:", currentWallpaper); - var fileType = getFileType(currentWallpaper); var matugenSource = getColorSource(currentWallpaper); - console.log("Using source for matugen:", matugenSource, "(type:", fileType + ")"); - // Stop existing processes if running to prioritize new request - if (matugenProcessWithConfig.running) { - matugenProcessWithConfig.running = false; - } - if (matugenProcessNormal.running) { - matugenProcessNormal.running = false; - } + if (matugenProcessWithConfig.running) matugenProcessWithConfig.running = false; + if (matugenProcessNormal.running) matugenProcessNormal.running = false; - // Ejecutar matugen con configuración específica - var commandWithConfig = ["matugen", "image", matugenSource, "--source-color-index", "0", "-c", decodeURIComponent(Qt.resolvedUrl("../../../../assets/matugen/config.toml").toString().replace("file://", "")), "-t", wallpaperConfig.adapter.matugenScheme]; - if (Config.theme.lightMode) { - commandWithConfig.push("-m", "light"); - } + var commandWithConfig = ["matugen", "image", matugenSource, "--source-color-index", "0", + "-c", decodeURIComponent(Qt.resolvedUrl("../../../../assets/matugen/config.toml").toString().replace("file://", "")), + "-t", wallpaperConfig.adapter.matugenScheme]; + if (Config.theme.lightMode) commandWithConfig.push("-m", "light"); matugenProcessWithConfig.command = commandWithConfig; matugenProcessWithConfig.running = true; - // Ejecutar matugen normal en paralelo - var commandNormal = ["matugen", "image", matugenSource, "--source-color-index", "0", "-t", wallpaperConfig.adapter.matugenScheme]; - if (Config.theme.lightMode) { - commandNormal.push("-m", "light"); - } + var commandNormal = ["matugen", "image", matugenSource, "--source-color-index", "0", + "-t", wallpaperConfig.adapter.matugenScheme]; + if (Config.theme.lightMode) commandNormal.push("-m", "light"); matugenProcessNormal.command = commandNormal; matugenProcessNormal.running = true; } } - function updateMpvRuntime(enable) { - var cmdString; - if (enable) { - // Since we are using unique filenames, we can just set the new path. - // MPV will handle the switch smoothly and won't use cached versions. - var setCmd = JSON.stringify({ - "command": ["set_property", "glsl-shaders", mpvShaderPath] - }); - cmdString = "echo '" + setCmd + "' | socat - " + mpvSocket; - } else { - // Clear shaders - var jsonCmd = JSON.stringify({ - "command": ["set_property", "glsl-shaders", ""] - }); - cmdString = "echo '" + jsonCmd + "' | socat - " + mpvSocket; - } - - mpvIpcProcess.command = ["bash", "-c", cmdString]; - mpvIpcProcess.running = true; - } - - function requestVideoSync() { - if (GlobalStates.wallpaperManager !== wallpaper) { - if (GlobalStates.wallpaperManager) { - GlobalStates.wallpaperManager.requestVideoSync(); - } - return; - } - videoSyncTimer.restart(); - } - - Timer { - id: videoSyncTimer - interval: 1200 // give mpvpaper processes time to spawn and initialize - repeat: false - onTriggered: { - console.log("Broadcasting video sync to all mpvpaper sockets..."); - mpvSyncProcess.running = true; - } - } - - Process { - id: mpvSyncProcess - running: false - command: ["bash", "-c", "for sock in /tmp/ambxst_mpv_socket_*; do echo '{ \"command\": [\"set_property\", \"time-pos\", 0] }' | socat - \"$sock\" 2>/dev/null; done"] - onExited: code => { - console.log("Video sync broadcast completed with code:", code); - } - } - - function updateMpvShader() { - if (getFileType(effectiveWallpaper) !== "video") { - return; - } - if (!wallpaperAdapter.tintEnabled) { - updateMpvRuntime(false); - return; - } - - var colors = []; - // Log the first color to see if it changed - var firstColorRaw = Colors[optimizedPalette[0]]; - console.log("Generating MPV shader. First palette color (" + optimizedPalette[0] + "):", firstColorRaw); - - for (var i = 0; i < optimizedPalette.length; i++) { - var rawColor = Colors[optimizedPalette[i]]; - if (rawColor) { - var c = Qt.darker(rawColor, 1.0); - if (c && !isNaN(c.r) && !isNaN(c.g) && !isNaN(c.b)) { - colors.push({ - r: c.r, - g: c.g, - b: c.b - }); - } - } - } - - if (colors.length === 0) { - console.warn("MpvShaderGenerator: No valid colors found for palette! Aborting."); - return; - } - - var shaderContent = ShaderGenerator.generate(colors); - - // Generate a unique filename in a dedicated directory - var timestamp = Date.now(); - var currentShaderPath = mpvShaderDir + "/tint_" + timestamp + ".glsl"; - - // Store the current active path so updateMpvRuntime knows which one to use - wallpaper.mpvShaderPath = currentShaderPath; - - var cmd = ["python3", "-c", "import sys, os, pathlib; " + "d = pathlib.Path(sys.argv[1]); " + "d.mkdir(parents=True, exist_ok=True); " + "[f.unlink() for f in d.iterdir() if f.is_file()]; " + "pathlib.Path(sys.argv[2]).write_text(sys.argv[3]); " + "print('Wrote shader to ' + sys.argv[2]); " + "legacy_dir = os.path.dirname(sys.argv[1]); " + "[pathlib.Path(legacy_dir, f).unlink(missing_ok=True) for f in ['mpv_tint_0.glsl', 'mpv_tint_1.glsl', 'mpv_tint.glsl']]", mpvShaderDir, currentShaderPath, shaderContent]; - - mpvShaderWriter.command = cmd; - mpvShaderWriter.running = true; - } - - property int ipcRetryCount: 0 - - Timer { - id: ipcRetryTimer - interval: 200 - repeat: false - onTriggered: { - // Retry the last command (which is currently set in mpvIpcProcess) - mpvIpcProcess.running = true; - } - } - - Process { - id: mpvIpcProcess - running: false - onExited: code => { - if (code !== 0) { - console.warn("MPV IPC failed (is mpvpaper running?) Code:", code); - if (ipcRetryCount < 10) { - ipcRetryCount++; - console.log("Retrying IPC (" + ipcRetryCount + "/10)..."); - ipcRetryTimer.restart(); - } - } else { - ipcRetryCount = 0; - } - } - } - - Process { - id: mpvShaderWriter - running: false - command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("mpvShaderWriter stdout:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("mpvShaderWriter stderr:", text); - } - } - } - - onExited: code => { - if (code === 0) { - console.log("MPV tint shader generated at:", mpvShaderPath); - mpvShaderReady = true; - // Apply immediately via IPC - updateMpvRuntime(true); - } else { - console.warn("Failed to generate MPV shader"); - } - } - } - - // Trigger update when colors change - Timer { - id: shaderUpdateDebounce - interval: 500 - onTriggered: { - console.log("Shader debounce triggered, updating MPV..."); - updateMpvShader(); - } - } - - Connections { - target: Colors - // Watch for file reload (theme change) - function onFileChanged() { - console.log("Colors file changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - // Watch for background change (OLED mode often affects this first/only) - function onBackgroundChanged() { - console.log("Colors background changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - // Fallback - function onPrimaryChanged() { - console.log("Colors primary changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - } - - Connections { - target: Config - function onOledModeChanged() { - console.log("Config OLED mode changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - } - - onTintEnabledChanged: { - console.log("Tint enabled changed to", tintEnabled); - updateMpvShader(); - } - - onEffectiveWallpaperChanged: { - if (getFileType(effectiveWallpaper) === "video") { - shaderUpdateDebounce.restart(); - } - } - Component.onCompleted: { - // Only the first Wallpaper instance should manage scanning - // Other instances (for other screens) share the same data via GlobalStates if (GlobalStates.wallpaperManager !== null) { - // Another instance already registered, skip initialization _wallpaperDirInitialized = true; return; } - GlobalStates.wallpaperManager = wallpaper; - // Verificar si existe wallpapers.json, si no, crear con fallback checkWallpapersJson.running = true; - - // Initial scans - do these once after config is loaded scanColorPresets(); - // Start directory monitoring presetsWatcher.reload(); officialPresetsWatcher.reload(); - // Load initial wallpaper config - this will trigger onWallPathChanged which does the actual scan wallpaperConfig.reload(); - // Generate lockscreen frame for initial wallpaper after a short delay Qt.callLater(function () { if (currentWallpaper) { generateLockscreenFrame(currentWallpaper); - } - // Force shader generation on startup if enabled - if (tintEnabled) { - updateMpvShader(); + loadCustomPalette(currentWallpaper); } }); } + // ------------------------------------------------------------------- + // Configuration file handling + // ------------------------------------------------------------------- FileView { id: wallpaperConfig - // QUICKSHELL-GIT: path: Quickshell.cachePath("wallpapers.json") path: Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json" watchChanges: true @@ -697,11 +470,9 @@ PanelWindow { onFileChanged: reload() onAdapterUpdated: { - // Ensure matugenScheme has a default value if (!wallpaperConfig.adapter.matugenScheme) { wallpaperConfig.adapter.matugenScheme = "scheme-tonal-spot"; } - // Update the currentMatugenScheme property to trigger UI updates currentMatugenScheme = Qt.binding(function () { return wallpaperConfig.adapter.matugenScheme; }); @@ -724,17 +495,9 @@ PanelWindow { } onCurrentWallChanged: { - // Skip during initial load - scanWallpapers handles this - if (!wallpaper._wallpaperDirInitialized) - return; - - // Siempre actualizar si es diferente al actual + if (!wallpaper._wallpaperDirInitialized) return; if (currentWall && currentWall !== wallpaper.currentWallpaper) { - // If paths are not loaded yet, wait for scanWallpapers to finish - if (wallpaper.wallpaperPaths.length === 0) { - return; - } - + if (wallpaper.wallpaperPaths.length === 0) return; var pathIndex = wallpaper.wallpaperPaths.indexOf(currentWall); if (pathIndex !== -1) { wallpaper.currentIndex = pathIndex; @@ -751,22 +514,19 @@ PanelWindow { onWallPathChanged: { if (wallPath) { console.log("Config wallPath updated:", wallPath); - - // Initialize scanning on first valid wallPath load if (!wallpaper._wallpaperDirInitialized && GlobalStates.wallpaperManager === wallpaper) { wallpaper._wallpaperDirInitialized = true; - - // Set up directory watcher directoryWatcher.path = wallPath; directoryWatcher.reload(); - // Perform initial wallpaper scan - var cmd = ["find", wallPath, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; + var cmd = ["find", wallPath, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; scanWallpapers.command = cmd; scanWallpapers.running = true; wallpaper.scanSubfolders(); - - // Start thumbnail generation delayedThumbnailGen.start(); } } @@ -774,12 +534,13 @@ PanelWindow { } } + // ------------------------------------------------------------------- + // External processes + // ------------------------------------------------------------------- Process { id: checkWallpapersJson running: false - // QUICKSHELL-GIT: command: ["test", "-f", Quickshell.cachePath("wallpapers.json")] command: ["test", "-f", Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json"] - onExited: function (exitCode) { if (exitCode !== 0) { console.log("wallpapers.json does not exist, creating with fallbackDir"); @@ -794,77 +555,28 @@ PanelWindow { id: matugenProcessWithConfig running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Matugen (with config) output:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Matugen (with config) error:", text); - } - } - } - - onExited: { - console.log("Matugen with config finished"); - } + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Matugen (with config) output:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Matugen (with config) error:", text); } } + onExited: { console.log("Matugen with config finished"); } } Process { id: matugenProcessNormal running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Matugen (normal) output:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Matugen (normal) error:", text); - } - } - } - - onExited: { - console.log("Matugen normal finished"); - } + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Matugen (normal) output:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Matugen (normal) error:", text); } } + onExited: { console.log("Matugen normal finished"); } } - // Proceso para generar thumbnails de videos Process { id: thumbnailGeneratorScript running: false - // QUICKSHELL-GIT: command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), Quickshell.cacheDir + "/wallpapers.json", Quickshell.cacheDir, fallbackDir] - command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), Quickshell.env("HOME") + "/.cache/ambxst" + "/wallpapers.json", Quickshell.env("HOME") + "/.cache/ambxst", fallbackDir] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Thumbnail Generator:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Thumbnail Generator Error:", text); - } - } - } - + command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), + Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json", + Quickshell.env("HOME") + "/.cache/ambxst", fallbackDir] + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Thumbnail Generator:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Thumbnail Generator Error:", text); } } onExited: function (exitCode) { if (exitCode === 0) { console.log("✅ Video thumbnails generated successfully"); @@ -877,39 +589,20 @@ PanelWindow { Timer { id: delayedThumbnailGen - interval: 2000 // Delay 2 seconds after change to not block + interval: 2000 repeat: false onTriggered: thumbnailGeneratorScript.running = true } - // Proceso para generar frame de lockscreen con el script de Python Process { id: lockscreenWallpaperScript running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Lockscreen Wallpaper Generator:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Lockscreen Wallpaper Generator Error:", text); - } - } - } - + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Lockscreen Wallpaper Generator:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Lockscreen Wallpaper Generator Error:", text); } } onExited: function (exitCode) { - if (exitCode === 0) { - console.log("✅ Lockscreen wallpaper ready"); - } else { - console.warn("⚠️ Lockscreen wallpaper generation failed with code:", exitCode); - } + if (exitCode === 0) console.log("✅ Lockscreen wallpaper ready"); + else console.warn("⚠️ Lockscreen wallpaper generation failed with code:", exitCode); } } @@ -917,18 +610,12 @@ PanelWindow { id: scanSubfoldersProcess running: false command: wallpaperDir ? ["find", wallpaperDir, "-mindepth", "1", "-name", ".*", "-prune", "-o", "-type", "d", "-print"] : [] - stdout: StdioCollector { onStreamFinished: { console.log("scanSubfolders stdout:", text); - var rawPaths = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); - + var rawPaths = text.trim().split("\n").filter(function (f) { return f.length > 0; }); allSubdirs = rawPaths; - var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; - var topLevelFolders = rawPaths.filter(function (path) { var relative = path.replace(basePath, ""); return relative.indexOf("/") === -1; @@ -937,58 +624,38 @@ PanelWindow { }).filter(function (name) { return name.length > 0 && !name.startsWith("."); }); - topLevelFolders.sort(); subfolderFilters = topLevelFolders; - subfolderFiltersChanged(); // Emitir señal manualmente console.log("Updated subfolderFilters:", subfolderFilters); } } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Error scanning subfolders:", text); - } - } - } - + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Error scanning subfolders:", text); } } onRunningChanged: { - if (running) { - console.log("Starting scanSubfolders for directory:", wallpaperDir); - } else { - console.log("Finished scanSubfolders"); - } + if (running) console.log("Starting scanSubfolders for directory:", wallpaperDir); + else console.log("Finished scanSubfolders"); } } - // Directory watcher using FileView to monitor the wallpaper directory + // ------------------------------------------------------------------- + // Directory watchers + // ------------------------------------------------------------------- FileView { id: directoryWatcher path: wallpaperDir watchChanges: true printErrors: false - onFileChanged: { - if (wallpaperDir === "") - return; + if (wallpaperDir === "") return; console.log("Wallpaper directory changed, rescanning..."); scanWallpapers.running = true; scanSubfoldersProcess.running = true; - // Regenerar thumbnails si hay nuevos videos (delayed) - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); } - - // Remove onLoadFailed to prevent premature fallback activation } - // Recursive directory watchers for subfolders Instantiator { model: allSubdirs - delegate: FileView { path: modelData watchChanges: true @@ -997,36 +664,28 @@ PanelWindow { console.log("Subdirectory content changed (" + path + "), rescanning..."); scanWallpapers.running = true; scanSubfoldersProcess.running = true; - - // Regenerar thumbnails (delayed) - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); } } } - // Directory watcher for user color presets FileView { id: presetsWatcher path: colorPresetsDir watchChanges: true printErrors: false - onFileChanged: { console.log("User color presets directory changed, rescanning..."); scanPresetsProcess.running = true; } } - // Directory watcher for official color presets FileView { id: officialPresetsWatcher path: officialColorPresetsDir watchChanges: true printErrors: false - onFileChanged: { console.log("Official color presets directory changed, rescanning..."); scanPresetsProcess.running = true; @@ -1036,41 +695,34 @@ PanelWindow { Process { id: scanWallpapers running: false - command: wallpaperDir ? ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] : [] - + command: wallpaperDir ? ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] : [] onRunningChanged: { if (running && wallpaperDir === "") { console.log("Blocking scanWallpapers because wallpaperDir is empty"); running = false; } } - stdout: StdioCollector { onStreamFinished: { - var files = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); + var files = text.trim().split("\n").filter(function (f) { return f.length > 0; }); if (files.length === 0) { console.log("No wallpapers found in main directory, using fallback"); usingFallback = true; scanFallback.running = true; } else { usingFallback = false; - // Only update if the list has actually changed var newFiles = files.sort(); var listChanged = JSON.stringify(newFiles) !== JSON.stringify(wallpaperPaths); if (listChanged) { console.log("Wallpaper directory updated. Found", newFiles.length, "images"); wallpaperPaths = newFiles; - - // Always try to load the saved wallpaper when list changes if (wallpaperPaths.length > 0) { - // Trigger thumbnail generation if list changed - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); - + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); if (wallpaperConfig.adapter.currentWall) { var savedIndex = wallpaperPaths.indexOf(wallpaperConfig.adapter.currentWall); if (savedIndex !== -1) { @@ -1083,25 +735,21 @@ PanelWindow { } else { currentIndex = 0; } - if (!initialLoadCompleted) { if (!wallpaperConfig.adapter.currentWall) { wallpaperConfig.adapter.currentWall = wallpaperPaths[0]; } initialLoadCompleted = true; - // runMatugenForCurrentWallpaper() will be called by onCurrentWallChanged } } } } } } - stderr: StdioCollector { onStreamFinished: { if (text.length > 0) { console.warn("Error scanning wallpaper directory:", text); - // Only fallback if we don't already have wallpapers loaded AND we have a valid directory that failed if (wallpaperPaths.length === 0 && wallpaperDir !== "") { console.log("Directory scan failed for " + wallpaperDir + ", using fallback"); usingFallback = true; @@ -1115,38 +763,30 @@ PanelWindow { Process { id: scanFallback running: false - command: ["find", fallbackDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] - + command: ["find", fallbackDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] stdout: StdioCollector { onStreamFinished: { - var files = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); + var files = text.trim().split("\n").filter(function (f) { return f.length > 0; }); console.log("Using fallback wallpapers. Found", files.length, "images"); - - // Only use fallback if we don't already have main wallpapers loaded if (usingFallback) { wallpaperPaths = files.sort(); - - // Initialize fallback wallpaper selection if (wallpaperPaths.length > 0) { if (wallpaperConfig.adapter.currentWall) { var savedIndex = wallpaperPaths.indexOf(wallpaperConfig.adapter.currentWall); - if (savedIndex !== -1) { - currentIndex = savedIndex; - } else { - currentIndex = 0; - } + if (savedIndex !== -1) currentIndex = savedIndex; + else currentIndex = 0; } else { currentIndex = 0; } - if (!initialLoadCompleted) { if (!wallpaperConfig.adapter.currentWall) { wallpaperConfig.adapter.currentWall = wallpaperPaths[0]; } initialLoadCompleted = true; - // runMatugenForCurrentWallpaper() will be called by onCurrentWallChanged } } } @@ -1157,9 +797,7 @@ PanelWindow { Process { id: scanPresetsProcess running: false - // Scan both directories. find will complain to stderr if one is missing but still output what it finds. command: ["find", officialColorPresetsDir, colorPresetsDir, "-mindepth", "1", "-maxdepth", "1", "-type", "d"] - stdout: StdioCollector { onStreamFinished: { console.log("Scan Presets Output:", text); @@ -1167,286 +805,302 @@ PanelWindow { var uniqueNames = []; for (var i = 0; i < rawLines.length; i++) { var line = rawLines[i].trim(); - if (line.length === 0) - continue; + if (line.length === 0) continue; var name = line.split('/').pop(); - // Deduplicate - if (uniqueNames.indexOf(name) === -1) { - uniqueNames.push(name); - } + if (uniqueNames.indexOf(name) === -1) uniqueNames.push(name); } uniqueNames.sort(); console.log("Found color presets:", uniqueNames); colorPresets = uniqueNames; } } - - stderr: StdioCollector { - onStreamFinished: { - // Suppress common "No such file or directory" if one dir is missing - // console.warn("Scan Presets Error:", text); - } - } + stderr: StdioCollector { onStreamFinished: { /* suppress errors */ } } } Process { id: applyPresetProcess running: false command: [] - onExited: code => { - if (code === 0) - console.log("Color preset applied successfully"); - else - console.warn("Failed to apply color preset, code:", code); + if (code === 0) console.log("Color preset applied successfully"); + else console.warn("Failed to apply color preset, code:", code); } } - Rectangle { - id: background - anchors.fill: parent - color: "black" - focus: true - - Keys.onLeftPressed: { - if (wallpaper.wallpaperPaths.length > 0) { - wallpaper.previousWallpaper(); - } - } - - Keys.onRightPressed: { - if (wallpaper.wallpaperPaths.length > 0) { - wallpaper.nextWallpaper(); - } - } - - WallpaperImage { - id: wallImage - anchors.fill: parent - source: wallpaper.effectiveWallpaper - } + // ------------------------------------------------------------------- + // Reusable shader effect for palette tinting + // ------------------------------------------------------------------- + component PaletteShaderEffect: ShaderEffect { + id: effect + property var source: null + property var paletteTexture: null + property real paletteSize: 0 + property real texWidth: 1 + property real texHeight: 1 + + vertexShader: "palette.vert.qsb" + fragmentShader: "palette.frag.qsb" } - component WallpaperImage: Item { - property string source - property string previousSource + // ------------------------------------------------------------------- + // Component for static images (jpg, png, webp, etc.) + // ------------------------------------------------------------------- + Component { + id: staticImageComponent + Item { + id: staticImageRoot + anchors.fill: parent + property string sourceFile + property bool tint: wallpaper.tintEnabled - Process { - id: killMpvpaperProcess - running: false - command: ["pkill", "-f", wallpaper.mpvSocket] + onSourceFileChanged: console.log("staticImageComponent: sourceFile =", sourceFile) + onTintChanged: console.log("staticImageComponent: tint =", tint) - onExited: function (exitCode) { - console.log("Killed mpvpaper processes on socket", wallpaper.mpvSocket, ", exit code:", exitCode); - } - } + // Hidden item that builds a 1D texture from the effective palette + Item { + id: paletteSourceItem + visible: true + width: wallpaper.effectivePaletteSize + height: 1 + opacity: 0 - // Trigger animation when source changes - onSourceChanged: { - if (previousSource !== "" && source !== previousSource) { - if (Config.animDuration > 0) { - transitionAnimation.restart(); + Row { + anchors.fill: parent + Repeater { + model: wallpaper.effectivePalette + Rectangle { + width: 1 + height: 1 + color: { + if (typeof modelData === "string") { + if (modelData.charAt(0) === '#') return modelData; + else return Colors[modelData] || "black"; + } + return modelData; + } + } + } } - } - previousSource = source; - // Kill mpvpaper if switching to a static image - if (source) { - var fileType = getFileType(source); - if (fileType === 'image') { - killMpvpaperProcess.running = true; - } + Component.onCompleted: { if (width > 0) paletteTextureSource.scheduleUpdate(); } + onWidthChanged: { if (width > 0) paletteTextureSource.scheduleUpdate(); } } - } - SequentialAnimation { - id: transitionAnimation + ShaderEffectSource { + id: paletteTextureSource + sourceItem: paletteSourceItem + hideSource: true + visible: false + smooth: false + recursive: false + } - ParallelAnimation { - NumberAnimation { - target: wallImage - property: "scale" - to: 1.01 - duration: Config.animDuration - easing.type: Easing.OutCubic - } - NumberAnimation { - target: wallImage - property: "opacity" - to: 0.5 - duration: Config.animDuration - easing.type: Easing.OutCubic + // Force palette texture update when effective palette changes + Connections { + target: wallpaper + function onEffectivePaletteChanged() { + paletteTextureSource.scheduleUpdate(); } } - ParallelAnimation { - NumberAnimation { - target: wallImage - property: "scale" - to: 1.0 - duration: Config.animDuration - easing.type: Easing.OutCubic + // Image with layer effect for tinting + Image { + id: rawImage + anchors.fill: parent + source: staticImageRoot.sourceFile ? "file://" + staticImageRoot.sourceFile : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + smooth: true + mipmap: true + visible: true + + // Layer effect for palette tinting + layer.enabled: staticImageRoot.tint && wallpaper.effectivePaletteSize > 0 + layer.effect: PaletteShaderEffect { + paletteTexture: paletteTextureSource + paletteSize: wallpaper.effectivePaletteSize + texWidth: rawImage.width + texHeight: rawImage.height } - NumberAnimation { - target: wallImage - property: "opacity" - to: 1.0 - duration: Config.animDuration - easing.type: Easing.OutCubic + + onStatusChanged: { + if (status === Image.Ready) { + console.log("rawImage ready"); + } } } } + } - Loader { + // ------------------------------------------------------------------- + // Component for videos and GIFs (animated content) + // ------------------------------------------------------------------- + Component { + id: videoComponent + Item { + id: videoRoot anchors.fill: parent - sourceComponent: { - if (!parent.source) - return null; + property string sourceFile + property bool tint: wallpaper.tintEnabled - var fileType = getFileType(parent.source); - if (fileType === 'image') { - return staticImageComponent; - } else if (fileType === 'gif' || fileType === 'video') { - return mpvpaperComponent; - } - return staticImageComponent; // fallback - } + onSourceFileChanged: console.log("videoComponent: sourceFile =", sourceFile) + onTintChanged: console.log("videoComponent: tint =", tint) - property string sourceFile: parent.source - } - - Component { - id: staticImageComponent Item { - id: staticImageRoot - width: parent.width - height: parent.height - property string sourceFile: parent.sourceFile - property bool tint: wallpaper.tintEnabled - - // Subset of colors for optimization (approx 25 colors vs 98) - readonly property var optimizedPalette: ["background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", "cyan", "lightCyan", "magenta", "lightMagenta"] - - // Palette generation for the shader - Item { - id: paletteSourceItem - // Must be visible for ShaderEffectSource to capture it, - // but we hide it visually by placing it behind or expecting ShaderEffectSource hideSource behavior. - visible: true - width: staticImageRoot.optimizedPalette.length - height: 1 - opacity: 0 // Make invisible to eye but maintain presence for capture if needed (though hideSource usually handles this) - - Row { - anchors.fill: parent - Repeater { - model: staticImageRoot.optimizedPalette - Rectangle { - width: 1 - height: 1 - color: Colors[modelData] + id: paletteSourceItem + visible: true + width: wallpaper.effectivePaletteSize + height: 1 + opacity: 0 + + Row { + anchors.fill: parent + Repeater { + model: wallpaper.effectivePalette + Rectangle { + width: 1 + height: 1 + color: { + if (typeof modelData === "string") { + if (modelData.charAt(0) === '#') return modelData; + else return Colors[modelData] || "black"; + } + return modelData; } } } } - ShaderEffectSource { - id: paletteTextureSource - sourceItem: paletteSourceItem - hideSource: true - visible: false // The source object itself doesn't need to be visible in the scene graph - smooth: false - recursive: false + Component.onCompleted: { if (width > 0) paletteTextureSource.scheduleUpdate(); } + onWidthChanged: { if (width > 0) paletteTextureSource.scheduleUpdate(); } + } + + ShaderEffectSource { + id: paletteTextureSource + sourceItem: paletteSourceItem + hideSource: true + visible: false + smooth: false + recursive: false + } + + Connections { + target: wallpaper + function onEffectivePaletteChanged() { + paletteTextureSource.scheduleUpdate(); } + } - Image { - mipmap: true - id: rawImage - anchors.fill: parent - source: parent.sourceFile ? "file://" + parent.sourceFile : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - smooth: true - sourceSize.width: wallpaper.width - sourceSize.height: wallpaper.height - layer.enabled: parent.tint - layer.effect: ShaderEffect { - property var paletteTexture: paletteTextureSource - property real paletteSize: staticImageRoot.optimizedPalette.length - property real texWidth: rawImage.width - property real texHeight: rawImage.height - - vertexShader: "palette.vert.qsb" - fragmentShader: "palette.frag.qsb" - } + Video { + id: videoPlayer + anchors.fill: parent + source: videoRoot.sourceFile ? "file://" + videoRoot.sourceFile : "" + loops: MediaPlayer.Infinite + autoPlay: true + muted: true + fillMode: VideoOutput.PreserveAspectCrop + visible: true + + // Layer effect for palette tinting + layer.enabled: videoRoot.tint && wallpaper.effectivePaletteSize > 0 + layer.effect: PaletteShaderEffect { + paletteTexture: paletteTextureSource + paletteSize: wallpaper.effectivePaletteSize + texWidth: videoPlayer.width + texHeight: videoPlayer.height } } } + } - Component { - id: mpvpaperComponent - Item { - property string sourceFile: parent.sourceFile - property string scriptPath: decodeURIComponent(Qt.resolvedUrl("mpvpaper.sh").toString().replace("file://", "")) - - Timer { - id: mpvpaperRestartTimer - interval: 100 - onTriggered: { - if (sourceFile) { - console.log("Restarting mpvpaper for:", sourceFile); - mpvpaperProcess.running = true; - wallpaper.requestVideoSync(); - } - } - } + // ------------------------------------------------------------------- + // Main wallpaper display area + // ------------------------------------------------------------------- + Rectangle { + id: background + anchors.fill: parent + color: "black" + focus: true - onSourceFileChanged: { - if (sourceFile) { - console.log("Source file changed to:", sourceFile); - mpvpaperProcess.running = false; - mpvpaperRestartTimer.restart(); - } - } + Keys.onLeftPressed: { + if (wallpaper.wallpaperPaths.length > 0) wallpaper.previousWallpaper(); + } - Component.onCompleted: { - if (sourceFile) { - console.log("Initial mpvpaper run for:", sourceFile); - mpvpaperProcess.running = true; - wallpaper.requestVideoSync(); - } - } + Keys.onRightPressed: { + if (wallpaper.wallpaperPaths.length > 0) wallpaper.nextWallpaper(); + } - Component.onDestruction: - // mpvpaper script handles killing previous instances - {} + // Container that handles source changes, transitions, and palette loading + Item { + id: wallImageContainer + anchors.fill: parent + property string source: wallpaper.effectiveWallpaper + property string previousSource: "" - Process { - id: mpvpaperProcess - running: false - command: sourceFile && wallpaper.currentScreenName ? ["bash", scriptPath, sourceFile, (wallpaper.tintEnabled ? wallpaper.mpvShaderPath : ""), wallpaper.currentScreenName] : [] + onSourceChanged: { + console.log("wallImageContainer source changed to:", source); + if (source) wallpaper.loadCustomPalette(source); + // Animation will be triggered after loader finishes loading + } - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("mpvpaper output:", text); - } - } - } + SequentialAnimation { + id: transitionAnimation + ParallelAnimation { + NumberAnimation { target: wallImageContainer; property: "scale"; to: 1.01; duration: Config.animDuration; easing.type: Easing.OutCubic } + NumberAnimation { target: wallImageContainer; property: "opacity"; to: 0.5; duration: Config.animDuration; easing.type: Easing.OutCubic } + } + ParallelAnimation { + NumberAnimation { target: wallImageContainer; property: "scale"; to: 1.0; duration: Config.animDuration; easing.type: Easing.OutCubic } + NumberAnimation { target: wallImageContainer; property: "opacity"; to: 1.0; duration: Config.animDuration; easing.type: Easing.OutCubic } + } + } - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("mpvpaper error:", text); - } - } + Loader { + id: wallImageLoader + anchors.fill: parent + asynchronous: true + sourceComponent: { + if (!wallImageContainer.source) return null; + var fileType = wallpaper.getFileType(wallImageContainer.source); + console.log("Loader: fileType =", fileType, "source =", wallImageContainer.source); + if (fileType === 'image') return staticImageComponent; + else if (fileType === 'gif' || fileType === 'video') return videoComponent; + return staticImageComponent; + } + + onLoaded: { + console.log("Loader: item loaded, assigning sourceFile =", wallImageContainer.source); + if (item) { + item.sourceFile = wallImageContainer.source; } + // Trigger animation after new content is loaded + if (wallImageContainer.previousSource !== "" && + wallImageContainer.source !== wallImageContainer.previousSource && + Config.animDuration > 0) { + transitionAnimation.restart(); + } + wallImageContainer.previousSource = wallImageContainer.source; + } + + // Bind sourceFile directly to wallImageContainer.source + Binding { + target: wallImageLoader.item + property: "sourceFile" + value: wallImageContainer.source + when: wallImageLoader.item !== null + } + } - onExited: function (exitCode) { - console.log("mpvpaper process exited with code:", exitCode); + // Fallback in case Binding doesn't trigger + Connections { + target: wallImageContainer + function onSourceChanged() { + if (wallImageLoader.item) { + console.log("Connections: updating sourceFile to", wallImageContainer.source); + wallImageLoader.item.sourceFile = wallImageContainer.source; } } } } } -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/mpvpaper.sh b/modules/widgets/dashboard/wallpapers/mpvpaper.sh deleted file mode 100755 index edcb0dd4..00000000 --- a/modules/widgets/dashboard/wallpapers/mpvpaper.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$1" ]; then - echo "Use: $0 /path/to/wallpaper [shader_path] [monitor_target]" - exit 1 -fi - -WALLPAPER="$1" -SHADER="$2" -MONITOR="${3:-ALL}" - -# When a specific monitor is targeted, we don't kill all mpvpaper instances, -# just the one for that monitor if possible. However mpvpaper doesn't -# natively support killing by monitor easily via pkill. -# For now, we'll kill by checking the command line args if MONITOR != ALL. -# We must avoid killing this script itself, so we filter by the exact executable name. -if [ "$MONITOR" = "ALL" ]; then - pkill -x "mpvpaper" 2>/dev/null -else - pgrep -x mpvpaper | while read -r pid; do - if ps -p "$pid" -o args= | grep -q "$MONITOR"; then - kill "$pid" 2>/dev/null - fi - done -fi -SOCKET="/tmp/ambxst_mpv_socket_${MONITOR}" - -MPV_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 load-scripts=no input-ipc-server=$SOCKET" - -# Si el shader no está vacío y el archivo existe, agregarlo a MPV_OPTS -if [ -n "$SHADER" ] && [ -f "$SHADER" ]; then - MPV_OPTS="$MPV_OPTS glsl-shaders=$SHADER" -fi - -nohup mpvpaper -o "$MPV_OPTS" "$MONITOR" "$WALLPAPER" >/tmp/mpvpaper.log 2>&1 & diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index eabcd2eb..2d1a672c 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -1,4 +1,11 @@ #version 440 + +// Precision statement for ES compatibility (ignored by desktop GLSL) +#ifdef GL_ES +precision highp float; +precision mediump int; +#endif + layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; @@ -13,42 +20,82 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; +// Fast exp(-k*x) approximation: 1 / (1 + x + 0.5*x^2) +// Slightly rewritten to minimize operations +float fastExpWeight(float k, float distSq) { + float x = k * distSq; + return 1.0 / (1.0 + x * (1.0 + 0.5 * x)); +} + void main() { vec4 tex = texture(source, qt_TexCoord0); vec3 color = tex.rgb; - vec3 accumulatedColor = vec3(0.0); - float totalWeight = 0.0; - + // Early exit for fully transparent pixels + if (tex.a < 0.001) { + fragColor = vec4(0.0); + return; + } + int size = int(ubuf.paletteSize); - - // "Sharpness" factor. - // Higher value = colors stick closer to the palette (more posterized). - // Lower value = colors blend more (more washed out/grey). - // 15.0 - 20.0 is a good sweet spot for keeping identity while allowing gradients. - float distributionSharpness = 20.0; - - for (int i = 0; i < 128; i++) { + if (size <= 0) { + fragColor = tex * ubuf.qt_Opacity; + return; + } + + const float sharpness = 20.0; + const float weightThresh = 0.001; + const float maxDistSq = 0.3454; // -ln(0.001)/20.0 + const float epsilon = 1e-5; + + // Use mediump for accumulators to save registers/cycles + mediump vec3 accum = vec3(0.0); + mediump float sumW = 0.0; + + mediump float minDist = 1e10; + lowp vec3 nearest = vec3(0.0); // lowp sufficient for palette colors + + // Precompute stepping factors to avoid division inside loop + float invSize = 1.0 / float(size); + float uStep = invSize; + float uStart = 0.5 * invSize; + + // Hint to compiler to unroll the loop for better pipelining + #pragma unroll + for (int i = 0; i < 128; ++i) { if (i >= size) break; - - float u = (float(i) + 0.5) / ubuf.paletteSize; - vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - - vec3 diff = color - pColor; - // Euclidean squared distance - float distSq = dot(diff, diff); - - // Gaussian Weighting function: e^(-k * d^2) - // This creates a smooth bell curve of influence around each palette color. - float weight = exp(-distributionSharpness * distSq); - - accumulatedColor += pColor * weight; - totalWeight += weight; + + float u = float(i) * uStep + uStart; + // Use texelFetch instead of texture() – avoids UV filtering & derivative overhead + // Assumes paletteTexture is a 2D texture with height=1. + lowp vec3 pColor = texelFetch(paletteTexture, ivec2(i, 0), 0).rgb; + + // Difference and squared distance (dot product is hardware accelerated) + mediump vec3 diff = color - pColor; + mediump float distSq = dot(diff, diff); + + // Nearest neighbor tracking (for fallback) + if (distSq < minDist) { + minDist = distSq; + nearest = pColor; + } + + // Skip if contribution < threshold + if (distSq > maxDistSq) continue; + + // Weight using fast approximation + mediump float w = fastExpWeight(sharpness, distSq); + + accum += pColor * w; + sumW += w; } - // Normalize - vec3 finalColor = accumulatedColor / (totalWeight + 0.00001); // Avoid div by zero + vec3 finalColor; + if (sumW < weightThresh) { + finalColor = nearest; + } else { + finalColor = accum / (sumW + epsilon); + } - // Pre-multiply alpha for proper blending in Qt Quick fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 1b3548ee..db987734 100644 Binary files a/modules/widgets/dashboard/wallpapers/palette.frag.qsb and b/modules/widgets/dashboard/wallpapers/palette.frag.qsb differ diff --git a/modules/widgets/dashboard/wallpapers/palette.vert b/modules/widgets/dashboard/wallpapers/palette.vert index fbecaa9d..9567262f 100644 --- a/modules/widgets/dashboard/wallpapers/palette.vert +++ b/modules/widgets/dashboard/wallpapers/palette.vert @@ -14,4 +14,4 @@ layout(std140, binding = 0) uniform buf { void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = ubuf.qt_Matrix * qt_Vertex; -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.vert.qsb b/modules/widgets/dashboard/wallpapers/palette.vert.qsb index a1bd0d0b..48996f69 100644 Binary files a/modules/widgets/dashboard/wallpapers/palette.vert.qsb and b/modules/widgets/dashboard/wallpapers/palette.vert.qsb differ diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 6d620894..40e133be 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -59,6 +59,9 @@ let export QML2_IMPORT_PATH="${envAmbxst}/lib/qt-6/qml:$QML2_IMPORT_PATH" export QML_IMPORT_PATH="$QML2_IMPORT_PATH" + # Ensure QtMultimedia uses GStreamer backend + export QT_MEDIA_BACKEND=gstreamer + # Make bundled fonts available to fontconfig export FONTCONFIG_PATH="${fontconfigConf}/etc/fonts:''${FONTCONFIG_PATH:-}" diff --git a/nix/packages/media.nix b/nix/packages/media.nix index 56074e79..2b3e303e 100644 --- a/nix/packages/media.nix +++ b/nix/packages/media.nix @@ -3,7 +3,14 @@ with pkgs; [ gpu-screen-recorder - mpvpaper + + # GStreamer backend for QtMultimedia + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + gst_all_1.gst-libav ffmpeg x264