refactor(in-app-menu): refactor in-app-menu plugin (#1710)

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Su-Yong
2024-02-05 22:26:25 +09:00
committed by GitHub
parent cd8701d0e5
commit b3c05c8647
23 changed files with 1318 additions and 492 deletions

View File

@ -11,6 +11,7 @@ import pluginLoader from './vite-plugins/plugin-loader.mjs';
import type { UserConfig } from 'vite';
import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
import solidPlugin from 'vite-plugin-solid';
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -117,6 +118,7 @@ export default defineConfig({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
}),
solidPlugin(),
],
root: './src/',
build: {

View File

@ -142,6 +142,7 @@
"@electron/remote": "2.1.2",
"@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.6.1",
"@foobar404/wave": "2.0.5",
"@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4",
@ -174,6 +175,10 @@
"semver": "7.5.4",
"serve": "14.2.1",
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
"solid-floating-ui": "0.3.1",
"solid-js": "1.8.12",
"solid-styled-components": "0.28.5",
"solid-transition-group": "0.2.3",
"ts-morph": "21.0.1",
"vudio": "2.1.1",
"x11": "2.3.0",
@ -211,6 +216,7 @@
"vite": "5.0.12",
"vite-plugin-inspect": "0.8.3",
"vite-plugin-resolve": "2.5.1",
"vite-plugin-solid": "2.9.1",
"ws": "8.16.0"
},
"auto-changelog": {

254
pnpm-lock.yaml generated
View File

@ -39,6 +39,9 @@ dependencies:
'@ffmpeg.wasm/main':
specifier: 0.12.0
version: 0.12.0
'@floating-ui/dom':
specifier: 1.6.1
version: 1.6.1
'@foobar404/wave':
specifier: 2.0.5
version: 2.0.5
@ -135,6 +138,18 @@ dependencies:
simple-youtube-age-restriction-bypass:
specifier: github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9
version: github.com/organization/Simple-YouTube-Age-Restriction-Bypass/4e2db89ccb2fb880c5110add9ff3f1dfb78d0ff6
solid-floating-ui:
specifier: 0.3.1
version: 0.3.1(@floating-ui/dom@1.6.1)(solid-js@1.8.12)
solid-js:
specifier: 1.8.12
version: 1.8.12
solid-styled-components:
specifier: 0.28.5
version: 0.28.5(solid-js@1.8.12)
solid-transition-group:
specifier: 0.2.3
version: 0.2.3(solid-js@1.8.12)
ts-morph:
specifier: 21.0.1
version: 21.0.1
@ -242,6 +257,9 @@ devDependencies:
vite-plugin-resolve:
specifier: 2.5.1
version: 2.5.1
vite-plugin-solid:
specifier: 2.9.1
version: 2.9.1(solid-js@1.8.12)(vite@5.0.12)
ws:
specifier: 8.16.0
version: 8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
@ -350,6 +368,13 @@ packages:
'@babel/types': 7.23.6
dev: true
/@babel/helper-module-imports@7.18.6:
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.6
dev: true
/@babel/helper-module-imports@7.22.15:
resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
engines: {node: '>=6.9.0'}
@ -433,6 +458,16 @@ packages:
'@babel/types': 7.23.6
dev: true
/@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.7):
resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.7
'@babel/helper-plugin-utils': 7.22.5
dev: true
/@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.7):
resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==}
engines: {node: '>=6.9.0'}
@ -1137,6 +1172,23 @@ packages:
regenerator-runtime: 0.13.11
dev: false
/@floating-ui/core@1.6.0:
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
dependencies:
'@floating-ui/utils': 0.2.1
dev: false
/@floating-ui/dom@1.6.1:
resolution: {integrity: sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==}
dependencies:
'@floating-ui/core': 1.6.0
'@floating-ui/utils': 0.2.1
dev: false
/@floating-ui/utils@0.2.1:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
dev: false
/@foobar404/wave@2.0.5:
resolution: {integrity: sha512-V/ydadtv5ObCw8aEg+Qy3YSq1eyinEWzJfRI43Ovmj7VmAvEdWAdL7MatoMbiIVYPATkNDVF7GOxX1xirxM9dA==}
dev: false
@ -1466,6 +1518,31 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
/@solid-primitives/refs@1.0.6(solid-js@1.8.12):
resolution: {integrity: sha512-ruh4YdVMxThEVnvqbpeLXKojW442vpFU8q7dSKtElGOTa31aKOAkRb9BTbdaTwVjN4BEq79fiiYIXozJNl4dSw==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
'@solid-primitives/utils': 6.2.2(solid-js@1.8.12)
solid-js: 1.8.12
dev: false
/@solid-primitives/transition-group@1.0.4(solid-js@1.8.12):
resolution: {integrity: sha512-9nPg6HYAmEi7riH0C2bSCVw/2asgGSzHuN0yFFYyK9JgmXqJgyeyA+6thZbj7GgUQMRhtBxpH8yG7N2nEh8ttA==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.8.12
dev: false
/@solid-primitives/utils@6.2.2(solid-js@1.8.12):
resolution: {integrity: sha512-11ypVbp987XxETeRqY5Y3OmmTpm8/jZqJXRvo6AyqBthzkvvjEdReuUMU2yVb+pwWGxfZpWHZ6EUCcGXUMhfwg==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.8.12
dev: false
/@szmarczak/http-timer@4.0.6:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
@ -1490,6 +1567,35 @@ packages:
path-browserify: 1.0.1
dev: false
/@types/babel__core@7.20.5:
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
dependencies:
'@babel/parser': 7.23.6
'@babel/types': 7.23.6
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.5
dev: true
/@types/babel__generator@7.6.8:
resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
dependencies:
'@babel/types': 7.23.6
dev: true
/@types/babel__template@7.4.4:
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
dependencies:
'@babel/parser': 7.23.6
'@babel/types': 7.23.6
dev: true
/@types/babel__traverse@7.20.5:
resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==}
dependencies:
'@babel/types': 7.23.6
dev: true
/@types/cacheable-request@6.0.3:
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
dependencies:
@ -2156,6 +2262,28 @@ packages:
- debug
dev: false
/babel-plugin-jsx-dom-expressions@0.37.16(@babel/core@7.23.7):
resolution: {integrity: sha512-ItMD16axbk+FqVb9vIbc7AOpNowy46VaSUHaMYPn+erPGpMCxsahQ1Iv+qhPMthjxtn5ROVMZ5AJtQvzjxjiNA==}
peerDependencies:
'@babel/core': ^7.20.12
dependencies:
'@babel/core': 7.23.7
'@babel/helper-module-imports': 7.18.6
'@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7)
'@babel/types': 7.23.6
html-entities: 2.3.3
validate-html-nesting: 1.2.2
dev: true
/babel-preset-solid@1.8.12(@babel/core@7.23.7):
resolution: {integrity: sha512-Fx1dYokeRwouWqjLkdobA6qvTAPxFSEU2c5PlkfJjlNyONlSMJQPaX0Bae5pc+5/LNteb9BseOp4UHwQu6VC9Q==}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.23.7
babel-plugin-jsx-dom-expressions: 0.37.16(@babel/core@7.23.7)
dev: true
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -2673,6 +2801,9 @@ packages:
engines: {node: '>= 6'}
dev: false
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/custom-electron-prompt@1.5.7(electron@29.0.0-beta.5):
resolution: {integrity: sha512-ptRPJr6CpT06GWLMtg3GD2Lr7gWfXdWI+hR1S39eq+m/mUa2E118YmX6mPCbHdg5QB/W9UVhSpRqBM8FUh1G8w==}
peerDependencies:
@ -4069,6 +4200,14 @@ packages:
slash: 4.0.0
dev: true
/goober@2.1.14(csstype@3.1.3):
resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==}
peerDependencies:
csstype: ^3.0.10
dependencies:
csstype: 3.1.3
dev: false
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
@ -4160,6 +4299,10 @@ packages:
resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==}
dev: false
/html-entities@2.3.3:
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
dev: true
/html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@ -4530,6 +4673,11 @@ packages:
get-intrinsic: 1.2.2
dev: false
/is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
dev: true
/is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
@ -4847,6 +4995,13 @@ packages:
yargs-parser: 20.2.9
dev: true
/merge-anything@5.1.7:
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
engines: {node: '>=12.13'}
dependencies:
is-what: 4.1.16
dev: true
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: false
@ -5800,6 +5955,18 @@ packages:
type-fest: 0.20.2
dev: false
/seroval-plugins@1.0.4(seroval@1.0.4):
resolution: {integrity: sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
dependencies:
seroval: 1.0.4
/seroval@1.0.4:
resolution: {integrity: sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==}
engines: {node: '>=10'}
/serve-handler@6.1.5:
resolution: {integrity: sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==}
dependencies:
@ -5943,6 +6110,56 @@ packages:
ip: 2.0.0
smart-buffer: 4.2.0
/solid-floating-ui@0.3.1(@floating-ui/dom@1.6.1)(solid-js@1.8.12):
resolution: {integrity: sha512-o/QmGsWPS2Z3KidAxP0nDvN7alI7Kqy0kU+wd85Fz+au5SYcnYm7I6Fk3M60Za35azsPX0U+5fEtqfOuk6Ao0Q==}
engines: {node: '>=10'}
peerDependencies:
'@floating-ui/dom': ^1.5
solid-js: ^1.8
dependencies:
'@floating-ui/dom': 1.6.1
solid-js: 1.8.12
dev: false
/solid-js@1.8.12:
resolution: {integrity: sha512-sLE/i6M9FSWlov3a2pTC5ISzanH2aKwqXTZj+bbFt4SUrVb4iGEa7fpILBMOxsQjkv3eXqEk6JVLlogOdTe0UQ==}
dependencies:
csstype: 3.1.3
seroval: 1.0.4
seroval-plugins: 1.0.4(seroval@1.0.4)
/solid-refresh@0.6.3(solid-js@1.8.12):
resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
peerDependencies:
solid-js: ^1.3
dependencies:
'@babel/generator': 7.23.6
'@babel/helper-module-imports': 7.22.15
'@babel/types': 7.23.6
solid-js: 1.8.12
dev: true
/solid-styled-components@0.28.5(solid-js@1.8.12):
resolution: {integrity: sha512-vwTcdp76wZNnESIzB6rRZ3U55NgcSAQXCiiRIiEFhxTFqT0bEh/warNT1qaRZu4OkAzrBkViOngF35ktI8sc4A==}
peerDependencies:
solid-js: ^1.4.4
dependencies:
csstype: 3.1.3
goober: 2.1.14(csstype@3.1.3)
solid-js: 1.8.12
dev: false
/solid-transition-group@0.2.3(solid-js@1.8.12):
resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==}
engines: {node: '>=18.0.0', pnpm: '>=8.6.0'}
peerDependencies:
solid-js: ^1.6.12
dependencies:
'@solid-primitives/refs': 1.0.6(solid-js@1.8.12)
'@solid-primitives/transition-group': 1.0.4(solid-js@1.8.12)
solid-js: 1.8.12
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@ -6422,6 +6639,10 @@ packages:
hasBin: true
dev: false
/validate-html-nesting@1.2.2:
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
dev: true
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
@ -6476,6 +6697,28 @@ packages:
lib-esm: 0.4.2
dev: true
/vite-plugin-solid@2.9.1(solid-js@1.8.12)(vite@5.0.12):
resolution: {integrity: sha512-RC4hj+lbvljw57BbMGDApvEOPEh14lwrr/GeXRLNQLcR1qnOdzOwwTSFy13Gj/6FNIZpBEl0bWPU+VYFawrqUw==}
peerDependencies:
'@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.*
solid-js: ^1.7.2
vite: ^3.0.0 || ^4.0.0 || ^5.0.0
peerDependenciesMeta:
'@testing-library/jest-dom':
optional: true
dependencies:
'@babel/core': 7.23.7
'@types/babel__core': 7.20.5
babel-preset-solid: 1.8.12(@babel/core@7.23.7)
merge-anything: 5.1.7
solid-js: 1.8.12
solid-refresh: 0.6.3(solid-js@1.8.12)
vite: 5.0.12(@types/node@20.11.0)
vitefu: 0.2.5(vite@5.0.12)
transitivePeerDependencies:
- supports-color
dev: true
/vite@5.0.12(@types/node@20.11.0):
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -6512,6 +6755,17 @@ packages:
fsevents: 2.3.3
dev: true
/vitefu@0.2.5(vite@5.0.12):
resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0
peerDependenciesMeta:
vite:
optional: true
dependencies:
vite: 5.0.12(@types/node@20.11.0)
dev: true
/vudio@2.1.1(patch_hash=7iux5msqpgl3octdmwy4uspwoe):
resolution: {integrity: sha512-VkFQcFt/b/kpF5Eg5Sq+oXUo1Zp5aRFF4BSmIrOzau5o+5WMWwX9ae/EGJZstCyZFiCTU5iw1Y+u2BCGW6Y6Jw==}
dev: false

View File

@ -1,3 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 392 B

View File

@ -1,3 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 252 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 338 B

View File

@ -1,3 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z" />
</svg>

Before

Width:  |  Height:  |  Size: 174 B

View File

@ -1,3 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 546 B

View File

@ -0,0 +1,11 @@
export interface InAppMenuConfig {
enabled: boolean;
hideDOMWindowControls: boolean;
}
export const defaultInAppMenuConfig: InAppMenuConfig = {
enabled:
(typeof window !== 'undefined' &&
!window.navigator?.userAgent?.includes('mac')) ||
(typeof global !== 'undefined' && global.process?.platform !== 'darwin'),
hideDOMWindowControls: false,
};

View File

@ -2,24 +2,15 @@ import titlebarStyle from './titlebar.css?inline';
import { createPlugin } from '@/utils';
import { onMainLoad } from './main';
import { onMenu } from './menu';
import { onPlayerApiReady, onRendererLoad } from './renderer';
import { onConfigChange, onPlayerApiReady, onRendererLoad } from './renderer';
import { t } from '@/i18n';
import { defaultInAppMenuConfig } from './constants';
export interface InAppMenuConfig {
enabled: boolean;
hideDOMWindowControls: boolean;
}
export default createPlugin({
name: () => t('plugins.in-app-menu.name'),
description: () => t('plugins.in-app-menu.description'),
restartNeeded: true,
config: {
enabled:
(typeof window !== 'undefined' &&
!window.navigator?.userAgent?.includes('mac')) ||
(typeof global !== 'undefined' && global.process?.platform !== 'darwin'),
hideDOMWindowControls: false,
} as InAppMenuConfig,
config: defaultInAppMenuConfig,
stylesheets: [titlebarStyle],
menu: onMenu,
@ -27,5 +18,6 @@ export default createPlugin({
renderer: {
start: onRendererLoad,
onPlayerApiReady,
onConfigChange,
},
});

View File

@ -3,7 +3,7 @@ import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
import type { BackendContext } from '@/types/contexts';
import type { InAppMenuConfig } from './index';
import type { InAppMenuConfig } from './constants';
export const onMainLoad = ({
window: win,

View File

@ -1,14 +0,0 @@
const Icons = {
submenu:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
checkbox:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
radio: {
checked:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
unchecked:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
},
};
export default Icons;

View File

@ -1,220 +0,0 @@
import Icons from './icons';
import { ElementFromHtml } from '../../utils/renderer';
import type { MenuItem } from 'electron';
interface PanelOptions {
placement?: 'bottom' | 'right';
order?: number;
openOnHover?: boolean;
}
export const createPanel = (
parent: HTMLElement,
anchor: HTMLElement,
items: MenuItem[],
options: PanelOptions = { placement: 'bottom', order: 0, openOnHover: false },
) => {
const childPanels: HTMLElement[] = [];
const panel = document.createElement('menu-panel');
panel.style.zIndex = `${options.order}`;
const updateIconState = async (iconWrapper: HTMLElement, item: MenuItem) => {
if (item.type === 'checkbox') {
if (item.checked) iconWrapper.innerHTML = Icons.checkbox;
else iconWrapper.innerHTML = '';
} else if (item.type === 'radio') {
if (item.checked) iconWrapper.innerHTML = Icons.radio.checked;
else iconWrapper.innerHTML = Icons.radio.unchecked;
} else {
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})`;
}
};
const radioGroups: [MenuItem, HTMLElement][] = [];
items.map((item) => {
if (!item.visible) return;
if (item.type === 'separator')
return panel.appendChild(document.createElement('menu-separator'));
const menu = document.createElement('menu-item');
const iconWrapper = document.createElement('menu-icon');
updateIconState(iconWrapper, item);
menu.appendChild(iconWrapper);
menu.append(item.label);
if (item.sublabel) {
menu.classList.add('badge');
const menuBadge = document.createElement('menu-item-badge');
menuBadge.append(item.sublabel);
menu.append(menuBadge);
}
if (item.toolTip) {
const menuTooltip = document.createElement('menu-item-tooltip');
menuTooltip.append(item.toolTip);
menu.addEventListener('mouseenter', () => {
const rect = menu.getBoundingClientRect();
menuTooltip.style.setProperty('max-width', `${rect.width - 8}px`);
menuTooltip.style.setProperty('--x', `${rect.left}px`);
menuTooltip.style.setProperty('--y', `${rect.top + rect.height}px`);
menuTooltip.classList.add('show');
});
menu.addEventListener('mouseleave', () => {
menuTooltip.classList.remove('show');
});
parent.append(menuTooltip);
}
menu.addEventListener('click', async () => {
await window.ipcRenderer.invoke('ytmd:menu-event', item.commandId);
const menuItem = (await window.ipcRenderer.invoke(
'get-menu-by-id',
item.commandId,
)) as MenuItem | null;
if (menuItem) {
updateIconState(iconWrapper, menuItem);
if (menuItem.type === 'radio') {
await Promise.all(
radioGroups.map(async ([item, iconWrapper]) => {
if (item.commandId === menuItem.commandId) return;
const newItem = (await window.ipcRenderer.invoke(
'get-menu-by-id',
item.commandId,
)) as MenuItem | null;
if (newItem) updateIconState(iconWrapper, newItem);
}),
);
}
}
});
if (item.type === 'radio') {
radioGroups.push([item, iconWrapper]);
}
if (item.type === 'submenu') {
const subMenuIcon = document.createElement('menu-icon');
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
menu.appendChild(subMenuIcon);
const [child, , children] = createPanel(
parent,
menu,
item.submenu?.items ?? [],
{
placement: 'right',
order: (options?.order ?? 0) + 1,
openOnHover: true,
},
);
childPanels.push(child);
childPanels.push(...children);
}
return panel.appendChild(menu);
});
/* methods */
const isOpened = () => panel.getAttribute('open') === 'true';
const close = () => panel.setAttribute('open', 'false');
const open = () => {
const rect = anchor.getBoundingClientRect();
if (options.placement === 'bottom') {
panel.style.setProperty('--x', `${rect.x}px`);
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
} else {
panel.style.setProperty('--x', `${rect.x + rect.width}px`);
panel.style.setProperty('--y', `${rect.y}px`);
}
panel.setAttribute('open', 'true');
// Children are placed below their parent item, which can cause
// long lists to squeeze their children at the bottom of the screen
// (This needs to be done *after* setAttribute)
panel.classList.remove('position-by-bottom');
if (
options.placement === 'right' &&
panel.scrollHeight > panel.clientHeight
) {
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
panel.classList.add('position-by-bottom');
}
};
if (options.openOnHover) {
let timeout: number | null = null;
anchor.addEventListener('mouseenter', () => {
if (timeout) window.clearTimeout(timeout);
timeout = window.setTimeout(() => {
if (!isOpened()) open();
}, 225);
});
anchor.addEventListener('mouseleave', () => {
if (timeout) window.clearTimeout(timeout);
let mouseX = 0, mouseY = 0;
const onMouseMove = (event: MouseEvent) => {
mouseX = event.clientX;
mouseY = event.clientY;
};
document.addEventListener('mousemove', onMouseMove);
timeout = window.setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const now = document.elementFromPoint(mouseX, mouseY);
if (now === panel || panel.contains(now)) {
const onLeave = () => {
document.addEventListener('mousemove', onMouseMove);
timeout = window.setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const now = document.elementFromPoint(mouseX, mouseY);
if (now === panel || panel.contains(now) || childPanels.some((it) => it.contains(now))) return;
if (isOpened()) close();
panel.removeEventListener('mouseleave', onLeave);
}, 225);
};
panel.addEventListener('mouseleave', onLeave);
return;
}
if (isOpened()) close();
}, 225);
});
}
anchor.addEventListener('click', () => {
if (isOpened()) close();
else open();
});
document.body.addEventListener('click', (event) => {
const path = event.composedPath();
const isInside = path.some(
(it) =>
it === panel ||
it === anchor ||
childPanels.includes(it as HTMLElement),
);
if (!isInside) close();
});
parent.appendChild(panel);
return [panel, { isOpened, close, open }, childPanels] as const;
};

View File

@ -1,217 +0,0 @@
import { createPanel } from './menu/panel';
import logoRaw from './assets/menu.svg?inline';
import closeRaw from './assets/close.svg?inline';
import minimizeRaw from './assets/minimize.svg?inline';
import maximizeRaw from './assets/maximize.svg?inline';
import unmaximizeRaw from './assets/unmaximize.svg?inline';
import type { Menu } from 'electron';
import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '@/plugins/in-app-menu/index';
const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS =
!navigator.userAgent.includes('Windows') && !isMacOS;
export const onRendererLoad = async ({
getConfig,
ipc: { invoke, on },
}: RendererContext<InAppMenuConfig>) => {
const config = await getConfig();
const hideDOMWindowControls = config.hideDOMWindowControls;
let hideMenu = window.mainConfig.get('options.hideMenu');
const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
let maximizeButton: HTMLButtonElement;
let panelClosers: (() => void)[] = [];
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
const logo = document.createElement('img');
const close = document.createElement('img');
const minimize = document.createElement('img');
const maximize = document.createElement('img');
const unmaximize = document.createElement('img');
if (window.ELECTRON_RENDERER_URL) {
logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw;
close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw;
minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw;
maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw;
unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw;
} else {
logo.src = logoRaw;
close.src = closeRaw;
minimize.src = minimizeRaw;
maximize.src = maximizeRaw;
unmaximize.src = unmaximizeRaw;
}
logo.classList.add('title-bar-icon');
const logoClick = () => {
hideMenu = !hideMenu;
let visibilityStyle: string;
if (hideMenu) {
visibilityStyle = 'hidden';
} else {
visibilityStyle = 'visible';
}
const menus = document.querySelectorAll<HTMLElement>('menu-button');
menus.forEach((menu) => {
menu.style.visibility = visibilityStyle;
});
};
logo.onclick = logoClick;
on('toggle-in-app-menu', logoClick);
if (!isMacOS) titleBar.appendChild(logo);
document.body.appendChild(titleBar);
const addWindowControls = async () => {
// Create window control buttons
const minimizeButton = document.createElement('button');
minimizeButton.classList.add('window-control');
minimizeButton.appendChild(minimize);
minimizeButton.onclick = () => invoke('window-minimize');
maximizeButton = document.createElement('button');
if (await invoke('window-is-maximized')) {
maximizeButton.classList.add('window-control');
maximizeButton.appendChild(unmaximize);
} else {
maximizeButton.classList.add('window-control');
maximizeButton.appendChild(maximize);
}
maximizeButton.onclick = async () => {
if (await invoke('window-is-maximized')) {
// change icon to maximize
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(maximize);
// call unmaximize
await invoke('window-unmaximize');
} else {
// change icon to unmaximize
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(unmaximize);
// call maximize
await invoke('window-maximize');
}
};
const closeButton = document.createElement('button');
closeButton.classList.add('window-control');
closeButton.appendChild(close);
closeButton.onclick = () => invoke('window-close');
// Create a container div for the window control buttons
const windowControlsContainer = document.createElement('div');
windowControlsContainer.classList.add('window-controls-container');
windowControlsContainer.appendChild(minimizeButton);
windowControlsContainer.appendChild(maximizeButton);
windowControlsContainer.appendChild(closeButton);
// Add window control buttons to the title bar
titleBar.appendChild(windowControlsContainer);
};
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
if (navBar) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(() => {
titleBar.style.setProperty(
'--titlebar-background-color',
navBar.style.backgroundColor,
);
document
.querySelector('html')!
.style.setProperty(
'--titlebar-background-color',
navBar.style.backgroundColor,
);
});
});
observer.observe(navBar, { attributes: true, attributeFilter: ['style'] });
}
const updateMenu = async () => {
const children = [...titleBar.children];
children.forEach((child) => {
if (child !== logo) child.remove();
});
panelClosers = [];
const menu = (await invoke('get-menu')) as Menu | null;
if (!menu) return;
menu.items.forEach((menuItem) => {
const menu = document.createElement('menu-button');
const [, { close: closer }] = createPanel(
titleBar,
menu,
menuItem.submenu?.items ?? [],
);
panelClosers.push(closer);
menu.append(menuItem.label);
titleBar.appendChild(menu);
if (hideMenu) {
menu.style.visibility = 'hidden';
}
});
if (isNotWindowsOrMacOS && !hideDOMWindowControls)
await addWindowControls();
};
await updateMenu();
document.title = 'Youtube Music';
on('close-all-in-app-menu-panel', () => {
panelClosers.forEach((closer) => closer());
});
on('refresh-in-app-menu', () => updateMenu());
on('window-maximize', () => {
if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
on('window-unmaximize', () => {
if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
if (window.mainConfig.plugins.isEnabled('picture-in-picture')) {
on('pip-toggle', () => {
updateMenu();
});
}
};
export const onPlayerApiReady = () => {
const htmlHeadStyle = document.querySelector('head > div > style');
if (htmlHeadStyle) {
// HACK: This is a hack to remove the scrollbar width
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace(
'html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);',
'html::-webkit-scrollbar {',
);
}
};

View File

@ -0,0 +1,57 @@
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import { TitleBar } from './renderer/TitleBar';
import { defaultInAppMenuConfig, InAppMenuConfig } from './constants';
import type { RendererContext } from '@/types/contexts';
const scrollStyle = `
html::-webkit-scrollbar {
background-color: red;
}
`;
const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS =
!navigator.userAgent.includes('Windows') && !isMacOS;
const [config, setConfig] = createSignal<InAppMenuConfig>(defaultInAppMenuConfig);
export const onRendererLoad = async ({
getConfig,
ipc,
}: RendererContext<InAppMenuConfig>) => {
setConfig(await getConfig());
document.title = 'YouTube Music';
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(scrollStyle);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
render(() => (
<TitleBar
ipc={ipc}
isMacOS={isMacOS}
enableController={isNotWindowsOrMacOS && !config().hideDOMWindowControls}
initialCollapsed={window.mainConfig.get('options.hideMenu')}
/>
), document.body);
};
export const onPlayerApiReady = () => {
// NOT WORKING AFTER YTM UPDATE (last checked 2024-02-04)
//
// const htmlHeadStyle = document.querySelector('head > div > style');
// if (htmlHeadStyle) {
// // HACK: This is a hack to remove the scrollbar width
// htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace(
// 'html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);',
// 'html::-webkit-scrollbar { width: 0;',
// );
// }
};
export const onConfigChange = (newConfig: InAppMenuConfig) => {
setConfig(newConfig);
};

View File

@ -0,0 +1,40 @@
import { JSX } from 'solid-js';
import { css } from 'solid-styled-components';
const iconButton = css`
background: transparent;
width: 24px;
height: 24px;
padding: 2px;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
color: white;
outline: none;
border: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background: rgba(255, 255, 255, 0.1);
}
&:active {
scale: 0.9;
}
`;
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
export const IconButton = (props: CollapseIconButtonProps) => {
return (
<button {...props} class={iconButton}>
{props.children}
</button>
);
};

View File

@ -0,0 +1,42 @@
import { JSX, splitProps } from 'solid-js';
import { css } from 'solid-styled-components';
const menuStyle = css`
-webkit-app-region: none;
display: flex;
justify-content: center;
align-items: center;
align-self: stretch;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
scale: 0.9;
}
&[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
}
`;
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
text?: string;
selected?: boolean;
};
export const MenuButton = (props: MenuButtonProps) => {
const [local, leftProps] = splitProps(props, ['text']);
return (
<li {...leftProps} class={menuStyle} data-selected={props.selected}>
{local.text}
</li>
);
};

View File

@ -0,0 +1,148 @@
import { createSignal, JSX, Show, splitProps } from 'solid-js';
import { mergeProps, Portal } from 'solid-js/web';
import { css } from 'solid-styled-components';
import { Transition } from 'solid-transition-group';
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
import { useFloating } from 'solid-floating-ui';
const panelStyle = css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
z-index: 10000;
width: fit-content;
height: fit-content;
padding: 4px;
box-sizing: border-box;
border-radius: 8px;
overflow: auto;
background-color: color-mix(
in srgb,
var(--titlebar-background-color, #030303) 50%,
rgba(0, 0, 0, 0.1)
);
backdrop-filter: blur(8px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.2);
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
`;
const animationStyle = {
enter: css`
opacity: 0;
transform: scale(0.9);
`,
enterActive: css`
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
`,
exitTo: css`
opacity: 0;
transform: scale(0.9);
`,
exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`,
};
export type Placement =
'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'right-start'
| 'right-end'
| 'left-start'
| 'left-end';
export type PanelProps = JSX.HTMLAttributes<HTMLUListElement> & {
open?: boolean;
anchor?: HTMLElement | null;
children: JSX.Element;
placement?: Placement;
offset?: OffsetOptions;
};
export const Panel = (props: PanelProps) => {
const [elements, local, leftProps] = splitProps(
mergeProps({ placement: 'bottom' }, props),
['anchor', 'children'],
['open', 'placement', 'offset'],
);
const [panel, setPanel] = createSignal<HTMLElement | null>(null);
const position = useFloating(() => elements.anchor, panel, {
whileElementsMounted: autoUpdate,
placement: local.placement as Placement,
strategy: 'fixed',
middleware: [
offset(local.offset),
size({
padding: 8,
apply({ elements, availableWidth, availableHeight }) {
elements.floating.style.setProperty('--max-width', `${Math.max(200, availableWidth)}px`);
elements.floating.style.setProperty('--max-height', `${Math.max(200, availableHeight)}px`);
}
}),
flip({ fallbackStrategy: 'initialPlacement' }),
],
});
const originX = () => {
if (position.placement.includes('left')) return '100%';
if (position.placement.includes('right')) return '0';
if (position.placement.includes('top') || position.placement.includes('bottom')) {
if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%';
}
return '50%';
};
const originY = () => {
if (position.placement.includes('top')) return '100%';
if (position.placement.includes('bottom')) return '0';
if (position.placement.includes('left') || position.placement.includes('right')) {
if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%';
}
return '50%';
};
return (
<Portal>
<Transition
appear
enterClass={animationStyle.enter}
enterActiveClass={animationStyle.enterActive}
exitToClass={animationStyle.exitTo}
exitActiveClass={animationStyle.exitActive}
>
<Show when={local.open}>
<ul
{...leftProps}
ref={setPanel}
class={panelStyle}
style={{
'--offset-x': `${position.x}px`,
'--offset-y': `${position.y}px`,
'--origin-x': originX(),
'--origin-y': originY(),
}}
>
{elements.children}
</ul>
</Show>
</Transition>
</Portal>
);
};

View File

@ -0,0 +1,331 @@
import { createSignal, Match, Show, Switch } from 'solid-js';
import { JSX } from 'solid-js/jsx-runtime';
import { css } from 'solid-styled-components';
import { Portal } from 'solid-js/web';
import { Transition } from 'solid-transition-group';
import { useFloating } from 'solid-floating-ui';
import { autoUpdate, offset, size } from '@floating-ui/dom';
import { Panel } from './Panel';
const itemStyle = css`
position: relative;
-webkit-app-region: none;
min-height: 32px;
height: 32px;
display: grid;
grid-template-columns: 32px 1fr auto minmax(32px, auto);
justify-content: flex-start;
align-items: center;
border-radius: 4px;
cursor: pointer;
box-sizing: border-box;
user-select: none;
-webkit-user-drag: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
background-color: rgba(255, 255, 255, 0.2);
}
&[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
}
& * {
box-sizing: border-box;
}
`;
const itemIconStyle = css`
height: 32px;
padding: 4px;
color: white;
`;
const itemLabelStyle = css`
font-size: 12px;
color: white;
`;
const itemChipStyle = css`
display: flex;
justify-content: center;
align-items: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
margin-left: 8px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1;
font-size: 10px;
font-weight: 500;
line-height: 1;
`;
const toolTipStyle = css`
min-width: 32px;
width: 100%;
height: 100%;
padding: 4px;
max-width: calc(var(--max-width, 100%) - 8px);
max-height: calc(var(--max-height, 100%) - 8px);
border-radius: 4px;
background-color: rgba(25, 25, 25, 0.8);
color: #f1f1f1;
font-size: 10px;
`;
const popupStyle = css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
z-index: 100000000;
pointer-events: none;
`;
const animationStyle = {
enter: css`
opacity: 0;
transform: scale(0.9);
`,
enterActive: css`
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
`,
exitTo: css`
opacity: 0;
transform: scale(0.9);
`,
exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`,
};
const getParents = (element: Element | null): (HTMLElement | null)[] => {
const parents: (HTMLElement | null)[] = [];
let now = element;
while (now) {
parents.push(now as HTMLElement | null);
now = now.parentElement;
}
return parents;
};
type BasePanelItemProps = {
name: string;
label?: string;
chip?: string;
toolTip?: string;
commandId?: number;
};
type NormalPanelItemProps = BasePanelItemProps & {
type: 'normal';
onClick?: () => void;
};
type SubmenuItemProps = BasePanelItemProps & {
type: 'submenu';
level: number[];
children: JSX.Element;
};
type RadioPanelItemProps = BasePanelItemProps & {
type: 'radio';
checked: boolean;
onChange?: (checked: boolean) => void;
};
type CheckboxPanelItemProps = BasePanelItemProps & {
type: 'checkbox';
checked: boolean;
onChange?: (checked: boolean) => void;
};
export type PanelItemProps = NormalPanelItemProps | SubmenuItemProps | RadioPanelItemProps | CheckboxPanelItemProps;
export const PanelItem = (props: PanelItemProps) => {
const [open, setOpen] = createSignal(false);
const [toolTipOpen, setToolTipOpen] = createSignal(false);
const [toolTip, setToolTip] = createSignal<HTMLElement | null>(null);
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
const [child, setChild] = createSignal<HTMLElement | null>(null);
const position = useFloating(anchor, toolTip, {
whileElementsMounted: autoUpdate,
placement: 'bottom-start',
strategy: 'fixed',
middleware: [
offset({ mainAxis: 8 }),
size({
apply({ rects, elements }) {
elements.floating.style.setProperty('--max-width', `${rects.reference.width}px`);
}
}),
],
});
const handleHover = (event: MouseEvent) => {
setToolTipOpen(true);
event.target?.addEventListener('mouseleave', () => {
setToolTipOpen(false);
}, { once: true });
if (props.type === 'submenu') {
const timer = setTimeout(() => {
setOpen(true);
let mouseX = event.clientX;
let mouseY = event.clientY;
const onMouseMove = (event: MouseEvent) => {
mouseX = event.clientX;
mouseY = event.clientY;
};
document.addEventListener('mousemove', onMouseMove);
event.target?.addEventListener('mouseleave', () => {
setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const parents = getParents(document.elementFromPoint(mouseX, mouseY));
if (!parents.includes(child())) {
setOpen(false);
} else {
const onOtherHover = (event: MouseEvent) => {
const parents = getParents(event.target as HTMLElement);
const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? '';
const path = event.composedPath();
const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle));
const isChild = closestLevel.startsWith(props.level.join('/'));
if (isOtherItem && !isChild) {
setOpen(false);
document.removeEventListener('mousemove', onOtherHover);
}
};
document.addEventListener('mousemove', onOtherHover);
}
}, 225);
}, { once: true });
}, 225);
event.target?.addEventListener('mouseleave', () => {
clearTimeout(timer);
}, { once: true });
}
};
const handleClick = async () => {
await window.ipcRenderer.invoke('ytmd:menu-event', props.commandId);
if (props.type === 'radio') {
props.onChange?.(!props.checked);
} else if (props.type === 'checkbox') {
props.onChange?.(!props.checked);
} else if (props.type === 'normal') {
props.onClick?.();
}
};
return (
<li
ref={setAnchor}
class={itemStyle}
onMouseEnter={handleHover}
onClick={handleClick}
data-selected={open()}
>
<Switch fallback={<div class={itemIconStyle}/>}>
<Match when={props.type === 'checkbox' && props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10"/>
</svg>
</Match>
<Match when={props.type === 'radio' && props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}>
<path fill="currentColor"
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
</svg>
</Match>
<Match when={props.type === 'radio' && !props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}>
<path fill="currentColor"
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
</svg>
</Match>
</Switch>
<span class={itemLabelStyle}>
{props.name}
</span>
<Show when={props.chip} fallback={<div/>}>
<span class={itemChipStyle}>
{props.chip}
</span>
</Show>
<Show when={props.type === 'submenu'}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<polyline points="9 6 15 12 9 18"/>
</svg>
<Panel
ref={setChild}
open={open()}
anchor={anchor()}
placement={'right-start'}
data-level={props.type === 'submenu' && props.level.join('/')}
offset={{ mainAxis: 8 }}
>
{props.type === 'submenu' && props.children}
</Panel>
</Show>
<Show when={props.toolTip}>
<Portal>
<div
ref={setToolTip}
class={popupStyle}
style={{
'--offset-x': `${position.x}px`,
'--offset-y': `${position.y}px`,
}}
>
<Transition
appear
enterClass={animationStyle.enter}
enterActiveClass={animationStyle.enterActive}
exitToClass={animationStyle.exitTo}
exitActiveClass={animationStyle.exitActive}
>
<Show when={toolTipOpen()}>
<div class={toolTipStyle}>
{props.toolTip}
</div>
</Show>
</Transition>
</div>
</Portal>
</Show>
</li>
);
};

View File

@ -0,0 +1,342 @@
import { Menu, MenuItem } from 'electron';
import { createEffect, createResource, createSignal, Index, Match, onMount, Show, Switch } from 'solid-js';
import { css } from 'solid-styled-components';
import { TransitionGroup } from 'solid-transition-group';
import { MenuButton } from './MenuButton';
import { Panel } from './Panel';
import { PanelItem } from './PanelItem';
import { IconButton } from './IconButton';
import { WindowController } from './WindowController';
import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '../constants';
const titleStyle = css`
-webkit-app-region: drag;
box-sizing: border-box;
position: fixed;
top: 0;
z-index: 10000000;
width: 100%;
height: var(--menu-bar-height, 32px);
display: flex;
flex-flow: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
color: #f1f1f1;
font-size: 12px;
padding: 4px 4px 4px var(--offset-left, 4px);
background-color: var(--titlebar-background-color, #030303);
user-select: none;
transition: opacity 200ms ease 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
&[data-macos="true"] {
padding: 4px 4px 4px 74px;
}
`;
const separatorStyle = css`
min-height: 1px;
height: 1px;
margin: 4px 0;
background-color: rgba(255, 255, 255, 0.2);
`;
const animationStyle = {
enter: css`
opacity: 0;
transform: translateX(-50%) scale(0.8);
`,
enterActive: css`
transition: opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1), transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
`,
exitTo: css`
opacity: 0;
transform: translateX(-50%) scale(0.8);
`,
exitActive: css`
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0), transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
`,
move: css`
transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1);
`,
fakeTarget: css`
position: absolute;
opacity: 0;
`,
fake: css`
transition: all 0.00000000001s;
`,
};
export type PanelRendererProps = {
items: Electron.Menu['items'];
level?: number[];
onClick?: (commandId: number, radioGroup?: MenuItem[]) => void;
}
const PanelRenderer = (props: PanelRendererProps) => {
const radioGroup = () => props.items.filter((it) => it.type === 'radio');
return (
<Index each={props.items}>
{(subItem) => (
<Show when={subItem().visible}>
<Switch>
<Match when={subItem().type === 'normal'}>
<PanelItem
type={'normal'}
name={subItem().label}
chip={subItem().sublabel}
toolTip={subItem().toolTip}
commandId={subItem().commandId}
onClick={() => props.onClick?.(subItem().commandId)}
/>
</Match>
<Match when={subItem().type === 'submenu'}>
<PanelItem
type={'submenu'}
name={subItem().label}
chip={subItem().sublabel}
toolTip={subItem().toolTip}
level={[...props.level ?? [], subItem().commandId]}
commandId={subItem().commandId}
>
<PanelRenderer
items={subItem().submenu?.items ?? []}
level={[...props.level ?? [], subItem().commandId]}
onClick={props.onClick}
/>
</PanelItem>
</Match>
<Match when={subItem().type === 'checkbox'}>
<PanelItem
type={'checkbox'}
name={subItem().label}
checked={subItem().checked}
chip={subItem().sublabel}
toolTip={subItem().toolTip}
commandId={subItem().commandId}
onChange={() => props.onClick?.(subItem().commandId)}
/>
</Match>
<Match when={subItem().type === 'radio'}>
<PanelItem
type={'radio'}
name={subItem().label}
checked={subItem().checked}
chip={subItem().sublabel}
toolTip={subItem().toolTip}
commandId={subItem().commandId}
onChange={() => props.onClick?.(subItem().commandId, radioGroup())}
/>
</Match>
<Match when={subItem().type === 'separator'}>
<hr class={separatorStyle}/>
</Match>
</Switch>
</Show>
)}
</Index>
);
};
export type TitleBarProps = {
ipc: RendererContext<InAppMenuConfig>['ipc'];
isMacOS?: boolean;
enableController?: boolean;
initialCollapsed?: boolean;
};
export const TitleBar = (props: TitleBarProps) => {
const [collapsed, setCollapsed] = createSignal(props.initialCollapsed);
const [ignoreTransition, setIgnoreTransition] = createSignal(false);
const [openTarget, setOpenTarget] = createSignal<HTMLElement | null>(null);
const [menu, setMenu] = createSignal<Menu | null>(null);
const [data, { refetch }] = createResource(async () => await props.ipc.invoke('get-menu') as Promise<Menu | null>);
const [isMaximized, { refetch: refetchMaximize }] = createResource(async () => await props.ipc.invoke('window-is-maximized') as Promise<boolean>);
const handleToggleMaximize = async () => {
if (isMaximized()) {
await props.ipc.invoke('window-unmaximize');
} else {
await props.ipc.invoke('window-maximize');
}
await refetchMaximize();
};
const handleMinimize = async () => {
await props.ipc.invoke('window-minimize');
};
const handleClose = async () => {
await props.ipc.invoke('window-close');
};
const refreshMenuItem = async (originalMenu: Menu, commandId: number) => {
const menuItem = (await window.ipcRenderer.invoke(
'get-menu-by-id',
commandId,
)) as MenuItem | null;
const newMenu = structuredClone(originalMenu);
const stack = [...newMenu?.items ?? []];
let now: MenuItem | undefined = stack.pop();
while (now) {
const index = now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ?? -1;
if (index >= 0) {
if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem);
else now?.submenu?.items?.splice(index, 1);
}
if (now?.submenu) {
stack.push(...now.submenu.items);
}
now = stack.pop();
}
return newMenu;
};
const handleItemClick = async (commandId: number, radioGroup?: MenuItem[]) => {
const menuData = menu();
if (!menuData) return;
if (Array.isArray(radioGroup)) {
let newMenu = menuData;
for await (const item of radioGroup) {
newMenu = await refreshMenuItem(newMenu, item.commandId);
}
setMenu(newMenu);
return;
}
setMenu(await refreshMenuItem(menuData, commandId));
};
onMount(() => {
props.ipc.on('close-all-in-app-menu-panel', async () => {
setIgnoreTransition(true);
setMenu(null);
await refetch();
setMenu(data() ?? null);
setIgnoreTransition(false);
});
props.ipc.on('refresh-in-app-menu', async () => {
setIgnoreTransition(true);
await refetch();
setMenu(data() ?? null);
setIgnoreTransition(false);
});
props.ipc.on('toggle-in-app-menu', () => {
setCollapsed(!collapsed());
});
props.ipc.on('window-maximize', refetchMaximize);
props.ipc.on('window-unmaximize', refetchMaximize);
});
createEffect(() => {
if (!menu() && data()) {
setMenu(data() ?? null);
}
});
return (
<nav class={titleStyle} data-macos={props.isMacOS}>
<IconButton
onClick={() => setCollapsed(!collapsed())}
style={{
'border-top-left-radius': '4px',
}}
>
<svg width={16} height={16} viewBox={'0 0 24 24'}>
<path
d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z"
fill="currentColor"
/>
</svg>
</IconButton>
<TransitionGroup
enterClass={ignoreTransition() ? animationStyle.fakeTarget : animationStyle.enter}
enterActiveClass={ignoreTransition() ? animationStyle.fake : animationStyle.enterActive}
exitToClass={ignoreTransition() ? animationStyle.fakeTarget : animationStyle.exitTo}
exitActiveClass={ignoreTransition() ? animationStyle.fake : animationStyle.exitActive}
onBeforeEnter={(element) => {
if (ignoreTransition()) return;
const index = Number(element.getAttribute('data-index') ?? 0);
(element as HTMLElement).style.setProperty('transition-delay', `${(index * 0.025)}s`);
}}
onAfterEnter={(element) => {
(element as HTMLElement).style.removeProperty('transition-delay');
}}
onBeforeExit={(element) => {
if (ignoreTransition()) return;
const index = Number(element.getAttribute('data-index') ?? 0);
const length = Number(element.getAttribute('data-length') ?? 1);
(element as HTMLElement).style.setProperty('transition-delay', `${(length * 0.025) - (index * 0.025)}s`);
}}
>
<Show when={!collapsed()}>
<Index each={menu()?.items}>
{(item, index) => {
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
const handleClick = () => {
if (openTarget() === anchor()) {
setOpenTarget(null);
} else {
setOpenTarget(anchor());
}
};
return (
<>
<MenuButton
ref={setAnchor}
text={item().label}
onClick={handleClick}
selected={openTarget() === anchor()}
data-index={index}
data-length={data()?.items.length}
/>
<Panel
open={openTarget() === anchor()}
anchor={anchor()}
placement={'bottom-start'}
offset={{ mainAxis: 8 }}
>
<PanelRenderer
items={item().submenu?.items ?? []}
onClick={handleItemClick}
/>
</Panel>
</>
);
}}
</Index>
</Show>
</TransitionGroup>
<Show when={props.enableController}>
<div style={{ flex: 1 }}/>
<WindowController
isMaximize={isMaximized()}
onToggleMaximize={handleToggleMaximize}
onMinimize={handleMinimize}
onClose={handleClose}
/>
</Show>
</nav>
);
};

View File

@ -0,0 +1,65 @@
import { css } from 'solid-styled-components';
import { Show } from 'solid-js';
import { IconButton } from './IconButton';
const containerStyle = css`
display: flex;
justify-content: flex-end;
align-items: center;
& > *:last-of-type {
border-top-right-radius: 4px;
&:hover {
background: rgba(255, 0, 0, 0.5);
}
}
`;
export type WindowControllerProps = {
isMaximize?: boolean;
onToggleMaximize?: () => void;
onMinimize?: () => void;
onClose?: () => void;
}
export const WindowController = (props: WindowControllerProps) => {
return (
<div class={containerStyle}>
<IconButton onClick={props.onMinimize}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/>
</svg>
</IconButton>
<IconButton onClick={props.onToggleMaximize}>
<Show
when={props.isMaximize}
fallback={
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"
/>
</svg>
}
>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"
/>
</svg>
</Show>
</IconButton>
<IconButton onClick={props.onClose}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"
/>
</svg>
</IconButton>
</div>
);
};

View File

@ -31,7 +31,7 @@ title-bar {
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
}
menu-button {
.menu-button {
-webkit-app-region: none;
display: flex;
@ -44,11 +44,11 @@ menu-button {
cursor: pointer;
}
menu-button:hover {
.menu-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
menu-panel {
.menu-panel {
position: fixed;
top: var(--y, 0);
left: var(--x, 0);
@ -84,18 +84,18 @@ menu-panel {
opacity 200ms ease 0s,
transform 200ms ease 0s;
}
menu-panel[open='true'] {
.menu-panel[open='true'] {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
menu-panel.position-by-bottom {
.menu-panel.position-by-bottom {
top: unset;
bottom: calc(100vh - var(--y, 100%));
max-height: calc(var(--y, 0) - var(--menu-bar-height, 36px) - 16px);
}
menu-item {
.menu-item {
position: relative;
-webkit-app-region: none;
@ -110,21 +110,21 @@ menu-item {
border-radius: 4px;
cursor: pointer;
}
menu-item.badge {
.menu-item.badge {
grid-template-columns: 32px 1fr auto minmax(32px, auto);
}
menu-item:hover {
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
menu-item > menu-icon {
.menu-item > .menu-icon {
height: 32px;
padding: 4px;
box-sizing: border-box;
}
menu-separator {
.menu-separator {
min-height: 1px;
height: 1px;
margin: 4px 0;
@ -132,7 +132,7 @@ menu-separator {
background-color: rgba(255, 255, 255, 0.2);
}
menu-item-badge {
.menu-item-badge {
display: flex;
justify-content: center;
align-items: center;
@ -150,7 +150,7 @@ menu-item-badge {
line-height: 1;
}
menu-item-tooltip {
.menu-item-tooltip {
position: fixed;
left: var(--x, 0);
@ -177,7 +177,7 @@ menu-item-tooltip {
transform-origin: 50% 0;
transition: opacity 0.225s ease-out, scale 0.225s ease-out;
}
menu-item-tooltip.show {
.menu-item-tooltip.show {
opacity: 1;
scale: 1.0;
}

View File

@ -9,6 +9,8 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"baseUrl": ".",
"outDir": "./dist",
"strict": true,