diff --git a/README.md b/README.md index 6bbd3f0..3be1e0b 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ It automatically generates beautiful color themes from your wallpapers, and lets - [`linux-wallpaperengine`](https://github.com/catsout/wallpaper-engine-kde-plugin) - for Wallpaper Engine - [`cage`](https://github.com/cage-kiosk/cage) - runs wallpapers in an isolated, invisible Wayland session specifically so we can screenshot them without affecting the user’s desktop - [`grim`](https://github.com/emersion/grim) - takes a screenshot of the wallpaper running inside cage -- [`wayland-info`](https://gitlab.freedesktop.org/wayland/wayland-utils/) - used to query Wayland compositor outputs protocol +- [`ffmpeg`](https://github.com/FFmpeg/FFmpeg) - generate thumbnails for video files ### Arch Linux (AUR) diff --git a/forge.config.ts b/forge.config.ts index 6f22eb7..cc0b8ea 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -85,6 +85,7 @@ const config: ForgeConfig = { canvas: "^3.1.2", conf: "^14.0.0", keytar: "^7.9.0", + sharp: "^0.34.3", }; fs.writeFileSync(path.resolve(buildPath, "package.json"), JSON.stringify(packageJson)); diff --git a/package-lock.json b/package-lock.json index 19e3c17..50298ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "react-dom": "^19.1.1", "react-intersection-observer": "^9.13.0", "react-virtualized": "^9.22.6", + "sharp": "^0.34.3", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", @@ -1314,6 +1315,16 @@ "node": ">=14.14" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -2006,6 +2017,424 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -5522,11 +5951,23 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5539,9 +5980,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -11761,6 +12211,48 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11912,6 +12404,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", diff --git a/package.json b/package.json index 180cd77..24b892c 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "react-dom": "^19.1.1", "react-intersection-observer": "^9.13.0", "react-virtualized": "^9.22.6", + "sharp": "^0.34.3", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 91b9a6c..1606c57 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -8,7 +8,7 @@ url="https://github.com/kasper24/walltone" license=("GPL3") depends=("nss" "libsecret" "cairo" "pango" "libjpeg-turbo" "giflib" "libsvgtiny" "swaybg" "mpvpaper" -"linux-wallpaperengine" "cage" "grim" "wayland-utils") +"linux-wallpaperengine" "cage" "grim" "ffmpeg") makedepends=("npm" "nodejs" "git") source=("$pkgname::git+$url.git") sha256sums=("SKIP") diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix index b168931..5442c2f 100644 --- a/packaging/nix/package.nix +++ b/packaging/nix/package.nix @@ -19,7 +19,8 @@ linux-wallpaperengine, cage, grim, - wayland-utils, + ffmpeg, + vips, lib, }: @@ -46,7 +47,7 @@ buildNpmPackage rec { src = ../../.; - npmDepsHash = "sha256-1RJDuZVHJGvvn9ozMZdRtVXpy4O4N+wMA/pW8JJaFtk="; + npmDepsHash = "sha256-bTD9ZQ9p+Q2CGiBXRrIRQgDtz809HNilnuOhplrGtUw="; dontNpmBuild = true; makeCacheWritable = true; @@ -75,6 +76,9 @@ buildNpmPackage rec { libpng librsvg giflib + + # Sharp dependencies + vips ]; nativeBuildInputs = [ @@ -96,7 +100,7 @@ buildNpmPackage rec { linux-wallpaperengine cage grim - wayland-utils + ffmpeg ] } diff --git a/src/electron/main/lib/index.ts b/src/electron/main/lib/index.ts index 80c0238..76f831c 100644 --- a/src/electron/main/lib/index.ts +++ b/src/electron/main/lib/index.ts @@ -7,6 +7,7 @@ const execute = ({ env = {}, shell = false, detached = false, + ignoreErrors = false, logStdout = true, logStderr = true, }: { @@ -15,6 +16,7 @@ const execute = ({ env?: NodeJS.ProcessEnv; shell?: boolean; detached?: boolean; + ignoreErrors?: boolean; logStdout?: boolean; logStderr?: boolean; }): Promise<{ stdout: string; stderr: string }> => { @@ -46,6 +48,11 @@ const execute = ({ }); child.on("close", (code) => { + if (ignoreErrors && code !== 0) { + console.warn(`[${command}] Process exited with code ${code}, but ignoring errors`); + return resolve({ stdout, stderr }); + } + if (code === 0) { if (logStderr) console.log(`[${command}] Process completed successfully`); resolve({ stdout, stderr }); @@ -58,7 +65,7 @@ const execute = ({ child.on("error", (error) => { if (logStderr) console.error(`[${command}] Process error:`, error); - reject(error); + if (!ignoreErrors) reject(error); }); }); }; diff --git a/src/electron/main/trpc/routes/api/pexels/index.ts b/src/electron/main/trpc/routes/api/pexels/index.ts index 79f26dc..8d0c2ad 100644 --- a/src/electron/main/trpc/routes/api/pexels/index.ts +++ b/src/electron/main/trpc/routes/api/pexels/index.ts @@ -1,7 +1,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; -import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/index.js"; +import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/types.js"; interface PexelsPhoto { id: number; @@ -69,10 +69,11 @@ interface PexelsSearchResponse { const transformPhotos = (photos: PexelsPhoto[]): ApiWallpaper[] => { return photos.map((photo) => ({ - type: "api", + type: "image", id: photo.id.toString(), name: photo.alt || `Photo by ${photo.photographer}`, - previewPath: photo.src.large, + thumbnailPath: photo.src.large, + fullSizePath: photo.src.original, downloadUrl: photo.src.original, })); }; @@ -87,10 +88,11 @@ const transformVideos = (videos: PexelsVideo[]): ApiWallpaper[] => { }); return { - type: "api", + type: "video", id: video.id.toString(), name: `Video by ${video.user.name}`, - previewPath: video.image, + thumbnailPath: video.image, + fullSizePath: bestVideo.link, downloadUrl: bestVideo.link, }; }); diff --git a/src/electron/main/trpc/routes/api/unsplash/index.ts b/src/electron/main/trpc/routes/api/unsplash/index.ts index 39518a4..447391f 100644 --- a/src/electron/main/trpc/routes/api/unsplash/index.ts +++ b/src/electron/main/trpc/routes/api/unsplash/index.ts @@ -1,7 +1,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; -import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/index.js"; +import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/types.js"; interface UnsplashPhoto { id: string; @@ -154,10 +154,11 @@ interface UnsplashSearchResult { const transformWallpapers = (photos: UnsplashPhoto[]): ApiWallpaper[] => { return photos.map((photo) => ({ - type: "api", + type: "image", id: photo.id, name: photo.alt_description || photo.description || `Photo by ${photo.user.name}`, - previewPath: photo.urls.regular, + thumbnailPath: photo.urls.regular, + fullSizePath: photo.urls.full, downloadUrl: photo.urls.full, })); }; diff --git a/src/electron/main/trpc/routes/api/wallhaven/index.ts b/src/electron/main/trpc/routes/api/wallhaven/index.ts index b074956..1f7e63c 100644 --- a/src/electron/main/trpc/routes/api/wallhaven/index.ts +++ b/src/electron/main/trpc/routes/api/wallhaven/index.ts @@ -1,7 +1,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; -import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/index.js"; +import { type ApiWallpaper } from "@electron/main/trpc/routes/wallpaper/types.js"; export type WallhavenSorting = "date_added" | "random" | "views" | "favorites" | "toplist"; export type WallhavenCategory = "general" | "anime" | "people"; @@ -60,10 +60,11 @@ const convertPurity = (purity: WallhavenPurity[]): string => { const transformWallpapers = (wallpapers: WallhavenWallpaper[]): ApiWallpaper[] => { return wallpapers.map((wallpaper) => ({ - type: "api", + type: "image", id: wallpaper.id, name: wallpaper.id, - previewPath: wallpaper.thumbs.large, + thumbnailPath: wallpaper.thumbs.large, + fullSizePath: wallpaper.path, downloadUrl: wallpaper.path, })); }; diff --git a/src/electron/main/trpc/routes/api/wallpaper-engine/index.ts b/src/electron/main/trpc/routes/api/wallpaper-engine/index.ts index 5ea91c5..225d9ec 100644 --- a/src/electron/main/trpc/routes/api/wallpaper-engine/index.ts +++ b/src/electron/main/trpc/routes/api/wallpaper-engine/index.ts @@ -1,7 +1,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; -import { type BaseWallpaper } from "@electron/main/trpc/routes/wallpaper/index.js"; +import { type BaseWallpaper } from "@electron/main/trpc/routes/wallpaper/types.js"; interface WallpaperEngineWorkshopItem { result: number; @@ -72,10 +72,11 @@ interface WallpaperEngineWorkshopSearchResponse { const transformWallpapers = (wallpapers: WallpaperEngineWorkshopItem[]): BaseWallpaper[] => { return wallpapers.map((wallpaper) => ({ - type: "api", + type: "image", id: wallpaper.publishedfileid, name: wallpaper.title, - previewPath: wallpaper.preview_url, + thumbnailPath: wallpaper.preview_url, + fullSizePath: wallpaper.preview_url, })); }; diff --git a/src/electron/main/trpc/routes/monitor/index.ts b/src/electron/main/trpc/routes/monitor/index.ts index 95cbc51..c200638 100644 --- a/src/electron/main/trpc/routes/monitor/index.ts +++ b/src/electron/main/trpc/routes/monitor/index.ts @@ -1,102 +1,32 @@ -import { TRPCError } from "@trpc/server"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; -import { execute } from "@electron/main/lib/index.js"; +import { screen } from "electron"; export type Monitor = { + id: string; name: string; - make: string; - model: string; x: number; y: number; width: number; height: number; - refreshRate: number; scale: number; }; export const monitorRouter = router({ search: publicProcedure.query(async () => { - try { - const { stdout } = await execute({ - command: "wayland-info", - logStdout: false, - logStderr: false, - }); - - const lines = stdout.split("\n"); - - const monitors: Monitor[] = []; - let current: Partial = {}; - let isInOutputBlock = false; - for (const line of lines) { - const trimmed = line.trim(); - - // New output block - if (trimmed.includes("interface: 'wl_output'")) { - if (current.name) { - monitors.push(current as Monitor); - } - current = {}; - isInOutputBlock = true; - continue; - } - - if (!isInOutputBlock) continue; - - const kvPairs = trimmed.matchAll(/(\w+):\s*'?([^',]+)'?/g); - for (const [, key, rawValue] of kvPairs) { - const value = rawValue.replace(/^'|'$/g, "").trim(); // Remove quotes if any - - switch (key) { - case "name": - current.name = value; - break; - case "make": - current.make = value; - break; - case "model": - current.model = value; - break; - case "x": - current.x = parseInt(value); - break; - case "y": - current.y = parseInt(value); - break; - case "scale": - current.scale = parseInt(value); - break; - case "width": - current.width = parseInt(value); - break; - case "height": - current.height = parseInt(value); - break; - case "refresh": - current.refreshRate = parseFloat(value); - break; - } - } - } - - // Push last monitor - if (current.name) { - monitors.push(current as Monitor); - } - - return monitors; - } catch (error) { - if (error instanceof TRPCError) { - throw error; - } - - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to get monitor configuration: ${errorMessage}`, - cause: error, - }); - } + const displays = screen.getAllDisplays(); + const monitors: Monitor[] = displays.map((display) => { + const id = display.label.match(/\((.*?)\)/)?.[1] || ""; + const name = display.label.replace(/\(.*?\)/, "").trim(); + return { + id, + name: name, + x: display.bounds.x, + y: display.bounds.y, + width: display.size.width * display.scaleFactor, + height: display.size.height * display.scaleFactor, + scale: display.scaleFactor, + }; + }); + return monitors; }), }); diff --git a/src/electron/main/trpc/routes/settings/index.ts b/src/electron/main/trpc/routes/settings/index.ts index 86488c5..dfb98b1 100644 --- a/src/electron/main/trpc/routes/settings/index.ts +++ b/src/electron/main/trpc/routes/settings/index.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import Conf, { Schema } from "conf"; import { TRPCError } from "@trpc/server"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; +import { SetWallpaperInput } from "@electron/main/trpc/routes/wallpaper/types.js"; export interface SettingsSchema { /** Settings for the application's appearance and startup behavior. */ @@ -49,12 +50,10 @@ export interface SettingsSchema { /** Internal state, not typically edited by the user. */ internal: { - lastWallpaperCmd: { - command: string; - args: string[]; - }; + lastWallpaper: Record; }; + /** API keys for third-party services. */ apiKeys?: { pexels: string; unsplash: string; @@ -130,12 +129,53 @@ const schema: Schema = { internal: { type: "object", properties: { - lastWallpaperCmd: { + lastWallpaper: { type: "object", - properties: { - command: { type: "string", default: "" }, - args: { type: "array", default: [], items: { type: "string" } }, + patternProperties: { + ".*": { + type: "object", + properties: { + type: { type: "string", enum: ["image", "video", "wallpaper-engine"] }, + id: { type: "string", minLength: 1 }, + name: { type: "string", minLength: 1 }, + path: { type: "string", minLength: 1 }, + monitors: { + type: "array", + minItems: 1, + items: { + type: "object", + properties: { + id: { type: "string", minLength: 1 }, + scalingMethod: { type: "string" }, + }, + required: ["id"], + }, + }, + wallpaperEngineOptions: { + type: "object", + properties: { + silent: { type: "boolean" }, + volume: { type: "number", minimum: 0, maximum: 100 }, + noAutomute: { type: "boolean" }, + noAudioProcessing: { type: "boolean" }, + fps: { type: "number", minimum: 1, maximum: 200 }, + clamping: { type: "string", enum: ["clamp", "border", "repeat"] }, + disableMouse: { type: "boolean" }, + disableParallax: { type: "boolean" }, + noFullscreenPause: { type: "boolean" }, + }, + }, + videoOptions: { + type: "object", + properties: { + mute: { type: "boolean" }, + }, + }, + }, + required: ["type", "id", "name", "path", "monitors"], + }, }, + additionalProperties: false, default: {}, }, }, diff --git a/src/electron/main/trpc/routes/wallpaper/index.ts b/src/electron/main/trpc/routes/wallpaper/index.ts index 4d68d78..4b9d6e0 100644 --- a/src/electron/main/trpc/routes/wallpaper/index.ts +++ b/src/electron/main/trpc/routes/wallpaper/index.ts @@ -1,66 +1,26 @@ import path from "path"; -import { promises as fs } from "fs"; +import { Worker } from "worker_threads"; import z from "zod"; -import { TRPCError } from "@trpc/server"; -import { execute, killProcess, santize, renderString } from "@electron/main/lib/index.js"; import { publicProcedure, router } from "@electron/main/trpc/index.js"; import { caller } from "@electron/main/trpc/routes/index.js"; -import { type SettingsSchema, type SettingKey } from "@electron/main/trpc/routes/settings/index.js"; - -const SUPPORTED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff"]; -const SUPPORTED_VIDEO_EXTENSIONS = [".mp4", ".mkv", ".webm", ".avi", ".mov"]; - -const CAGE_INIT_TIME = 5; -const CAGE_SCREENSHOT_PATH = "/tmp/walltone-wallpaper-screenshot.png"; - -export interface BaseWallpaper { - type: "image" | "video" | "wallpaper-engine" | "api"; - id: string; - name: string; - previewPath: string; -} - -export interface ApiWallpaper extends BaseWallpaper { - type: "api"; - downloadUrl: string; -} - -interface ImageWallpaper extends BaseWallpaper { - type: "image"; - path: string; - dateAdded: number; - tags: string[]; -} - -interface VideoWallpaper extends BaseWallpaper { - type: "video"; - path: string; - dateAdded: number; - tags: string[]; -} - -interface WallpaperEngineWallpaper extends BaseWallpaper { - type: "wallpaper-engine"; - path: string; - dateAdded: number; - tags: string[]; - workshopId: string; - file: string; - sceneType: string; -} - -export type LibraryWallpaper = ImageWallpaper | VideoWallpaper | WallpaperEngineWallpaper; - -export interface WallpaperData { - data: T[]; - currentPage: number; - prevPage: number | null; - nextPage: number | null; - totalItems: number; - totalPages: number; -} - -const searchWallpapersSchema = z.object({ +import { type SettingsSchema } from "@electron/main/trpc/routes/settings/index.js"; +import { type WallpaperData, type LibraryWallpaper } from "./types.js"; +import { + getImageAndVideoWallpapers, + getWallpaperEngineWallpapers, + filterWallpapers, + sortWallpapers, + paginateData, +} from "./search.js"; +import { + populateMonitorsIfEmpty, + saveLastWallpaper, + killWallpaperProcesses, + screenshotWallpaper, + setWallpaper, +} from "./set.js"; + +export const searchWallpapersSchema = z.object({ type: z.enum(["image", "video", "wallpaper-engine", "all"]), page: z.number().min(1).default(1), perPage: z.number().min(1).default(20), @@ -70,19 +30,19 @@ const searchWallpapersSchema = z.object({ matchAll: z.boolean().default(false), }); -const setWallpaperSchema = z.object({ +export const monitorsSchema = z.array( + z.object({ + id: z.string().min(1), + scalingMethod: z.string().optional(), + }) +); + +export const setWallpaperSchema = z.object({ type: z.enum(["image", "video", "wallpaper-engine"]), id: z.string().min(1), name: z.string().min(1), path: z.string().min(1), - monitors: z - .array( - z.object({ - name: z.string().min(1), - scalingMethod: z.string().optional(), - }) - ) - .min(1), + monitors: monitorsSchema.min(1), wallpaperEngineOptions: z .object({ silent: z.boolean().optional(), @@ -108,20 +68,12 @@ export const wallpaperRouter = router({ const wallpapers: LibraryWallpaper[] = []; if (input.type === "image" || input.type === "all") { - const imageWallpapers = await getMediaWallpapers( - "image", - "wallpaperSources.imageFolders", - SUPPORTED_IMAGE_EXTENSIONS - ); + const imageWallpapers = await getImageAndVideoWallpapers("image"); wallpapers.push(...imageWallpapers); } if (input.type === "video" || input.type === "all") { - const videoWallpapers = await getMediaWallpapers( - "video", - "wallpaperSources.videoFolders", - SUPPORTED_VIDEO_EXTENSIONS - ); + const videoWallpapers = await getImageAndVideoWallpapers("video"); wallpapers.push(...videoWallpapers); } @@ -130,65 +82,39 @@ export const wallpaperRouter = router({ wallpapers.push(...weWallpapers); } - const filteredWallpapers = filterWallpapers( - wallpapers, - input.query, - input.tags, - input.matchAll - ); - const sortedWallpapers = sortWallpapers(filteredWallpapers, input.sorting); + const filtered = filterWallpapers(wallpapers, input.query, input.tags, input.matchAll); + const sorted = sortWallpapers(filtered, input.sorting); + const paginated = paginateData(sorted, input.page, input.perPage); - return paginateData(sortedWallpapers, input.page, input.perPage); - }), + return (await new Promise((resolve, reject) => { + const workerPath = path.join(import.meta.dirname, "thumbnail-generator.js"); + const worker = new Worker(workerPath); - set: publicProcedure.input(setWallpaperSchema).mutation(async ({ input }) => { - killProcess("swaybg"); - killProcess("mpvpaper"); - killProcess("linux-wallpaperengine"); - // Wait for processes to terminate - await new Promise((resolve) => setTimeout(resolve, 1000)); + worker.on("message", (event) => { + const result = event.data; + if (event.status === "success") { + resolve(result); + } else { + reject(new Error(result.error || "Worker failed with an unknown error.")); + } + worker.terminate(); + }); + + worker.on("error", (error) => { + reject(error); + worker.terminate(); + }); - switch (input.type) { - case "image": - await copyWallpaperToDestinations(input.id, input.name, input.path); - await setImageWallpaper(input.path, input.monitors); - break; - case "video": - await screenshotWallpaperInCage(["mpv", "panscan=1.0", input.path]); - await copyWallpaperToDestinations(input.id, input.name, CAGE_SCREENSHOT_PATH); - await setVideoWallpaper(input.path, input.monitors, input.videoOptions); - break; - case "wallpaper-engine": { - const assetsPath = await caller.settings.get({ - key: "wallpaperSources.wallpaperEngineAssetsFolder", - }); - if (typeof assetsPath !== "string" || !assetsPath) - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Wallpaper Engine assets folder is not set.", - }); - await screenshotWallpaperInCage([ - "linux-wallpaperengine", - "--silent", - "--fps", - "1", - "--assets-dir", - assetsPath, - "--window", - "0x0x1280x720", - input.path, - ]); - await copyWallpaperToDestinations(input.id, input.name, CAGE_SCREENSHOT_PATH); + worker.postMessage({ data: paginated }); + })) as unknown as Promise>; + }), - await setWallpaperEngineWallpaper( - assetsPath, - input.path, - input.monitors, - input.wallpaperEngineOptions - ); - break; - } - } + set: publicProcedure.input(setWallpaperSchema).mutation(async ({ input }) => { + await populateMonitorsIfEmpty(input); + await saveLastWallpaper(input); + await killWallpaperProcesses(); + await screenshotWallpaper(input); + await setWallpaper(input, false); }), restoreOnStart: publicProcedure.mutation(async () => { @@ -197,396 +123,14 @@ export const wallpaperRouter = router({ }); if (!restoreOnStart) return; - const lastWallpaperCmd = await caller.settings.get({ - key: "internal.lastWallpaperCmd", - }); + const lastWallpaper = (await caller.settings.get({ + key: "internal.lastWallpaper", + })) as SettingsSchema["internal"]["lastWallpaper"]; - if (lastWallpaperCmd.command) { - killProcess("swaybg"); - killProcess("mpvpaper"); - killProcess("linux-wallpaperengine"); - execute(lastWallpaperCmd); - } + Object.values(lastWallpaper).forEach(async (wallpaper) => { + await populateMonitorsIfEmpty(wallpaper); + await killWallpaperProcesses(); + await setWallpaper(wallpaper, true); + }); }), }); - -const paginateData = ( - data: T[], - page: number, - itemsPerPage: number -): WallpaperData => { - const currentPage = page; - const totalItems = data.length; - const totalPages = Math.ceil(totalItems / itemsPerPage); - const startIndex = (page - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedData = data.slice(startIndex, endIndex); - - return { - data: paginatedData, - currentPage, - prevPage: currentPage > 1 ? currentPage - 1 : null, - nextPage: currentPage < totalPages ? currentPage + 1 : null, - totalItems, - totalPages, - }; -}; - -const getMediaWallpapers = async ( - mediaType: "image" | "video", - settingsKey: SettingKey, - fileTypes: string[] -) => { - const wallpapers: LibraryWallpaper[] = []; - const folders = await caller.settings.get({ - key: settingsKey, - }); - - if (!folders || !Array.isArray(folders) || folders.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `No ${mediaType} folders configured.`, - }); - } - - for (const folder of folders) { - try { - const files = await searchForFiles(folder, fileTypes); - files.forEach(async (file) => { - wallpapers.push({ - id: path.basename(file.path), - name: file.name, - path: file.path, - previewPath: mediaType !== "video" ? `image://${file.path}` : `video://${file.path}`, - dateAdded: Date.now(), - tags: [mediaType], - type: mediaType, - }); - }); - } catch (error) { - console.error(`Failed to search ${mediaType} files in ${folder}:`, error); - } - } - - return wallpapers; -}; - -const searchForFiles = async ( - folderPath: string, - fileTypes: string[] -): Promise<{ name: string; path: string }[]> => { - const files: { name: string; path: string }[] = []; - const dirents = await fs.readdir(folderPath, { withFileTypes: true }); - - for (const dirent of dirents) { - const dirPath = path.join(folderPath, dirent.name); - const ext = path.extname(dirent.name).toLowerCase(); - - if (dirent.isDirectory()) { - const subdirectoryFiles = await searchForFiles(dirPath, fileTypes); - files.push(...subdirectoryFiles); - } else if (dirent.isFile() && fileTypes.includes(ext)) { - const name = path.basename(dirent.name, path.extname(dirent.name)); - files.push({ name, path: dirPath }); - } - } - - return files; -}; - -const getWallpaperEngineWallpapers = async () => { - const wallpapers: WallpaperEngineWallpaper[] = []; - const folders = await caller.settings.get({ - key: "wallpaperSources.wallpaperEngineFolders", - }); - if (!folders || !Array.isArray(folders) || folders.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "No Wallpaper Engine folders configured.", - }); - } - - for (const folder of folders) { - try { - const subdirectories = await fs.readdir(folder, { withFileTypes: true }); - - for (const dirent of subdirectories) { - if (dirent.isDirectory()) { - const subdirectoryPath = path.join(folder, dirent.name); - const jsonFilePath = path.join(subdirectoryPath, "project.json"); - - try { - const jsonFileExists = await fs - .access(jsonFilePath) - .then(() => true) - .catch(() => false); - - if (jsonFileExists) { - const jsonData = await fs.readFile(jsonFilePath, "utf-8"); - const parsedData = JSON.parse(jsonData); - - if (parsedData.preview) { - const stat = await fs.stat(jsonFilePath); - const tags = [ - ...(parsedData.tags || []), - parsedData.type, - parsedData.contentrating, - ].filter(Boolean); - - wallpapers.push({ - type: "wallpaper-engine", - id: path.basename(subdirectoryPath), - name: parsedData.title, - path: subdirectoryPath, - previewPath: `image://${path.join(subdirectoryPath, parsedData.preview)}`, - dateAdded: stat.mtime.getTime(), - workshopId: dirent.name, - file: parsedData.file, - sceneType: parsedData.type, - tags, - }); - } - } - } catch (jsonError) { - console.error(`Failed to read or parse ${jsonFilePath}:`, jsonError); - } - } - } - } catch (error) { - console.error(`Error reading wallpaper engine directory ${folder}:`, error); - } - } - - return wallpapers; -}; - -const filterWallpapers = ( - wallpapers: LibraryWallpaper[], - query?: string, - tags?: string[], - matchAll?: boolean -) => { - return wallpapers.filter((wallpaper) => { - const matchesQuery = query ? wallpaper.name.toLowerCase().includes(query.toLowerCase()) : true; - - const matchesTags = - tags && tags.length > 0 - ? matchAll - ? tags.every((tag) => wallpaper.tags.includes(tag)) - : tags.some((tag) => wallpaper.tags.includes(tag)) - : true; - - return matchesQuery && matchesTags; - }); -}; - -const sortWallpapers = (wallpapers: LibraryWallpaper[], sorting: string) => { - if (sorting === "name") { - return wallpapers.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); - } else if (sorting === "date_added") { - return wallpapers.sort((a, b) => b.dateAdded - a.dateAdded); - } else { - return wallpapers.sort((a, b) => Number(a.id) - Number(b.id)); - } -}; - -const setImageWallpaper = async ( - imagePath: string, - monitors: { name: string; scalingMethod?: string }[] -) => { - const args: string[] = []; - - monitors.forEach((monitor) => { - args.push( - "--output", - monitor.name, - "--image", - imagePath, - "--mode", - monitor.scalingMethod || "crop" - ); - }); - - await caller.settings.set({ - key: "internal.lastWallpaperCmd", - value: { - command: "swaybg", - args, - }, - }); - - await execute({ command: "swaybg", args }); -}; - -const setVideoWallpaper = async ( - videoPath: string, - monitors: { name: string; scalingMethod?: string }[], - options?: { - mute?: boolean; - } -) => { - // Build mpv options array - const mpvOptions: string[] = ["loop"]; - - // Add video-specific options - if (options?.mute) { - mpvOptions.push("no-audio"); - } - - // Add scaling options for each monitor - monitors.forEach((monitor) => { - if (monitor.scalingMethod) { - switch (monitor.scalingMethod) { - case "fill": - mpvOptions.push("panscan=1.0"); - break; - case "fit": - mpvOptions.push("panscan=0.0"); - break; - case "stretch": - mpvOptions.push("keepaspect=no"); - break; - case "center": - case "tile": - // These don't have specific mpv options - break; - } - } - }); - - const args = []; - - // Add mpv options if any exist - if (mpvOptions.length > 0) { - args.push("-o", mpvOptions.join(" ")); - } - - // Add monitor names (use ALL if multiple monitors, otherwise specific monitor) - if (monitors.length === 1) { - args.push(monitors[0].name); - } else { - args.push("ALL"); - } - - // Add video path - args.push(videoPath); - - await caller.settings.set({ - key: "internal.lastWallpaperCmd", - value: { - command: "mpvpaper", - args, - }, - }); - - await execute({ command: "mpvpaper", args }); -}; - -const setWallpaperEngineWallpaper = async ( - assetsPath: string, - wallpaperPath: string, - monitors: { name: string; scalingMethod?: string }[], - options?: { - silent?: boolean; - volume?: number; - noAutomute?: boolean; - noAudioProcessing?: boolean; - fps?: number; - clamping?: string; - disableMouse?: boolean; - disableParallax?: boolean; - noFullscreenPause?: boolean; - } -) => { - const args = [ - ...monitors.flatMap((monitor) => [ - "--screen-root", - monitor.name, - "--bg", - wallpaperPath, - "--scaling", - monitor.scalingMethod || "default", - ]), - "--assets-dir", - assetsPath, - ]; - - if (options?.silent) { - args.push("--silent"); - } - - if (options?.volume !== undefined) { - args.push("--volume", options.volume.toString()); - } - - if (options?.noAutomute) { - args.push("--noautomute"); - } - - if (options?.noAudioProcessing) { - args.push("--no-audio-processing"); - } - - if (options?.fps !== undefined) { - args.push("--fps", options.fps.toString()); - } - - if (options?.clamping) { - args.push("--clamping", options.clamping); - } - - if (options?.disableMouse) { - args.push("--disable-mouse"); - } - - if (options?.disableParallax) { - args.push("--disable-parallax"); - } - - if (options?.noFullscreenPause) { - args.push("--no-fullscreen-pause"); - } - - await caller.settings.set({ - key: "internal.lastWallpaperCmd", - value: { - command: "linux-wallpaperengine", - args, - }, - }); - - await execute({ command: "linux-wallpaperengine", args }); -}; - -const screenshotWallpaperInCage = async (cmd: string[]) => { - const args = [ - "--", - "sh", - "-c", - `${cmd.join(" ")} & pid=$!; sleep ${CAGE_INIT_TIME} && grim -g "0,0 1280x720" ${CAGE_SCREENSHOT_PATH} && kill $pid`, - ]; - await execute({ command: "cage", args, env: { WLR_BACKENDS: "headless" } }); -}; - -const copyWallpaperToDestinations = async ( - wallpaperId: string, - wallpaperName: string, - wallpaperPath: string -) => { - const wallpaperDestinations = (await caller.settings.get({ - key: "themeOutput.wallpaperCopyDestinations", - })) as SettingsSchema["themeOutput"]["wallpaperCopyDestinations"]; - - await Promise.all( - wallpaperDestinations.map(async (destination) => { - destination = await renderString(destination, { - wallpaper: { - id: santize(wallpaperId), - name: santize(wallpaperName), - }, - }); - await fs.mkdir(path.dirname(destination), { recursive: true }); - await fs.copyFile(wallpaperPath, destination); - }) - ); -}; diff --git a/src/electron/main/trpc/routes/wallpaper/search.ts b/src/electron/main/trpc/routes/wallpaper/search.ts new file mode 100644 index 0000000..c813ec9 --- /dev/null +++ b/src/electron/main/trpc/routes/wallpaper/search.ts @@ -0,0 +1,198 @@ +import path from "path"; +import { promises as fs } from "fs"; +import { TRPCError } from "@trpc/server"; +import { caller } from "@electron/main/trpc/routes/index.js"; +import { + type WallpaperEngineWallpaper, + type LibraryWallpaper, + type WallpaperData, +} from "./types.js"; + +const WALLPAPERS_TYPE_TO_EXTS = { + image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff"], + video: [".mp4", ".mkv", ".webm", ".avi", ".mov"], +}; + +const getImageAndVideoWallpapers = async (type: "image" | "video") => { + const wallpapers: LibraryWallpaper[] = []; + + const folders = await caller.settings.get({ + key: `wallpaperSources.${type}Folders`, + }); + + if (!folders || !Array.isArray(folders) || folders.length === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `No ${type} folders configured.`, + }); + } + + for (const folder of folders) { + try { + const files = await searchForFiles(folder, WALLPAPERS_TYPE_TO_EXTS[type]); + files.forEach(async (file) => { + wallpapers.push({ + id: path.basename(file.path), + name: file.name, + path: file.path, + thumbnailPath: "", // Generated later + fullSizePath: `${type}://${file.path}`, + dateAdded: Date.now(), + tags: [type], + type: type, + }); + }); + } catch (error) { + console.error(`Failed to search ${type} files in ${folder}:`, error); + } + } + + return wallpapers; +}; + +const searchForFiles = async (folderPath: string, fileTypes: string[]) => { + const files: { name: string; path: string }[] = []; + const dirents = await fs.readdir(folderPath, { withFileTypes: true }); + + for (const dirent of dirents) { + const dirPath = path.join(folderPath, dirent.name); + const ext = path.extname(dirent.name).toLowerCase(); + + if (dirent.isDirectory()) { + const subdirectoryFiles = await searchForFiles(dirPath, fileTypes); + files.push(...subdirectoryFiles); + } else if (dirent.isFile() && fileTypes.includes(ext)) { + const name = path.basename(dirent.name, path.extname(dirent.name)); + files.push({ name, path: dirPath }); + } + } + + return files; +}; + +const getWallpaperEngineWallpapers = async () => { + const wallpapers: WallpaperEngineWallpaper[] = []; + const folders = await caller.settings.get({ + key: "wallpaperSources.wallpaperEngineFolders", + }); + if (!folders || !Array.isArray(folders) || folders.length === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No Wallpaper Engine folders configured.", + }); + } + + for (const folder of folders) { + try { + const subdirectories = await fs.readdir(folder, { withFileTypes: true }); + + for (const dirent of subdirectories) { + if (dirent.isDirectory()) { + const subdirectoryPath = path.join(folder, dirent.name); + const jsonFilePath = path.join(subdirectoryPath, "project.json"); + + try { + const jsonFileExists = await fs + .access(jsonFilePath) + .then(() => true) + .catch(() => false); + + if (jsonFileExists) { + const jsonData = await fs.readFile(jsonFilePath, "utf-8"); + const parsedData = JSON.parse(jsonData); + + if (parsedData.preview) { + const stat = await fs.stat(jsonFilePath); + const tags = [ + ...(parsedData.tags || []), + parsedData.type, + parsedData.contentrating, + ].filter(Boolean); + + wallpapers.push({ + type: "wallpaper-engine", + id: path.basename(subdirectoryPath), + name: parsedData.title, + path: subdirectoryPath, + thumbnailPath: "", // Generated later + fullSizePath: `image://${path.join(subdirectoryPath, parsedData.preview)}`, + dateAdded: stat.mtime.getTime(), + workshopId: dirent.name, + sceneFile: parsedData.file, + sceneType: parsedData.type, + tags, + }); + } + } + } catch (jsonError) { + console.error(`Failed to read or parse ${jsonFilePath}:`, jsonError); + } + } + } + } catch (error) { + console.error(`Error reading wallpaper engine directory ${folder}:`, error); + } + } + + return wallpapers; +}; + +const filterWallpapers = ( + wallpapers: LibraryWallpaper[], + query?: string, + tags?: string[], + matchAll?: boolean +) => { + return wallpapers.filter((wallpaper) => { + const matchesQuery = query ? wallpaper.name.toLowerCase().includes(query.toLowerCase()) : true; + + const matchesTags = + tags && tags.length > 0 + ? matchAll + ? tags.every((tag) => wallpaper.tags.includes(tag)) + : tags.some((tag) => wallpaper.tags.includes(tag)) + : true; + + return matchesQuery && matchesTags; + }); +}; + +const sortWallpapers = (wallpapers: LibraryWallpaper[], sorting: string) => { + if (sorting === "name") { + return wallpapers.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + } else if (sorting === "date_added") { + return wallpapers.sort((a, b) => b.dateAdded - a.dateAdded); + } else { + return wallpapers.sort((a, b) => Number(a.id) - Number(b.id)); + } +}; + +const paginateData = ( + data: T[], + page: number, + itemsPerPage: number +): WallpaperData => { + const currentPage = page; + const totalItems = data.length; + const totalPages = Math.ceil(totalItems / itemsPerPage); + const startIndex = (page - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedData = data.slice(startIndex, endIndex); + + return { + data: paginatedData, + currentPage, + prevPage: currentPage > 1 ? currentPage - 1 : null, + nextPage: currentPage < totalPages ? currentPage + 1 : null, + totalItems, + totalPages, + }; +}; + +export { + getImageAndVideoWallpapers, + getWallpaperEngineWallpapers, + filterWallpapers, + sortWallpapers, + paginateData, +}; diff --git a/src/electron/main/trpc/routes/wallpaper/set.ts b/src/electron/main/trpc/routes/wallpaper/set.ts new file mode 100644 index 0000000..8d2af7e --- /dev/null +++ b/src/electron/main/trpc/routes/wallpaper/set.ts @@ -0,0 +1,283 @@ +import path from "path"; +import { promises as fs } from "fs"; +import { TRPCError } from "@trpc/server"; +import { execute, killProcess, santize, renderString } from "@electron/main/lib/index.js"; +import { caller } from "@electron/main/trpc/routes/index.js"; +import { type SettingsSchema } from "@electron/main/trpc/routes/settings/index.js"; +import { SetWallpaperInput } from "./types.js"; + +const CAGE_INIT_TIME = 5; +const CAGE_SCREENSHOT_PATH = "/tmp/walltone-wallpaper-screenshot.png"; + +const populateMonitorsIfEmpty = async (input: SetWallpaperInput) => { + if (input.monitors.length === 0) + input.monitors = (await caller.monitor.search()).map((monitor) => ({ + id: monitor.id, + })); +}; + +const killWallpaperProcesses = async () => { + await killProcess("swaybg"); + await killProcess("mpvpaper"); + await killProcess("linux-wallpaperengine"); +}; + +const saveLastWallpaper = async (input: SetWallpaperInput) => { + await caller.settings.set({ + key: "internal.lastWallpaper", + value: Object.fromEntries(input.monitors.map((monitor) => [monitor.id, input])), + }); +}; + +const screenshotWallpaper = async (input: SetWallpaperInput) => { + if (input.type === "image") { + await copyWallpaperToDestinations(input.id, input.name, input.path); + } else if (input.type === "video") { + await screenshotWallpaperInCage(["mpv", "panscan=1.0", input.path]); + await copyWallpaperToDestinations(input.id, input.name, CAGE_SCREENSHOT_PATH); + } else if (input.type === "wallpaper-engine") { + const assetsPath = await caller.settings.get({ + key: "wallpaperSources.wallpaperEngineAssetsFolder", + }); + + if (typeof assetsPath !== "string" || !assetsPath) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Wallpaper Engine assets folder is not set.", + }); + } + + await screenshotWallpaperInCage([ + "linux-wallpaperengine", + "--silent", + "--fps", + "1", + "--assets-dir", + assetsPath, + "--window", + "0x0x1280x720", + input.path, + ]); + await copyWallpaperToDestinations(input.id, input.name, CAGE_SCREENSHOT_PATH); + } +}; + +const setWallpaper = async (input: SetWallpaperInput, detached: boolean) => { + if (input.type === "image") { + await setImageWallpaper(input.path, input.monitors, detached); + } else if (input.type === "video") { + await setVideoWallpaper(input.path, input.monitors, input.videoOptions, detached); + } else if (input.type === "wallpaper-engine") { + const assetsPath = await caller.settings.get({ + key: "wallpaperSources.wallpaperEngineAssetsFolder", + }); + + if (typeof assetsPath !== "string" || !assetsPath) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Wallpaper Engine assets folder is not set.", + }); + } + + await setWallpaperEngineWallpaper( + assetsPath, + input.path, + input.monitors, + input.wallpaperEngineOptions, + detached + ); + } +}; + +const setImageWallpaper = async ( + imagePath: string, + monitors: { id: string; scalingMethod?: string }[], + detached: boolean +) => { + const args: string[] = []; + + monitors.forEach((monitor) => { + args.push( + "--output", + monitor.id, + "--image", + imagePath, + "--mode", + monitor.scalingMethod || "crop" + ); + }); + + await execute({ command: "swaybg", args, detached, logStdout: false }); +}; + +const setVideoWallpaper = async ( + videoPath: string, + monitors: { id: string; scalingMethod?: string }[], + options?: { + mute?: boolean; + }, + detached?: boolean +) => { + // Build mpv options array + const mpvOptions: string[] = ["loop"]; + + // Add video-specific options + if (options?.mute) { + mpvOptions.push("no-audio"); + } + + // Add scaling options for each monitor + monitors.forEach((monitor) => { + if (monitor.scalingMethod) { + switch (monitor.scalingMethod) { + case "fill": + mpvOptions.push("panscan=1.0"); + break; + case "fit": + mpvOptions.push("panscan=0.0"); + break; + case "stretch": + mpvOptions.push("keepaspect=no"); + break; + case "center": + case "tile": + // These don't have specific mpv options + break; + } + } + }); + + const args = []; + + // Add mpv options if any exist + if (mpvOptions.length > 0) { + args.push("-o", mpvOptions.join(" ")); + } + + // Add monitor names (use ALL if multiple monitors, otherwise specific monitor) + if (monitors.length === 1) { + args.push(monitors[0].id); + } else { + args.push("ALL"); + } + + // Add video path + args.push(videoPath); + + await execute({ command: "mpvpaper", args, detached, logStdout: false }); +}; + +const setWallpaperEngineWallpaper = async ( + assetsPath: string, + wallpaperPath: string, + monitors: { id: string; scalingMethod?: string }[], + options?: { + silent?: boolean; + volume?: number; + noAutomute?: boolean; + noAudioProcessing?: boolean; + fps?: number; + clamping?: string; + disableMouse?: boolean; + disableParallax?: boolean; + noFullscreenPause?: boolean; + }, + detached?: boolean +) => { + const args = [ + ...monitors.flatMap((monitor) => [ + "--screen-root", + monitor.id, + "--bg", + wallpaperPath, + "--scaling", + monitor.scalingMethod || "default", + ]), + "--assets-dir", + assetsPath, + ]; + + if (options?.silent) { + args.push("--silent"); + } + + if (options?.volume !== undefined) { + args.push("--volume", options.volume.toString()); + } + + if (options?.noAutomute) { + args.push("--noautomute"); + } + + if (options?.noAudioProcessing) { + args.push("--no-audio-processing"); + } + + if (options?.fps !== undefined) { + args.push("--fps", options.fps.toString()); + } + + if (options?.clamping) { + args.push("--clamping", options.clamping); + } + + if (options?.disableMouse) { + args.push("--disable-mouse"); + } + + if (options?.disableParallax) { + args.push("--disable-parallax"); + } + + if (options?.noFullscreenPause) { + args.push("--no-fullscreen-pause"); + } + + await execute({ + command: "linux-wallpaperengine", + args, + detached, + logStdout: false, + }); +}; + +const screenshotWallpaperInCage = async (cmd: string[]) => { + const args = [ + "--", + "sh", + "-c", + `${cmd.join(" ")} & pid=$!; sleep ${CAGE_INIT_TIME} && grim -g "0,0 1280x720" ${CAGE_SCREENSHOT_PATH} && kill $pid`, + ]; + await execute({ command: "cage", args, env: { WLR_BACKENDS: "headless" } }); +}; + +const copyWallpaperToDestinations = async ( + wallpaperId: string, + wallpaperName: string, + wallpaperPath: string +) => { + const wallpaperDestinations = (await caller.settings.get({ + key: "themeOutput.wallpaperCopyDestinations", + })) as SettingsSchema["themeOutput"]["wallpaperCopyDestinations"]; + + await Promise.all( + wallpaperDestinations.map(async (destination) => { + destination = await renderString(destination, { + wallpaper: { + id: santize(wallpaperId), + name: santize(wallpaperName), + }, + }); + await fs.mkdir(path.dirname(destination), { recursive: true }); + await fs.copyFile(wallpaperPath, destination); + }) + ); +}; + +export { + populateMonitorsIfEmpty, + saveLastWallpaper, + killWallpaperProcesses, + screenshotWallpaper, + setWallpaper, +}; diff --git a/src/electron/main/trpc/routes/wallpaper/thumbnail.ts b/src/electron/main/trpc/routes/wallpaper/thumbnail.ts new file mode 100644 index 0000000..76b0982 --- /dev/null +++ b/src/electron/main/trpc/routes/wallpaper/thumbnail.ts @@ -0,0 +1,98 @@ +import { parentPort } from "worker_threads"; +import os from "os"; +import path from "path"; +import crypto from "crypto"; +import { promises as fs } from "fs"; +import sharp from "sharp"; +import { execute } from "@electron/main/lib/index.js"; +import { LibraryWallpaper, WallpaperData } from "./types.js"; + +const THUMB_CACHE_DIR = path.join(os.homedir(), ".cache", "walltone", "thumbnails"); +const THUMBNAIL_WIDTH = 640; + +const getFileHash = async (filePath: string): Promise => { + const stat = await fs.stat(filePath); + return crypto.createHash("sha1").update(`${stat.mtimeMs}-${stat.size}`).digest("hex"); +}; + +const getOrCreateThumbnail = async (wallpaper: LibraryWallpaper) => { + await fs.mkdir(THUMB_CACHE_DIR, { recursive: true }); + const fullSizePath = wallpaper.fullSizePath.replace("image://", "").replace("video://", ""); + + const hash = await getFileHash(fullSizePath); + const thumbPath = path.join(THUMB_CACHE_DIR, `${hash}.jpeg`); + + try { + await fs.access(thumbPath); + console.log("Cache hit: ", wallpaper.fullSizePath); + } catch { + console.log("Cache miss: ", wallpaper.fullSizePath); + + if (wallpaper.type === "image" || wallpaper.type === "wallpaper-engine") + await sharp(fullSizePath) + .rotate() + .resize(THUMBNAIL_WIDTH, null, { + withoutEnlargement: true, + }) + .jpeg({ + quality: 80, + mozjpeg: true, + }) + .toFile(thumbPath); + else if (wallpaper.type === "video") + await execute({ + ignoreErrors: true, + logStdout: false, + logStderr: false, + command: "ffmpeg", + args: [ + "-y", + "-ss", + "1", // seek to 1s + "-i", + fullSizePath, + "-frames:v", + "1", // grab one frame + "-vf", + `scale='if(gt(iw,${THUMBNAIL_WIDTH}),${THUMBNAIL_WIDTH},iw)':-1`, + "-q:v", + "1", // quality (1 = best, 31 = worst) + thumbPath, // save directly to file + ], + }); + } + + return `image://${thumbPath}`; +}; + +const resolveThumbnailPath = async (wallpaper: LibraryWallpaper) => { + if (wallpaper.type === "image") { + return await getOrCreateThumbnail(wallpaper); + } else if (wallpaper.type === "video") { + return await getOrCreateThumbnail(wallpaper); + } else if (wallpaper.type === "wallpaper-engine") { + // If the preview is a gif, just use it directly + if (path.extname(wallpaper.thumbnailPath).toLowerCase() === ".gif") { + return wallpaper.thumbnailPath; + } + // Otherwise, generate a thumbnail + return await getOrCreateThumbnail(wallpaper); + } + + return ""; +}; + +parentPort?.on("message", async ({ data }: { data: WallpaperData }) => { + try { + data.data = await Promise.all( + data.data.map(async (wallpaper) => { + wallpaper.thumbnailPath = await resolveThumbnailPath(wallpaper); + return wallpaper; + }) + ); + + parentPort?.postMessage({ status: "success", data }); + } catch (error) { + parentPort?.postMessage({ status: "error", error: (error as Error).message }); + } +}); diff --git a/src/electron/main/trpc/routes/wallpaper/types.ts b/src/electron/main/trpc/routes/wallpaper/types.ts new file mode 100644 index 0000000..c5014b2 --- /dev/null +++ b/src/electron/main/trpc/routes/wallpaper/types.ts @@ -0,0 +1,52 @@ +import z from "zod"; +import { monitorsSchema, setWallpaperSchema } from "./index.js"; + +export interface BaseWallpaper { + type: "image" | "video" | "wallpaper-engine"; + id: string; + name: string; + thumbnailPath: string; + fullSizePath: string; +} + +export interface ApiWallpaper extends BaseWallpaper { + downloadUrl: string; +} + +interface ImageWallpaper extends BaseWallpaper { + type: "image"; + path: string; + dateAdded: number; + tags: string[]; +} + +interface VideoWallpaper extends BaseWallpaper { + type: "video"; + path: string; + dateAdded: number; + tags: string[]; +} + +export interface WallpaperEngineWallpaper extends BaseWallpaper { + type: "wallpaper-engine"; + path: string; + dateAdded: number; + tags: string[]; + workshopId: string; + sceneFile: string; + sceneType: string; +} + +export type LibraryWallpaper = ImageWallpaper | VideoWallpaper | WallpaperEngineWallpaper; + +export interface WallpaperData { + data: T[]; + currentPage: number; + prevPage: number | null; + nextPage: number | null; + totalItems: number; + totalPages: number; +} + +export type SetWallpaperInput = z.infer; +export type MonitorConfig = z.infer; diff --git a/src/renderer/components/wallpaper-dialog/apply-dialog.tsx b/src/renderer/components/wallpaper-dialog/apply-dialog.tsx index 5b607db..517a9e2 100644 --- a/src/renderer/components/wallpaper-dialog/apply-dialog.tsx +++ b/src/renderer/components/wallpaper-dialog/apply-dialog.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Check, Monitor as MonitorIcon, Loader2 } from "lucide-react"; import { type Monitor } from "@electron/main/trpc/routes/monitor/index.js"; -import { type BaseWallpaper } from "@electron/main/trpc/routes/wallpaper/index.js"; +import { type BaseWallpaper } from "@electron/main/trpc/routes/wallpaper/types.js"; import { DialogContent, DialogDescription, @@ -68,9 +68,9 @@ const ApplyWallpaperDialog = ({ const handleApply = React.useCallback(() => { if (onApply) { - const monitorConfigs = Array.from(selectedMonitors).map((name) => ({ - name, - scalingMethod: monitorScalingMethods[name] || scalingOptions?.[0]?.key || "fill", + const monitorConfigs = Array.from(selectedMonitors).map((id) => ({ + id, + scalingMethod: monitorScalingMethods[id] || scalingOptions?.[0]?.key || "fill", })); applyMutation.mutate({ onApply, @@ -211,13 +211,13 @@ const WallpaperPreview = ({ wallpaper }: { wallpaper: BaseWallpaper }) => { {wallpaper.type !== "video" ? ( {wallpaper.name} ) : (