diff --git a/.eslintignore b/.eslintignore index 97bae8d4f0..c91110d8ae 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ .eslintrc.js rollup.main.config.ts rollup.preload.config.ts +rollup.renderer.config.ts diff --git a/package.json b/package.json index 50e8a50129..59a90f8f11 100644 --- a/package.json +++ b/package.json @@ -89,9 +89,10 @@ "scripts": { "test": "playwright test", "test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test", + "rollup:renderer": "rollup -c rollup.renderer.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", "rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", "rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", - "build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main", + "build": "yarpm-pnpm run rollup:renderer && yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main", "start": "yarpm-pnpm run build && electron ./dist/index.js", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start", "clean": "del-cli dist && del-cli pack", @@ -113,7 +114,7 @@ }, "pnpm": { "overrides": { - "rollup": "4.2.0", + "rollup": "4.3.0", "node-gyp": "10.0.1", "xml2js": "0.6.2", "node-fetch": "3.3.2", @@ -122,7 +123,7 @@ } }, "overrides": { - "rollup": "4.2.0", + "rollup": "4.3.0", "node-gyp": "10.0.1", "xml2js": "0.6.2", "node-fetch": "3.3.2", @@ -130,8 +131,9 @@ "@babel/runtime": "7.23.2" }, "dependencies": { - "@cliqz/adblocker-electron": "1.26.10", - "@cliqz/adblocker-electron-preload": "1.26.10", + "@cliqz/adblocker-electron": "1.26.11", + "@cliqz/adblocker-electron-preload": "1.26.11", + "@fastify/deepmerge": "1.3.0", "@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/main": "0.12.0", "@foobar404/wave": "2.0.4", @@ -181,12 +183,12 @@ "electron": "27.0.3", "electron-builder": "24.6.4", "electron-devtools-installer": "3.2.0", - "eslint": "8.52.0", + "eslint": "8.53.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-prettier": "5.0.1", "node-gyp": "10.0.1", "playwright": "1.39.0", - "rollup": "4.2.0", + "rollup": "4.3.0", "rollup-plugin-copy": "3.5.0", "rollup-plugin-import-css": "3.3.5", "rollup-plugin-string": "3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac1a33f824..7a58067689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - rollup: 4.2.0 + rollup: 4.3.0 node-gyp: 10.0.1 xml2js: 0.6.2 node-fetch: 3.3.2 @@ -14,11 +14,14 @@ overrides: dependencies: '@cliqz/adblocker-electron': - specifier: 1.26.10 - version: 1.26.10(electron@27.0.3) + specifier: 1.26.11 + version: 1.26.11(electron@27.0.3) '@cliqz/adblocker-electron-preload': - specifier: 1.26.10 - version: 1.26.10(electron@27.0.3) + specifier: 1.26.11 + version: 1.26.11(electron@27.0.3) + '@fastify/deepmerge': + specifier: 1.3.0 + version: 1.3.0 '@ffmpeg.wasm/core-mt': specifier: 0.12.0 version: 0.12.0 @@ -110,25 +113,25 @@ devDependencies: version: 1.39.0 '@rollup/plugin-commonjs': specifier: 25.0.7 - version: 25.0.7(rollup@4.2.0) + version: 25.0.7(rollup@4.3.0) '@rollup/plugin-image': specifier: 3.0.3 - version: 3.0.3(rollup@4.2.0) + version: 3.0.3(rollup@4.3.0) '@rollup/plugin-json': specifier: 6.0.1 - version: 6.0.1(rollup@4.2.0) + version: 6.0.1(rollup@4.3.0) '@rollup/plugin-node-resolve': specifier: 15.2.3 - version: 15.2.3(rollup@4.2.0) + version: 15.2.3(rollup@4.3.0) '@rollup/plugin-terser': specifier: 0.4.4 - version: 0.4.4(rollup@4.2.0) + version: 0.4.4(rollup@4.3.0) '@rollup/plugin-typescript': specifier: 11.1.5 - version: 11.1.5(rollup@4.2.0)(typescript@5.2.2) + version: 11.1.5(rollup@4.3.0)(typescript@5.2.2) '@rollup/plugin-wasm': specifier: 6.2.2 - version: 6.2.2(rollup@4.2.0) + version: 6.2.2(rollup@4.3.0) '@total-typescript/ts-reset': specifier: 0.5.1 version: 0.5.1 @@ -143,7 +146,7 @@ devDependencies: version: 9.0.3 '@typescript-eslint/eslint-plugin': specifier: 6.9.1 - version: 6.9.1(@typescript-eslint/parser@6.7.5)(eslint@8.52.0)(typescript@5.2.2) + version: 6.9.1(@typescript-eslint/parser@6.7.5)(eslint@8.53.0)(typescript@5.2.2) builtin-modules: specifier: ^3.3.0 version: 3.3.0 @@ -163,14 +166,14 @@ devDependencies: specifier: 3.2.0 version: 3.2.0 eslint: - specifier: 8.52.0 - version: 8.52.0 + specifier: 8.53.0 + version: 8.53.0 eslint-plugin-import: specifier: 2.29.0 - version: 2.29.0(@typescript-eslint/parser@6.7.5)(eslint@8.52.0) + version: 2.29.0(@typescript-eslint/parser@6.7.5)(eslint@8.53.0) eslint-plugin-prettier: specifier: 5.0.1 - version: 5.0.1(eslint@8.52.0)(prettier@3.0.3) + version: 5.0.1(eslint@8.53.0)(prettier@3.0.3) node-gyp: specifier: 10.0.1 version: 10.0.1 @@ -178,14 +181,14 @@ devDependencies: specifier: 1.39.0 version: 1.39.0 rollup: - specifier: 4.2.0 - version: 4.2.0 + specifier: 4.3.0 + version: 4.3.0 rollup-plugin-copy: specifier: 3.5.0 version: 3.5.0 rollup-plugin-import-css: specifier: 3.3.5 - version: 3.3.5(rollup@4.2.0) + version: 3.3.5(rollup@4.3.0) rollup-plugin-string: specifier: 3.0.0 version: 3.0.0 @@ -240,41 +243,41 @@ packages: regenerator-runtime: 0.14.0 dev: false - /@cliqz/adblocker-content@1.26.10: - resolution: {integrity: sha512-tbN5c+V7Z1XJp27zIhxPBXqSiOSQdzj1B6OJ/sUoGcPOg7GJ6W8Uz6NquI6CkNWJkRhNyLwGWaT+JsQVvdgTqQ==} + /@cliqz/adblocker-content@1.26.11: + resolution: {integrity: sha512-2p4lDiyoadVjUQDe5Pao4C0agFE5HHjXvsWzXEMG9sDP37Ji3krxsBEUzTkQPC8CuWmOjbzsrUu9rYjKOGupaA==} dependencies: - '@cliqz/adblocker-extended-selectors': 1.26.10 + '@cliqz/adblocker-extended-selectors': 1.26.11 dev: false - /@cliqz/adblocker-electron-preload@1.26.10(electron@27.0.3): - resolution: {integrity: sha512-KeescoTX9nH4fAnAIg6rO/3tU90nYJkh591Ilt2fnX+ECK7yO+BlTIogBaJD7BSQxj2EZJJl/7d63Fld84JCrg==} + /@cliqz/adblocker-electron-preload@1.26.11(electron@27.0.3): + resolution: {integrity: sha512-sqv/dsFztDdnis+GOhZdsOxoBggHgFuEOgsd5ht5jbEXWBSjtw4z6dHJP9p1XBsBn1CgOh9aIKHtieoMAe+mYg==} peerDependencies: electron: '>11' dependencies: - '@cliqz/adblocker-content': 1.26.10 + '@cliqz/adblocker-content': 1.26.11 electron: 27.0.3 dev: false - /@cliqz/adblocker-electron@1.26.10(electron@27.0.3): - resolution: {integrity: sha512-FgXKI2JKw2E4/2LOrFfGkjm8YVucqGWpUNwV4io7WdOftxa2WKkJvc5MJIFXYw6xfJHw9XIuYoKXIA9nswlJ3g==} + /@cliqz/adblocker-electron@1.26.11(electron@27.0.3): + resolution: {integrity: sha512-ekb9YIq6tcXsmiiuy7wgm6VtSc2jCCV0dOWw/1KRg1VzVPd6a1+X0b1ziSYVs0wEiWfFyvPQSZaUu9ATfGcyDA==} peerDependencies: electron: '>11' dependencies: - '@cliqz/adblocker': 1.26.10 - '@cliqz/adblocker-electron-preload': 1.26.10(electron@27.0.3) + '@cliqz/adblocker': 1.26.11 + '@cliqz/adblocker-electron-preload': 1.26.11(electron@27.0.3) electron: 27.0.3 tldts-experimental: 6.0.16 dev: false - /@cliqz/adblocker-extended-selectors@1.26.10: - resolution: {integrity: sha512-ElSkgMM8z83AZQJQcyAAaJ/MojkAcHEYKuZTEA5KbjSNM/7hYoQMwvaMuc/KscNbDuED85+GKwmY4LrpwDAYQA==} + /@cliqz/adblocker-extended-selectors@1.26.11: + resolution: {integrity: sha512-jSnvM0LlPKPiksyUGYYZzMlC2wewUnG8/yw+WHHVqHdiaYBNz1x4wn1xfaCIh1Ee9cqGRbIEppFIO97raTxwFQ==} dev: false - /@cliqz/adblocker@1.26.10: - resolution: {integrity: sha512-aZjo0BYMDwU0OzU7Jeo42RNeYL9ocn8/xxC1UYiSZ71sQwMZG2i3YmL/monLJIVo9qP0GdTSwvthvpHHhJyHQQ==} + /@cliqz/adblocker@1.26.11: + resolution: {integrity: sha512-rrMgT+F0Wf1Rdo7RINVzWY111swOe9PqT625n5EcS95smN/E8JKIAjL3xaucjJ/gai+VoHkxJTKKRQAzmr612A==} dependencies: - '@cliqz/adblocker-content': 1.26.10 - '@cliqz/adblocker-extended-selectors': 1.26.10 + '@cliqz/adblocker-content': 1.26.11 + '@cliqz/adblocker-extended-selectors': 1.26.11 '@remusao/guess-url-type': 1.2.1 '@remusao/small': 1.2.1 '@remusao/smaz': 1.9.1 @@ -358,13 +361,13 @@ packages: - supports-color dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.52.0 + eslint: 8.53.0 eslint-visitor-keys: 3.4.3 dev: true @@ -373,8 +376,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.2: - resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -390,8 +393,8 @@ packages: - supports-color dev: true - /@eslint/js@8.52.0: - resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -400,6 +403,10 @@ packages: engines: {node: '>=14'} dev: false + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: false + /@ffmpeg.wasm/core-mt@0.12.0: resolution: {integrity: sha512-M9pjL7JQX4AYl3WI8vGcPGPTz/O7JmhW8ac/fHA3oXTxoRAPwYSY/OsY1N9C0XahIM0+fxa1QSLN9Ekz8sBM/Q==} dev: false @@ -620,89 +627,89 @@ packages: resolution: {integrity: sha512-yvwa+aCyYI/UjeD39BnpMypG8N06l86wIDW1/PAc6ihBRnodIfZDwccxQN3n1t74wduzaz74m4ZMHZnB06567Q==} dev: false - /@rollup/plugin-commonjs@25.0.7(rollup@4.2.0): + /@rollup/plugin-commonjs@25.0.7(rollup@4.3.0): resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) commondir: 1.0.1 estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.5 - rollup: 4.2.0 + rollup: 4.3.0 dev: true - /@rollup/plugin-image@3.0.3(rollup@4.2.0): + /@rollup/plugin-image@3.0.3(rollup@4.3.0): resolution: {integrity: sha512-qXWQwsXpvD4trSb8PeFPFajp8JLpRtqqOeNYRUKnEQNHm7e5UP7fuSRcbjQAJ7wDZBbnJvSdY5ujNBQd9B1iFg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) mini-svg-data-uri: 1.4.4 - rollup: 4.2.0 + rollup: 4.3.0 dev: true - /@rollup/plugin-json@6.0.1(rollup@4.2.0): + /@rollup/plugin-json@6.0.1(rollup@4.3.0): resolution: {integrity: sha512-RgVfl5hWMkxN1h/uZj8FVESvPuBJ/uf6ly6GTj0GONnkfoBN5KC0MSz+PN2OLDgYXMhtG0mWpTrkiOjoxAIevw==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) - rollup: 4.2.0 + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) + rollup: 4.3.0 dev: true - /@rollup/plugin-node-resolve@15.2.3(rollup@4.2.0): + /@rollup/plugin-node-resolve@15.2.3(rollup@4.3.0): resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 - rollup: 4.2.0 + rollup: 4.3.0 dev: true - /@rollup/plugin-terser@0.4.4(rollup@4.2.0): + /@rollup/plugin-terser@0.4.4(rollup@4.3.0): resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - rollup: 4.2.0 + rollup: 4.3.0 serialize-javascript: 6.0.1 smob: 1.4.1 terser: 5.21.0 dev: true - /@rollup/plugin-typescript@11.1.5(rollup@4.2.0)(typescript@5.2.2): + /@rollup/plugin-typescript@11.1.5(rollup@4.3.0)(typescript@5.2.2): resolution: {integrity: sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 tslib: '*' typescript: '>=3.7.0' peerDependenciesMeta: @@ -711,30 +718,30 @@ packages: tslib: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) resolve: 1.22.8 - rollup: 4.2.0 + rollup: 4.3.0 typescript: 5.2.2 dev: true - /@rollup/plugin-wasm@6.2.2(rollup@4.2.0): + /@rollup/plugin-wasm@6.2.2(rollup@4.3.0): resolution: {integrity: sha512-gpC4R1G9Ni92ZIRTexqbhX7U+9estZrbhP+9SRb0DW9xpB9g7j34r+J2hqrcW/lRI7dJaU84MxZM0Rt82tqYPQ==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) - rollup: 4.2.0 + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) + rollup: 4.3.0 dev: true - /@rollup/pluginutils@5.0.5(rollup@4.2.0): + /@rollup/pluginutils@5.0.5(rollup@4.3.0): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 peerDependenciesMeta: rollup: optional: true @@ -742,99 +749,99 @@ packages: '@types/estree': 1.0.2 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.2.0 + rollup: 4.3.0 dev: true - /@rollup/rollup-android-arm-eabi@4.2.0: - resolution: {integrity: sha512-8PlggAxGxavr+pkCNeV1TM2wTb2o+cUWDg9M1cm9nR27Dsn287uZtSLYXoQqQcmq+sYfF7lHfd3sWJJinH9GmA==} + /@rollup/rollup-android-arm-eabi@4.3.0: + resolution: {integrity: sha512-/4pns6BYi8MXdwnXM44yoGAcFYVHL/BYlB2q1HXZ6AzH++LaiEVWFpBWQ/glXhbMbv3E3o09igrHFbP/snhAvA==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.2.0: - resolution: {integrity: sha512-+71T85hbMFrJI+zKQULNmSYBeIhru55PYoF/u75MyeN2FcxE4HSPw20319b+FcZ4lWx2Nx/Ql9tN+hoaD3GH/A==} + /@rollup/rollup-android-arm64@4.3.0: + resolution: {integrity: sha512-nLO/JsL9idr416vzi3lHm3Xm+QZh4qHij8k3Er13kZr5YhL7/+kBAx84kDmPc7HMexLmwisjDCeDIKNFp8mDlQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.2.0: - resolution: {integrity: sha512-IIIQLuG43QIElT1JZqUP/zqIdiJl4t9U/boa0GZnQTw9m1X0k3mlBuysbgYXeloLT1RozdL7bgw4lpSaI8GOXw==} + /@rollup/rollup-darwin-arm64@4.3.0: + resolution: {integrity: sha512-dGhVBlllt4iHwTGy21IEoMOTN5wZoid19zEIxsdY29xcEiOEHqzDa7Sqrkh5OE7LKCowL61eFJXxYe/+pYa7ZQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.2.0: - resolution: {integrity: sha512-BXcXvnLaea1Xz900omrGJhxHFJfH9jZ0CpJuVsbjjhpniJ6qiLXz3xA8Lekaa4MuhFcJd4f0r+Ky1G4VFbYhWw==} + /@rollup/rollup-darwin-x64@4.3.0: + resolution: {integrity: sha512-h8wRfHeLEbU3NzaP1Oku7BYXCJQiTRr+8U0lklyOQXxXiEpHLL8tk1hFl+tezoRKLcPJD7joKaK74ASsqt3Ekg==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.2.0: - resolution: {integrity: sha512-f4K3MKw9Y4AKi4ANGnmPIglr+S+8tO858YrGVuqAHXxJdVghBmz9CPU9kDpOnGvT4g4vg5uNyIFpOOFvffXyMA==} + /@rollup/rollup-linux-arm-gnueabihf@4.3.0: + resolution: {integrity: sha512-wP4VgR/gfV18sylTuym3sxRTkAgUR2vh6YLeX/GEznk5jCYcYSlx585XlcUcl0c8UffIZlRJ09raWSX3JDb4GA==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.2.0: - resolution: {integrity: sha512-bNsTYQBgp4H7w6cT7FZhesxpcUPahsSIy4NgdZjH1ZwEoZHxi4XKglj+CsSEkhsKi+x6toVvMylhjRKhEMYfnA==} + /@rollup/rollup-linux-arm64-gnu@4.3.0: + resolution: {integrity: sha512-v/14JCYVkqRSJeQbxFx4oUkwVQQw6lFMN7bd4vuARBc3X2lmomkxBsc+BFiIDL/BK+CTx5AOh/k9XmqDnKWRVg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.2.0: - resolution: {integrity: sha512-Jp1NxBJpGLuxRU2ihrQk4IZ+ia5nffobG6sOFUPW5PMYkF0kQtxEbeDuCa69Xif211vUOcxlOnf5IOEIpTEySA==} + /@rollup/rollup-linux-arm64-musl@4.3.0: + resolution: {integrity: sha512-tNhfYqFH5OxtRzfkTOKdgFYlPSZnlDLNW4+leNEvQZhwTJxoTwsZAAhR97l3qVry/kkLyJPBK+Q8EAJLPinDIg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.2.0: - resolution: {integrity: sha512-3p3iRtQmv2aXw+vtKNyZMLOQ+LSRsqArXjKAh2Oj9cqwfIRe7OXvdkOzWfZOIp1F/x5KJzVAxGxnniF4cMbnsQ==} + /@rollup/rollup-linux-x64-gnu@4.3.0: + resolution: {integrity: sha512-pw77m8QywdsoFdFOgmc8roF1inBI0rciqzO8ffRUgLoq7+ee9o5eFqtEcS6hHOOplgifAUUisP8cAnwl9nUYPw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.2.0: - resolution: {integrity: sha512-atih7IF/reUZe4LBLC5Izd44hth2tfDIG8LaPp4/cQXdHh9jabcZEvIeRPrpDq0i/Uu487Qu5gl5KwyAnWajnw==} + /@rollup/rollup-linux-x64-musl@4.3.0: + resolution: {integrity: sha512-tJs7v2MnV2F8w6X1UpPHl/43OfxjUy9SuJ2ZPoxn79v9vYteChVYO/ueLHCpRMmyTUIVML3N9z4azl9ENH8Xxg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.2.0: - resolution: {integrity: sha512-vYxF3tKJeUE4ceYzpNe2p84RXk/fGK30I8frpRfv/MyPStej/mRlojztkN7Jtd1014HHVeq/tYaMBz/3IxkxZw==} + /@rollup/rollup-win32-arm64-msvc@4.3.0: + resolution: {integrity: sha512-OKGxp6kATQdTyI2DF+e9s+hB3/QZB45b6e+dzcfW1SUqiF6CviWyevhmT4USsMEdP3mlpC9zxLz3Oh+WaTMOSw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.2.0: - resolution: {integrity: sha512-1LZJ6zpl93SaPQvas618bMFarVwufWTaczH4ESAbFcwiC4OtznA6Ym+hFPyIGaJaGEB8uMWWac0uXGPXOg5FGA==} + /@rollup/rollup-win32-ia32-msvc@4.3.0: + resolution: {integrity: sha512-DDZ5AH68JJ2ClQFEA1aNnfA7Ybqyeh0644rGbrLOdNehTmzfICHiWSn0OprzYi9HAshTPQvlwrM+bi2kuaIOjQ==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.2.0: - resolution: {integrity: sha512-dgQfFdHCNg08nM5zBmqxqc9vrm0DVzhWotpavbPa0j4//MAOKZEB75yGAfzQE9fUJ+4pvM1239Y4IhL8f6sSog==} + /@rollup/rollup-win32-x64-msvc@4.3.0: + resolution: {integrity: sha512-dMvGV8p92GQ8jhNlGIKpyhVZPzJlT258pPrM5q2F8lKcc9Iv9BbfdnhX1OfinYWnb9ms5zLw6MlaMnqLfUkKnQ==} cpu: [x64] os: [win32] requiresBuild: true @@ -1020,7 +1027,7 @@ packages: '@types/node': 20.8.6 optional: true - /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.7.5)(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.7.5)(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1032,13 +1039,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.7.5(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/type-utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.9.1 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -1049,7 +1056,7 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.7.5(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.7.5(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1064,7 +1071,7 @@ packages: '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -1086,7 +1093,7 @@ packages: '@typescript-eslint/visitor-keys': 6.9.1 dev: true - /@typescript-eslint/type-utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.9.1(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1097,9 +1104,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -1158,19 +1165,19 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.9.1(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@types/json-schema': 7.0.13 '@types/semver': 7.5.3 '@typescript-eslint/scope-manager': 6.9.1 '@typescript-eslint/types': 6.9.1 '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - eslint: 8.52.0 + eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -2484,7 +2491,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint@8.52.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -2505,15 +2512,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.7.5(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.53.0)(typescript@5.2.2) debug: 3.2.7 - eslint: 8.52.0 + eslint: 8.53.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.7.5)(eslint@8.52.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.7.5)(eslint@8.53.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -2523,16 +2530,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.5(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.53.0)(typescript@5.2.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.52.0 + eslint: 8.53.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint@8.52.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -2548,7 +2555,7 @@ packages: - supports-color dev: true - /eslint-plugin-prettier@5.0.1(eslint@8.52.0)(prettier@3.0.3): + /eslint-plugin-prettier@5.0.1(eslint@8.53.0)(prettier@3.0.3): resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -2562,7 +2569,7 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.52.0 + eslint: 8.53.0 prettier: 3.0.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 @@ -2581,15 +2588,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.52.0: - resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@eslint-community/regexpp': 4.9.1 - '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.52.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -4624,14 +4631,14 @@ packages: is-plain-object: 3.0.1 dev: true - /rollup-plugin-import-css@3.3.5(rollup@4.2.0): + /rollup-plugin-import-css@3.3.5(rollup@4.3.0): resolution: {integrity: sha512-wSfzveEzvUDlVevo70kmVD5Mk785UN55NG4C7VVnrmdE0qZ8apcVVFajyCPfFYSNxq5YkccOcrGUT2T/2HnEcQ==} engines: {node: '>=16'} peerDependencies: - rollup: 4.2.0 + rollup: 4.3.0 dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.2.0) - rollup: 4.2.0 + '@rollup/pluginutils': 5.0.5(rollup@4.3.0) + rollup: 4.3.0 dev: true /rollup-plugin-string@3.0.0: @@ -4646,23 +4653,23 @@ packages: estree-walker: 0.6.1 dev: true - /rollup@4.2.0: - resolution: {integrity: sha512-deaMa9Z+jPVeBD2dKXv+h7EbdKte9++V2potc/ADqvVgEr6DEJ3ia9u0joarjC2lX/ubaCRYz3QVx0TzuVqAJA==} + /rollup@4.3.0: + resolution: {integrity: sha512-scIi1NrKLDIYSPK66jjECtII7vIgdAMFmFo8h6qm++I6nN9qDSV35Ku6erzGVqYjx+lj+j5wkusRMr++8SyDZg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.2.0 - '@rollup/rollup-android-arm64': 4.2.0 - '@rollup/rollup-darwin-arm64': 4.2.0 - '@rollup/rollup-darwin-x64': 4.2.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.2.0 - '@rollup/rollup-linux-arm64-gnu': 4.2.0 - '@rollup/rollup-linux-arm64-musl': 4.2.0 - '@rollup/rollup-linux-x64-gnu': 4.2.0 - '@rollup/rollup-linux-x64-musl': 4.2.0 - '@rollup/rollup-win32-arm64-msvc': 4.2.0 - '@rollup/rollup-win32-ia32-msvc': 4.2.0 - '@rollup/rollup-win32-x64-msvc': 4.2.0 + '@rollup/rollup-android-arm-eabi': 4.3.0 + '@rollup/rollup-android-arm64': 4.3.0 + '@rollup/rollup-darwin-arm64': 4.3.0 + '@rollup/rollup-darwin-x64': 4.3.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.3.0 + '@rollup/rollup-linux-arm64-gnu': 4.3.0 + '@rollup/rollup-linux-arm64-musl': 4.3.0 + '@rollup/rollup-linux-x64-gnu': 4.3.0 + '@rollup/rollup-linux-x64-musl': 4.3.0 + '@rollup/rollup-win32-arm64-msvc': 4.3.0 + '@rollup/rollup-win32-ia32-msvc': 4.3.0 + '@rollup/rollup-win32-x64-msvc': 4.3.0 fsevents: 2.3.3 dev: true diff --git a/rollup.renderer.config.ts b/rollup.renderer.config.ts new file mode 100644 index 0000000000..c363a17e9f --- /dev/null +++ b/rollup.renderer.config.ts @@ -0,0 +1,57 @@ +import { defineConfig } from 'rollup'; +import builtinModules from 'builtin-modules'; +import typescript from '@rollup/plugin-typescript'; +import commonjs from '@rollup/plugin-commonjs'; +import nodeResolvePlugin from '@rollup/plugin-node-resolve'; +import json from '@rollup/plugin-json'; +import terser from '@rollup/plugin-terser'; +import { string } from 'rollup-plugin-string'; +import css from 'rollup-plugin-import-css'; +import wasmPlugin from '@rollup/plugin-wasm'; +import image from '@rollup/plugin-image'; + +export default defineConfig({ + plugins: [ + typescript({ + module: 'ESNext', + }), + nodeResolvePlugin({ + browser: false, + preferBuiltins: true, + }), + commonjs({ + ignoreDynamicRequires: true, + }), + json(), + string({ + include: '**/*.html', + }), + css(), + wasmPlugin({ + maxFileSize: 0, + targetEnv: 'browser', + }), + image({ dom: true }), + terser({ + ecma: 2020, + }), + { + closeBundle() { + if (!process.env.ROLLUP_WATCH) { + setTimeout(() => process.exit(0)); + } + }, + name: 'force-close' + }, + ], + input: './src/renderer.ts', + output: { + format: 'cjs', + name: '[name].js', + dir: './dist', + }, + external: [ + 'electron', + ...builtinModules, + ], +}); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 6b49c36aad..04946e6fc0 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -172,7 +172,7 @@ const defaultConfig = { 'pip-position': [10, 10], 'pip-size': [450, 275], 'isInPiP': false, - 'useNativePiP': false, + 'useNativePiP': true, }, 'playback-speed': {}, 'precise-volume': { diff --git a/src/config/dynamic-renderer.ts b/src/config/dynamic-renderer.ts new file mode 100644 index 0000000000..4458a178cb --- /dev/null +++ b/src/config/dynamic-renderer.ts @@ -0,0 +1,182 @@ +import defaultConfig from './defaults'; + +import { Entries } from '../utils/type-utils'; + +import type { OneOfDefaultConfigKey, ConfigType, PluginConfigOptions } from './dynamic'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig } = {}; + +export const getActivePlugins + = async () => await window.ipcRenderer.invoke('get-active-plugins') as Promise; + +export const isActive + = async (plugin: string) => plugin in (await window.ipcRenderer.invoke('get-active-plugins')); + +/** + * This class is used to create a dynamic synced config for plugins. + * + * @param {string} name - The name of the plugin. + * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false. + * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store. + * + * @example + * const { PluginConfig } = require("../../config/dynamic"); + * const config = new PluginConfig("plugin-name", { enableFront: true }); + * module.exports = { ...config }; + * + * // or + * + * module.exports = (win, options) => { + * const config = new PluginConfig("plugin-name", { + * enableFront: true, + * initialOptions: options, + * }); + * setupMyPlugin(win, config); + * }; + */ +type ValueOf = T[keyof T]; + +export class PluginConfig { + private readonly name: string; + private readonly config: ConfigType; + private readonly defaultConfig: ConfigType; + private readonly enableFront: boolean; + + private subscribers: { [key in keyof ConfigType]?: (config: ConfigType) => void } = {}; + private allSubscribers: ((config: ConfigType) => void)[] = []; + + constructor( + name: T, + options: PluginConfigOptions = { + enableFront: false, + }, + ) { + const pluginDefaultConfig = defaultConfig.plugins[name] ?? {}; + const pluginConfig = options.initialOptions || window.mainConfig.plugins.getOptions(name) || {}; + + this.name = name; + this.enableFront = options.enableFront; + this.defaultConfig = pluginDefaultConfig; + this.config = { ...pluginDefaultConfig, ...pluginConfig }; + + if (this.enableFront) { + this.setupFront(); + } + + activePlugins[name] = this; + } + + get = keyof ConfigType>(key: Key): ConfigType[Key] { + return this.config?.[key]; + } + + set(key: keyof ConfigType, value: ValueOf>) { + this.config[key] = value; + this.onChange(key); + this.save(); + } + + getAll(): ConfigType { + return { ...this.config }; + } + + setAll(options: Partial>) { + if (!options || typeof options !== 'object') { + throw new Error('Options must be an object.'); + } + + let changed = false; + for (const [key, value] of Object.entries(options) as Entries) { + if (this.config[key] !== value) { + if (value !== undefined) this.config[key] = value; + this.onChange(key, false); + changed = true; + } + } + + if (changed) { + for (const fn of this.allSubscribers) { + fn(this.config); + } + } + + this.save(); + } + + getDefaultConfig() { + return this.defaultConfig; + } + + /** + * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true` + * + * Used for options that require a restart to take effect. + */ + setAndMaybeRestart(key: keyof ConfigType, value: ValueOf>) { + this.config[key] = value; + window.mainConfig.plugins.setMenuOptions(this.name, this.config); + this.onChange(key); + } + + subscribe(valueName: keyof ConfigType, fn: (config: ConfigType) => void) { + this.subscribers[valueName] = fn; + } + + subscribeAll(fn: (config: ConfigType) => void) { + this.allSubscribers.push(fn); + } + + /** Called only from back */ + private save() { + window.mainConfig.plugins.setOptions(this.name, this.config); + } + + private onChange(valueName: keyof ConfigType, single: boolean = true) { + this.subscribers[valueName]?.(this.config[valueName] as ConfigType); + if (single) { + for (const fn of this.allSubscribers) { + fn(this.config); + } + } + } + + private setupFront() { + const ignoredMethods = ['subscribe', 'subscribeAll']; + + for (const [fnName, fn] of Object.entries(this) as Entries) { + if (typeof fn !== 'function' || fn.name in ignoredMethods) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return + this[fnName] = (async (...args: any) => await window.ipcRenderer.invoke( + `${this.name}-config-${String(fnName)}`, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ...args, + )) as typeof this[keyof this]; + + this.subscribe = (valueName, fn: (config: ConfigType) => void) => { + if (valueName in this.subscribers) { + console.error(`Already subscribed to ${String(valueName)}`); + } + + this.subscribers[valueName] = fn; + window.ipcRenderer.on( + `${this.name}-config-changed-${String(valueName)}`, + (_, value: ConfigType) => { + fn(value); + }, + ); + window.ipcRenderer.send(`${this.name}-config-subscribe`, valueName); + }; + + this.subscribeAll = (fn: (config: ConfigType) => void) => { + window.ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType) => { + fn(value); + }); + window.ipcRenderer.send(`${this.name}-config-subscribe-all`); + }; + } + } +} diff --git a/src/config/dynamic.ts b/src/config/dynamic.ts index e9381bca72..12d34f1970 100644 --- a/src/config/dynamic.ts +++ b/src/config/dynamic.ts @@ -1,12 +1,9 @@ -/* eslint-disable @typescript-eslint/require-await */ - -import { ipcMain, ipcRenderer } from 'electron'; +import { ipcMain } from 'electron'; import defaultConfig from './defaults'; import { getOptions, setMenuOptions, setOptions } from './plugins'; - import { sendToFront } from '../providers/app-controls'; import { Entries } from '../utils/type-utils'; @@ -17,30 +14,15 @@ export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfig // eslint-disable-next-line @typescript-eslint/no-explicit-any const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig } = {}; -/** - * [!IMPORTANT!] - * The method is **sync** in the main process and **async** in the renderer process. - */ -export const getActivePlugins - = process.type === 'renderer' - ? async () => ipcRenderer.invoke('get-active-plugins') - : () => activePlugins; +export const getActivePlugins = () => activePlugins; if (process.type === 'browser') { ipcMain.handle('get-active-plugins', getActivePlugins); } -/** - * [!IMPORTANT!] - * The method is **sync** in the main process and **async** in the renderer process. - */ -export const isActive - = process.type === 'renderer' - ? async (plugin: string) => - plugin in (await ipcRenderer.invoke('get-active-plugins')) - : (plugin: string): boolean => plugin in activePlugins; +export const isActive = (plugin: string): boolean => plugin in activePlugins; -interface PluginConfigOptions { +export interface PluginConfigOptions { enableFront: boolean; initialOptions?: OneOfDefaultConfig; } @@ -48,9 +30,6 @@ interface PluginConfigOptions { /** * This class is used to create a dynamic synced config for plugins. * - * [!IMPORTANT!] - * The methods are **sync** in the main process and **async** in the renderer process. - * * @param {string} name - The name of the plugin. * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false. * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store. @@ -72,7 +51,7 @@ interface PluginConfigOptions { */ export type ConfigType = typeof defaultConfig.plugins[T]; type ValueOf = T[keyof T]; -type Mode = Mode extends 'r' ? Promise : T; + export class PluginConfig { private readonly name: string; private readonly config: ConfigType; @@ -180,62 +159,25 @@ export class PluginConfig { private setupFront() { const ignoredMethods = ['subscribe', 'subscribeAll']; - if (process.type === 'renderer') { - for (const [fnName, fn] of Object.entries(this) as Entries) { - if (typeof fn !== 'function' || fn.name in ignoredMethods) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return - this[fnName] = (async (...args: any) => await ipcRenderer.invoke( - `${this.name}-config-${String(fnName)}`, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ...args, - )) as typeof this[keyof this]; - - this.subscribe = (valueName, fn: (config: ConfigType) => void) => { - if (valueName in this.subscribers) { - console.error(`Already subscribed to ${String(valueName)}`); - } - - this.subscribers[valueName] = fn; - ipcRenderer.on( - `${this.name}-config-changed-${String(valueName)}`, - (_, value: ConfigType) => { - fn(value); - }, - ); - ipcRenderer.send(`${this.name}-config-subscribe`, valueName); - }; - - this.subscribeAll = (fn: (config: ConfigType) => void) => { - ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType) => { - fn(value); - }); - ipcRenderer.send(`${this.name}-config-subscribe-all`); - }; - } - } else if (process.type === 'browser') { - for (const [fnName, fn] of Object.entries(this) as Entries) { - if (typeof fn !== 'function' || fn.name in ignoredMethods) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return - ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args)); + for (const [fnName, fn] of Object.entries(this) as Entries) { + if (typeof fn !== 'function' || fn.name in ignoredMethods) { + return; } - ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType) => { - this.subscribe(valueName, (value) => { - sendToFront(`${this.name}-config-changed-${String(valueName)}`, value); - }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return + ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args)); + } + + ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType) => { + this.subscribe(valueName, (value) => { + sendToFront(`${this.name}-config-changed-${String(valueName)}`, value); }); + }); - ipcMain.on(`${this.name}-config-subscribe-all`, () => { - this.subscribeAll((value) => { - sendToFront(`${this.name}-config-changed`, value); - }); + ipcMain.on(`${this.name}-config-subscribe-all`, () => { + this.subscribeAll((value) => { + sendToFront(`${this.name}-config-changed`, value); }); - } + }); } } diff --git a/src/config/plugins.ts b/src/config/plugins.ts index c2357f6ff8..3092b1d211 100644 --- a/src/config/plugins.ts +++ b/src/config/plugins.ts @@ -1,3 +1,5 @@ +import { deepmerge } from '@fastify/deepmerge'; + import store from './store'; import defaultConfig from './defaults'; @@ -9,11 +11,12 @@ interface Plugin { } type DefaultPluginsConfig = typeof defaultConfig.plugins; +const deepmergeFn = deepmerge(); export function getEnabled() { - const plugins = store.get('plugins') as DefaultPluginsConfig; - return (Object.entries(plugins) as Entries).filter(([plugin]) => - isEnabled(plugin), + const plugins = deepmergeFn(defaultConfig.plugins, (store.get('plugins') as DefaultPluginsConfig)); + return (Object.entries(plugins) as Entries).filter(([, options]) => + (options as Plugin).enabled, ); } diff --git a/src/index.ts b/src/index.ts index f5eaaaed50..ab88b559d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import path from 'node:path'; +import url from 'node:url'; +import fs from 'node:fs'; import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron'; import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request'; @@ -195,11 +197,8 @@ async function createMainWindow() { backgroundColor: '#000', show: false, webPreferences: { - // TODO: re-enable contextIsolation once it can work with FFMpeg.wasm - // Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126 - contextIsolation: false, + contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - nodeIntegrationInSubFrames: true, ...(isTesting() ? undefined : { @@ -335,6 +334,14 @@ async function createMainWindow() { removeContentSecurityPolicy(); + win.webContents.on('dom-ready', () => { + const rendererScriptPath = path.join(__dirname, 'renderer.js'); + win.webContents.executeJavaScriptInIsolatedWorld(0, [{ + code: fs.readFileSync(rendererScriptPath, 'utf-8') + ';0', + url: url.pathToFileURL(rendererScriptPath).toString(), + }], true); + }); + win.webContents.loadURL(urlToLoad); return win; diff --git a/src/plugins/adblocker/config.ts b/src/plugins/adblocker/config.ts index d646b0e2d2..0ff54eb325 100644 --- a/src/plugins/adblocker/config.ts +++ b/src/plugins/adblocker/config.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/await-thenable */ /* renderer */ import { blockers } from './blocker-types'; diff --git a/src/plugins/album-color-theme/front.ts b/src/plugins/album-color-theme/front.ts index a068bf4705..a07d08fd24 100644 --- a/src/plugins/album-color-theme/front.ts +++ b/src/plugins/album-color-theme/front.ts @@ -1,6 +1,6 @@ import { FastAverageColor } from 'fast-average-color'; -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; function hexToHSL(H: string) { // Convert hex to RGB first diff --git a/src/plugins/ambient-mode/front.ts b/src/plugins/ambient-mode/front.ts index 1fae36406e..227686fe71 100644 --- a/src/plugins/ambient-mode/front.ts +++ b/src/plugins/ambient-mode/front.ts @@ -1,6 +1,4 @@ -import { ipcRenderer } from 'electron'; - -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; export default (config: ConfigType<'ambient-mode'>) => { let interpolationTime = config.interpolationTime; // interpolation time (ms) @@ -30,7 +28,7 @@ export default (config: ConfigType<'ambient-mode'>) => { /* effect */ let lastEffectWorkId: number | null = null; let lastImageData: ImageData | null = null; - + const onSync = () => { if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId); @@ -40,6 +38,7 @@ export default (config: ConfigType<'ambient-mode'>) => { const width = qualityRatio; let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1); if (!Number.isFinite(height)) height = width; + if (!height) return; context.globalAlpha = 1; if (lastImageData) { @@ -50,8 +49,7 @@ export default (config: ConfigType<'ambient-mode'>) => { } context.drawImage(video, 0, 0, width, height); - const nowImageData = context.getImageData(0, 0, width, height); - lastImageData = nowImageData; + lastImageData = context.getImageData(0, 0, width, height); // current image data lastEffectWorkId = null; }); @@ -102,8 +100,8 @@ export default (config: ConfigType<'ambient-mode'>) => { applyVideoAttributes(); }; - ipcRenderer.on('ambient-mode:config-change', onConfigSync); - + window.ipcRenderer.on('ambient-mode:config-change', onConfigSync); + /* hooking */ let canvasInterval: NodeJS.Timeout | null = null; canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); @@ -135,17 +133,17 @@ export default (config: ConfigType<'ambient-mode'>) => { observer.disconnect(); resizeObserver.disconnect(); - ipcRenderer.off('ambient-mode:config-change', onConfigSync); + window.ipcRenderer.removeListener('ambient-mode:config-change', onConfigSync); window.removeEventListener('resize', applyVideoAttributes); wrapper.removeChild(blurCanvas); }; }; - + const playerPage = document.querySelector('#player-page'); const ytmusicAppLayout = document.querySelector('#layout'); - + const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes') { @@ -164,4 +162,4 @@ export default (config: ConfigType<'ambient-mode'>) => { if (playerPage) { observer.observe(playerPage, { attributes: true }); } -}; \ No newline at end of file +}; diff --git a/src/plugins/captions-selector/config-renderer.ts b/src/plugins/captions-selector/config-renderer.ts new file mode 100644 index 0000000000..867ab9dc74 --- /dev/null +++ b/src/plugins/captions-selector/config-renderer.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic-renderer'; + +const configRenderer = new PluginConfig('captions-selector', { enableFront: true }); +export default configRenderer; diff --git a/src/plugins/captions-selector/front.ts b/src/plugins/captions-selector/front.ts index 113e3ef873..95d973ba6f 100644 --- a/src/plugins/captions-selector/front.ts +++ b/src/plugins/captions-selector/front.ts @@ -1,13 +1,8 @@ -/* eslint-disable @typescript-eslint/await-thenable */ -/* renderer */ - -import { ipcRenderer } from 'electron'; - -import configProvider from './config'; +import configProvider from './config-renderer'; import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { YoutubePlayer } from '../../types/youtube-player'; import type { ConfigType } from '../../config/dynamic'; @@ -25,18 +20,17 @@ interface LanguageOptions { vss_id: string; } -let config: ConfigType<'captions-selector'>; +let captionsSelectorConfig: ConfigType<'captions-selector'>; const $ = (selector: string): Element => document.querySelector(selector)!; const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML); -export default async () => { - // RENDERER - config = await configProvider.getAll(); +export default () => { + captionsSelectorConfig = configProvider.getAll(); configProvider.subscribeAll((newConfig) => { - config = newConfig; + captionsSelectorConfig = newConfig; }); document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true }); }; @@ -47,7 +41,7 @@ function setup(api: YoutubePlayer) { let captionTrackList = api.getOption('captions', 'tracklist') ?? []; $('video').addEventListener('srcChanged', () => { - if (config.disableCaptions) { + if (captionsSelectorConfig.disableCaptions) { setTimeout(() => api.unloadModule('captions'), 100); captionsSettingsButton.style.display = 'none'; return; @@ -58,9 +52,9 @@ function setup(api: YoutubePlayer) { setTimeout(() => { captionTrackList = api.getOption('captions', 'tracklist') ?? []; - if (config.autoload && config.lastCaptionsCode) { + if (captionsSelectorConfig.autoload && captionsSelectorConfig.lastCaptionsCode) { api.setOption('captions', 'track', { - languageCode: config.lastCaptionsCode, + languageCode: captionsSelectorConfig.lastCaptionsCode, }); } @@ -82,7 +76,7 @@ function setup(api: YoutubePlayer) { 'None', ]; - currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number; + currentIndex = await window.ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number; if (currentIndex === null) { return; } diff --git a/src/plugins/crossfade/config-renderer.ts b/src/plugins/crossfade/config-renderer.ts new file mode 100644 index 0000000000..d9c1af2783 --- /dev/null +++ b/src/plugins/crossfade/config-renderer.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic-renderer'; + +const config = new PluginConfig('crossfade', { enableFront: true }); +export default config; diff --git a/src/plugins/crossfade/front.ts b/src/plugins/crossfade/front.ts index a38cd713c7..cc96d4e619 100644 --- a/src/plugins/crossfade/front.ts +++ b/src/plugins/crossfade/front.ts @@ -1,13 +1,9 @@ -/* eslint-disable @typescript-eslint/await-thenable */ -/* renderer */ - -import { ipcRenderer } from 'electron'; import { Howl } from 'howler'; // Extracted from https://github.com/bitfasching/VolumeFader import { VolumeFader } from './fader'; -import configProvider from './config'; +import configProvider from './config-renderer'; import defaultConfigs from '../../config/defaults'; @@ -19,11 +15,11 @@ let waitForTransition: Promise; const defaultConfig = defaultConfigs.plugins.crossfade; -let config: ConfigType<'crossfade'>; +let crossfadeConfig: ConfigType<'crossfade'>; -const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number); +const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(crossfadeConfig[key]) || (defaultConfig[key] as number); -const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise; +const getStreamURL = async (videoID: string) => window.ipcRenderer.invoke('audio-url', videoID) as Promise; const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); @@ -119,7 +115,7 @@ const onApiLoaded = () => { return; } - await createAudioForCrossfade(url); + createAudioForCrossfade(url); }); }; @@ -150,11 +146,11 @@ const crossfade = (cb: () => void) => { }); }; -export default async () => { - config = await configProvider.getAll(); +export default () => { + crossfadeConfig = configProvider.getAll(); configProvider.subscribeAll((newConfig) => { - config = newConfig; + crossfadeConfig = newConfig; }); document.addEventListener('apiLoaded', onApiLoaded, { diff --git a/src/plugins/downloader/front.ts b/src/plugins/downloader/front.ts index 1519366b57..ef29862a1e 100644 --- a/src/plugins/downloader/front.ts +++ b/src/plugins/downloader/front.ts @@ -1,10 +1,8 @@ -import { ipcRenderer } from 'electron'; - import downloadHTML from './templates/download.html'; import defaultConfig from '../../config/defaults'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { getSongInfo } from '../../providers/song-info-front'; let menu: Element | null = null; @@ -13,59 +11,55 @@ const downloadButton = ElementFromHtml(downloadHTML); let doneFirstLoad = false; -const menuObserver = new MutationObserver(() => { - if (!menu) { - menu = getSongMenu(); +export default () => { + const menuObserver = new MutationObserver(() => { if (!menu) { - return; + menu = getSongMenu(); + if (!menu) { + return; + } } - } - if (menu.contains(downloadButton)) { - return; - } + if (menu.contains(downloadButton)) { + return; + } - const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; - if (!menuUrl?.includes('watch?') && doneFirstLoad) { - return; - } + const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href; + if (!menuUrl?.includes('watch?') && doneFirstLoad) { + return; + } - menu.prepend(downloadButton); - progress = document.querySelector('#ytmcustom-download'); + menu.prepend(downloadButton); + progress = document.querySelector('#ytmcustom-download'); - if (doneFirstLoad) { - return; - } + if (doneFirstLoad) { + return; + } - setTimeout(() => doneFirstLoad ||= true, 500); -}); + setTimeout(() => doneFirstLoad ||= true, 500); + }); -// TODO: re-enable once contextIsolation is set to true -// contextBridge.exposeInMainWorld("downloader", { -// download: () => { -// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access -(global as any).download = () => { - let videoUrl = getSongMenu() - // Selector of first button which is always "Start Radio" - ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint') - ?.getAttribute('href'); - if (videoUrl) { - if (videoUrl.startsWith('watch?')) { - videoUrl = defaultConfig.url + '/' + videoUrl; - } + window.download = () => { + let videoUrl = getSongMenu() + // Selector of first button which is always "Start Radio" + ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint') + ?.getAttribute('href'); + if (videoUrl) { + if (videoUrl.startsWith('watch?')) { + videoUrl = defaultConfig.url + '/' + videoUrl; + } - if (videoUrl.includes('?playlist=')) { - ipcRenderer.send('download-playlist-request', videoUrl); - return; + if (videoUrl.includes('?playlist=')) { + window.ipcRenderer.send('download-playlist-request', videoUrl); + return; + } + } else { + videoUrl = getSongInfo().url || window.location.href; } - } else { - videoUrl = getSongInfo().url || window.location.href; - } - ipcRenderer.send('download-song', videoUrl); -}; + window.ipcRenderer.send('download-song', videoUrl); + }; -export default () => { document.addEventListener('apiLoaded', () => { menuObserver.observe(document.querySelector('ytmusic-popup-container')!, { childList: true, @@ -73,7 +67,7 @@ export default () => { }); }, { once: true, passive: true }); - ipcRenderer.on('downloader-feedback', (_, feedback: string) => { + window.ipcRenderer.on('downloader-feedback', (_, feedback: string) => { if (progress) { progress.innerHTML = feedback || 'Download'; } else { diff --git a/src/plugins/downloader/style.css b/src/plugins/downloader/style.css index ea721f300e..052838ca1f 100644 --- a/src/plugins/downloader/style.css +++ b/src/plugins/downloader/style.css @@ -1,4 +1,4 @@ -.menu-item { +.ytmd-menu-item { display: var(--ytmusic-menu-item_-_display); height: var(--ytmusic-menu-item_-_height); align-items: var(--ytmusic-menu-item_-_align-items); @@ -6,11 +6,11 @@ cursor: pointer; } -.menu-item > .yt-simple-endpoint:hover { +.ytmd-menu-item > .yt-simple-endpoint:hover { background-color: var(--ytmusic-menu-item-hover-background-color); } -.menu-icon { +.ytmd-menu-item { flex: var(--ytmusic-menu-item-icon_-_flex); margin: var(--ytmusic-menu-item-icon_-_margin); fill: var(--ytmusic-menu-item-icon_-_fill); diff --git a/src/plugins/downloader/templates/download.html b/src/plugins/downloader/templates/download.html index 4079a4f51a..8c50dcbbf4 100644 --- a/src/plugins/downloader/templates/download.html +++ b/src/plugins/downloader/templates/download.html @@ -6,13 +6,13 @@ role="option" tabindex="-1" > - + diff --git a/src/plugins/in-app-menu/back.ts b/src/plugins/in-app-menu/back.ts index 673ced9350..9f5f3f05c4 100644 --- a/src/plugins/in-app-menu/back.ts +++ b/src/plugins/in-app-menu/back.ts @@ -1,6 +1,6 @@ import { register } from 'electron-localshortcut'; -import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron'; +import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron'; import titlebarStyle from './titlebar.css'; @@ -64,4 +64,9 @@ export default (win: BrowserWindow) => { win.on('maximize', () => win.webContents.send('window-maximize')); ipcMain.handle('window-unmaximize', () => win.unmaximize()); win.on('unmaximize', () => win.webContents.send('window-unmaximize')); + + ipcMain.handle('image-path-to-data-url', (_, imagePath: string) => { + const nativeImageIcon = nativeImage.createFromPath(imagePath); + return nativeImageIcon?.toDataURL(); + }); }; diff --git a/src/plugins/in-app-menu/front.ts b/src/plugins/in-app-menu/front.ts index d7f96793e8..885ff4096f 100644 --- a/src/plugins/in-app-menu/front.ts +++ b/src/plugins/in-app-menu/front.ts @@ -1,5 +1,3 @@ -import { ipcRenderer, Menu } from 'electron'; - import { createPanel } from './menu/panel'; import logo from './assets/menu.svg'; @@ -8,8 +6,7 @@ import minimize from './assets/minimize.svg'; import maximize from './assets/maximize.svg'; import unmaximize from './assets/unmaximize.svg'; -import { isEnabled } from '../../config/plugins'; -import config from '../../config'; +import type { Menu } from 'electron'; function $(selector: string) { return document.querySelector(selector); @@ -19,8 +16,8 @@ const isMacOS = navigator.userAgent.includes('Macintosh'); const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; export default async () => { - const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls'); - let hideMenu = config.get('options.hideMenu'); + const hideDOMWindowControls = window.mainConfig.get('plugins.in-app-menu.hideDOMWindowControls'); + let hideMenu = window.mainConfig.get('options.hideMenu'); const titleBar = document.createElement('title-bar'); const navBar = document.querySelector('#nav-bar-background'); let maximizeButton: HTMLButtonElement; @@ -42,7 +39,7 @@ export default async () => { }; logo.onclick = logoClick; - ipcRenderer.on('toggleMenu', logoClick); + window.ipcRenderer.on('toggleMenu', logoClick); if (!isMacOS) titleBar.appendChild(logo); document.body.appendChild(titleBar); @@ -55,10 +52,10 @@ export default async () => { const minimizeButton = document.createElement('button'); minimizeButton.classList.add('window-control'); minimizeButton.appendChild(minimize); - minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize'); + minimizeButton.onclick = () => window.ipcRenderer.invoke('window-minimize'); maximizeButton = document.createElement('button'); - if (await ipcRenderer.invoke('window-is-maximized')) { + if (await window.ipcRenderer.invoke('window-is-maximized')) { maximizeButton.classList.add('window-control'); maximizeButton.appendChild(unmaximize); } else { @@ -66,27 +63,27 @@ export default async () => { maximizeButton.appendChild(maximize); } maximizeButton.onclick = async () => { - if (await ipcRenderer.invoke('window-is-maximized')) { + if (await window.ipcRenderer.invoke('window-is-maximized')) { // change icon to maximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(maximize); // call unmaximize - await ipcRenderer.invoke('window-unmaximize'); + await window.ipcRenderer.invoke('window-unmaximize'); } else { // change icon to unmaximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(unmaximize); // call maximize - await ipcRenderer.invoke('window-maximize'); + await window.ipcRenderer.invoke('window-maximize'); } }; const closeButton = document.createElement('button'); closeButton.classList.add('window-control'); closeButton.appendChild(close); - closeButton.onclick = () => ipcRenderer.invoke('window-close'); + closeButton.onclick = () => window.ipcRenderer.invoke('window-close'); // Create a container div for the window control buttons const windowControlsContainer = document.createElement('div'); @@ -118,7 +115,7 @@ export default async () => { if (child !== logo) child.remove(); }); - const menu = await ipcRenderer.invoke('get-menu') as Menu | null; + const menu = await window.ipcRenderer.invoke('get-menu') as Menu | null; if (!menu) return; menu.items.forEach((menuItem) => { @@ -137,22 +134,22 @@ export default async () => { document.title = 'Youtube Music'; - ipcRenderer.on('refreshMenu', () => updateMenu()); - ipcRenderer.on('window-maximize', () => { + window.ipcRenderer.on('refreshMenu', () => updateMenu()); + window.ipcRenderer.on('window-maximize', () => { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.appendChild(unmaximize); } }); - ipcRenderer.on('window-unmaximize', () => { + window.ipcRenderer.on('window-unmaximize', () => { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.appendChild(unmaximize); } }); - if (isEnabled('picture-in-picture')) { - ipcRenderer.on('pip-toggle', () => { + if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { + window.ipcRenderer.on('pip-toggle', () => { updateMenu(); }); } diff --git a/src/plugins/in-app-menu/menu/panel.ts b/src/plugins/in-app-menu/menu/panel.ts index c15057b3e4..18bd39f725 100644 --- a/src/plugins/in-app-menu/menu/panel.ts +++ b/src/plugins/in-app-menu/menu/panel.ts @@ -1,8 +1,8 @@ -import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron'; - import Icons from './icons'; -import { ElementFromHtml } from '../../utils'; +import { ElementFromHtml } from '../../utils-renderer'; + +import type { MenuItem } from 'electron'; interface PanelOptions { placement?: 'bottom' | 'right'; @@ -19,7 +19,7 @@ export const createPanel = ( const panel = document.createElement('menu-panel'); panel.style.zIndex = `${options.order}`; - const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => { + const updateIconState = async (iconWrapper: HTMLElement, item: MenuItem) => { if (item.type === 'checkbox') { if (item.checked) iconWrapper.innerHTML = Icons.checkbox; else iconWrapper.innerHTML = ''; @@ -27,8 +27,8 @@ export const createPanel = ( if (item.checked) iconWrapper.innerHTML = Icons.radio.checked; else iconWrapper.innerHTML = Icons.radio.unchecked; } else { - const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon; - const iconURL = nativeImageIcon?.toDataURL(); + const iconURL = typeof item.icon === 'string' ? + await window.ipcRenderer.invoke('image-path-to-data-url') as string : item.icon?.toDataURL(); if (iconURL) iconWrapper.style.background = `url(${iconURL})`; } @@ -46,8 +46,8 @@ export const createPanel = ( menu.append(item.label); menu.addEventListener('click', async () => { - await ipcRenderer.invoke('menu-event', item.commandId); - const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + await window.ipcRenderer.invoke('menu-event', item.commandId); + const menuItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; if (menuItem) { updateIconState(iconWrapper, menuItem); @@ -56,7 +56,7 @@ export const createPanel = ( await Promise.all( radioGroups.map(async ([item, iconWrapper]) => { if (item.commandId === menuItem.commandId) return; - const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + const newItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; if (newItem) updateIconState(iconWrapper, newItem); }) diff --git a/src/plugins/lyrics-genius/front.ts b/src/plugins/lyrics-genius/front.ts index ebfc00714d..c86b0ddd62 100644 --- a/src/plugins/lyrics-genius/front.ts +++ b/src/plugins/lyrics-genius/front.ts @@ -1,6 +1,3 @@ -import { ipcRenderer } from 'electron'; -import is from 'electron-is'; - import type { SongInfo } from '../../providers/song-info'; export default () => { @@ -22,9 +19,9 @@ export default () => { } }; - let unregister: (() => void) | null = null; + let unregister: (() => void) | null = null; - ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { + window.ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { unregister?.(); setTimeout(async () => { @@ -38,7 +35,7 @@ export default () => { // Check if disabled if (!tabs.lyrics?.hasAttribute('disabled')) return; - const lyrics = await ipcRenderer.invoke( + const lyrics = await window.ipcRenderer.invoke( 'search-genius-lyrics', extractedSongInfo, ) as string | null; @@ -50,7 +47,7 @@ export default () => { return; } - if (is.dev()) { + if (window.electronIs.dev()) { console.log('Fetched lyrics from Genius'); } @@ -58,7 +55,7 @@ export default () => { const lyricsContainer = document.querySelector( '[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer', ); - + if (lyricsContainer) { callback?.(); diff --git a/src/plugins/navigation/front.ts b/src/plugins/navigation/front.ts index 61972b9f9c..cc03a98be5 100644 --- a/src/plugins/navigation/front.ts +++ b/src/plugins/navigation/front.ts @@ -1,12 +1,10 @@ -import { ipcRenderer } from 'electron'; - import forwardHTML from './templates/forward.html'; import backHTML from './templates/back.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; export function run() { - ipcRenderer.on('navigation-css-ready', () => { + window.ipcRenderer.on('navigation-css-ready', () => { const forwardButton = ElementFromHtml(forwardHTML); const backButton = ElementFromHtml(backHTML); const menu = document.querySelector('#right-content'); diff --git a/src/plugins/picture-in-picture/front.ts b/src/plugins/picture-in-picture/front.ts index 96236eba51..406137a332 100644 --- a/src/plugins/picture-in-picture/front.ts +++ b/src/plugins/picture-in-picture/front.ts @@ -1,4 +1,3 @@ -import { ipcRenderer } from 'electron'; import { toKeyEvent } from 'keyboardevent-from-electron-accelerator'; import keyEventAreEqual from 'keyboardevents-areequal'; @@ -6,7 +5,7 @@ import pipHTML from './templates/picture-in-picture.html'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import type { ConfigType } from '../../config/dynamic'; @@ -85,12 +84,11 @@ const togglePictureInPicture = async () => { } } - ipcRenderer.send('picture-in-picture'); + window.ipcRenderer.send('picture-in-picture'); return false; }; // For UI (HTML) -// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access -(global as any).togglePictureInPicture = togglePictureInPicture; +window.togglePictureInPicture = togglePictureInPicture; const listenForToggle = () => { const originalExitButton = $('.exit-fullscreen-button'); @@ -106,7 +104,7 @@ const listenForToggle = () => { const titlebar = $('.cet-titlebar'); - ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { + window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { if (originalExitButton && player) { if (isPip) { replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture()); diff --git a/src/plugins/playback-speed/front.ts b/src/plugins/playback-speed/front.ts index 5c491a0337..b8a0864e06 100644 --- a/src/plugins/playback-speed/front.ts +++ b/src/plugins/playback-speed/front.ts @@ -1,7 +1,7 @@ import sliderHTML from './templates/slider.html'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { singleton } from '../../providers/decorators'; diff --git a/src/plugins/precise-volume/front.ts b/src/plugins/precise-volume/front.ts index e5bd14935d..d4c9bfb413 100644 --- a/src/plugins/precise-volume/front.ts +++ b/src/plugins/precise-volume/front.ts @@ -1,6 +1,3 @@ -import { ipcRenderer } from 'electron'; - -import { setOptions, setMenuOptions, isEnabled } from '../../config/plugins'; import { debounce } from '../../providers/decorators'; import { YoutubePlayer } from '../../types/youtube-player'; @@ -18,15 +15,15 @@ export default (_options: ConfigType<'precise-volume'>) => { options = _options; document.addEventListener('apiLoaded', (e) => { api = e.detail; - ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); - ipcRenderer.on('setVolume', (_, value: number) => setVolume(value)); + window.ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); + window.ipcRenderer.on('setVolume', (_, value: number) => setVolume(value)); firstRun(); }, { once: true, passive: true }); }; // Without this function it would rewrite config 20 time when volume change by 20 const writeOptions = debounce(() => { - setOptions('precise-volume', options); + window.mainConfig.plugins.setOptions('precise-volume', options); }, 1000); export const moveVolumeHud = debounce((showVideo: boolean) => { @@ -68,7 +65,7 @@ function firstRun() { injectVolumeHud(noVid); if (!noVid) { setupVideoPlayerOnwheel(); - if (!isEnabled('video-toggle')) { + if (!window.mainConfig.plugins.isEnabled('video-toggle')) { // Video-toggle handles hud positioning on its own const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; $('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode())); @@ -76,9 +73,9 @@ function firstRun() { } // Change options from renderer to keep sync - ipcRenderer.on('setOptions', (_event, newOptions = {}) => { + window.ipcRenderer.on('setOptions', (_event, newOptions = {}) => { Object.assign(options, newOptions); - setMenuOptions('precise-volume', options); + window.mainConfig.plugins.setMenuOptions('precise-volume', options); }); } diff --git a/src/plugins/quality-changer/front.ts b/src/plugins/quality-changer/front.ts index 38514113c8..404447dc2a 100644 --- a/src/plugins/quality-changer/front.ts +++ b/src/plugins/quality-changer/front.ts @@ -1,8 +1,6 @@ -import { ipcRenderer } from 'electron'; - import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { YoutubePlayer } from '../../types/youtube-player'; function $(selector: string): HTMLElement | null { @@ -23,7 +21,7 @@ function setup(event: CustomEvent) { const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality()); - ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => { + window.ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => { if (promise.response === -1) { return; } diff --git a/src/plugins/sponsorblock/front.ts b/src/plugins/sponsorblock/front.ts index ccb2f808d4..022ff950bb 100644 --- a/src/plugins/sponsorblock/front.ts +++ b/src/plugins/sponsorblock/front.ts @@ -1,12 +1,9 @@ -import { ipcRenderer } from 'electron'; -import is from 'electron-is'; - import { Segment } from './types'; let currentSegments: Segment[] = []; export default () => { - ipcRenderer.on('sponsorblock-skip', (_, segments: Segment[]) => { + window.ipcRenderer.on('sponsorblock-skip', (_, segments: Segment[]) => { currentSegments = segments; }); @@ -24,7 +21,7 @@ export default () => { && target.currentTime < segment[1] ) { target.currentTime = segment[1]; - if (is.dev()) { + if (window.electronIs.dev()) { console.log('SponsorBlock: skipping segment', segment); } } diff --git a/src/plugins/utils-renderer.ts b/src/plugins/utils-renderer.ts new file mode 100644 index 0000000000..9b9e85ae25 --- /dev/null +++ b/src/plugins/utils-renderer.ts @@ -0,0 +1,8 @@ +// Creates a DOM element from an HTML string +export const ElementFromHtml = (html: string): HTMLElement => { + const template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + + return template.content.firstElementChild as HTMLElement; +}; diff --git a/src/plugins/utils.ts b/src/plugins/utils.ts index 0bd6a85105..26659fcadc 100644 --- a/src/plugins/utils.ts +++ b/src/plugins/utils.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { app, ipcMain, ipcRenderer } from 'electron'; +import { app } from 'electron'; import is from 'electron-is'; import { ValueOf } from '../utils/type-utils'; @@ -34,31 +34,6 @@ export const saveMediaIcon = () => { } }; -// Creates a DOM element from an HTML string -export const ElementFromHtml = (html: string): HTMLElement => { - const template = document.createElement('template'); - html = html.trim(); // Never return a text node of whitespace as the result - template.innerHTML = html; - - return template.content.firstElementChild as HTMLElement; -}; - -// Creates a DOM element from a HTML file -export const ElementFromFile = (filepath: fs.PathOrFileDescriptor) => ElementFromHtml(fs.readFileSync(filepath, 'utf8')); - -export const templatePath = (pluginPath: string, name: string) => path.join(pluginPath, 'templates', name); - -export const Actions = { - NEXT: 'next', - BACK: 'back', -}; - -export const triggerAction = (channel: string, action: ValueOf, ...args: Parameters) => ipcRenderer.send(channel, action, ...args); - -export const triggerActionSync = (channel: string, action: ValueOf, ...args: Parameters): unknown => ipcRenderer.sendSync(channel, action, ...args); - -export const listenAction = (channel: string, callback: (event: Electron.IpcMainEvent, action: string) => void) => ipcMain.on(channel, callback); - export const fileExists = ( path: fs.PathLike, callbackIfExists: { (): void; (): void; (): void; }, diff --git a/src/plugins/video-toggle/front.ts b/src/plugins/video-toggle/front.ts index 39e7f6e10d..6dd3a381b0 100644 --- a/src/plugins/video-toggle/front.ts +++ b/src/plugins/video-toggle/front.ts @@ -1,7 +1,6 @@ import buttonTemplate from './templates/button_template.html'; -import { ElementFromHtml } from '../utils'; -import { setOptions, isEnabled } from '../../config/plugins'; +import { ElementFromHtml } from '../utils-renderer'; import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/front'; @@ -10,7 +9,7 @@ import { ThumbnailElement } from '../../types/get-player-response'; import type { ConfigType } from '../../config/dynamic'; -const moveVolumeHud = isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; +const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; function $(selector: string): E | null { return document.querySelector(selector); @@ -99,7 +98,7 @@ function setup(e: CustomEvent) { function setVideoState(showVideo: boolean) { options.hideVideo = !showVideo; - setOptions('video-toggle', options); + window.mainConfig.plugins.setOptions('video-toggle', options); const checkbox = $('.video-switch-button-checkbox'); // custom mode if (checkbox) checkbox.checked = !options.hideVideo; diff --git a/src/plugins/visualizer/visualizers/butterchurn.ts b/src/plugins/visualizer/visualizers/butterchurn.ts index 5c44cc3326..6d3b070ea5 100644 --- a/src/plugins/visualizer/visualizers/butterchurn.ts +++ b/src/plugins/visualizer/visualizers/butterchurn.ts @@ -3,7 +3,7 @@ import ButterchurnPresets from 'butterchurn-presets'; import { Visualizer } from './visualizer'; -import { ConfigType } from '../../../config/dynamic'; +import type { ConfigType } from '../../../config/dynamic'; class ButterchurnVisualizer extends Visualizer { name = 'butterchurn'; diff --git a/src/preload.ts b/src/preload.ts index bfa1d4678b..6b19f88432 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,40 +1,14 @@ -import { ipcRenderer } from 'electron'; +import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import is from 'electron-is'; import config from './config'; -import setupSongInfo from './providers/song-info-front'; -import { setupSongControls } from './providers/song-controls-front'; -import { startingPages } from './providers/extracted-data'; - -import albumColorThemeRenderer from './plugins/album-color-theme/front'; -import ambientModeRenderer from './plugins/ambient-mode/front'; -import audioCompressorRenderer from './plugins/audio-compressor/front'; -import bypassAgeRestrictionsRenderer from './plugins/bypass-age-restrictions/front'; -import captionsSelectorRenderer from './plugins/captions-selector/front'; -import compactSidebarRenderer from './plugins/compact-sidebar/front'; -import crossfadeRenderer from './plugins/crossfade/front'; -import disableAutoplayRenderer from './plugins/disable-autoplay/front'; -import downloaderRenderer from './plugins/downloader/front'; -import exponentialVolumeRenderer from './plugins/exponential-volume/front'; -import inAppMenuRenderer from './plugins/in-app-menu/front'; -import lyricsGeniusRenderer from './plugins/lyrics-genius/front'; -import navigationRenderer from './plugins/navigation/front'; -import noGoogleLogin from './plugins/no-google-login/front'; -import pictureInPictureRenderer from './plugins/picture-in-picture/front'; -import playbackSpeedRenderer from './plugins/playback-speed/front'; -import preciseVolumeRenderer from './plugins/precise-volume/front'; -import qualityChangerRenderer from './plugins/quality-changer/front'; -import skipSilencesRenderer from './plugins/skip-silences/front'; -import sponsorblockRenderer from './plugins/sponsorblock/front'; -import videoToggleRenderer from './plugins/video-toggle/front'; -import visualizerRenderer from './plugins/visualizer/front'; import adblockerPreload from './plugins/adblocker/preload'; import preciseVolumePreload from './plugins/precise-volume/preload'; import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic'; -type PluginMapper = { +export type PluginMapper = { [Key in OneOfDefaultConfigKey]?: ( Type extends 'renderer' ? (options: ConfigType) => (Promise | void) : Type extends 'preload' ? () => (Promise | void) : @@ -42,31 +16,6 @@ type PluginMapper = { ) }; -const rendererPlugins: PluginMapper<'renderer'> = { - 'album-color-theme': albumColorThemeRenderer, - 'ambient-mode': ambientModeRenderer, - 'audio-compressor': audioCompressorRenderer, - 'bypass-age-restrictions': bypassAgeRestrictionsRenderer, - 'captions-selector': captionsSelectorRenderer, - 'compact-sidebar': compactSidebarRenderer, - 'crossfade': crossfadeRenderer, - 'disable-autoplay': disableAutoplayRenderer, - 'downloader': downloaderRenderer, - 'exponential-volume': exponentialVolumeRenderer, - 'in-app-menu': inAppMenuRenderer, - 'lyrics-genius': lyricsGeniusRenderer, - 'navigation': navigationRenderer, - 'no-google-login': noGoogleLogin, - 'picture-in-picture': pictureInPictureRenderer, - 'playback-speed': playbackSpeedRenderer, - 'precise-volume': preciseVolumeRenderer, - 'quality-changer': qualityChangerRenderer, - 'skip-silences': skipSilencesRenderer, - 'sponsorblock': sponsorblockRenderer, - 'video-toggle': videoToggleRenderer, - 'visualizer': visualizerRenderer, -}; - const preloadPlugins: PluginMapper<'preload'> = { 'adblocker': adblockerPreload, 'precise-volume': preciseVolumePreload, @@ -74,10 +23,6 @@ const preloadPlugins: PluginMapper<'preload'> = { const enabledPluginNameAndOptions = config.plugins.getEnabled(); -const $ = document.querySelector.bind(document); - -let api: Element | null = null; - enabledPluginNameAndOptions.forEach(async ([plugin, options]) => { if (Object.hasOwn(preloadPlugins, plugin)) { const handler = preloadPlugins[plugin]; @@ -89,120 +34,17 @@ enabledPluginNameAndOptions.forEach(async ([plugin, options]) => { } }); -document.addEventListener('DOMContentLoaded', () => { - enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { - if (Object.hasOwn(rendererPlugins, pluginName)) { - const handler = rendererPlugins[pluginName]; - try { - await handler?.(options as never); - } catch (error) { - console.error(`Error in plugin "${pluginName}": ${String(error)}`); - } - } - }); - - // Wait for complete load of YouTube api - listenForApiLoad(); - - // Inject song-info provider - setupSongInfo(); - - // Inject song-controls - setupSongControls(); - - // Add action for reloading - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any - (global as any).reload = () => ipcRenderer.send('reload'); - - // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min - setInterval(() => window._lact = Date.now(), 900_000); - - // Setup back to front logger - if (is.dev()) { - ipcRenderer.on('log', (_event, log: string) => { - console.log(JSON.parse(log)); - }); - } +contextBridge.exposeInMainWorld('mainConfig', config); +contextBridge.exposeInMainWorld('electronIs', is); +contextBridge.exposeInMainWorld('ipcRenderer', { + on: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.on(channel, listener), + off: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.off(channel, listener), + once: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.once(channel, listener), + send: (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args), + removeListener: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.removeListener(channel, listener), + removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel), + invoke: async (channel: string, ...args: unknown[]): Promise => ipcRenderer.invoke(channel, ...args), + sendSync: (channel: string, ...args: unknown[]): unknown => ipcRenderer.sendSync(channel, ...args), + sendToHost: (channel: string, ...args: unknown[]) => ipcRenderer.sendToHost(channel, ...args), }); - -function listenForApiLoad() { - api = $('#movie_player'); - if (api) { - onApiLoaded(); - return; - } - - const observer = new MutationObserver(() => { - api = $('#movie_player'); - if (api) { - observer.disconnect(); - onApiLoaded(); - } - }); - - observer.observe(document.documentElement, { childList: true, subtree: true }); -} - -interface YouTubeMusicAppElement extends HTMLElement { - navigate_(page: string): void; -} - -function onApiLoaded() { - const video = $('video')!; - const audioContext = new AudioContext(); - const audioSource = audioContext.createMediaElementSource(video); - audioSource.connect(audioContext.destination); - - video.addEventListener( - 'loadstart', - () => { - // Emit "audioCanPlay" for each video - video.addEventListener( - 'canplaythrough', - () => { - document.dispatchEvent( - new CustomEvent('audioCanPlay', { - detail: { - audioContext, - audioSource, - }, - }), - ); - }, - { once: true }, - ); - }, - { passive: true }, - );! - - document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); - ipcRenderer.send('apiLoaded'); - - // Navigate to "Starting page" - const startingPage: string = config.get('options.startingPage'); - if (startingPage && startingPages[startingPage]) { - $('ytmusic-app')?.navigate_(startingPages[startingPage]); - } - - // Remove upgrade button - if (config.get('options.removeUpgradeButton')) { - const styles = document.createElement('style'); - styles.innerHTML = `ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:nth-child(4) { - display: none; - }`; - document.head.appendChild(styles); - } - - // Hide / Force show like buttons - const likeButtonsOptions: string = config.get('options.likeButtons'); - if (likeButtonsOptions) { - const likeButtons: HTMLElement | null = $('ytmusic-like-button-renderer'); - if (likeButtons) { - likeButtons.style.display - = { - hide: 'none', - force: 'inherit', - }[likeButtonsOptions] || ''; - } - } -} +contextBridge.exposeInMainWorld('reload', () => ipcRenderer.send('reload')); diff --git a/src/providers/song-controls-front.ts b/src/providers/song-controls-front.ts index 012a75b4ed..be080c81e9 100644 --- a/src/providers/song-controls-front.ts +++ b/src/providers/song-controls-front.ts @@ -1,8 +1,6 @@ -import { ipcRenderer } from 'electron'; - export const setupSongControls = () => { document.addEventListener('apiLoaded', (event) => { - ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t)); - ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t)); + window.ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t)); + window.ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t)); }, { once: true, passive: true }); }; diff --git a/src/providers/song-info-front.ts b/src/providers/song-info-front.ts index 0c308c7087..05ae3f2a4c 100644 --- a/src/providers/song-info-front.ts +++ b/src/providers/song-info-front.ts @@ -1,21 +1,18 @@ -import { ipcRenderer } from 'electron'; - import { singleton } from './decorators'; -import { getImage, SongInfo } from './song-info'; -import { YoutubePlayer } from '../types/youtube-player'; -import { GetState } from '../types/datahost-get-state'; -import { VideoDataChangeValue } from '../types/player-api-events'; +import type { YoutubePlayer } from '../types/youtube-player'; +import type { GetState } from '../types/datahost-get-state'; +import type { VideoDataChangeValue } from '../types/player-api-events'; + +import type { SongInfo } from './song-info'; let songInfo: SongInfo = {} as SongInfo; export const getSongInfo = () => songInfo; const $ = (s: string): E | null => document.querySelector(s); -const $$ = (s: string): NodeListOf => document.querySelectorAll(s); -ipcRenderer.on('update-song-info', async (_, extractedSongInfo: SongInfo) => { +window.ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { songInfo = extractedSongInfo; - if (songInfo.imageSrc) songInfo.image = await getImage(songInfo.imageSrc); }); // Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473) @@ -24,7 +21,7 @@ const srcChangedEvent = new CustomEvent('srcChanged'); export const setupSeekedListener = singleton(() => { $('video')?.addEventListener('seeked', (v) => { if (v.target instanceof HTMLVideoElement) { - ipcRenderer.send('seeked', v.target.currentTime); + window.ipcRenderer.send('seeked', v.target.currentTime); } }); }); @@ -33,7 +30,7 @@ export const setupTimeChangedListener = singleton(() => { const progressObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { const target = mutation.target as Node & { value: string }; - ipcRenderer.send('timeChanged', target.value); + window.ipcRenderer.send('timeChanged', target.value); songInfo.elapsedSeconds = Number(target.value); } }); @@ -47,7 +44,7 @@ export const setupRepeatChangedListener = singleton(() => { const repeatObserver = new MutationObserver((mutations) => { // provided by YouTube Music - ipcRenderer.send( + window.ipcRenderer.send( 'repeatChanged', (mutations[0].target as Node & { __dataHost: { @@ -60,7 +57,7 @@ export const setupRepeatChangedListener = singleton(() => { // Emit the initial value as well; as it's persistent between launches. // provided by YouTube Music - ipcRenderer.send( + window.ipcRenderer.send( 'repeatChanged', $ GetState; @@ -70,33 +67,33 @@ export const setupRepeatChangedListener = singleton(() => { export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => { $('video')?.addEventListener('volumechange', () => { - ipcRenderer.send('volumeChanged', api.getVolume()); + window.ipcRenderer.send('volumeChanged', api.getVolume()); }); // Emit the initial value as well; as it's persistent between launches. - ipcRenderer.send('volumeChanged', api.getVolume()); + window.ipcRenderer.send('volumeChanged', api.getVolume()); }); export default () => { document.addEventListener('apiLoaded', (apiEvent) => { - ipcRenderer.on('setupTimeChangedListener', () => { + window.ipcRenderer.on('setupTimeChangedListener', () => { setupTimeChangedListener(); }); - ipcRenderer.on('setupRepeatChangedListener', () => { + window.ipcRenderer.on('setupRepeatChangedListener', () => { setupRepeatChangedListener(); }); - ipcRenderer.on('setupVolumeChangedListener', () => { + window.ipcRenderer.on('setupVolumeChangedListener', () => { setupVolumeChangedListener(apiEvent.detail); }); - ipcRenderer.on('setupSeekedListener', () => { + window.ipcRenderer.on('setupSeekedListener', () => { setupSeekedListener(); }); const playPausedHandler = (e: Event, status: string) => { if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) { - ipcRenderer.send('playPaused', { + window.ipcRenderer.send('playPaused', { isPaused: status === 'pause', elapsedSeconds: Math.floor(e.target.currentTime), }); @@ -137,7 +134,13 @@ export default () => { data.videoDetails.album = videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(0)?.text; data.videoDetails.elapsedSeconds = 0; data.videoDetails.isPaused = false; - ipcRenderer.send('video-src-changed', data); + + // HACK: This is a workaround for "podcast" type video. GREAT JOB GOOGLE. + if (data.playabilityStatus.transportControlsConfig) { + data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name; + } + + window.ipcRenderer.send('video-src-changed', data); } }, { once: true, passive: true }); }; diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 0000000000..030662258c --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,171 @@ +import setupSongInfo from './providers/song-info-front'; +import { setupSongControls } from './providers/song-controls-front'; +import { startingPages } from './providers/extracted-data'; + +import albumColorThemeRenderer from './plugins/album-color-theme/front'; +import ambientModeRenderer from './plugins/ambient-mode/front'; +import audioCompressorRenderer from './plugins/audio-compressor/front'; +import bypassAgeRestrictionsRenderer from './plugins/bypass-age-restrictions/front'; +import captionsSelectorRenderer from './plugins/captions-selector/front'; +import compactSidebarRenderer from './plugins/compact-sidebar/front'; +import crossfadeRenderer from './plugins/crossfade/front'; +import disableAutoplayRenderer from './plugins/disable-autoplay/front'; +import downloaderRenderer from './plugins/downloader/front'; +import exponentialVolumeRenderer from './plugins/exponential-volume/front'; +import inAppMenuRenderer from './plugins/in-app-menu/front'; +import lyricsGeniusRenderer from './plugins/lyrics-genius/front'; +import navigationRenderer from './plugins/navigation/front'; +import noGoogleLogin from './plugins/no-google-login/front'; +import pictureInPictureRenderer from './plugins/picture-in-picture/front'; +import playbackSpeedRenderer from './plugins/playback-speed/front'; +import preciseVolumeRenderer from './plugins/precise-volume/front'; +import qualityChangerRenderer from './plugins/quality-changer/front'; +import skipSilencesRenderer from './plugins/skip-silences/front'; +import sponsorblockRenderer from './plugins/sponsorblock/front'; +import videoToggleRenderer from './plugins/video-toggle/front'; +import visualizerRenderer from './plugins/visualizer/front'; + +import type { PluginMapper } from './preload'; + +const rendererPlugins: PluginMapper<'renderer'> = { + 'album-color-theme': albumColorThemeRenderer, + 'ambient-mode': ambientModeRenderer, + 'audio-compressor': audioCompressorRenderer, + 'bypass-age-restrictions': bypassAgeRestrictionsRenderer, + 'captions-selector': captionsSelectorRenderer, + 'compact-sidebar': compactSidebarRenderer, + 'crossfade': crossfadeRenderer, + 'disable-autoplay': disableAutoplayRenderer, + 'downloader': downloaderRenderer, + 'exponential-volume': exponentialVolumeRenderer, + 'in-app-menu': inAppMenuRenderer, + 'lyrics-genius': lyricsGeniusRenderer, + 'navigation': navigationRenderer, + 'no-google-login': noGoogleLogin, + 'picture-in-picture': pictureInPictureRenderer, + 'playback-speed': playbackSpeedRenderer, + 'precise-volume': preciseVolumeRenderer, + 'quality-changer': qualityChangerRenderer, + 'skip-silences': skipSilencesRenderer, + 'sponsorblock': sponsorblockRenderer, + 'video-toggle': videoToggleRenderer, + 'visualizer': visualizerRenderer, +}; + +const enabledPluginNameAndOptions = window.mainConfig.plugins.getEnabled(); + +let api: Element | null = null; + +function listenForApiLoad() { + api = document.querySelector('#movie_player'); + if (api) { + onApiLoaded(); + return; + } + + const observer = new MutationObserver(() => { + api = document.querySelector('#movie_player'); + if (api) { + observer.disconnect(); + onApiLoaded(); + } + }); + + observer.observe(document.documentElement, { childList: true, subtree: true }); +} + +interface YouTubeMusicAppElement extends HTMLElement { + navigate_(page: string): void; +} + +function onApiLoaded() { + const video = document.querySelector('video')!; + const audioContext = new AudioContext(); + const audioSource = audioContext.createMediaElementSource(video); + audioSource.connect(audioContext.destination); + + video.addEventListener( + 'loadstart', + () => { + // Emit "audioCanPlay" for each video + video.addEventListener( + 'canplaythrough', + () => { + document.dispatchEvent( + new CustomEvent('audioCanPlay', { + detail: { + audioContext, + audioSource, + }, + }), + ); + }, + { once: true }, + ); + }, + { passive: true }, + );! + + document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); + window.ipcRenderer.send('apiLoaded'); + + // Navigate to "Starting page" + const startingPage: string = window.mainConfig.get('options.startingPage'); + if (startingPage && startingPages[startingPage]) { + document.querySelector('ytmusic-app')?.navigate_(startingPages[startingPage]); + } + + // Remove upgrade button + if (window.mainConfig.get('options.removeUpgradeButton')) { + const styles = document.createElement('style'); + styles.innerHTML = `ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:last-child { + display: none; + }`; + document.head.appendChild(styles); + } + + // Hide / Force show like buttons + const likeButtonsOptions: string = window.mainConfig.get('options.likeButtons'); + if (likeButtonsOptions) { + const likeButtons: HTMLElement | null = document.querySelector('ytmusic-like-button-renderer'); + if (likeButtons) { + likeButtons.style.display + = { + hide: 'none', + force: 'inherit', + }[likeButtonsOptions] || ''; + } + } +} + +(() => { + enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { + if (Object.hasOwn(rendererPlugins, pluginName)) { + const handler = rendererPlugins[pluginName]; + try { + await handler?.(options as never); + } catch (error) { + console.error(`Error in plugin "${pluginName}": ${String(error)}`); + } + } + }); + + // Inject song-info provider + setupSongInfo(); + + // Inject song-controls + setupSongControls(); + + // Wait for complete load of YouTube api + listenForApiLoad(); + + // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min + setInterval(() => window._lact = Date.now(), 900_000); + + // Setup back to front logger + if (window.electronIs.dev()) { + window.ipcRenderer.on('log', (_event, log: string) => { + console.log(JSON.parse(log)); + }); + } +})(); diff --git a/src/reset.d.ts b/src/reset.d.ts index b3b3d2907c..ae20968bf5 100644 --- a/src/reset.d.ts +++ b/src/reset.d.ts @@ -1,4 +1,9 @@ import '@total-typescript/ts-reset'; + +import { ipcRenderer as electronIpcRenderer } from 'electron'; +import is from 'electron-is'; + +import config from './config'; import { YoutubePlayer } from './types/youtube-player'; declare global { @@ -13,11 +18,17 @@ declare global { } interface Window { + ipcRenderer: typeof electronIpcRenderer; + mainConfig: typeof config; + electronIs: typeof is; /** * YouTube Music internal variable (Last interaction time) */ _lact: number; navigation: Navigation; + download: () => void; + togglePictureInPicture: () => void; + reload: () => void; } } diff --git a/src/types/get-player-response.ts b/src/types/get-player-response.ts index 3a1955004d..54bd7d5a9f 100644 --- a/src/types/get-player-response.ts +++ b/src/types/get-player-response.ts @@ -217,6 +217,17 @@ export interface PlayabilityStatus { audioOnlyPlayability: AudioOnlyPlayability; miniplayer: Miniplayer; contextParams: string; + transportControlsConfig?: TransportControlsConfig; +} + +type ReplaceDefaultType = { + replaceDefault: boolean, +}; + +export interface TransportControlsConfig { + seekForwardStatus: ReplaceDefaultType; + seekBackwardStatus: ReplaceDefaultType; + playbackRateStatus: ReplaceDefaultType; } export interface AudioOnlyPlayability {