From ed9531c9973358d9896250bdc7c02878634ebba8 Mon Sep 17 00:00:00 2001 From: BurntToasters <61037367+BurntToasters@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:43:16 -0700 Subject: [PATCH 1/2] v & pkg bump --- com.burnttoasters.rosi.metainfo.xml | 1 + package-lock.json | 776 +++++++--------------------- package.json | 2 +- src/renderer/splash.html | 2 +- 4 files changed, 176 insertions(+), 605 deletions(-) diff --git a/com.burnttoasters.rosi.metainfo.xml b/com.burnttoasters.rosi.metainfo.xml index 4b70777..de3e019 100644 --- a/com.burnttoasters.rosi.metainfo.xml +++ b/com.burnttoasters.rosi.metainfo.xml @@ -34,6 +34,7 @@ + diff --git a/package-lock.json b/package-lock.json index 5190db6..c26cee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rosi", - "version": "4.2.1", + "version": "4.3.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rosi", - "version": "4.2.1", + "version": "4.3.0-beta.1", "license": "MPL-2.0", "dependencies": { "electron-log": "^5.3.4", @@ -184,9 +184,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -208,9 +208,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", "dev": true, "funding": [ { @@ -225,7 +225,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -259,9 +259,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -649,9 +649,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1493,17 +1493,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1516,7 +1516,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1532,16 +1532,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -1557,14 +1557,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -1579,14 +1579,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1597,9 +1597,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -1614,15 +1614,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1639,9 +1639,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -1653,16 +1653,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1681,16 +1681,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1705,13 +1705,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1933,33 +1933,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -2526,39 +2507,6 @@ "node": ">=8" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2574,72 +2522,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -3110,9 +2992,9 @@ } }, "node_modules/electron": { - "version": "43.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.1.tgz", - "integrity": "sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA==", + "version": "43.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.2.0.tgz", + "integrity": "sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==", "dev": true, "license": "MIT", "dependencies": { @@ -3342,19 +3224,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -3648,13 +3517,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -3694,9 +3556,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -3813,9 +3675,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", "dev": true, "license": "ISC" }, @@ -3892,19 +3754,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4338,19 +4187,13 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-glob": { @@ -4759,9 +4602,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -4775,23 +4618,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -4810,9 +4653,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -4831,9 +4674,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -4852,9 +4695,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -4873,9 +4716,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -4894,9 +4737,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -4918,9 +4761,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -4942,9 +4785,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -4966,9 +4809,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -4990,9 +4833,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -5011,9 +4854,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -5032,14 +4875,13 @@ } }, "node_modules/lint-staged": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", - "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.1.1.tgz", + "integrity": "sha512-FnHWpSe5cPRtrDG+soOuNdBxb4XQb2gN5EqpEWKdweyqyOfpl4QSjbrz3ilcIf0WXmkiNQGZZRQ23R5YtB3TEw==", "dev": true, "license": "MIT", "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, @@ -5056,23 +4898,6 @@ "yaml": "^2.9.0" } }, - "node_modules/listr2": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", - "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, - "engines": { - "node": ">=22.13.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5109,99 +4934,6 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -5330,19 +5062,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -5673,22 +5392,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5920,9 +5623,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "dev": true, "funding": [ { @@ -5940,7 +5643,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5989,9 +5692,9 @@ } }, "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -6277,36 +5980,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -6317,13 +5990,6 @@ "node": ">= 4" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -6518,36 +6184,6 @@ "node": ">=10" } }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", @@ -6709,36 +6345,31 @@ } }, "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/sumchecker": { @@ -6788,9 +6419,9 @@ "license": "MIT" }, "node_modules/tar": { - "version": "7.5.20", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", - "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "version": "7.5.21", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz", + "integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7057,16 +6688,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", - "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.64.0", - "@typescript-eslint/parser": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0" + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7450,36 +7081,23 @@ } }, "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=20" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7577,54 +7195,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8b7928b..d60a99b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rosi", - "version": "4.2.1", + "version": "4.3.0-beta.1", "private": true, "description": "Electron GUI for yt-dlp", "desktopName": "com.burnttoasters.rosi.desktop", diff --git a/src/renderer/splash.html b/src/renderer/splash.html index fa3d728..5d5cb29 100644 --- a/src/renderer/splash.html +++ b/src/renderer/splash.html @@ -327,7 +327,7 @@

ROSI

Loading
-
v4.2.1
+
v4.3.0-beta.1
From 348765f2925253a9adc2c626f995128d8408758a Mon Sep 17 00:00:00 2001 From: BurntToasters <61037367+BurntToasters@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:52:11 -0700 Subject: [PATCH 2/2] B1 --- CHANGELOG.md | 31 ++- src/main/appMenu.ts | 121 +++++++++ src/main/constants.ts | 2 +- src/main/download/commandBuilders.ts | 23 +- src/main/download/jobProgressReporter.ts | 221 ++++++++++++++++ src/main/downloader.ts | 103 +++++++- src/main/main.ts | 20 +- src/main/preload.ts | 12 + src/main/settings.ts | 5 + src/main/taskbarProgress.ts | 23 ++ src/renderer/index.html | 6 + src/renderer/modules/downloads.ts | 1 + src/renderer/renderer-globals.d.ts | 14 + src/renderer/rosiEngine.ts | 146 ++++++++--- src/tests/appMenu.test.ts | 41 +++ src/tests/commandBuilders.test.ts | 104 +++++--- src/tests/downloadJobProgress.test.ts | 137 ++++++++++ src/tests/ipc.contract.test.ts | 2 + src/tests/preload.test.ts | 9 + src/tests/rosiEngine.dom.test.ts | 5 +- src/tests/settingsMigration.test.ts | 1 + src/tests/taskbarProgress.test.ts | 40 +++ src/types.ts | 25 ++ src/utils/downloadJobProgress.ts | 318 +++++++++++++++++++++++ src/utils/ipcValidation.ts | 2 + 25 files changed, 1311 insertions(+), 101 deletions(-) create mode 100644 src/main/appMenu.ts create mode 100644 src/main/download/jobProgressReporter.ts create mode 100644 src/main/taskbarProgress.ts create mode 100644 src/tests/appMenu.test.ts create mode 100644 src/tests/downloadJobProgress.test.ts create mode 100644 src/tests/taskbarProgress.test.ts create mode 100644 src/utils/downloadJobProgress.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c09760..11cfdca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,13 @@ - +> [!NOTE] +> 🅱️ This is a Beta build. # ⬇️ Downloads -| Windows | macOS | Linux | -| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-arm64.AppImage) | -|
| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-arm64.deb) | -| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.2.1/ROSI-Linux-aarch64.rpm) | +| Windows | macOS | Linux | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-arm64.AppImage) | +|
| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-arm64.deb) | +| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.3.0-beta.1/ROSI-Linux-aarch64.rpm) | > [!IMPORTANT] > The `.sig` files in this repo are NOT normal GPG signatures — they are for ROSI's built-in updater to verify the integrity of updates before downloading and installing. @@ -18,10 +18,22 @@ --- +## Changes in `v4.3.0-beta.1:` + +- **macOS:** Added a native application menu (About, Settings, Check for Updates, Edit, View, Window, Help) with actions wired into the existing UI. +- **Progress:** yt-dlp and FFmpeg now report structured progress for more accurate in-app percentages, including merge and conversion phases. +- **Taskbar / Dock:** Download and conversion progress appears in the Windows taskbar and macOS Dock; queue runs show an overall percent across items. +- **Settings:** New **Taskbar / Dock progress** toggle (settings schema v6). + ## Changes in `v4.2.1:` - **PKG:** Updated packages. +### FULL CHANGELOG: + +
+ ℹ️ Click here to see the full change-log for v4! + ## Changes in `v4.2.0:` - **NEW - Windows code signing:** WOO HOO!! Windows Codesigning is here! @@ -53,11 +65,6 @@ - **Testing:** Cleaned up ESLint configuration to suppress `no-unsafe-*` noise in test files while keeping full strictness on production code. - **PKG:** Updated packages. -### FULL CHANGELOG: - -
- ℹ️ Click here to see the full change-log for v4! - ## Changes in `v4.1.0:` - **NEW - Preview:** Added video preview before downloading so ROSI can show the title, uploader, duration, thumbnail, playlist info, and other basic metadata before saving. diff --git a/src/main/appMenu.ts b/src/main/appMenu.ts new file mode 100644 index 0000000..b32a31f --- /dev/null +++ b/src/main/appMenu.ts @@ -0,0 +1,121 @@ +import { app, Menu, shell, type BrowserWindow } from 'electron'; +import type { MenuAction } from '../types'; + +const DOCS_URL = 'https://github.com/BurntToasters/ROSI#readme'; +const ISSUES_URL = 'https://github.com/BurntToasters/ROSI/issues'; + +export interface AppMenuOptions { + getMainWindow: () => BrowserWindow | null; + isMsStore: boolean; +} + +function sendMenuAction(win: BrowserWindow | null, action: MenuAction): void { + if (!win || win.isDestroyed()) return; + win.webContents.send('menu-action', action); +} + +export function configureAboutPanel(): void { + if (process.platform !== 'darwin') return; + app.setAboutPanelOptions({ + applicationName: 'ROSI', + applicationVersion: app.getVersion(), + version: app.getVersion(), + copyright: 'Copyright © BurntToasters', + }); +} + +export function buildDarwinApplicationMenu(options: AppMenuOptions): Menu { + const { getMainWindow, isMsStore } = options; + + const rosieMenuItems: Electron.MenuItemConstructorOptions[] = [ + { role: 'about' }, + { type: 'separator' }, + { + label: 'Settings…', + accelerator: 'Cmd+,', + click: () => sendMenuAction(getMainWindow(), 'open-settings'), + }, + ]; + + if (!isMsStore) { + rosieMenuItems.push({ + label: 'Check for Updates…', + click: () => sendMenuAction(getMainWindow(), 'check-for-updates'), + }); + } + + rosieMenuItems.push( + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ); + + const template: Electron.MenuItemConstructorOptions[] = [ + { + label: app.name, + submenu: rosieMenuItems, + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' }, + ], + }, + { + label: 'View', + submenu: [ + { + label: 'Toggle Sidebar', + click: () => sendMenuAction(getMainWindow(), 'toggle-sidebar'), + }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { + label: 'Window', + submenu: [{ role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }], + }, + { + label: 'Help', + submenu: [ + { + label: 'Documentation', + click: () => { + void shell.openExternal(DOCS_URL); + }, + }, + { + label: 'Report an Issue', + click: () => { + void shell.openExternal(ISSUES_URL); + }, + }, + { type: 'separator' }, + { + label: 'View Licenses', + click: () => sendMenuAction(getMainWindow(), 'show-licenses'), + }, + ], + }, + ]; + + return Menu.buildFromTemplate(template); +} + +export function installDarwinApplicationMenu(options: AppMenuOptions): void { + if (process.platform !== 'darwin') return; + configureAboutPanel(); + Menu.setApplicationMenu(buildDarwinApplicationMenu(options)); +} diff --git a/src/main/constants.ts b/src/main/constants.ts index 950b775..ddff654 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -27,4 +27,4 @@ export const ALLOWED_BROWSERS = new Set([ export const MAX_QUEUE_SIZE = 500; export const MAX_FORMAT_COUNTS = 10_000; export const MAX_SETTINGS_IMPORT_BYTES = 1_048_576; -export const CURRENT_SETTINGS_VERSION = 5; +export const CURRENT_SETTINGS_VERSION = 6; diff --git a/src/main/download/commandBuilders.ts b/src/main/download/commandBuilders.ts index cbe6088..6b8c9bc 100644 --- a/src/main/download/commandBuilders.ts +++ b/src/main/download/commandBuilders.ts @@ -9,6 +9,7 @@ import { MAX_ERROR_BUFFER, SUBTITLE_LANGS_PATTERN, } from '../constants'; +import { parseFfmpegDurationFromProbe } from '../../utils/downloadJobProgress'; const VALID_FORMAT_ID = FORMAT_ID_PATTERN; const CODEC_PROBE_TIMEOUT_MS = 30_000; @@ -17,6 +18,7 @@ export { ALLOWED_BROWSERS }; export interface SourceCodecs { video?: string; audio?: string; + durationSeconds?: number | null; } export function probeMediaCodecs(ffmpegCommand: string, inputPath: string): Promise { @@ -37,6 +39,7 @@ export function probeMediaCodecs(ffmpegCommand: string, inputPath: string): Prom const audioMatch = stderr.match(/Stream #\d+:\d+.*: Audio: (\w+)/); if (videoMatch?.[1]) codecs.video = videoMatch[1]; if (audioMatch?.[1]) codecs.audio = audioMatch[1]; + codecs.durationSeconds = parseFfmpegDurationFromProbe(stderr); return codecs; }; @@ -108,7 +111,18 @@ export function buildFfmpegArgs( const canCopyAudio = (targetFormat === 'm4a' && (srcAudio === 'aac' || srcAudio === 'mp4a')) || (targetFormat === 'mp3' && srcAudio === 'mp3'); - return ['-i', inputPath, '-vn', '-c:a', canCopyAudio ? 'copy' : targetCodec, '-y', outputPath]; + return [ + '-progress', + 'pipe:1', + '-nostats', + '-i', + inputPath, + '-vn', + '-c:a', + canCopyAudio ? 'copy' : targetCodec, + '-y', + outputPath, + ]; } const srcVideo = srcCodecs?.video?.toLowerCase(); @@ -119,6 +133,9 @@ export function buildFfmpegArgs( srcCodecs && srcAudio && CONTAINER_COMPATIBLE_AUDIO.has(srcAudio) ? 'copy' : 'aac'; return [ + '-progress', + 'pipe:1', + '-nostats', '-i', inputPath, '-c:v', @@ -173,6 +190,10 @@ export function buildYtdlpArgs({ '--progress', '--progress-delta', '1', + '--progress-template', + 'download:%(progress)j', + '--progress-template', + 'postprocess:%(progress)j', '-f', settings.bestQuality ? 'bestvideo+bestaudio/best' : 'best[ext=mp4]/best[ext=webm]/best', '--', diff --git a/src/main/download/jobProgressReporter.ts b/src/main/download/jobProgressReporter.ts new file mode 100644 index 0000000..b42551b --- /dev/null +++ b/src/main/download/jobProgressReporter.ts @@ -0,0 +1,221 @@ +import type { BrowserWindow } from 'electron'; +import { applyTaskbarProgress } from '../taskbarProgress'; +import type { + DownloadJobPhase, + DownloadSession, + JobProgressEvent, + QueueDownloadProgress, +} from '../../types'; +import { + buildJobProgressEvent, + formatYtdlpProgressSummary, + parseYtdlpProgress, + tryParseYtdlpProgressJson, + ytdlpJsonToPhasePercent, + type JobProgressPlan, + type YtdlpProgressJson, +} from '../../utils/downloadJobProgress'; + +const EMIT_INTERVAL_MS = 200; + +export class JobProgressReporter { + private lastEmitAt = 0; + private lastOverall = -1; + private plan: JobProgressPlan; + private queue: QueueDownloadProgress | null; + private mainWindow: BrowserWindow | null; + private showTaskbarProgress: boolean; + + constructor( + mainWindow: BrowserWindow | null, + plan: JobProgressPlan, + queue: QueueDownloadProgress | null, + showTaskbarProgress: boolean + ) { + this.mainWindow = mainWindow; + this.plan = plan; + this.queue = queue; + this.showTaskbarProgress = showTaskbarProgress; + } + + clearTaskbar(): void { + if (!this.showTaskbarProgress) return; + applyTaskbarProgress(this.mainWindow, 'none'); + } + + emitIdle(session: DownloadSession): void { + this.emit(session, { + phase: 'idle', + phasePercent: 0, + overallPercent: 0, + status: 'Ready', + indeterminate: false, + }); + this.clearTaskbar(); + } + + handleYtdlpOutputLine(session: DownloadSession, line: string): boolean { + const trimmed = line.trim(); + if (!trimmed) return false; + + const json = tryParseYtdlpProgressJson(trimmed); + if (json) { + let phase: DownloadJobPhase = session.ytdlpPostprocess ? 'merge' : 'download'; + if (!session.ytdlpPostprocess && json.status === 'finished') { + session.ytdlpDownloadFinished = true; + phase = 'download'; + } else if (session.ytdlpDownloadFinished && !session.ytdlpPostprocess) { + session.ytdlpPostprocess = true; + phase = 'merge'; + } + session.jobPhase = phase; + + const phasePercent = + json.status === 'finished' && phase === 'download' ? 100 : ytdlpJsonToPhasePercent(json); + const { status, details } = formatYtdlpProgressSummary(json, phasePercent); + const label = phase === 'merge' ? 'Merging...' : status; + this.emit( + session, + buildJobProgressEvent( + phase, + phasePercent, + this.plan, + this.queue, + label, + details, + !Number.isFinite(phasePercent) + ) + ); + return true; + } + + const legacy = parseYtdlpProgress(trimmed); + if (legacy) { + session.jobPhase = 'download'; + let detailsText = ''; + if (legacy.speed && legacy.eta) { + detailsText = `${legacy.totalSize} • ${legacy.speed} • ETA: ${legacy.eta}`; + } else if (legacy.totalSize) { + detailsText = `Size: ${legacy.totalSize}`; + } + this.emit( + session, + buildJobProgressEvent( + 'download', + legacy.percent, + this.plan, + this.queue, + 'Downloading...', + detailsText + ) + ); + return true; + } + + if (trimmed.includes('Merging formats') || trimmed.includes('[Merger]')) { + session.jobPhase = 'merge'; + session.ytdlpPostprocess = true; + this.emit( + session, + buildJobProgressEvent( + 'merge', + Number.NaN, + this.plan, + this.queue, + 'Merging video and audio...', + undefined, + true + ) + ); + return false; + } + + return false; + } + + emitConvertProgress( + session: DownloadSession, + phasePercent: number | null, + status = 'Converting...', + details?: string + ): void { + session.jobPhase = 'convert'; + const percent = phasePercent ?? Number.NaN; + this.emit( + session, + buildJobProgressEvent( + 'convert', + percent, + this.plan, + this.queue, + status, + details, + phasePercent === null || !Number.isFinite(percent) + ) + ); + } + + emitPhaseIndeterminate(session: DownloadSession, phase: DownloadJobPhase, status: string): void { + session.jobPhase = phase; + this.emit( + session, + buildJobProgressEvent(phase, Number.NaN, this.plan, this.queue, status, undefined, true) + ); + } + + emitDownloadComplete(session: DownloadSession): void { + const phase: DownloadJobPhase = this.plan.expectConvert ? 'convert' : 'download'; + this.emit( + session, + buildJobProgressEvent( + phase, + 100, + this.plan, + this.queue, + 'Download complete', + undefined, + false + ) + ); + } + + private emit(session: DownloadSession, event: JobProgressEvent): void { + const now = Date.now(); + const overallRounded = Math.round(event.overallPercent * 10) / 10; + if ( + now - this.lastEmitAt < EMIT_INTERVAL_MS && + overallRounded === this.lastOverall && + !event.indeterminate + ) { + return; + } + this.lastEmitAt = now; + this.lastOverall = overallRounded; + + if (!session.sender || session.sender.isDestroyed()) return; + try { + session.sender.send('job-progress', event); + } catch { + // ignore destroyed sender + } + + if (!this.showTaskbarProgress) return; + if (event.phase === 'idle') { + applyTaskbarProgress(this.mainWindow, 'none'); + return; + } + if (event.indeterminate) { + applyTaskbarProgress(this.mainWindow, 'indeterminate'); + return; + } + applyTaskbarProgress(this.mainWindow, event.overallPercent / 100); + } +} + +export function summarizeYtdlpJsonForConsole(data: YtdlpProgressJson): string | null { + if (data.status !== 'downloading' && data.status !== 'finished') return null; + const phasePercent = ytdlpJsonToPhasePercent(data); + const { details } = formatYtdlpProgressSummary(data, phasePercent); + const prefix = data.status === 'finished' ? '[download] Complete' : '[download]'; + return `${prefix} ${details}`; +} diff --git a/src/main/downloader.ts b/src/main/downloader.ts index f356ad5..3aca9db 100644 --- a/src/main/downloader.ts +++ b/src/main/downloader.ts @@ -18,6 +18,12 @@ import { resolveVideoEncoder, probeMediaCodecs, } from './download/commandBuilders'; +import { JobProgressReporter, summarizeYtdlpJsonForConsole } from './download/jobProgressReporter'; +import { + createFfmpegProgressParseState, + parseFfmpegProgressChunk, + resolveJobProgressPlanFromSettings, +} from '../utils/downloadJobProgress'; import { isSafeHttpUrl } from '../utils/validation'; import { isMac } from './platform'; import { @@ -41,12 +47,14 @@ import type { DownloadOutcome, FormatsProcess, Settings, + QueueDownloadProgress, } from '../types'; let activeDownloadSession: DownloadSession | null = null; let downloadSessionOwner: DownloadSessionOwner | null = null; let downloadSessionCounter = 0; let formatsProcess: FormatsProcess | null = null; +let activeProgressReporter: JobProgressReporter | null = null; export function getDownloadSessionOwner(): DownloadSessionOwner | null { return downloadSessionOwner; @@ -80,6 +88,40 @@ function sendProgress(session: DownloadSession | null, message: string) { safeSend(session.sender, 'progress', message); } +function handleYtdlpOutputLines( + session: DownloadSession, + reporter: JobProgressReporter, + chunk: string, + isStderr: boolean +) { + const lines = chunk.split(/\r?\n/); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + if (trimmed.startsWith('{')) { + reporter.handleYtdlpOutputLine(session, trimmed); + try { + const parsed = JSON.parse(trimmed) as Parameters[0]; + const summary = summarizeYtdlpJsonForConsole(parsed); + if (summary) sendProgress(session, summary); + } catch { + // ignore invalid json + } + continue; + } + + const handled = reporter.handleYtdlpOutputLine(session, trimmed); + if (handled) continue; + + if (isStderr) { + sendProgress(session, `[yt-dlp stderr] ${trimmed}`); + } else { + sendProgress(session, trimmed); + } + } +} + interface CompletionMeta { format?: string; bytes?: number; @@ -125,6 +167,11 @@ function completeSession( activeDownloadSession = null; downloadSessionOwner = null; + if (activeProgressReporter) { + activeProgressReporter.emitIdle(session); + activeProgressReporter.clearTaskbar(); + activeProgressReporter = null; + } } function killProcess(proc: ChildProcess | null, label: string) { @@ -161,6 +208,10 @@ export function cancelActiveSession(notify = true) { session.lifecycle = markTerminalEventEmitted(session.lifecycle); activeDownloadSession = null; downloadSessionOwner = null; + if (activeProgressReporter) { + activeProgressReporter.clearTaskbar(); + activeProgressReporter = null; + } } export function killAllProcesses() { @@ -184,6 +235,10 @@ export function killAllProcesses() { session.lifecycle = markTerminalEventEmitted(session.lifecycle); activeDownloadSession = null; downloadSessionOwner = null; + if (activeProgressReporter) { + activeProgressReporter.clearTaskbar(); + activeProgressReporter = null; + } } export function fetchFormats(ytdlpPath: string, url: string): Promise { @@ -271,7 +326,8 @@ async function runConversion( downloadedFilePath: string, effectiveSettings: Settings, ffmpegCommand: string, - mainWindow: Electron.BrowserWindow | null + mainWindow: Electron.BrowserWindow | null, + progressReporter: JobProgressReporter | null ) { sendProgress(session, '⏳ Checking if conversion is needed...'); try { @@ -323,6 +379,8 @@ async function runConversion( const srcCodecs = await probeMediaCodecs(ffmpegCommand, inputPath); const videoEncoder = await resolveVideoEncoder(effectiveSettings); + progressReporter?.emitConvertProgress(session, 0, 'Converting...'); + const ffmpegArgs = buildFfmpegArgs( inputPath, outputPath, @@ -342,6 +400,8 @@ async function runConversion( }); session.ffmpegProcess = ffProc; + const ffmpegProgressState = createFfmpegProgressParseState(srcCodecs.durationSeconds ?? null); + const conversionTimeout = setTimeout(() => { if (!isActiveSession(session) || !ffProc || ffProc.killed) return; sendProgress(session, '❌ Conversion timed out after 10 minutes.'); @@ -351,11 +411,15 @@ async function runConversion( ffProc.stdout?.on('data', (data: Buffer) => { if (!isActiveSession(session)) return; - sendProgress(session, `[ffmpeg] ${data.toString().trim()}`); + const percent = parseFfmpegProgressChunk(ffmpegProgressState, data.toString()); + if (percent !== null) { + progressReporter?.emitConvertProgress(session, percent, 'Converting...'); + } }); ffProc.stderr?.on('data', (data: Buffer) => { if (!isActiveSession(session)) return; - sendProgress(session, `[ffmpeg] ${data.toString().trim()}`); + const text = data.toString().trim(); + if (text) sendProgress(session, `[ffmpeg] ${text}`); }); ffProc.on('close', (ffmpegCode) => { @@ -372,6 +436,7 @@ async function runConversion( } if (ffExitType === 'success') { + progressReporter?.emitConvertProgress(session, 100, 'Conversion complete'); sendProgress(session, `🎉 Successfully converted to ${outputPath}`); const shouldDelete = !effectiveSettings.keepOriginalAfterConvert; const pathsDiffer = isWindows @@ -455,7 +520,8 @@ export function startDownload( options: DownloadRequestOptions, mainWindow: Electron.BrowserWindow | null, onComplete?: (statusMessage: string, outcome?: DownloadOutcome) => void, - owner: DownloadSessionOwner = 'manual' + owner: DownloadSessionOwner = 'manual', + queueProgress: QueueDownloadProgress | null = null ) { if (activeDownloadSession && downloadSessionOwner !== owner) { throw new Error('Download session already active with a different owner.'); @@ -473,6 +539,10 @@ export function startDownload( ytdlpProcess: null, ffmpegProcess: null, onComplete, + queueProgress, + jobPhase: 'download', + ytdlpPostprocess: false, + ytdlpDownloadFinished: false, }; activeDownloadSession = session; downloadSessionOwner = owner; @@ -547,6 +617,23 @@ export function startDownload( }); statusMessages.forEach((message) => sendProgress(session, message)); + const progressPlan = resolveJobProgressPlanFromSettings({ + downloadProfilesEnabled: effectiveSettings.downloadProfilesEnabled, + downloadMode: effectiveSettings.downloadMode, + bestQuality: effectiveSettings.bestQuality, + audioOnly: effectiveSettings.audioOnly, + advancedOptions: effectiveSettings.advancedOptions, + convertEnabled: effectiveSettings.convertEnabled, + }); + const progressReporter = new JobProgressReporter( + mainWindow, + progressPlan, + queueProgress, + effectiveSettings.showTaskbarProgress + ); + activeProgressReporter = progressReporter; + progressReporter.emitPhaseIndeterminate(session, 'download', 'Starting download...'); + sendProgress(session, `🚀 Starting download: ${url}`); sendProgress(session, ` Command: ${ytdlpBinary} ${ytdlpArgs.join(' ')}`); const ytProc = spawnWithEnv(ytdlpPath, ytdlpArgs, { @@ -567,7 +654,7 @@ export function startDownload( downloadOutputData = downloadOutputData.slice(-MAX_OUTPUT_BUFFER / 2); } downloadOutputData += message; - sendProgress(session, message.trim()); + handleYtdlpOutputLines(session, progressReporter, message, false); }); ytProc.stderr?.on('data', (data: Buffer) => { @@ -576,7 +663,7 @@ export function startDownload( if (downloadErrorData.length < MAX_ERROR_BUFFER) { downloadErrorData += message; } - sendProgress(session, `[yt-dlp stderr] ${message.trim()}`); + handleYtdlpOutputLines(session, progressReporter, message, true); }); ytProc.on('close', (code) => { @@ -674,10 +761,12 @@ export function startDownload( downloadedFilePath, effectiveSettings, ffmpegCommand, - mainWindow + mainWindow, + progressReporter ); } else { sendProgress(session, 'ℹ️ Conversion not enabled for this download.'); + progressReporter.emitDownloadComplete(session); const ext = path.extname(downloadedFilePath).replace('.', '').toLowerCase() || undefined; completeSession(session, '✅ Download complete (no conversion).', 'success', { format: ext, diff --git a/src/main/main.ts b/src/main/main.ts index e6c6c5e..8eb6386 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -45,6 +45,7 @@ import { validateDownloadPath, } from '../utils/ipcValidation'; import { SPLASH_SHOW_DELAY_MS, SPLASH_FADE_DELAY_MS, MAX_QUEUE_SIZE } from './constants'; +import { installDarwinApplicationMenu } from './appMenu'; import type { DownloadRequestOptions, DownloadOutcome, QueueItem } from '../types'; log.initialize(); @@ -404,7 +405,12 @@ function createWindow() { mainWindow.setMenuBarVisibility(isDev); mainWindow.setAutoHideMenuBar(!isDev); - if (!isDev) { + if (process.platform === 'darwin') { + installDarwinApplicationMenu({ + getMainWindow: () => mainWindow, + isMsStore: process.env.CHANNEL === 'msstore' || Boolean(process.windowsStore), + }); + } else if (!isDev) { mainWindow.removeMenu(); } @@ -1077,6 +1083,15 @@ async function processQueue() { keepOriginal: settings.convertEnabled ? settings.keepOriginalAfterConvert : undefined, }; + const completedItems = downloadQueue.filter( + (item) => + item.status === 'completed' || item.status === 'failed' || item.status === 'cancelled' + ).length; + const queueProgress = { + completedItems, + queueTotal: downloadQueue.length, + }; + let settled = false; const completeListener = (statusMessage: string, outcome?: DownloadOutcome) => { if (settled) return; @@ -1105,7 +1120,8 @@ async function processQueue() { options, mainWindow, completeListener, - 'queue' + 'queue', + queueProgress ); } catch (error) { nextItem.status = 'failed'; diff --git a/src/main/preload.ts b/src/main/preload.ts index ae797d8..32f52bf 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -2,6 +2,8 @@ import { contextBridge, ipcRenderer } from 'electron'; import type { DownloadRequestOptions, DownloadStats, + JobProgressEvent, + MenuAction, NotificationRequest, QueueItem, RendererApi, @@ -50,6 +52,16 @@ const api: RendererApi = { ipcRenderer.on('progress', listener); return () => ipcRenderer.removeListener('progress', listener); }, + onJobProgress: (callback: (data: JobProgressEvent) => void) => { + const listener = (_: Electron.IpcRendererEvent, data: JobProgressEvent) => callback(data); + ipcRenderer.on('job-progress', listener); + return () => ipcRenderer.removeListener('job-progress', listener); + }, + onMenuAction: (callback: (action: MenuAction) => void) => { + const listener = (_: Electron.IpcRendererEvent, action: MenuAction) => callback(action); + ipcRenderer.on('menu-action', listener); + return () => ipcRenderer.removeListener('menu-action', listener); + }, onComplete: (callback: (message: string) => void) => { const listener = (_: Electron.IpcRendererEvent, message: string) => callback(message); ipcRenderer.on('complete', listener); diff --git a/src/main/settings.ts b/src/main/settings.ts index 7e808a3..0de2ead 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -54,6 +54,7 @@ const defaultSettings: Settings = { embedThumbnail: false, embedMetadata: false, sponsorblockRemove: false, + showTaskbarProgress: true, }; export function getDefaultSettings(): Settings { @@ -244,6 +245,10 @@ export function migrateSettings(rawSettings: unknown): Settings { rawSettings.sponsorblockRemove, defaultSettings.sponsorblockRemove ), + showTaskbarProgress: readBoolean( + rawSettings.showTaskbarProgress, + defaultSettings.showTaskbarProgress + ), }; } diff --git a/src/main/taskbarProgress.ts b/src/main/taskbarProgress.ts new file mode 100644 index 0000000..2261d14 --- /dev/null +++ b/src/main/taskbarProgress.ts @@ -0,0 +1,23 @@ +import type { BrowserWindow } from 'electron'; + +export type TaskbarProgressMode = number | 'indeterminate' | 'none'; + +export function applyTaskbarProgress( + win: BrowserWindow | null | undefined, + mode: TaskbarProgressMode +): void { + if (!win || win.isDestroyed()) return; + const platform = process.platform; + if (platform !== 'win32' && platform !== 'darwin') return; + + if (mode === 'none') { + win.setProgressBar(-1); + return; + } + if (mode === 'indeterminate') { + win.setProgressBar(2); + return; + } + const clamped = Math.max(0, Math.min(1, mode)); + win.setProgressBar(clamped); +} diff --git a/src/renderer/index.html b/src/renderer/index.html index f075243..d3dc4eb 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -347,6 +347,12 @@

Settings

Download notifications + + diff --git a/src/renderer/modules/downloads.ts b/src/renderer/modules/downloads.ts index dcf11e4..7c4c738 100644 --- a/src/renderer/modules/downloads.ts +++ b/src/renderer/modules/downloads.ts @@ -18,6 +18,7 @@ type RosiWindow = Window & typeof globalThis & { rosiModules?: DownloadsModules }; function parseYtdlpProgress(message: string): ParsedYtdlpProgress | null { + // Keep in sync with src/utils/downloadJobProgress.ts (canonical for main process). const progressMatch = message.match( /\[download\]\s+(\d+\.?\d*)%\s+of\s+~?(\S+)\s+at\s+(\S+)\s+ETA\s+(\S+)/ ); diff --git a/src/renderer/renderer-globals.d.ts b/src/renderer/renderer-globals.d.ts index c2f5296..1ded808 100644 --- a/src/renderer/renderer-globals.d.ts +++ b/src/renderer/renderer-globals.d.ts @@ -24,6 +24,14 @@ interface RosiUpdaterProgressEvent { transferred: number; total: number; } +interface RosiJobProgressEvent { + phase: 'download' | 'merge' | 'convert' | 'idle'; + phasePercent: number; + overallPercent: number; + status: string; + details?: string; + indeterminate?: boolean; +} interface RosiDownloadStats { totalDownloads: number; successfulDownloads: number; @@ -66,6 +74,12 @@ interface RosiRendererApi { onUpdaterStatus: (callback: (data: RosiUpdaterStatusEvent) => void) => () => void; onUpdaterProgress: (callback: (data: RosiUpdaterProgressEvent) => void) => () => void; onProgress: (callback: (message: string) => void) => () => void; + onJobProgress: (callback: (data: RosiJobProgressEvent) => void) => () => void; + onMenuAction: ( + callback: ( + action: 'check-for-updates' | 'open-settings' | 'show-licenses' | 'toggle-sidebar' + ) => void + ) => () => void; onComplete: (callback: (message: string) => void) => () => void; openFileLocation: (filePath: string) => Promise>; showNotification: (options: { diff --git a/src/renderer/rosiEngine.ts b/src/renderer/rosiEngine.ts index 48ab888..97882e7 100644 --- a/src/renderer/rosiEngine.ts +++ b/src/renderer/rosiEngine.ts @@ -41,6 +41,47 @@ interface RosiSettings { embedThumbnail: boolean; embedMetadata: boolean; sponsorblockRemove: boolean; + showTaskbarProgress: boolean; +} + +interface RosiJobProgressEvent { + phase: 'download' | 'merge' | 'convert' | 'idle'; + phasePercent: number; + overallPercent: number; + status: string; + details?: string; + indeterminate?: boolean; +} + +function resolveProgressPhaseFlags( + activeSettings: RosiSettings, + advancedFormats?: { videoFormat?: string; audioFormat?: string } +) { + if ( + activeSettings.audioOnly || + (activeSettings.downloadProfilesEnabled && activeSettings.downloadMode === 'audio') + ) { + return { needsMerge: false, needsConvert: activeSettings.convertEnabled }; + } + const baseMerge = activeSettings.downloadProfilesEnabled + ? activeSettings.downloadMode === 'best-video' || activeSettings.downloadMode === 'custom' + : activeSettings.bestQuality || activeSettings.advancedOptions; + const needsMerge = + Boolean(advancedFormats?.videoFormat && advancedFormats?.audioFormat) || baseMerge; + return { + needsMerge, + needsConvert: activeSettings.convertEnabled, + }; +} + +function applyActiveDownloadProgressPhases( + activeSettings: RosiSettings, + status = 'Downloading...', + advancedFormats?: { videoFormat?: string; audioFormat?: string } +) { + const { needsMerge, needsConvert } = resolveProgressPhaseFlags(activeSettings, advancedFormats); + configureProgressPhases(needsMerge, needsConvert); + showProgressBar(status); } const rosiModules = window.rosiModules || {}; @@ -484,7 +525,7 @@ function showKeyboardShortcuts() { const modKey = getModifierKeyName(); showModal({ title: 'Keyboard Shortcuts', - message: `${modKey}+D - Restart application\n${modKey}+F - Focus URL input field\n${modKey}+, - Open settings\n${modKey}+Enter - Submit queue URLs (when focused)`, + message: `${modKey}+D - Restart application\n${modKey}+F - Focus URL input field\n${modKey}+, - Open settings (macOS menu)\n${modKey}+Shift+, - Toggle settings sidebar\n${modKey}+Enter - Submit queue URLs (when focused)`, buttons: [{ label: 'OK', primary: true }], }); } @@ -849,6 +890,20 @@ function setProgressIndeterminate(status = 'Processing...') { if (details) details.textContent = ''; } +function applyJobProgress(event: RosiJobProgressEvent) { + if (event.phase && event.phase !== 'idle') { + setProgressPhase(event.phase); + } + if (event.indeterminate) { + setProgressIndeterminate(event.status); + return; + } + const displayPercent = Number.isFinite(event.overallPercent) + ? event.overallPercent + : event.phasePercent; + updateProgressBar(displayPercent, event.status, event.details ?? null); +} + function hideProgressBar() { const container = document.getElementById('progress-container'); const bar = document.getElementById('progress-bar'); @@ -863,13 +918,6 @@ function hideProgressBar() { hideProgressComplete(); } -function parseYtdlpProgress(message: string) { - if (downloadsModule && typeof downloadsModule.parseYtdlpProgress === 'function') { - return downloadsModule.parseYtdlpProgress(message); - } - return null; -} - function formatBytes(bytes: number) { if (downloadsModule && typeof downloadsModule.formatBytes === 'function') { return downloadsModule.formatBytes(bytes); @@ -1710,7 +1758,7 @@ document.addEventListener('DOMContentLoaded', async () => { } catch (error) { logError('Failed to load settings', error); settings = { - settingsVersion: 5, + settingsVersion: 6, theme: 'system', showConsoleOutput: false, consoleCollapsed: false, @@ -1744,6 +1792,7 @@ document.addEventListener('DOMContentLoaded', async () => { embedThumbnail: false, embedMetadata: false, sponsorblockRemove: false, + showTaskbarProgress: true, }; showModal({ title: 'Settings Error', @@ -1886,6 +1935,7 @@ document.addEventListener('DOMContentLoaded', async () => { const askDownloadLocationToggle = byId('askDownloadLocationToggle'); const downloadOutputSummary = byId('downloadOutputSummary'); const notificationsToggle = byId('notificationsToggle'); + const taskbarProgressToggle = byId('taskbarProgressToggle'); const checkUpdatesOnStartupToggle = byId('checkUpdatesOnStartupToggle'); const checkUpdatesOnStartupLabel = byId('checkUpdatesOnStartupLabel'); const updateChannelSelect = byId('updateChannelSelect'); @@ -2141,6 +2191,9 @@ document.addEventListener('DOMContentLoaded', async () => { if (notificationsToggle) { notificationsToggle.checked = settings.notifications ?? true; } + if (taskbarProgressToggle) { + taskbarProgressToggle.checked = settings.showTaskbarProgress ?? true; + } if (embedMetadataToggle) { embedMetadataToggle.checked = settings.embedMetadata ?? false; @@ -2814,6 +2867,13 @@ document.addEventListener('DOMContentLoaded', async () => { }); } + if (taskbarProgressToggle) { + taskbarProgressToggle.addEventListener('change', (e) => { + settings.showTaskbarProgress = (e.target as HTMLInputElement).checked; + void persistSettings(); + }); + } + if (embedMetadataToggle) { embedMetadataToggle.addEventListener('change', (e) => { settings.embedMetadata = (e.target as HTMLInputElement).checked; @@ -3246,6 +3306,7 @@ document.addEventListener('DOMContentLoaded', async () => { try { const result = await window.api.startQueue(); if (result && result.ok) { + applyActiveDownloadProgressPhases(settings, 'Queue download...'); announceQueueAction('Queue started processing.'); } else { const message = result?.error?.message || 'Could not start the queue.'; @@ -3428,10 +3489,10 @@ document.addEventListener('DOMContentLoaded', async () => { const videoFormat = settings.advancedOptions ? videoSelect?.value : undefined; const audioFormat = settings.advancedOptions ? audioSelect?.value : undefined; const convertFormat = settings.convertEnabled ? convertFormatSelect?.value : undefined; - const needsMerge = settings.bestQuality || (videoFormat && audioFormat); - const needsConvert = settings.convertEnabled && convertFormat; - configureProgressPhases(!!needsMerge, !!needsConvert); - showProgressBar('Starting download...'); + applyActiveDownloadProgressPhases(settings, 'Starting download...', { + videoFormat, + audioFormat, + }); const keepOriginal = settings.convertEnabled ? keepOriginalToggle?.checked : undefined; const startResult = await window.api.downloadVideo({ url, @@ -3476,27 +3537,6 @@ document.addEventListener('DOMContentLoaded', async () => { if (!outputEl) return; appendConsoleOutput(outputEl, message); - const progress = parseYtdlpProgress(message); - if (progress) { - let detailsText = ''; - if (progress.speed && progress.eta) { - detailsText = `${progress.totalSize} • ${progress.speed} • ETA: ${progress.eta}`; - } else if (progress.totalSize) { - detailsText = `Size: ${progress.totalSize}`; - } - updateProgressBar(progress.percent, 'Downloading...', detailsText); - } else if (message.includes('[download] Destination:')) { - setProgressIndeterminate('Preparing download...'); - } else if (message.includes('Merging formats')) { - setProgressPhase('merge'); - setProgressIndeterminate('Merging video and audio...'); - } else if (message.includes('Converting') || message.includes('[ffmpeg]')) { - setProgressPhase('convert'); - setProgressIndeterminate('Converting...'); - } else if (message.includes('100%')) { - updateProgressBar(100, 'Download complete!', ''); - } - if (message.includes('Identified file:') || message.includes('Successfully converted to')) { const fileMatch = message.match(/(?:Identified file:|Successfully converted to)\s*(.+)$/); if (fileMatch && fileMatch[1]) { @@ -3506,6 +3546,42 @@ document.addEventListener('DOMContentLoaded', async () => { }) ); + ipcCleanupFunctions.push( + window.api.onJobProgress((event) => { + const container = document.getElementById('progress-container'); + if (container && !container.classList.contains('visible') && event.phase !== 'idle') { + showProgressBar(event.status); + } + if (event.phase === 'idle') { + return; + } + applyJobProgress(event); + }) + ); + + ipcCleanupFunctions.push( + window.api.onMenuAction((action) => { + if (action === 'check-for-updates') { + void checkForUpdates(); + return; + } + if (action === 'open-settings') { + const sidebar = document.getElementById('sidebar'); + if (sidebar && !sidebar.classList.contains('open')) { + toggleSidebar(); + } + return; + } + if (action === 'toggle-sidebar') { + toggleSidebar(); + return; + } + if (action === 'show-licenses') { + showLicenses(); + } + }) + ); + ipcCleanupFunctions.push( window.api.onComplete((statusMessage) => { if (downloadBtn) { @@ -3722,7 +3798,7 @@ document.addEventListener('DOMContentLoaded', async () => { } } - if (modifierPressed && event.key === ',') { + if (modifierPressed && event.shiftKey && event.key === ',') { event.preventDefault(); toggleSidebar(); } diff --git a/src/tests/appMenu.test.ts b/src/tests/appMenu.test.ts new file mode 100644 index 0000000..297667d --- /dev/null +++ b/src/tests/appMenu.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest'; + +const templateStore: { template: unknown[] | null } = { template: null }; + +vi.mock('electron', () => ({ + app: { name: 'ROSI', getVersion: () => '4.3.0' }, + shell: { openExternal: vi.fn() }, + Menu: { + buildFromTemplate: (template: unknown[]) => { + templateStore.template = template; + return {}; + }, + }, +})); + +describe('appMenu', () => { + it('omits Check for Updates on Microsoft Store builds', async () => { + const { buildDarwinApplicationMenu } = await import('../main/appMenu'); + buildDarwinApplicationMenu({ + getMainWindow: () => null, + isMsStore: true, + }); + const appMenu = ( + templateStore.template as { label?: string; submenu?: { label?: string }[] }[] + )[0]; + const labels = (appMenu?.submenu ?? []).map((item) => item.label).filter(Boolean); + expect(labels).not.toContain('Check for Updates…'); + expect(labels).toContain('Settings…'); + }); + + it('includes Check for Updates on GitHub builds', async () => { + const { buildDarwinApplicationMenu } = await import('../main/appMenu'); + buildDarwinApplicationMenu({ + getMainWindow: () => null, + isMsStore: false, + }); + const appMenu = (templateStore.template as { submenu?: { label?: string }[] }[])[0]; + const labels = (appMenu?.submenu ?? []).map((item) => item.label).filter(Boolean); + expect(labels).toContain('Check for Updates…'); + }); +}); diff --git a/src/tests/commandBuilders.test.ts b/src/tests/commandBuilders.test.ts index c3bdc29..225b643 100644 --- a/src/tests/commandBuilders.test.ts +++ b/src/tests/commandBuilders.test.ts @@ -14,6 +14,8 @@ vi.mock('../main/platform', () => ({ spawnWithEnv: spawnWithEnvMock, })); +const withFfmpegProgress = (...args: string[]) => ['-progress', 'pipe:1', '-nostats', ...args]; + vi.mock('electron-log/main.js', () => ({ default: { warn: vi.fn(), error: vi.fn() }, })); @@ -92,6 +94,7 @@ describe('command builders', () => { await expect(probeMediaCodecs('ffmpeg', '/tmp/input.mkv')).resolves.toEqual({ video: 'h264', audio: 'aac', + durationSeconds: null, }); expect(spawnWithEnvMock).toHaveBeenCalledWith( 'ffmpeg', @@ -105,13 +108,16 @@ describe('command builders', () => { await expect(probeMediaCodecs('ffmpeg', '/tmp/input.webm')).resolves.toEqual({ video: 'vp9', + durationSeconds: null, }); }); it('probeMediaCodecs returns empty object when stderr has no stream info', async () => { createProbeProc(['ffmpeg version 6.0\n']); - await expect(probeMediaCodecs('ffmpeg', '/tmp/input.mp4')).resolves.toEqual({}); + await expect(probeMediaCodecs('ffmpeg', '/tmp/input.mp4')).resolves.toEqual({ + durationSeconds: null, + }); }); it('probeMediaCodecs returns empty object when spawn errors', async () => { @@ -141,7 +147,7 @@ describe('command builders', () => { proc.stderr.emit('data', ' Stream #0:0: Video: h264 (High), yuv420p\n'); await vi.advanceTimersByTimeAsync(30_000); - await expect(pending).resolves.toEqual({ video: 'h264' }); + await expect(pending).resolves.toEqual({ video: 'h264', durationSeconds: null }); expect(proc.kill).toHaveBeenCalledWith('SIGKILL'); vi.useRealTimers(); }); @@ -185,28 +191,34 @@ describe('command builders', () => { it('builds ffmpeg args for audio extraction', () => { const args = buildFfmpegArgs('input.mp4', 'output.mp3', 'mp3', 'copy'); - expect(args).toEqual(['-i', 'input.mp4', '-vn', '-c:a', 'libmp3lame', '-y', 'output.mp3']); + expect(args).toEqual( + withFfmpegProgress('-i', 'input.mp4', '-vn', '-c:a', 'libmp3lame', '-y', 'output.mp3') + ); }); it('builds ffmpeg args for m4a audio extraction', () => { const args = buildFfmpegArgs('input.mp4', 'output.m4a', 'm4a', 'copy'); - expect(args).toEqual(['-i', 'input.mp4', '-vn', '-c:a', 'aac', '-y', 'output.m4a']); + expect(args).toEqual( + withFfmpegProgress('-i', 'input.mp4', '-vn', '-c:a', 'aac', '-y', 'output.m4a') + ); }); it('builds ffmpeg args for video conversion', () => { const args = buildFfmpegArgs('input.webm', 'output.mp4', 'mp4', 'h264_nvenc'); - expect(args).toEqual([ - '-i', - 'input.webm', - '-c:v', - 'h264_nvenc', - '-c:a', - 'aac', - '-movflags', - '+faststart', - '-y', - 'output.mp4', - ]); + expect(args).toEqual( + withFfmpegProgress( + '-i', + 'input.webm', + '-c:v', + 'h264_nvenc', + '-c:a', + 'aac', + '-movflags', + '+faststart', + '-y', + 'output.mp4' + ) + ); }); it('copies a container-compatible video stream instead of re-encoding', () => { @@ -214,18 +226,20 @@ describe('command builders', () => { video: 'h264', audio: 'aac', }); - expect(args).toEqual([ - '-i', - 'input.mkv', - '-c:v', - 'copy', - '-c:a', - 'copy', - '-movflags', - '+faststart', - '-y', - 'output.mp4', - ]); + expect(args).toEqual( + withFfmpegProgress( + '-i', + 'input.mkv', + '-c:v', + 'copy', + '-c:a', + 'copy', + '-movflags', + '+faststart', + '-y', + 'output.mp4' + ) + ); }); it('re-encodes an incompatible video stream with the chosen encoder', () => { @@ -233,28 +247,34 @@ describe('command builders', () => { video: 'vp9', audio: 'opus', }); - expect(args).toEqual([ - '-i', - 'input.webm', - '-c:v', - 'h264_nvenc', - '-c:a', - 'aac', - '-movflags', - '+faststart', - '-y', - 'output.mp4', - ]); + expect(args).toEqual( + withFfmpegProgress( + '-i', + 'input.webm', + '-c:v', + 'h264_nvenc', + '-c:a', + 'aac', + '-movflags', + '+faststart', + '-y', + 'output.mp4' + ) + ); }); it('copies already-aac audio when extracting to m4a', () => { const args = buildFfmpegArgs('input.mp4', 'output.m4a', 'm4a', 'copy', { audio: 'aac' }); - expect(args).toEqual(['-i', 'input.mp4', '-vn', '-c:a', 'copy', '-y', 'output.m4a']); + expect(args).toEqual( + withFfmpegProgress('-i', 'input.mp4', '-vn', '-c:a', 'copy', '-y', 'output.m4a') + ); }); it('re-encodes non-aac audio when extracting to m4a', () => { const args = buildFfmpegArgs('input.webm', 'output.m4a', 'm4a', 'copy', { audio: 'opus' }); - expect(args).toEqual(['-i', 'input.webm', '-vn', '-c:a', 'aac', '-y', 'output.m4a']); + expect(args).toEqual( + withFfmpegProgress('-i', 'input.webm', '-vn', '-c:a', 'aac', '-y', 'output.m4a') + ); }); it('builds yt-dlp args for selected audio/video formats', () => { diff --git a/src/tests/downloadJobProgress.test.ts b/src/tests/downloadJobProgress.test.ts new file mode 100644 index 0000000..c787ee4 --- /dev/null +++ b/src/tests/downloadJobProgress.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from 'vitest'; +import { + applyQueueWeighting, + buildJobProgressEvent, + computeItemOverallPercent, + formatYtdlpProgressSummary, + inferJobProgressPlan, + parseFfmpegDurationFromProbe, + parseFfmpegProgressChunk, + parseYtdlpProgress, + tryParseYtdlpProgressJson, + ytdlpJsonToPhasePercent, + createFfmpegProgressParseState, + resolveJobProgressPlanFromSettings, +} from '../utils/downloadJobProgress'; + +describe('downloadJobProgress', () => { + it('parseYtdlpProgress parses full and simple lines', () => { + expect(parseYtdlpProgress('[download] 45.2% of ~10.5MiB at 1.2MiB/s ETA 00:08')).toEqual({ + percent: 45.2, + totalSize: '10.5MiB', + speed: '1.2MiB/s', + eta: '00:08', + }); + expect(parseYtdlpProgress('[download] 10.0% of ~5.00MiB')).toEqual({ + percent: 10, + totalSize: '5.00MiB', + speed: null, + eta: null, + }); + expect(parseYtdlpProgress('random')).toBeNull(); + }); + + it('parses yt-dlp JSON progress and computes percent from bytes', () => { + const json = tryParseYtdlpProgressJson( + '{"status":"downloading","downloaded_bytes":500,"total_bytes":1000,"eta":5,"speed":100}' + ); + expect(json).not.toBeNull(); + expect(ytdlpJsonToPhasePercent(json!)).toBe(50); + }); + + it('uses fragment index when byte totals are missing', () => { + const percent = ytdlpJsonToPhasePercent({ + status: 'downloading', + fragment_index: 2, + fragment_count: 4, + }); + expect(percent).toBe(50); + }); + + it('computes weighted overall percent for download-only jobs', () => { + const plan = { expectMerge: false, expectConvert: false }; + expect(computeItemOverallPercent('download', 50, plan)).toBe(50); + }); + + it('computes weighted overall percent with merge and convert', () => { + const plan = { expectMerge: true, expectConvert: true }; + expect(computeItemOverallPercent('download', 100, plan)).toBe(70); + expect(computeItemOverallPercent('merge', 100, plan)).toBe(85); + expect(computeItemOverallPercent('convert', 100, plan)).toBe(100); + }); + + it('blends queue progress', () => { + const blended = applyQueueWeighting(50, { completedItems: 1, queueTotal: 4 }); + expect(blended).toBe(37.5); + }); + + it('buildJobProgressEvent marks indeterminate when phase percent is NaN', () => { + const event = buildJobProgressEvent( + 'merge', + Number.NaN, + { expectMerge: true, expectConvert: false }, + null, + 'Merging...', + undefined, + true + ); + expect(event.indeterminate).toBe(true); + expect(event.overallPercent).toBeGreaterThanOrEqual(70); + }); + + it('formatYtdlpProgressSummary builds human-readable details', () => { + const summary = formatYtdlpProgressSummary( + { + status: 'downloading', + downloaded_bytes: 512000, + total_bytes: 1024000, + speed: 256000, + eta: 2, + }, + 50 + ); + expect(summary.status).toBe('Downloading...'); + expect(summary.details).toContain('50.0%'); + expect(summary.details).toContain('ETA'); + }); + + it('parses ffmpeg duration and progress chunks', () => { + const duration = parseFfmpegDurationFromProbe( + 'Input #0, mov, Duration: 00:01:30.50, start: 0.000000' + ); + expect(duration).toBeCloseTo(90.5, 1); + + const state = createFfmpegProgressParseState(100); + const percent = parseFfmpegProgressChunk(state, 'out_time_ms=50000\n'); + expect(percent).toBe(50); + }); + + it('inferJobProgressPlan detects merge from format selector', () => { + expect(inferJobProgressPlan(false, 'bestvideo+bestaudio/best').expectMerge).toBe(true); + expect(inferJobProgressPlan(true, 'best[ext=mp4]/best').expectConvert).toBe(true); + }); + + it('resolveJobProgressPlanFromSettings respects download profiles', () => { + expect( + resolveJobProgressPlanFromSettings({ + downloadProfilesEnabled: true, + downloadMode: 'best-video', + bestQuality: false, + audioOnly: false, + advancedOptions: false, + convertEnabled: true, + }) + ).toEqual({ expectMerge: true, expectConvert: true }); + + expect( + resolveJobProgressPlanFromSettings({ + downloadProfilesEnabled: true, + downloadMode: 'audio', + bestQuality: false, + audioOnly: false, + advancedOptions: false, + convertEnabled: false, + }) + ).toEqual({ expectMerge: false, expectConvert: false }); + }); +}); diff --git a/src/tests/ipc.contract.test.ts b/src/tests/ipc.contract.test.ts index e454384..094205e 100644 --- a/src/tests/ipc.contract.test.ts +++ b/src/tests/ipc.contract.test.ts @@ -34,6 +34,8 @@ describe('IPC channel contracts', () => { expect(rendererEventChannels.sort()).toEqual( [ 'complete', + 'job-progress', + 'menu-action', 'prepare-for-close', 'progress', 'queue-update', diff --git a/src/tests/preload.test.ts b/src/tests/preload.test.ts index 861f182..ac3d7af 100644 --- a/src/tests/preload.test.ts +++ b/src/tests/preload.test.ts @@ -104,6 +104,8 @@ describe('preload api contract', () => { 'isPackaged', 'logError', 'notifySettingsFlushed', + 'onJobProgress', + 'onMenuAction', 'onComplete', 'onPrepareForClose', 'onProgress', @@ -204,6 +206,13 @@ describe('preload api contract', () => { validateSubscription('onUpdaterProgress', 'updater-progress', { percent: 50 }); validateSubscription('onSettingsImported', 'settings-imported', { imported: true }); validateSubscription('onProgress', 'progress', 'line'); + validateSubscription('onJobProgress', 'job-progress', { + phase: 'download', + phasePercent: 10, + overallPercent: 7, + status: 'Downloading...', + }); + validateSubscription('onMenuAction', 'menu-action', 'open-settings'); validateSubscription('onComplete', 'complete', 'done'); validateSubscription('onQueueUpdate', 'queue-update', [{ id: 'q_1' }]); diff --git a/src/tests/rosiEngine.dom.test.ts b/src/tests/rosiEngine.dom.test.ts index a19ce89..24ecb74 100644 --- a/src/tests/rosiEngine.dom.test.ts +++ b/src/tests/rosiEngine.dom.test.ts @@ -41,7 +41,7 @@ interface MockApi { function defaultSettings() { return { - settingsVersion: 5, + settingsVersion: 6, theme: 'dark', showConsoleOutput: false, consoleCollapsed: false, @@ -75,6 +75,7 @@ function defaultSettings() { embedThumbnail: false, embedMetadata: false, sponsorblockRemove: false, + showTaskbarProgress: true, }; } @@ -119,6 +120,8 @@ function buildMockApi(overrides: Partial = {}): MockApi { installDeno: vi.fn(() => Promise.resolve({ success: true })), detectGpu: vi.fn(() => Promise.resolve({ nvidia: false, amd: false, intel: false })), onProgress: noop, + onJobProgress: noop, + onMenuAction: noop, onComplete: noop, onQueueUpdate: noop, onPrepareForClose: noop, diff --git a/src/tests/settingsMigration.test.ts b/src/tests/settingsMigration.test.ts index e1d6d97..ebc369d 100644 --- a/src/tests/settingsMigration.test.ts +++ b/src/tests/settingsMigration.test.ts @@ -48,6 +48,7 @@ describe('settings migration', () => { expect(migrated.updateChannel).toBe('beta'); expect(migrated.gpuType).toBe('nvidia'); expect(migrated.flatUi).toBe(true); + expect(migrated.showTaskbarProgress).toBe(true); expect(migrated.settingsVersion).toBe(CURRENT_SETTINGS_VERSION); }); diff --git a/src/tests/taskbarProgress.test.ts b/src/tests/taskbarProgress.test.ts new file mode 100644 index 0000000..e33a527 --- /dev/null +++ b/src/tests/taskbarProgress.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const setProgressBar = vi.fn(); + +vi.mock('electron', () => ({ + BrowserWindow: class {}, +})); + +describe('taskbarProgress', () => { + beforeEach(() => { + setProgressBar.mockClear(); + }); + + it('clears progress on none', async () => { + const win = { setProgressBar, isDestroyed: () => false }; + const { applyTaskbarProgress } = await import('../main/taskbarProgress'); + applyTaskbarProgress(win as never, 'none'); + expect(setProgressBar).toHaveBeenCalledWith(-1); + }); + + it('sets fractional progress on Windows', async () => { + const platform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + const win = { setProgressBar, isDestroyed: () => false }; + const { applyTaskbarProgress } = await import('../main/taskbarProgress'); + applyTaskbarProgress(win as never, 0.42); + expect(setProgressBar).toHaveBeenCalledWith(0.42); + Object.defineProperty(process, 'platform', { value: platform }); + }); + + it('no-ops on unsupported platforms', async () => { + const platform = process.platform; + Object.defineProperty(process, 'platform', { value: 'linux' }); + const win = { setProgressBar, isDestroyed: () => false }; + const { applyTaskbarProgress } = await import('../main/taskbarProgress'); + applyTaskbarProgress(win as never, 0.5); + expect(setProgressBar).not.toHaveBeenCalled(); + Object.defineProperty(process, 'platform', { value: platform }); + }); +}); diff --git a/src/types.ts b/src/types.ts index 87c09e4..f68efd9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,25 @@ export interface Settings { embedThumbnail: boolean; embedMetadata: boolean; sponsorblockRemove: boolean; + showTaskbarProgress: boolean; +} + +export type DownloadJobPhase = 'download' | 'merge' | 'convert' | 'idle'; + +export interface JobProgressEvent { + phase: DownloadJobPhase; + phasePercent: number; + overallPercent: number; + status: string; + details?: string; + indeterminate?: boolean; +} + +export type MenuAction = 'check-for-updates' | 'open-settings' | 'show-licenses' | 'toggle-sidebar'; + +export interface QueueDownloadProgress { + completedItems: number; + queueTotal: number; } export type UpdateChannel = 'auto' | 'stable' | 'beta'; @@ -76,6 +95,10 @@ export interface DownloadSession { ytdlpProcess: import('child_process').ChildProcess | null; ffmpegProcess: import('child_process').ChildProcess | null; onComplete?: (statusMessage: string, outcome: DownloadOutcome) => void; + queueProgress: QueueDownloadProgress | null; + jobPhase: DownloadJobPhase; + ytdlpPostprocess: boolean; + ytdlpDownloadFinished: boolean; } export interface DownloadRequestOptions { @@ -196,6 +219,8 @@ export interface RendererApi { onUpdaterStatus: (callback: (data: UpdaterStatusEvent) => void) => () => void; onUpdaterProgress: (callback: (data: UpdaterProgressEvent) => void) => () => void; onProgress: (callback: (message: string) => void) => () => void; + onJobProgress: (callback: (data: JobProgressEvent) => void) => () => void; + onMenuAction: (callback: (action: MenuAction) => void) => () => void; onComplete: (callback: (message: string) => void) => () => void; openFileLocation: (filePath: string) => Promise>; showNotification: (options: NotificationRequest) => Promise>; diff --git a/src/utils/downloadJobProgress.ts b/src/utils/downloadJobProgress.ts new file mode 100644 index 0000000..bb9d542 --- /dev/null +++ b/src/utils/downloadJobProgress.ts @@ -0,0 +1,318 @@ +export type DownloadJobPhase = 'download' | 'merge' | 'convert' | 'idle'; + +export interface JobProgressEvent { + phase: DownloadJobPhase; + /** 0–100 within the current phase; NaN means indeterminate for that phase */ + phasePercent: number; + /** 0–100 for the current queue item (or single download) */ + overallPercent: number; + status: string; + details?: string; + indeterminate?: boolean; +} + +export interface ParsedYtdlpProgress { + percent: number; + totalSize: string; + speed: string | null; + eta: string | null; +} + +export interface JobProgressPlan { + expectMerge: boolean; + expectConvert: boolean; +} + +export interface QueueProgressContext { + completedItems: number; + queueTotal: number; +} + +export interface YtdlpProgressJson { + status?: string; + downloaded_bytes?: number; + total_bytes?: number; + total_bytes_estimate?: number; + eta?: number; + speed?: number; + fragment_index?: number; + fragment_count?: number; +} + +const PHASE_DOWNLOAD_END = 70; +const PHASE_MERGE_END = 85; +const PHASE_CONVERT_END = 100; + +export function parseYtdlpProgress(message: string): ParsedYtdlpProgress | null { + const progressMatch = message.match( + /\[download\]\s+(\d+\.?\d*)%\s+of\s+~?(\S+)\s+at\s+(\S+)\s+ETA\s+(\S+)/ + ); + if (progressMatch?.[1] && progressMatch[2] && progressMatch[3] && progressMatch[4]) { + return { + percent: parseFloat(progressMatch[1]), + totalSize: progressMatch[2], + speed: progressMatch[3], + eta: progressMatch[4], + }; + } + + const simpleMatch = message.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?(\S+)/); + if (simpleMatch?.[1] && simpleMatch[2]) { + return { + percent: parseFloat(simpleMatch[1]), + totalSize: simpleMatch[2], + speed: null, + eta: null, + }; + } + + return null; +} + +export function tryParseYtdlpProgressJson(line: string): YtdlpProgressJson | null { + const trimmed = line.trim(); + if (!trimmed.startsWith('{')) return null; + try { + const parsed: unknown = JSON.parse(trimmed); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + return parsed as YtdlpProgressJson; + } catch { + return null; + } +} + +export function formatBytes(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + const value = bytes / Math.pow(k, i); + return `${parseFloat(value.toFixed(2))} ${sizes[i] ?? 'B'}`; +} + +export function formatEta(seconds: number | undefined): string | null { + if (seconds === undefined || !Number.isFinite(seconds) || seconds < 0) return null; + const total = Math.round(seconds); + const h = Math.floor(total / 3600); + const m = Math.floor((total % 3600) / 60); + const s = total % 60; + if (h > 0) { + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + +export function ytdlpJsonToPhasePercent(data: YtdlpProgressJson): number { + if (data.status === 'finished') return 100; + + const total = data.total_bytes ?? data.total_bytes_estimate; + if (typeof data.downloaded_bytes === 'number' && typeof total === 'number' && total > 0) { + return Math.min(100, (data.downloaded_bytes / total) * 100); + } + + if ( + typeof data.fragment_index === 'number' && + typeof data.fragment_count === 'number' && + data.fragment_count > 0 + ) { + return Math.min(100, (data.fragment_index / data.fragment_count) * 100); + } + + return Number.NaN; +} + +export function formatYtdlpProgressSummary( + data: YtdlpProgressJson, + phasePercent: number +): { status: string; details: string } { + const total = data.total_bytes ?? data.total_bytes_estimate; + const percentLabel = Number.isFinite(phasePercent) ? `${phasePercent.toFixed(1)}%` : '…'; + const totalLabel = typeof total === 'number' ? formatBytes(total) : 'unknown size'; + const speedLabel = + typeof data.speed === 'number' && data.speed > 0 ? `${formatBytes(data.speed)}/s` : null; + const etaLabel = formatEta(data.eta); + + const parts = [`${percentLabel} of ${totalLabel}`]; + if (speedLabel) parts.push(`at ${speedLabel}`); + if (etaLabel) parts.push(`ETA ${etaLabel}`); + + return { + status: 'Downloading...', + details: parts.join(' '), + }; +} + +export function computeItemOverallPercent( + phase: DownloadJobPhase, + phasePercent: number, + plan: JobProgressPlan +): number { + const clampedPhase = Number.isFinite(phasePercent) ? Math.max(0, Math.min(100, phasePercent)) : 0; + + const { expectMerge, expectConvert } = plan; + + if (!expectMerge && !expectConvert) { + if (phase === 'download') return clampedPhase; + if (phase === 'idle') return 0; + return 100; + } + + if (expectMerge && !expectConvert) { + if (phase === 'download') return (clampedPhase / 100) * PHASE_DOWNLOAD_END; + if (phase === 'merge') { + const span = PHASE_MERGE_END - PHASE_DOWNLOAD_END; + return PHASE_DOWNLOAD_END + (clampedPhase / 100) * span; + } + return phase === 'idle' ? 0 : 100; + } + + if (!expectMerge && expectConvert) { + if (phase === 'download') return (clampedPhase / 100) * PHASE_DOWNLOAD_END; + if (phase === 'convert') { + const span = PHASE_CONVERT_END - PHASE_DOWNLOAD_END; + return PHASE_DOWNLOAD_END + (clampedPhase / 100) * span; + } + return phase === 'idle' ? 0 : 100; + } + + // merge + convert + if (phase === 'download') return (clampedPhase / 100) * PHASE_DOWNLOAD_END; + if (phase === 'merge') { + const span = PHASE_MERGE_END - PHASE_DOWNLOAD_END; + return PHASE_DOWNLOAD_END + (clampedPhase / 100) * span; + } + if (phase === 'convert') { + const span = PHASE_CONVERT_END - PHASE_MERGE_END; + return PHASE_MERGE_END + (clampedPhase / 100) * span; + } + return phase === 'idle' ? 0 : 100; +} + +export function applyQueueWeighting( + itemOverallPercent: number, + queue: QueueProgressContext | null +): number { + if (!queue || queue.queueTotal <= 0) { + return Math.max(0, Math.min(100, itemOverallPercent)); + } + const itemFraction = Math.max(0, Math.min(100, itemOverallPercent)) / 100; + const blended = ((queue.completedItems + itemFraction) / queue.queueTotal) * 100; + return Math.max(0, Math.min(100, blended)); +} + +export function buildJobProgressEvent( + phase: DownloadJobPhase, + phasePercent: number, + plan: JobProgressPlan, + queue: QueueProgressContext | null, + status: string, + details?: string, + indeterminate?: boolean +): JobProgressEvent { + const itemOverall = computeItemOverallPercent(phase, phasePercent, plan); + const overallPercent = applyQueueWeighting(itemOverall, queue); + const isIndeterminate = + indeterminate === true || (indeterminate !== false && !Number.isFinite(phasePercent)); + + return { + phase, + phasePercent: Number.isFinite(phasePercent) ? phasePercent : Number.NaN, + overallPercent, + status, + details, + indeterminate: isIndeterminate, + }; +} + +export function parseFfmpegDurationFromProbe(stderr: string): number | null { + const match = stderr.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/); + if (!match?.[1] || !match[2] || !match[3]) return null; + const hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + const seconds = parseFloat(match[3]); + if (!Number.isFinite(hours + minutes + seconds)) return null; + return hours * 3600 + minutes * 60 + seconds; +} + +export interface FfmpegProgressParseState { + buffer: string; + durationSeconds: number | null; +} + +export function createFfmpegProgressParseState( + durationSeconds: number | null +): FfmpegProgressParseState { + return { buffer: '', durationSeconds }; +} + +export function parseFfmpegProgressChunk( + state: FfmpegProgressParseState, + chunk: string +): number | null { + state.buffer += chunk; + const lines = state.buffer.split('\n'); + state.buffer = lines.pop() ?? ''; + + let outTimeUs: number | null = null; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('out_time_us=')) { + const raw = trimmed.slice('out_time_us='.length); + const parsed = Number(raw); + if (Number.isFinite(parsed)) outTimeUs = parsed; + } + if (trimmed.startsWith('out_time_ms=')) { + const raw = trimmed.slice('out_time_ms='.length); + const parsed = Number(raw); + if (Number.isFinite(parsed)) outTimeUs = parsed * 1000; + } + } + + if (outTimeUs === null || state.durationSeconds === null || state.durationSeconds <= 0) { + return null; + } + const elapsedSeconds = outTimeUs / 1_000_000; + return Math.min(100, (elapsedSeconds / state.durationSeconds) * 100); +} + +export function inferJobProgressPlan( + convertEnabled: boolean, + formatSelector: string +): JobProgressPlan { + const expectMerge = /\+/.test(formatSelector) || /bestvideo/i.test(formatSelector); + return { + expectMerge, + expectConvert: convertEnabled, + }; +} + +export interface JobProgressSettingsInput { + downloadProfilesEnabled: boolean; + downloadMode: 'best-video' | 'audio' | 'custom'; + bestQuality: boolean; + audioOnly: boolean; + advancedOptions: boolean; + convertEnabled: boolean; +} + +/** Align phase weights with download profile / quality settings (not only yt-dlp -f string). */ +export function resolveJobProgressPlanFromSettings( + settings: JobProgressSettingsInput +): JobProgressPlan { + if ( + settings.audioOnly || + (settings.downloadProfilesEnabled && settings.downloadMode === 'audio') + ) { + return { expectMerge: false, expectConvert: settings.convertEnabled }; + } + if (settings.downloadProfilesEnabled) { + return { + expectMerge: settings.downloadMode === 'best-video' || settings.downloadMode === 'custom', + expectConvert: settings.convertEnabled, + }; + } + return { + expectMerge: settings.bestQuality || settings.advancedOptions, + expectConvert: settings.convertEnabled, + }; +} diff --git a/src/utils/ipcValidation.ts b/src/utils/ipcValidation.ts index aa35eee..a396905 100644 --- a/src/utils/ipcValidation.ts +++ b/src/utils/ipcValidation.ts @@ -312,6 +312,7 @@ function isValidSettingsKey(key: string): key is keyof Settings { key === 'animateBackground' || key === 'flatUi' || key === 'notifications' || + key === 'showTaskbarProgress' || key === 'denoReminderDismissed' || key === 'gpuAcceleration' || key === 'gpuType' || @@ -375,6 +376,7 @@ export function validateSettingsPatchPayload(value: unknown): ValidationResult