mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
feat: migrate from raw HTML to JSX (TSX / SolidJS) (#3583)
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -298,6 +298,7 @@
|
|||||||
"serve": "14.2.4",
|
"serve": "14.2.4",
|
||||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||||
"socks": "2.8.5",
|
"socks": "2.8.5",
|
||||||
|
"solid-element": "1.9.1",
|
||||||
"solid-floating-ui": "0.3.1",
|
"solid-floating-ui": "0.3.1",
|
||||||
"solid-js": "1.9.7",
|
"solid-js": "1.9.7",
|
||||||
"solid-styled-components": "0.28.5",
|
"solid-styled-components": "0.28.5",
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@ -208,6 +208,9 @@ importers:
|
|||||||
socks:
|
socks:
|
||||||
specifier: 2.8.5
|
specifier: 2.8.5
|
||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
|
solid-element:
|
||||||
|
specifier: 1.9.1
|
||||||
|
version: 1.9.1(solid-js@1.9.7)
|
||||||
solid-floating-ui:
|
solid-floating-ui:
|
||||||
specifier: 0.3.1
|
specifier: 0.3.1
|
||||||
version: 0.3.1(@floating-ui/dom@1.7.2)(solid-js@1.9.7)
|
version: 0.3.1(@floating-ui/dom@1.7.2)(solid-js@1.9.7)
|
||||||
@ -1976,6 +1979,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==}
|
resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
component-register@0.8.7:
|
||||||
|
resolution: {integrity: sha512-clPS/o1RNfJw7L1/w4q+nkj6l7JV32kFHCx6vW5nSPOEly4B9olMeADNilEgpLV/DdeS7y8JXhHKx9YvSj8vqQ==}
|
||||||
|
|
||||||
compressible@2.0.18:
|
compressible@2.0.18:
|
||||||
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -4280,6 +4286,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==}
|
resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==}
|
||||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||||
|
|
||||||
|
solid-element@1.9.1:
|
||||||
|
resolution: {integrity: sha512-baJy6Qz27oAUgkPlqOf3Y+7RsBiuVQrS51Nrh1ddDbrqrNPvJbIvehpUsTzLNFb2ZHIoHuNnDg330go/ZKcRdg==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.9.3
|
||||||
|
|
||||||
solid-floating-ui@0.3.1:
|
solid-floating-ui@0.3.1:
|
||||||
resolution: {integrity: sha512-o/QmGsWPS2Z3KidAxP0nDvN7alI7Kqy0kU+wd85Fz+au5SYcnYm7I6Fk3M60Za35azsPX0U+5fEtqfOuk6Ao0Q==}
|
resolution: {integrity: sha512-o/QmGsWPS2Z3KidAxP0nDvN7alI7Kqy0kU+wd85Fz+au5SYcnYm7I6Fk3M60Za35azsPX0U+5fEtqfOuk6Ao0Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -6644,6 +6655,8 @@ snapshots:
|
|||||||
|
|
||||||
compare-version@0.1.2: {}
|
compare-version@0.1.2: {}
|
||||||
|
|
||||||
|
component-register@0.8.7: {}
|
||||||
|
|
||||||
compressible@2.0.18:
|
compressible@2.0.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.54.0
|
mime-db: 1.54.0
|
||||||
@ -9218,6 +9231,11 @@ snapshots:
|
|||||||
ip-address: 9.0.5
|
ip-address: 9.0.5
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
|
|
||||||
|
solid-element@1.9.1(solid-js@1.9.7):
|
||||||
|
dependencies:
|
||||||
|
component-register: 0.8.7
|
||||||
|
solid-js: 1.9.7
|
||||||
|
|
||||||
solid-floating-ui@0.3.1(@floating-ui/dom@1.7.2)(solid-js@1.9.7):
|
solid-floating-ui@0.3.1(@floating-ui/dom@1.7.2)(solid-js@1.9.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.7.2
|
'@floating-ui/dom': 1.7.2
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import defaults from './defaults';
|
|||||||
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export type IStore = InstanceType<typeof import('conf/dist/source/index').default<Record<string, unknown>>>;
|
export type IStore = InstanceType<typeof import('conf').default<Record<string, unknown>>>;
|
||||||
|
|
||||||
const migrations = {
|
const migrations = {
|
||||||
'>=3.3.0'(store: IStore) {
|
'>=3.3.0'(store: IStore) {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "تفعيل الفيديو",
|
"name": "تفعيل الفيديو",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "أغنية"
|
"button-song": "أغنية"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -835,7 +835,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Превключване на видео",
|
"name": "Превключване на видео",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Песен"
|
"button-song": "Песен"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Botó de vídeo",
|
"name": "Botó de vídeo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Cançó"
|
"button-song": "Cançó"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -765,7 +765,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Přepínač videa",
|
"name": "Přepínač videa",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Písnička"
|
"button-song": "Písnička"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Videoumschalter",
|
"name": "Videoumschalter",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Lied"
|
"button-song": "Lied"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Εναλλαγή βίντεο",
|
"name": "Εναλλαγή βίντεο",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Τραγούδι"
|
"button-song": "Τραγούδι"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -381,6 +381,11 @@
|
|||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Open captions selector"
|
"title": "Open captions selector"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"caption-changed": "Caption changed to {{language}}",
|
||||||
|
"caption-disabled": "Captions disabled",
|
||||||
|
"no-captions": "No captions available for this song"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compact-sidebar": {
|
"compact-sidebar": {
|
||||||
@ -600,7 +605,15 @@
|
|||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser",
|
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser",
|
||||||
"name": "Navigation"
|
"name": "Navigation",
|
||||||
|
"templates": {
|
||||||
|
"back": {
|
||||||
|
"title": "Go to previous page"
|
||||||
|
},
|
||||||
|
"forward": {
|
||||||
|
"title": "Go to next page"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"no-google-login": {
|
"no-google-login": {
|
||||||
"description": "Remove Google login buttons and links from the interface",
|
"description": "Remove Google login buttons and links from the interface",
|
||||||
@ -691,6 +704,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"renderer": {
|
||||||
|
"quality-settings-button": {
|
||||||
|
"label": "Open player quality changer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"description": "Allows changing the video quality with a button on the video overlay",
|
"description": "Allows changing the video quality with a button on the video overlay",
|
||||||
"name": "Video Quality Changer"
|
"name": "Video Quality Changer"
|
||||||
},
|
},
|
||||||
@ -859,7 +877,8 @@
|
|||||||
},
|
},
|
||||||
"name": "Video Toggle",
|
"name": "Video Toggle",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Song"
|
"button-song": "Song",
|
||||||
|
"button-video": "Video"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Alternador de vídeo",
|
"name": "Alternador de vídeo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Canción"
|
"button-song": "Canción"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -831,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"name": "ویدیو به آهنگ",
|
"name": "ویدیو به آهنگ",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "ترانه"
|
"button-song": "ترانه"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -784,7 +784,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Kanta"
|
"button-song": "Kanta"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Basculer la vidéo",
|
"name": "Basculer la vidéo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Musique"
|
"button-song": "Musique"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -831,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Videó váltó",
|
"name": "Videó váltó",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Zeneszám"
|
"button-song": "Zeneszám"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Peralih Video",
|
"name": "Peralih Video",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Lagu"
|
"button-song": "Lagu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -799,7 +799,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Myndbandsrofi",
|
"name": "Myndbandsrofi",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Lag"
|
"button-song": "Lag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Selettore Brano/Video",
|
"name": "Selettore Brano/Video",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Brano"
|
"button-song": "Brano"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -857,7 +857,7 @@
|
|||||||
},
|
},
|
||||||
"name": "動画の切り替え",
|
"name": "動画の切り替え",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "曲"
|
"button-song": "曲"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,8 @@
|
|||||||
},
|
},
|
||||||
"name": "영상 전환",
|
"name": "영상 전환",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "노래"
|
"button-song": "노래",
|
||||||
|
"button-video": "영상"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -618,7 +618,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Vaizdo įrašo perjungimas",
|
"name": "Vaizdo įrašo perjungimas",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Daina"
|
"button-song": "Daina"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -381,7 +381,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Lagu"
|
"button-song": "Lagu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -577,7 +577,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Videoveksling",
|
"name": "Videoveksling",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Spor"
|
"button-song": "Spor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "भिडियो टगल",
|
"name": "भिडियो टगल",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "गीत"
|
"button-song": "गीत"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Videoschakelaar",
|
"name": "Videoschakelaar",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Nummer"
|
"button-song": "Nummer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Przełącznik wideo",
|
"name": "Przełącznik wideo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Utwór"
|
"button-song": "Utwór"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Alternar vídeo",
|
"name": "Alternar vídeo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Música"
|
"button-song": "Música"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Botão de Alternar Vídeo",
|
"name": "Botão de Alternar Vídeo",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Música"
|
"button-song": "Música"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Comutator video",
|
"name": "Comutator video",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Melodie"
|
"button-song": "Melodie"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Переключатель видео",
|
"name": "Переключатель видео",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Песня"
|
"button-song": "Песня"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -222,7 +222,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Låt"
|
"button-song": "Låt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "வீடியோ மாற்று",
|
"name": "வீடியோ மாற்று",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "பாடல்"
|
"button-song": "பாடல்"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "ปุ่มวิดีโอ",
|
"name": "ปุ่มวิดีโอ",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "เพลง"
|
"button-song": "เพลง"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Video Geçiş",
|
"name": "Video Geçiş",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Şarkı"
|
"button-song": "Şarkı"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Перемикач відео",
|
"name": "Перемикач відео",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Пісня"
|
"button-song": "Пісня"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -857,7 +857,7 @@
|
|||||||
},
|
},
|
||||||
"name": "Chuyển đổi video",
|
"name": "Chuyển đổi video",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Bài hát"
|
"button-song": "Bài hát"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "视频切换开关",
|
"name": "视频切换开关",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "歌曲"
|
"button-song": "歌曲"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -859,7 +859,7 @@
|
|||||||
},
|
},
|
||||||
"name": "歌曲/影片切換",
|
"name": "歌曲/影片切換",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "歌曲"
|
"button-song": "歌曲"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -763,7 +763,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
const splited = decodeURIComponent(command).split(' ');
|
const splited = decodeURIComponent(command).split(' ');
|
||||||
|
|
||||||
handleProtocol(splited.shift()!, splited);
|
handleProtocol(splited.shift()!, ...splited);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
import { waitForElement } from '@/utils/wait-for-element';
|
import { waitForElement } from '@/utils/wait-for-element';
|
||||||
|
|
||||||
import undislikeHTML from './templates/undislike.html?raw';
|
import {
|
||||||
import dislikeHTML from './templates/dislike.html?raw';
|
DislikeButton,
|
||||||
import likeHTML from './templates/like.html?raw';
|
LikeButton,
|
||||||
import unlikeHTML from './templates/unlike.html?raw';
|
UnDislikeButton,
|
||||||
|
UnLikeButton,
|
||||||
|
} from './templates';
|
||||||
|
|
||||||
export default createPlugin<
|
export default createPlugin<
|
||||||
unknown,
|
unknown,
|
||||||
@ -52,19 +57,69 @@ export default createPlugin<
|
|||||||
}
|
}
|
||||||
const continuations = await waitForElement<HTMLElement>('#continuations');
|
const continuations = await waitForElement<HTMLElement>('#continuations');
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
//Gets the for buttons
|
|
||||||
const buttons: Array<HTMLElement> = [
|
const [showUnDislike, setShowUnDislike] = createSignal(true);
|
||||||
ElementFromHtml(undislikeHTML),
|
const [showDislike, setShowDislike] = createSignal(true);
|
||||||
ElementFromHtml(dislikeHTML),
|
const [showLike, setShowLike] = createSignal(true);
|
||||||
ElementFromHtml(likeHTML),
|
const [showUnLike, setShowUnLike] = createSignal(true);
|
||||||
ElementFromHtml(unlikeHTML),
|
|
||||||
];
|
const DEFAULT_MASK_SIZE = '100% 50%';
|
||||||
|
const [unDislikeMaskSize, setUnDislikeMaskSize] =
|
||||||
|
createSignal(DEFAULT_MASK_SIZE);
|
||||||
|
const [dislikeMaskSize, setDislikeMaskSize] =
|
||||||
|
createSignal(DEFAULT_MASK_SIZE);
|
||||||
|
const [likeMaskSize, setLikeMaskSize] = createSignal(DEFAULT_MASK_SIZE);
|
||||||
|
const [unLikeMaskSize, setUnLikeMaskSize] =
|
||||||
|
createSignal(DEFAULT_MASK_SIZE);
|
||||||
|
|
||||||
|
const buttonContainer = document.createElement('div');
|
||||||
|
buttonContainer.style.display = 'flex';
|
||||||
|
buttonContainer.style.flexDirection = 'row';
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<Show when={showUnDislike()}>
|
||||||
|
<UnDislikeButton
|
||||||
|
onClick={this.loadFullList}
|
||||||
|
maskSize={unDislikeMaskSize()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={showDislike()}>
|
||||||
|
<DislikeButton
|
||||||
|
onClick={this.loadFullList}
|
||||||
|
maskSize={dislikeMaskSize()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={showLike()}>
|
||||||
|
<LikeButton
|
||||||
|
onClick={this.loadFullList}
|
||||||
|
maskSize={likeMaskSize()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={showUnLike()}>
|
||||||
|
<UnLikeButton
|
||||||
|
onClick={this.loadFullList}
|
||||||
|
maskSize={unLikeMaskSize()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
buttonContainer,
|
||||||
|
);
|
||||||
|
|
||||||
//Finds the playlist
|
//Finds the playlist
|
||||||
const playlist =
|
const playlist =
|
||||||
document.querySelector('ytmusic-playlist-shelf-renderer') ??
|
document.querySelector('ytmusic-playlist-shelf-renderer') ??
|
||||||
Array.prototype.at.call(document.querySelectorAll('ytmusic-shelf-renderer'), -1)!;
|
document.querySelector(':nth-last-child(1 of ytmusic-shelf-renderer)');
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Adds an observer for every button, so it gets updated when one is clicked
|
// Adds an observer for every button, so it gets updated when one is clicked
|
||||||
this.changeObserver?.disconnect();
|
this.changeObserver?.disconnect();
|
||||||
|
|
||||||
this.changeObserver = new MutationObserver(() => {
|
this.changeObserver = new MutationObserver(() => {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.start();
|
this.start();
|
||||||
@ -84,34 +139,57 @@ export default createPlugin<
|
|||||||
'#button-shape-dislike > button',
|
'#button-shape-dislike > button',
|
||||||
).length;
|
).length;
|
||||||
if (continuations.children.length == 0 && listsLength > 0) {
|
if (continuations.children.length == 0 && listsLength > 0) {
|
||||||
const counts = [
|
const counts = {
|
||||||
playlist?.querySelectorAll(
|
dislike: playlist?.querySelectorAll(
|
||||||
'#button-shape-dislike > button[aria-pressed=true]',
|
'#button-shape-dislike > button[aria-pressed=true]',
|
||||||
).length,
|
).length,
|
||||||
playlist?.querySelectorAll(
|
undislike: playlist?.querySelectorAll(
|
||||||
'#button-shape-dislike > button[aria-pressed=false]',
|
'#button-shape-dislike > button[aria-pressed=false]',
|
||||||
).length,
|
).length,
|
||||||
playlist?.querySelectorAll(
|
unlike: playlist?.querySelectorAll(
|
||||||
'#button-shape-like > button[aria-pressed=false]',
|
'#button-shape-like > button[aria-pressed=false]',
|
||||||
).length,
|
).length,
|
||||||
playlist?.querySelectorAll(
|
like: playlist?.querySelectorAll(
|
||||||
'#button-shape-like > button[aria-pressed=true]',
|
'#button-shape-like > button[aria-pressed=true]',
|
||||||
).length,
|
).length,
|
||||||
];
|
};
|
||||||
let i = 0;
|
for (const [name, size] of Object.entries(counts)) {
|
||||||
for (const count of counts) {
|
switch (name) {
|
||||||
if (count == 0) {
|
case 'dislike':
|
||||||
buttons.splice(i, 1);
|
if (size > 0) {
|
||||||
i--;
|
setShowDislike(true);
|
||||||
} else {
|
setDislikeMaskSize(`100% ${100 - (size / listsLength) * 100}%`);
|
||||||
(
|
} else {
|
||||||
buttons[i].children[0].children[0] as HTMLElement
|
setShowDislike(false);
|
||||||
).style.setProperty(
|
}
|
||||||
'-webkit-mask-size',
|
break;
|
||||||
`100% ${100 - (count / listsLength) * 100}%`,
|
case 'undislike':
|
||||||
);
|
if (size > 0) {
|
||||||
|
setShowUnDislike(true);
|
||||||
|
setUnDislikeMaskSize(
|
||||||
|
`100% ${100 - (size / listsLength) * 100}%`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setShowUnDislike(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'like':
|
||||||
|
if (size > 0) {
|
||||||
|
setShowLike(true);
|
||||||
|
setLikeMaskSize(`100% ${100 - (size / listsLength) * 100}%`);
|
||||||
|
} else {
|
||||||
|
setShowLike(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'unlike':
|
||||||
|
if (size > 0) {
|
||||||
|
setShowUnLike(true);
|
||||||
|
setUnLikeMaskSize(`100% ${100 - (size / listsLength) * 100}%`);
|
||||||
|
} else {
|
||||||
|
setShowUnLike(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const menuParent =
|
const menuParent =
|
||||||
@ -126,10 +204,7 @@ export default createPlugin<
|
|||||||
menu,
|
menu,
|
||||||
menuParent.children[menuParent.children.length - 1],
|
menuParent.children[menuParent.children.length - 1],
|
||||||
);
|
);
|
||||||
for (const button of buttons) {
|
menu.appendChild(buttonContainer);
|
||||||
menu.appendChild(button);
|
|
||||||
button.addEventListener('click', this.loadFullList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadFullList(event: MouseEvent) {
|
loadFullList(event: MouseEvent) {
|
||||||
@ -159,7 +234,7 @@ export default createPlugin<
|
|||||||
let playlistButtons: NodeListOf<HTMLElement> | undefined;
|
let playlistButtons: NodeListOf<HTMLElement> | undefined;
|
||||||
const playlist =
|
const playlist =
|
||||||
document.querySelector('ytmusic-playlist-shelf-renderer') ??
|
document.querySelector('ytmusic-playlist-shelf-renderer') ??
|
||||||
Array.prototype.at.call(document.querySelectorAll('ytmusic-shelf-renderer'), -1)!;
|
document.querySelector(':nth-last-child(1 of ytmusic-shelf-renderer)');
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'allundislike':
|
case 'allundislike':
|
||||||
playlistButtons = playlist?.querySelectorAll(
|
playlistButtons = playlist?.querySelectorAll(
|
||||||
99
src/plugins/album-actions/templates/dislike-button.tsx
Normal file
99
src/plugins/album-actions/templates/dislike-button.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
export interface DislikeButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
maskSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DislikeButton = (props: DislikeButtonProps) => (
|
||||||
|
<div class="style-scope">
|
||||||
|
<button
|
||||||
|
id="alldislike"
|
||||||
|
data-type="dislike"
|
||||||
|
data-filled="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
aria-pressed="false"
|
||||||
|
aria-label="Dislike all"
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'white',
|
||||||
|
'mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask-size': props.maskSize,
|
||||||
|
'-webkit-mask-repeat': 'no-repeat',
|
||||||
|
'z-index': 1,
|
||||||
|
'position': 'absolute',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div style="width: 24px; height: 24px">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<yt-touch-feedback-shape
|
||||||
|
style={{
|
||||||
|
'border-radius': 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||||
|
</div>
|
||||||
|
</yt-touch-feedback-shape>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<div class="style-scope">
|
|
||||||
<button
|
|
||||||
id="alldislike"
|
|
||||||
data-type="dislike"
|
|
||||||
data-filled="false"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Dislike all"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="
|
|
||||||
color: white;
|
|
||||||
-webkit-mask: linear-gradient(grey, grey);
|
|
||||||
-webkit-mask-size: 100% 50%;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
|
||||||
<div
|
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
|
||||||
</div>
|
|
||||||
</yt-touch-feedback-shape>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
4
src/plugins/album-actions/templates/index.ts
Normal file
4
src/plugins/album-actions/templates/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './like-button';
|
||||||
|
export * from './dislike-button';
|
||||||
|
export * from './undislike-button';
|
||||||
|
export * from './unlike-button';
|
||||||
90
src/plugins/album-actions/templates/like-button.tsx
Normal file
90
src/plugins/album-actions/templates/like-button.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
export interface LikeButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
maskSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LikeButton = (props: LikeButtonProps) => (
|
||||||
|
<div class="style-scope">
|
||||||
|
<button
|
||||||
|
id="alllike"
|
||||||
|
data-type="like"
|
||||||
|
data-filled="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
aria-pressed="false"
|
||||||
|
aria-label="Like all"
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'white',
|
||||||
|
'mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask-size': props.maskSize,
|
||||||
|
'-webkit-mask-repeat': 'no-repeat',
|
||||||
|
'z-index': 1,
|
||||||
|
'position': 'absolute',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div style="width: 24px; height: 24px">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="width: 24px; height: 24px">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<yt-touch-feedback-shape style="border-radius: inherit">
|
||||||
|
<div
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||||
|
</div>
|
||||||
|
</yt-touch-feedback-shape>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<div class="style-scope">
|
|
||||||
<button
|
|
||||||
id="alllike"
|
|
||||||
data-type="like"
|
|
||||||
data-filled="false"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Like all"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="
|
|
||||||
color: white;
|
|
||||||
-webkit-mask: linear-gradient(grey, grey);
|
|
||||||
-webkit-mask-size: 100% 50%;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
|
||||||
<div
|
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
|
||||||
</div>
|
|
||||||
</yt-touch-feedback-shape>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
104
src/plugins/album-actions/templates/undislike-button.tsx
Normal file
104
src/plugins/album-actions/templates/undislike-button.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
export interface UnDislikeButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
maskSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||||
|
<div class="style-scope">
|
||||||
|
<button
|
||||||
|
id="allundislike"
|
||||||
|
data-type="dislike"
|
||||||
|
data-filled="true"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
aria-pressed="false"
|
||||||
|
aria-label="Undislike all"
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
color: 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'white',
|
||||||
|
'mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask-size': props.maskSize,
|
||||||
|
'-webkit-mask-repeat': 'no-repeat',
|
||||||
|
'z-index': 1,
|
||||||
|
'position': 'absolute',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<yt-touch-feedback-shape
|
||||||
|
style={{
|
||||||
|
'border-radius': 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||||
|
</div>
|
||||||
|
</yt-touch-feedback-shape>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<div class="style-scope">
|
|
||||||
<button
|
|
||||||
id="allundislike"
|
|
||||||
data-type="dislike"
|
|
||||||
data-filled="true"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Undislike all"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="
|
|
||||||
color: white;
|
|
||||||
-webkit-mask: linear-gradient(grey, grey);
|
|
||||||
-webkit-mask-size: 100% 50%;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
|
||||||
<div
|
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
|
||||||
</div>
|
|
||||||
</yt-touch-feedback-shape>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
104
src/plugins/album-actions/templates/unlike-button.tsx
Normal file
104
src/plugins/album-actions/templates/unlike-button.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
export interface UnLikeButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
maskSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||||
|
<div class="style-scope">
|
||||||
|
<button
|
||||||
|
id="allunlike"
|
||||||
|
data-type="like"
|
||||||
|
data-filled="true"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
aria-pressed="false"
|
||||||
|
aria-label="Unlike all"
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-button-shape-next__icon"
|
||||||
|
style={{
|
||||||
|
'color': 'white',
|
||||||
|
'mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask': 'linear-gradient(grey, grey)',
|
||||||
|
'-webkit-mask-size': props.maskSize,
|
||||||
|
'-webkit-mask-repeat': 'no-repeat',
|
||||||
|
'z-index': 1,
|
||||||
|
'position': 'absolute',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<yt-touch-feedback-shape
|
||||||
|
style={{
|
||||||
|
'border-radius': 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||||
|
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||||
|
</div>
|
||||||
|
</yt-touch-feedback-shape>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<div class="style-scope">
|
|
||||||
<button
|
|
||||||
id="allunlike"
|
|
||||||
data-type="like"
|
|
||||||
data-filled="true"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Unlike all"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-spec-button-shape-next__icon"
|
|
||||||
style="
|
|
||||||
color: white;
|
|
||||||
-webkit-mask: linear-gradient(grey, grey);
|
|
||||||
-webkit-mask-size: 100% 50%;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="width: 24px; height: 24px">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
|
||||||
<div
|
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
|
||||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
|
||||||
</div>
|
|
||||||
</yt-touch-feedback-shape>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@ -7,7 +7,7 @@ import { t } from '@/i18n';
|
|||||||
export default createBackend({
|
export default createBackend({
|
||||||
start({ ipc: { handle }, window }) {
|
start({ ipc: { handle }, window }) {
|
||||||
handle(
|
handle(
|
||||||
'captionsSelector',
|
'ytmd:captions-selector',
|
||||||
async (captionLabels: Record<string, string>, currentIndex: string) =>
|
async (captionLabels: Record<string, string>, currentIndex: string) =>
|
||||||
await prompt(
|
await prompt(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,21 +1,20 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { YoutubePlayer } from '@/types/youtube-player';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import backend from './back';
|
import backend from './back';
|
||||||
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
||||||
import { t } from '@/i18n';
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
export default createPlugin<
|
export default createPlugin<
|
||||||
unknown,
|
unknown,
|
||||||
unknown,
|
unknown,
|
||||||
{
|
{
|
||||||
captionsSettingsButton: HTMLElement;
|
captionsSettingsButton?: HTMLElement;
|
||||||
captionTrackList: LanguageOptions[] | null;
|
captionTrackList: LanguageOptions[] | null;
|
||||||
api: YoutubePlayer | null;
|
api: YoutubePlayer | null;
|
||||||
config: CaptionsSelectorConfig | null;
|
config: CaptionsSelectorConfig | null;
|
||||||
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
|
|
||||||
videoChangeListener: () => void;
|
videoChangeListener: () => void;
|
||||||
captionsButtonClickListener: () => void;
|
|
||||||
},
|
},
|
||||||
CaptionsSelectorConfig
|
CaptionsSelectorConfig
|
||||||
>({
|
>({
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
import { createRenderer } from '@/utils';
|
|
||||||
|
|
||||||
import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html?raw';
|
|
||||||
|
|
||||||
import { YoutubePlayer } from '@/types/youtube-player';
|
|
||||||
|
|
||||||
export interface LanguageOptions {
|
|
||||||
displayName: string;
|
|
||||||
id: string | null;
|
|
||||||
is_default: boolean;
|
|
||||||
is_servable: boolean;
|
|
||||||
is_translateable: boolean;
|
|
||||||
kind: string;
|
|
||||||
languageCode: string; // 2 length
|
|
||||||
languageName: string;
|
|
||||||
name: string | null;
|
|
||||||
vss_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CaptionsSelectorConfig {
|
|
||||||
enabled: boolean;
|
|
||||||
disableCaptions: boolean;
|
|
||||||
autoload: boolean;
|
|
||||||
lastCaptionsCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createRenderer<
|
|
||||||
{
|
|
||||||
captionsSettingsButton: HTMLElement;
|
|
||||||
captionTrackList: LanguageOptions[] | null;
|
|
||||||
api: YoutubePlayer | null;
|
|
||||||
config: CaptionsSelectorConfig | null;
|
|
||||||
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
|
|
||||||
videoChangeListener: () => void;
|
|
||||||
captionsButtonClickListener: () => void;
|
|
||||||
},
|
|
||||||
CaptionsSelectorConfig
|
|
||||||
>({
|
|
||||||
captionsSettingsButton: ElementFromHtml(CaptionsSettingsButtonHTML),
|
|
||||||
captionTrackList: null,
|
|
||||||
api: null,
|
|
||||||
config: null,
|
|
||||||
setConfig: () => {},
|
|
||||||
async captionsButtonClickListener() {
|
|
||||||
if (this.captionTrackList?.length) {
|
|
||||||
const currentCaptionTrack = this.api!.getOption<LanguageOptions>(
|
|
||||||
'captions',
|
|
||||||
'track',
|
|
||||||
);
|
|
||||||
let currentIndex = currentCaptionTrack
|
|
||||||
? this.captionTrackList.indexOf(
|
|
||||||
this.captionTrackList.find(
|
|
||||||
(track) =>
|
|
||||||
track.languageCode === currentCaptionTrack.languageCode,
|
|
||||||
)!,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const captionLabels = [
|
|
||||||
...this.captionTrackList.map((track) => track.displayName),
|
|
||||||
'None',
|
|
||||||
];
|
|
||||||
|
|
||||||
currentIndex = (await window.ipcRenderer.invoke(
|
|
||||||
'captionsSelector',
|
|
||||||
captionLabels,
|
|
||||||
currentIndex,
|
|
||||||
)) as number;
|
|
||||||
if (currentIndex === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCaptions = this.captionTrackList[currentIndex];
|
|
||||||
this.setConfig({ lastCaptionsCode: newCaptions?.languageCode });
|
|
||||||
if (newCaptions) {
|
|
||||||
this.api?.setOption('captions', 'track', {
|
|
||||||
languageCode: newCaptions.languageCode,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.api?.setOption('captions', 'track', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => this.api?.playVideo());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoChangeListener() {
|
|
||||||
if (this.config?.disableCaptions) {
|
|
||||||
setTimeout(() => this.api!.unloadModule('captions'), 100);
|
|
||||||
this.captionsSettingsButton.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api!.loadModule('captions');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.captionTrackList =
|
|
||||||
this.api!.getOption('captions', 'tracklist') ?? [];
|
|
||||||
|
|
||||||
if (this.config!.autoload && this.config!.lastCaptionsCode) {
|
|
||||||
this.api?.setOption('captions', 'track', {
|
|
||||||
languageCode: this.config!.lastCaptionsCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.captionsSettingsButton.style.display = this.captionTrackList?.length
|
|
||||||
? 'inline-block'
|
|
||||||
: 'none';
|
|
||||||
}, 250);
|
|
||||||
},
|
|
||||||
async start({ getConfig, setConfig }) {
|
|
||||||
this.config = await getConfig();
|
|
||||||
this.setConfig = setConfig;
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
document
|
|
||||||
.querySelector('.right-controls-buttons')
|
|
||||||
?.removeChild(this.captionsSettingsButton);
|
|
||||||
document
|
|
||||||
.querySelector<YoutubePlayer & HTMLElement>('#movie_player')
|
|
||||||
?.unloadModule('captions');
|
|
||||||
document
|
|
||||||
.querySelector('video')
|
|
||||||
?.removeEventListener('ytmd:src-changed', this.videoChangeListener);
|
|
||||||
this.captionsSettingsButton.removeEventListener(
|
|
||||||
'click',
|
|
||||||
this.captionsButtonClickListener,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onPlayerApiReady(playerApi) {
|
|
||||||
this.api = playerApi;
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector('.right-controls-buttons')
|
|
||||||
?.append(this.captionsSettingsButton);
|
|
||||||
|
|
||||||
this.captionTrackList =
|
|
||||||
this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector('video')
|
|
||||||
?.addEventListener('ytmd:src-changed', this.videoChangeListener);
|
|
||||||
this.captionsSettingsButton.addEventListener(
|
|
||||||
'click',
|
|
||||||
this.captionsButtonClickListener,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onConfigChange(newConfig) {
|
|
||||||
this.config = newConfig;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
164
src/plugins/captions-selector/renderer.tsx
Normal file
164
src/plugins/captions-selector/renderer.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { render } from 'solid-js/web';
|
||||||
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
|
import { createRenderer } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { CaptionsSettingButton } from './templates/captions-settings-template';
|
||||||
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
import type { AppElement } from '@/types/queue';
|
||||||
|
|
||||||
|
export interface LanguageOptions {
|
||||||
|
displayName: string;
|
||||||
|
id: string | null;
|
||||||
|
is_default: boolean;
|
||||||
|
is_servable: boolean;
|
||||||
|
is_translateable: boolean;
|
||||||
|
kind: string;
|
||||||
|
languageCode: string; // 2 length
|
||||||
|
languageName: string;
|
||||||
|
name: string | null;
|
||||||
|
vss_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptionsSelectorConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
disableCaptions: boolean;
|
||||||
|
autoload: boolean;
|
||||||
|
lastCaptionsCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hidden, setHidden] = createSignal(false);
|
||||||
|
|
||||||
|
export default createRenderer<
|
||||||
|
{
|
||||||
|
captionsSettingsButton?: HTMLElement;
|
||||||
|
captionTrackList: LanguageOptions[] | null;
|
||||||
|
api: YoutubePlayer | null;
|
||||||
|
config: CaptionsSelectorConfig | null;
|
||||||
|
videoChangeListener: () => void;
|
||||||
|
},
|
||||||
|
CaptionsSelectorConfig
|
||||||
|
>({
|
||||||
|
captionTrackList: null,
|
||||||
|
api: null,
|
||||||
|
config: null,
|
||||||
|
videoChangeListener() {
|
||||||
|
if (this.config?.disableCaptions) {
|
||||||
|
setTimeout(() => this.api!.unloadModule('captions'), 100);
|
||||||
|
setHidden(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.api!.loadModule('captions');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.captionTrackList =
|
||||||
|
this.api!.getOption('captions', 'tracklist') ?? [];
|
||||||
|
|
||||||
|
if (this.config!.autoload && this.config!.lastCaptionsCode) {
|
||||||
|
this.api?.setOption('captions', 'track', {
|
||||||
|
languageCode: this.config!.lastCaptionsCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setHidden(!this.captionTrackList?.length);
|
||||||
|
}, 250);
|
||||||
|
},
|
||||||
|
async start({ getConfig }) {
|
||||||
|
this.config = await getConfig();
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.api?.unloadModule('captions');
|
||||||
|
document
|
||||||
|
.querySelector('video')
|
||||||
|
?.removeEventListener('ytmd:src-changed', this.videoChangeListener);
|
||||||
|
if (this.captionsSettingsButton) {
|
||||||
|
document
|
||||||
|
.querySelector('.right-controls-buttons')
|
||||||
|
?.removeChild(this.captionsSettingsButton);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPlayerApiReady(playerApi, { ipc, setConfig }) {
|
||||||
|
this.api = playerApi;
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<Show when={!hidden()}>
|
||||||
|
<CaptionsSettingButton
|
||||||
|
label={t('plugins.captions-selector.templates.title')}
|
||||||
|
onClick={async () => {
|
||||||
|
const appApi = document.querySelector<AppElement>('ytmusic-app');
|
||||||
|
|
||||||
|
if (this.captionTrackList?.length) {
|
||||||
|
const currentCaptionTrack =
|
||||||
|
playerApi.getOption<LanguageOptions>('captions', 'track');
|
||||||
|
|
||||||
|
let currentIndex = currentCaptionTrack
|
||||||
|
? this.captionTrackList.indexOf(
|
||||||
|
this.captionTrackList.find(
|
||||||
|
(track) =>
|
||||||
|
track.languageCode ===
|
||||||
|
currentCaptionTrack.languageCode,
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const captionLabels = [
|
||||||
|
...this.captionTrackList.map((track) => track.displayName),
|
||||||
|
'None',
|
||||||
|
];
|
||||||
|
|
||||||
|
currentIndex = (await ipc.invoke(
|
||||||
|
'ytmd:captions-selector',
|
||||||
|
captionLabels,
|
||||||
|
currentIndex,
|
||||||
|
)) as number;
|
||||||
|
if (currentIndex === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCaptions = this.captionTrackList[currentIndex];
|
||||||
|
setConfig({ lastCaptionsCode: newCaptions?.languageCode });
|
||||||
|
if (newCaptions) {
|
||||||
|
playerApi.setOption('captions', 'track', {
|
||||||
|
languageCode: newCaptions.languageCode,
|
||||||
|
});
|
||||||
|
appApi?.toastService?.show(
|
||||||
|
t('plugins.captions-selector.toast.caption-changed', {
|
||||||
|
language: newCaptions.displayName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
playerApi.setOption('captions', 'track', {});
|
||||||
|
appApi?.toastService?.show(
|
||||||
|
t('plugins.captions-selector.toast.caption-disabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => playerApi.playVideo());
|
||||||
|
} else {
|
||||||
|
appApi?.toastService?.show(
|
||||||
|
t('plugins.captions-selector.toast.no-captions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={this.captionsSettingsButton}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
document.querySelector('.right-controls-buttons')!,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.captionTrackList =
|
||||||
|
this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector('video')
|
||||||
|
?.addEventListener('ytmd:src-changed', this.videoChangeListener);
|
||||||
|
},
|
||||||
|
onConfigChange(newConfig) {
|
||||||
|
this.config = newConfig;
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<tp-yt-paper-icon-button
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-label="Open captions selector"
|
|
||||||
class="player-captions-button style-scope ytmusic-player"
|
|
||||||
icon="yt-icons:subtitles"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Open captions selector"
|
|
||||||
>
|
|
||||||
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
|
|
||||||
<svg
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
focusable="false"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
class="style-scope tp-yt-iron-icon"
|
|
||||||
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</tp-yt-iron-icon>
|
|
||||||
</tp-yt-paper-icon-button>
|
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
export interface CaptionsSettingsButtonProps {
|
||||||
|
label: string;
|
||||||
|
onClick: (event: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CaptionsSettingButton = (props: CaptionsSettingsButtonProps) => (
|
||||||
|
<yt-icon-button
|
||||||
|
aria-disabled={false}
|
||||||
|
aria-label={props.label}
|
||||||
|
class="player-captions-button style-scope ytmusic-player-bar"
|
||||||
|
icon={'yt-icons:subtitles'}
|
||||||
|
role={'button'}
|
||||||
|
tabindex={0}
|
||||||
|
title={props.label}
|
||||||
|
on:click={props.onClick}
|
||||||
|
>
|
||||||
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'block',
|
||||||
|
fill: 'currentcolor',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</yt-icon-button>
|
||||||
|
);
|
||||||
@ -1,26 +1,24 @@
|
|||||||
import downloadHTML from './templates/download.html?raw';
|
import { createSignal } from 'solid-js';
|
||||||
|
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
import defaultConfig from '@/config/defaults';
|
import defaultConfig from '@/config/defaults';
|
||||||
import { getSongMenu } from '@/providers/dom-elements';
|
import { getSongMenu } from '@/providers/dom-elements';
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
import { LoggerPrefix } from '@/utils';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||||
|
|
||||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
import { DownloadButton } from './templates/download';
|
||||||
|
|
||||||
import { ElementFromHtml } from '../utils/renderer';
|
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
import type { DownloaderPluginConfig } from './index';
|
import type { DownloaderPluginConfig } from './index';
|
||||||
|
|
||||||
let menu: Element | null = null;
|
let menu: HTMLElement | null = null;
|
||||||
let progress: Element | null = null;
|
let download: () => void;
|
||||||
const downloadButton = ElementFromHtml(downloadHTML);
|
|
||||||
|
|
||||||
let doneFirstLoad = false;
|
const [downloadButtonText, setDownloadButtonText] = createSignal<string>('');
|
||||||
|
|
||||||
|
let buttonContainer: HTMLDivElement | null = null;
|
||||||
|
|
||||||
const menuObserver = new MutationObserver(() => {
|
const menuObserver = new MutationObserver(() => {
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
@ -30,46 +28,24 @@ const menuObserver = new MutationObserver(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu.contains(downloadButton)) {
|
if (
|
||||||
|
menu.contains(buttonContainer) ||
|
||||||
|
!isMusicOrVideoTrack() ||
|
||||||
|
!buttonContainer
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for video (or music)
|
menu.prepend(buttonContainer);
|
||||||
let menuUrl = document.querySelector<HTMLAnchorElement>(
|
|
||||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
|
||||||
)?.href;
|
|
||||||
if (!menuUrl?.includes('watch?')) {
|
|
||||||
menuUrl = undefined;
|
|
||||||
// check for podcast
|
|
||||||
for (const it of document.querySelectorAll(
|
|
||||||
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
|
||||||
)) {
|
|
||||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
|
||||||
menuUrl = it.getAttribute('href')!;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!menuUrl && doneFirstLoad) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.prepend(downloadButton);
|
|
||||||
progress = document.querySelector('#ytmcustom-download');
|
|
||||||
|
|
||||||
if (!doneFirstLoad) {
|
|
||||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const onRendererLoad = ({
|
export const onRendererLoad = ({
|
||||||
ipc,
|
ipc,
|
||||||
}: RendererContext<DownloaderPluginConfig>) => {
|
}: RendererContext<DownloaderPluginConfig>) => {
|
||||||
window.download = () => {
|
download = () => {
|
||||||
const songMenu = getSongMenu();
|
const songMenu = getSongMenu();
|
||||||
|
|
||||||
let videoUrl = songMenu
|
let videoUrl = songMenu
|
||||||
// Selector of first button which is always "Start Radio"
|
|
||||||
?.querySelector(
|
?.querySelector(
|
||||||
'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
|
'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
|
||||||
)
|
)
|
||||||
@ -108,21 +84,30 @@ export const onRendererLoad = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
ipc.on('downloader-feedback', (feedback: string) => {
|
ipc.on('downloader-feedback', (feedback: string) => {
|
||||||
if (progress) {
|
const targetHtml = feedback || t('plugins.downloader.templates.button');
|
||||||
const targetHtml = feedback || t('plugins.downloader.templates.button');
|
setDownloadButtonText(targetHtml);
|
||||||
(progress.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
|
||||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
|
||||||
: targetHtml;
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
LoggerPrefix,
|
|
||||||
t('plugins.downloader.renderer.can-not-update-progress'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onPlayerApiReady = () => {
|
export const onPlayerApiReady = () => {
|
||||||
|
setDownloadButtonText(t('plugins.downloader.templates.button'));
|
||||||
|
|
||||||
|
buttonContainer = document.createElement('div');
|
||||||
|
buttonContainer.classList.add(
|
||||||
|
'style-scope',
|
||||||
|
'menu-item',
|
||||||
|
'ytmusic-menu-popup-renderer',
|
||||||
|
);
|
||||||
|
buttonContainer.setAttribute('aria-disabled', 'false');
|
||||||
|
buttonContainer.setAttribute('aria-selected', 'false');
|
||||||
|
buttonContainer.setAttribute('role', 'option');
|
||||||
|
buttonContainer.setAttribute('tabindex', '-1');
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => <DownloadButton onClick={download} text={downloadButtonText()} />,
|
||||||
|
buttonContainer,
|
||||||
|
);
|
||||||
|
|
||||||
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
@ -1,24 +1,23 @@
|
|||||||
<div
|
export const DownloadButton = (props: {
|
||||||
aria-disabled="false"
|
onClick: () => void;
|
||||||
aria-selected="false"
|
text: string;
|
||||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
}) => (
|
||||||
onclick="download()"
|
|
||||||
role="option"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="navigation-endpoint"
|
id="navigation-endpoint"
|
||||||
tabindex="-1"
|
tabindex={-1}
|
||||||
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<div
|
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||||
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
focusable="false"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
@ -39,7 +38,7 @@
|
|||||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="ytmcustom-download"
|
id="ytmcustom-download"
|
||||||
>
|
>
|
||||||
<ytmd-trans key="plugins.downloader.templates.button"></ytmd-trans>
|
{props.text}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
);
|
||||||
42
src/plugins/navigation/components/back-button.tsx
Normal file
42
src/plugins/navigation/components/back-button.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export interface BackButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackButton = (props: BackButtonProps) => (
|
||||||
|
<div
|
||||||
|
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||||
|
onClick={props.onClick}
|
||||||
|
role="tab"
|
||||||
|
tab-id="FEmusic_back"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-disabled="false"
|
||||||
|
class="search-icon style-scope ytmusic-search-box"
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
title={props.title}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="tab-icon style-scope paper-icon-button navigation-icon"
|
||||||
|
id="icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope iron-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
viewBox="0 0 492 492"
|
||||||
|
>
|
||||||
|
<g class="style-scope iron-icon">
|
||||||
|
<path d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
46
src/plugins/navigation/components/forward-button.tsx
Normal file
46
src/plugins/navigation/components/forward-button.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export interface ForwardButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ForwardButton = (props: ForwardButtonProps) => (
|
||||||
|
<div
|
||||||
|
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||||
|
onClick={props.onClick}
|
||||||
|
role="tab"
|
||||||
|
tab-id="FEmusic_next"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-disabled="false"
|
||||||
|
class="search-icon style-scope ytmusic-search-box"
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
title={props.title}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="tab-icon style-scope paper-icon-button navigation-icon"
|
||||||
|
id="icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope iron-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
viewBox="0 0 492 492"
|
||||||
|
>
|
||||||
|
<g class="style-scope iron-icon">
|
||||||
|
<path
|
||||||
|
d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
|
||||||
|
l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265
|
||||||
|
c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import style from './style.css?inline';
|
|
||||||
import { createPlugin } from '@/utils';
|
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
|
||||||
|
|
||||||
import forwardHTML from './templates/forward.html?raw';
|
|
||||||
import backHTML from './templates/back.html?raw';
|
|
||||||
|
|
||||||
export default createPlugin({
|
|
||||||
name: () => t('plugins.navigation.name'),
|
|
||||||
description: () => t('plugins.navigation.description'),
|
|
||||||
restartNeeded: false,
|
|
||||||
config: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
stylesheets: [style],
|
|
||||||
renderer: {
|
|
||||||
start() {
|
|
||||||
const forwardButton = ElementFromHtml(forwardHTML);
|
|
||||||
const backButton = ElementFromHtml(backHTML);
|
|
||||||
const menu = document.querySelector('#right-content');
|
|
||||||
menu?.prepend(backButton, forwardButton);
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
document.querySelector('[tab-id=FEmusic_back]')?.remove();
|
|
||||||
document.querySelector('[tab-id=FEmusic_next]')?.remove();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
48
src/plugins/navigation/index.tsx
Normal file
48
src/plugins/navigation/index.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import style from './style.css?inline';
|
||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { ForwardButton } from './components/forward-button';
|
||||||
|
import { BackButton } from './components/back-button';
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
|
name: () => t('plugins.navigation.name'),
|
||||||
|
description: () => t('plugins.navigation.description'),
|
||||||
|
restartNeeded: false,
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
stylesheets: [style],
|
||||||
|
renderer: {
|
||||||
|
buttonContainer: document.createElement('div'),
|
||||||
|
start() {
|
||||||
|
if (!this.buttonContainer) {
|
||||||
|
this.buttonContainer = document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<BackButton
|
||||||
|
onClick={() => history.back()}
|
||||||
|
title={t('plugins.navigation.templates.back.title')}
|
||||||
|
/>
|
||||||
|
<ForwardButton
|
||||||
|
onClick={() => history.forward()}
|
||||||
|
title={t('plugins.navigation.templates.forward.title')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
this.buttonContainer,
|
||||||
|
);
|
||||||
|
const menu = document.querySelector('#right-content');
|
||||||
|
menu?.prepend(this.buttonContainer);
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.buttonContainer.remove();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<div
|
|
||||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
|
||||||
onclick="history.back()"
|
|
||||||
role="tab"
|
|
||||||
tab-id="FEmusic_back"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-disabled="false"
|
|
||||||
class="search-icon style-scope ytmusic-search-box"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Go to previous page"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="tab-icon style-scope paper-icon-button navigation-icon"
|
|
||||||
id="icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="style-scope iron-icon"
|
|
||||||
focusable="false"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
viewBox="0 0 492 492"
|
|
||||||
>
|
|
||||||
<g class="style-scope iron-icon">
|
|
||||||
<path
|
|
||||||
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<div
|
|
||||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
|
||||||
onclick="history.forward()"
|
|
||||||
role="tab"
|
|
||||||
tab-id="FEmusic_next"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-disabled="false"
|
|
||||||
class="search-icon style-scope ytmusic-search-box"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Go to next page"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="tab-icon style-scope paper-icon-button navigation-icon"
|
|
||||||
id="icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="style-scope iron-icon"
|
|
||||||
focusable="false"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
viewBox="0 0 492 492"
|
|
||||||
>
|
|
||||||
<g class="style-scope iron-icon">
|
|
||||||
<path
|
|
||||||
d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
|
|
||||||
l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265
|
|
||||||
c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -309,9 +309,10 @@ export default (
|
|||||||
savedNotification?.close();
|
savedNotification?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
changeProtocolHandler((cmd, args) => {
|
changeProtocolHandler((cmd, ...args) => {
|
||||||
if (Object.keys(songControls).includes(cmd)) {
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
songControls[cmd as keyof typeof songControls](args as never);
|
// @ts-expect-error: cmd is a key of songControls
|
||||||
|
songControls[cmd as keyof typeof songControls](...args);
|
||||||
if (
|
if (
|
||||||
config().refreshOnPlayPause &&
|
config().refreshOnPlayPause &&
|
||||||
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
|
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { createPlugin } from '@/utils';
|
|||||||
|
|
||||||
import { onConfigChange, onMainLoad } from './main';
|
import { onConfigChange, onMainLoad } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
import { onPlayerApiReady } from './renderer';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type PictureInPicturePluginConfig = {
|
export type PictureInPicturePluginConfig = {
|
||||||
@ -41,7 +41,6 @@ export default createPlugin({
|
|||||||
onConfigChange,
|
onConfigChange,
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
start: onRendererLoad,
|
|
||||||
onPlayerApiReady,
|
onPlayerApiReady,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const onMainLoad = async ({
|
|||||||
window.setMaximizable(false);
|
window.setMaximizable(false);
|
||||||
window.setFullScreenable(false);
|
window.setFullScreenable(false);
|
||||||
|
|
||||||
send('pip-toggle', true);
|
send('ytmd:pip-toggle', true);
|
||||||
|
|
||||||
app.dock?.hide();
|
app.dock?.hide();
|
||||||
window.setVisibleOnAllWorkspaces(true, {
|
window.setVisibleOnAllWorkspaces(true, {
|
||||||
@ -63,7 +63,7 @@ export const onMainLoad = async ({
|
|||||||
window.setMaximizable(true);
|
window.setMaximizable(true);
|
||||||
window.setFullScreenable(true);
|
window.setFullScreenable(true);
|
||||||
|
|
||||||
send('pip-toggle', false);
|
send('ytmd:pip-toggle', false);
|
||||||
|
|
||||||
window.setVisibleOnAllWorkspaces(false);
|
window.setVisibleOnAllWorkspaces(false);
|
||||||
window.setAlwaysOnTop(false);
|
window.setAlwaysOnTop(false);
|
||||||
|
|||||||
@ -1,207 +0,0 @@
|
|||||||
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
|
||||||
import keyEventAreEqual from 'keyboardevents-areequal';
|
|
||||||
|
|
||||||
import pipHTML from './templates/picture-in-picture.html?raw';
|
|
||||||
|
|
||||||
import { getSongMenu } from '@/providers/dom-elements';
|
|
||||||
|
|
||||||
import { ElementFromHtml } from '../utils/renderer';
|
|
||||||
|
|
||||||
import type { PictureInPicturePluginConfig } from './index';
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
|
||||||
|
|
||||||
function $<E extends Element = Element>(selector: string) {
|
|
||||||
return document.querySelector<E>(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
let useNativePiP = false;
|
|
||||||
let menu: Element | null = null;
|
|
||||||
const pipButton = ElementFromHtml(pipHTML);
|
|
||||||
|
|
||||||
let doneFirstLoad = false;
|
|
||||||
|
|
||||||
// Will also clone
|
|
||||||
function replaceButton(query: string, button: Element) {
|
|
||||||
const svg = button.querySelector('#icon svg')?.cloneNode(true);
|
|
||||||
if (svg) {
|
|
||||||
button.replaceWith(button.cloneNode(true));
|
|
||||||
button.remove();
|
|
||||||
const newButton = $(query);
|
|
||||||
if (newButton) {
|
|
||||||
newButton.querySelector('#icon')?.append(svg);
|
|
||||||
}
|
|
||||||
return newButton;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneButton(query: string) {
|
|
||||||
const button = $(query);
|
|
||||||
if (button) {
|
|
||||||
replaceButton(query, button);
|
|
||||||
}
|
|
||||||
return $(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (!menu) {
|
|
||||||
menu = getSongMenu();
|
|
||||||
if (!menu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
menu.contains(pipButton) ||
|
|
||||||
!(
|
|
||||||
menu.parentElement as (HTMLElement & { eventSink_: Element }) | null
|
|
||||||
)?.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for video (or music)
|
|
||||||
let menuUrl = $<HTMLAnchorElement>(
|
|
||||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
|
||||||
)?.href;
|
|
||||||
|
|
||||||
if (!menuUrl?.includes('watch?')) {
|
|
||||||
menuUrl = undefined;
|
|
||||||
// check for podcast
|
|
||||||
for (const it of document.querySelectorAll(
|
|
||||||
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
|
||||||
)) {
|
|
||||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
|
||||||
menuUrl = it.getAttribute('href')!;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!menuUrl && doneFirstLoad) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.prepend(pipButton);
|
|
||||||
|
|
||||||
if (!doneFirstLoad) {
|
|
||||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const togglePictureInPicture = async () => {
|
|
||||||
if (useNativePiP) {
|
|
||||||
const isInPiP = document.pictureInPictureElement !== null;
|
|
||||||
const video = $<HTMLVideoElement>('video');
|
|
||||||
const togglePiP = () =>
|
|
||||||
isInPiP
|
|
||||||
? document.exitPictureInPicture.call(document)
|
|
||||||
: video?.requestPictureInPicture?.call(video);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await togglePiP();
|
|
||||||
$<HTMLButtonElement>('#icon')?.click(); // Close the menu
|
|
||||||
return true;
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.ipcRenderer.send('plugin:toggle-picture-in-picture');
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// For UI (HTML)
|
|
||||||
window.togglePictureInPicture = togglePictureInPicture;
|
|
||||||
|
|
||||||
const listenForToggle = () => {
|
|
||||||
const originalExitButton = $<HTMLButtonElement>('.exit-fullscreen-button');
|
|
||||||
const appLayout = $<HTMLElement>('ytmusic-app-layout');
|
|
||||||
const expandMenu = $<HTMLElement>('#expanding-menu');
|
|
||||||
const middleControls = $<HTMLButtonElement>('.middle-controls');
|
|
||||||
const playerPage = $<HTMLElement & { playerPageOpen_: boolean }>(
|
|
||||||
'ytmusic-player-page',
|
|
||||||
);
|
|
||||||
const togglePlayerPageButton = $<HTMLButtonElement>(
|
|
||||||
'.toggle-player-page-button',
|
|
||||||
);
|
|
||||||
const fullScreenButton = $<HTMLButtonElement>('.fullscreen-button');
|
|
||||||
const player = $<
|
|
||||||
HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }
|
|
||||||
>('#player');
|
|
||||||
const onPlayerDblClick = player?.onDoubleClick_;
|
|
||||||
const mouseLeaveEventListener = () => middleControls?.click();
|
|
||||||
|
|
||||||
const titlebar = $<HTMLElement>('.cet-titlebar');
|
|
||||||
|
|
||||||
window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => {
|
|
||||||
if (originalExitButton && player) {
|
|
||||||
if (isPip) {
|
|
||||||
replaceButton(
|
|
||||||
'.exit-fullscreen-button',
|
|
||||||
originalExitButton,
|
|
||||||
)?.addEventListener('click', () => togglePictureInPicture());
|
|
||||||
player.onDoubleClick_ = () => {};
|
|
||||||
|
|
||||||
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
|
|
||||||
if (!playerPage?.playerPageOpen_) {
|
|
||||||
togglePlayerPageButton?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
fullScreenButton?.click();
|
|
||||||
appLayout?.classList.add('pip');
|
|
||||||
if (titlebar) {
|
|
||||||
titlebar.style.display = 'none';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$('.exit-fullscreen-button')?.replaceWith(originalExitButton);
|
|
||||||
player.onDoubleClick_ = onPlayerDblClick;
|
|
||||||
expandMenu?.removeEventListener('mouseleave', mouseLeaveEventListener);
|
|
||||||
originalExitButton.click();
|
|
||||||
appLayout?.classList.remove('pip');
|
|
||||||
if (titlebar) {
|
|
||||||
titlebar.style.display = 'flex';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onRendererLoad = async ({
|
|
||||||
getConfig,
|
|
||||||
}: RendererContext<PictureInPicturePluginConfig>) => {
|
|
||||||
const config = await getConfig();
|
|
||||||
|
|
||||||
useNativePiP = config.useNativePiP;
|
|
||||||
|
|
||||||
if (config.hotkey) {
|
|
||||||
const hotkeyEvent = toKeyEvent(config.hotkey);
|
|
||||||
window.addEventListener('keydown', (event) => {
|
|
||||||
if (
|
|
||||||
keyEventAreEqual(event, hotkeyEvent) &&
|
|
||||||
!$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
|
|
||||||
) {
|
|
||||||
togglePictureInPicture();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onPlayerApiReady = () => {
|
|
||||||
listenForToggle();
|
|
||||||
|
|
||||||
cloneButton('.player-minimize-button')?.addEventListener(
|
|
||||||
'click',
|
|
||||||
async () => {
|
|
||||||
await togglePictureInPicture();
|
|
||||||
setTimeout(() => $<HTMLButtonElement>('#player')?.click());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Allows easily closing the menu by programmatically clicking outside of it
|
|
||||||
$('#expanding-menu')?.removeAttribute('no-cancel-on-outside-click');
|
|
||||||
// TODO: think about wether an additional button in songMenu is needed
|
|
||||||
const popupContainer = $('ytmusic-popup-container');
|
|
||||||
if (popupContainer)
|
|
||||||
observer.observe(popupContainer, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
166
src/plugins/picture-in-picture/renderer.tsx
Normal file
166
src/plugins/picture-in-picture/renderer.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||||
|
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { getSongMenu } from '@/providers/dom-elements';
|
||||||
|
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { PictureInPictureButton } from './templates/picture-in-picture-button';
|
||||||
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
import type { PictureInPicturePluginConfig } from './index';
|
||||||
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
export const onPlayerApiReady = async (
|
||||||
|
_: YoutubePlayer,
|
||||||
|
{ ipc, getConfig }: RendererContext<PictureInPicturePluginConfig>,
|
||||||
|
) => {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const togglePictureInPicture = async () => {
|
||||||
|
if ((await getConfig()).useNativePiP) {
|
||||||
|
const isInPiP = document.pictureInPictureElement !== null;
|
||||||
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
|
const togglePiP = () =>
|
||||||
|
isInPiP
|
||||||
|
? document.exitPictureInPicture()
|
||||||
|
: video?.requestPictureInPicture();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await togglePiP();
|
||||||
|
document.querySelector<HTMLButtonElement>('#icon')?.click(); // Close the menu
|
||||||
|
return true;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.send('plugin:toggle-picture-in-picture');
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.hotkey) {
|
||||||
|
const hotkeyEvent = toKeyEvent(config.hotkey);
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (
|
||||||
|
keyEventAreEqual(event, hotkeyEvent) &&
|
||||||
|
!document.querySelector<HTMLElement & { opened: boolean }>(
|
||||||
|
'ytmusic-search-box',
|
||||||
|
)?.opened
|
||||||
|
) {
|
||||||
|
togglePictureInPicture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitFullScreenButton = document.querySelector<HTMLButtonElement>(
|
||||||
|
'.exit-fullscreen-button',
|
||||||
|
);
|
||||||
|
const getPlayMinimizeButton = () =>
|
||||||
|
document.querySelector('.player-minimize-button');
|
||||||
|
const appLayout = document.querySelector<HTMLElement>('ytmusic-app-layout');
|
||||||
|
const expandMenu = document.querySelector<HTMLElement>('#expanding-menu');
|
||||||
|
const middleControls =
|
||||||
|
document.querySelector<HTMLButtonElement>('.middle-controls');
|
||||||
|
const playerPage = document.querySelector<
|
||||||
|
HTMLElement & { playerPageOpen_: boolean }
|
||||||
|
>('ytmusic-player-page');
|
||||||
|
const togglePlayerPageButton = document.querySelector<HTMLButtonElement>(
|
||||||
|
'.toggle-player-page-button',
|
||||||
|
);
|
||||||
|
const fullScreenButton =
|
||||||
|
document.querySelector<HTMLButtonElement>('.fullscreen-button');
|
||||||
|
const player = document.querySelector<
|
||||||
|
HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }
|
||||||
|
>('#player');
|
||||||
|
const onPlayerDblClick = player?.onDoubleClick_;
|
||||||
|
const mouseLeaveEventListener = () => middleControls?.click();
|
||||||
|
|
||||||
|
const titleBar = document.querySelector<HTMLElement>(
|
||||||
|
'nav[data-ytmd-main-panel]',
|
||||||
|
);
|
||||||
|
|
||||||
|
const pipClickEventListener = async (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
await togglePictureInPicture();
|
||||||
|
};
|
||||||
|
|
||||||
|
ipc.on('ytmd:pip-toggle', (isPip: boolean) => {
|
||||||
|
if (exitFullScreenButton && player) {
|
||||||
|
if (isPip) {
|
||||||
|
exitFullScreenButton?.addEventListener('click', pipClickEventListener);
|
||||||
|
getPlayMinimizeButton()?.removeEventListener(
|
||||||
|
'click',
|
||||||
|
pipClickEventListener,
|
||||||
|
);
|
||||||
|
player.onDoubleClick_ = () => {};
|
||||||
|
|
||||||
|
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
|
||||||
|
if (!playerPage?.playerPageOpen_) {
|
||||||
|
togglePlayerPageButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
fullScreenButton?.click();
|
||||||
|
appLayout?.classList.add('pip');
|
||||||
|
if (titleBar) {
|
||||||
|
titleBar.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exitFullScreenButton.removeEventListener(
|
||||||
|
'click',
|
||||||
|
pipClickEventListener,
|
||||||
|
);
|
||||||
|
getPlayMinimizeButton()?.addEventListener(
|
||||||
|
'click',
|
||||||
|
pipClickEventListener,
|
||||||
|
);
|
||||||
|
player.onDoubleClick_ = onPlayerDblClick;
|
||||||
|
expandMenu?.removeEventListener('mouseleave', mouseLeaveEventListener);
|
||||||
|
exitFullScreenButton.click();
|
||||||
|
appLayout?.classList.remove('pip');
|
||||||
|
if (titleBar) {
|
||||||
|
titleBar.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getPlayMinimizeButton()?.addEventListener('click', pipClickEventListener);
|
||||||
|
|
||||||
|
const pipButtonContainer = document.createElement('div');
|
||||||
|
pipButtonContainer.classList.add(
|
||||||
|
'style-scope',
|
||||||
|
'menu-item',
|
||||||
|
'ytmusic-menu-popup-renderer',
|
||||||
|
);
|
||||||
|
pipButtonContainer.setAttribute('aria-disabled', 'false');
|
||||||
|
pipButtonContainer.setAttribute('aria-selected', 'false');
|
||||||
|
pipButtonContainer.setAttribute('role', 'option');
|
||||||
|
pipButtonContainer.setAttribute('tabindex', '-1');
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<PictureInPictureButton
|
||||||
|
onClick={togglePictureInPicture}
|
||||||
|
text={t('plugins.picture-in-picture.templates.button')}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
pipButtonContainer,
|
||||||
|
);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const menu = getSongMenu();
|
||||||
|
|
||||||
|
if (menu?.contains(pipButtonContainer) || !isMusicOrVideoTrack()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu?.prepend(pipButtonContainer);
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
export interface PictureInPictureButtonProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PictureInPictureButton = (props: PictureInPictureButtonProps) => (
|
||||||
|
<a
|
||||||
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="navigation-endpoint"
|
||||||
|
tabindex={-1}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||||
|
<svg
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
style={{
|
||||||
|
'pointer-events': 'none',
|
||||||
|
'display': 'block',
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
}}
|
||||||
|
id="Layer_1"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
x="0px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
y="0px"
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon" id="XMLID_6_">
|
||||||
|
<path
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
fill="#aaaaaa"
|
||||||
|
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||||
|
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||||
|
v326.8H464.8z"
|
||||||
|
id="XMLID_11_"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="ytmcustom-pip"
|
||||||
|
>
|
||||||
|
{props.text}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
@ -1,52 +0,0 @@
|
|||||||
<div
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-selected="false"
|
|
||||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
|
||||||
onclick="togglePictureInPicture()"
|
|
||||||
role="option"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
id="navigation-endpoint"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
id="Layer_1"
|
|
||||||
style="enable-background: new 0 0 512 512"
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
x="0px"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
y="0px"
|
|
||||||
>
|
|
||||||
<style type="text/css">
|
|
||||||
.st0 {
|
|
||||||
fill: #aaaaaa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<g id="XMLID_6_">
|
|
||||||
<path
|
|
||||||
class="st0"
|
|
||||||
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
|
||||||
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
|
||||||
v326.8H464.8z"
|
|
||||||
id="XMLID_11_"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
id="ytmcustom-pip"
|
|
||||||
>
|
|
||||||
<ytmd-trans
|
|
||||||
key="plugins.picture-in-picture.templates.button"
|
|
||||||
></ytmd-trans>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
92
src/plugins/playback-speed/components/slider.tsx
Normal file
92
src/plugins/playback-speed/components/slider.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
export interface PlaybackSpeedSliderProps {
|
||||||
|
speed: number;
|
||||||
|
title: string;
|
||||||
|
onImmediateValueChanged?: (value: CustomEvent<{ value: number }>) => void;
|
||||||
|
onWheel?: (event: WheelEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlaybackSpeedSlider = (props: PlaybackSpeedSliderProps) => (
|
||||||
|
<div
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-selected="false"
|
||||||
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
|
role="option"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="navigation-endpoint"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<tp-yt-paper-slider
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-label={props.title}
|
||||||
|
aria-valuemax="2"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuenow={props.speed}
|
||||||
|
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
||||||
|
dir="ltr"
|
||||||
|
on:immediate-value-changed={props.onImmediateValueChanged}
|
||||||
|
onWheel={props.onWheel}
|
||||||
|
max="2"
|
||||||
|
min="0"
|
||||||
|
role="slider"
|
||||||
|
step="0.125"
|
||||||
|
style="display: inherit !important"
|
||||||
|
tabindex="0"
|
||||||
|
title={props.title}
|
||||||
|
value={props.speed}
|
||||||
|
>
|
||||||
|
<div class="style-scope tp-yt-paper-slider" id="sliderContainer">
|
||||||
|
<div class="bar-container style-scope tp-yt-paper-slider">
|
||||||
|
<tp-yt-paper-progress
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-valuemax="2"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuenow="1"
|
||||||
|
class="style-scope tp-yt-paper-slider"
|
||||||
|
id="sliderBar"
|
||||||
|
role="progressbar"
|
||||||
|
style="touch-action: none"
|
||||||
|
value="1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="style-scope tp-yt-paper-progress"
|
||||||
|
id="progressContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="style-scope tp-yt-paper-progress"
|
||||||
|
hidden={true}
|
||||||
|
id="secondaryProgress"
|
||||||
|
style="transform: scaleX(0)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="style-scope tp-yt-paper-progress"
|
||||||
|
id="primaryProgress"
|
||||||
|
style="transform: scaleX(0.5)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</tp-yt-paper-progress>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="slider-knob style-scope tp-yt-paper-slider"
|
||||||
|
id="sliderKnob"
|
||||||
|
style="left: 50%; touch-action: none"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
||||||
|
value={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</tp-yt-paper-slider>
|
||||||
|
<div
|
||||||
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="ytmcustom-playback-speed"
|
||||||
|
>
|
||||||
|
{props.title} ({props.speed})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,141 +0,0 @@
|
|||||||
import sliderHTML from './templates/slider.html?raw';
|
|
||||||
|
|
||||||
import { getSongMenu } from '@/providers/dom-elements';
|
|
||||||
import { singleton } from '@/providers/decorators';
|
|
||||||
|
|
||||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
|
||||||
|
|
||||||
import { ElementFromHtml } from '../utils/renderer';
|
|
||||||
|
|
||||||
const slider = ElementFromHtml(sliderHTML);
|
|
||||||
|
|
||||||
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
|
|
||||||
|
|
||||||
const MIN_PLAYBACK_SPEED = 0.07;
|
|
||||||
const MAX_PLAYBACK_SPEED = 16;
|
|
||||||
|
|
||||||
let playbackSpeed = 1;
|
|
||||||
|
|
||||||
const updatePlayBackSpeed = () => {
|
|
||||||
const videoElement = document.querySelector<HTMLVideoElement>('video');
|
|
||||||
if (videoElement) {
|
|
||||||
videoElement.playbackRate = playbackSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playbackSpeedElement = document.querySelector('#playback-speed-value');
|
|
||||||
if (playbackSpeedElement) {
|
|
||||||
const targetHtml = String(playbackSpeed);
|
|
||||||
(playbackSpeedElement.innerHTML as string | TrustedHTML) =
|
|
||||||
defaultTrustedTypePolicy
|
|
||||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
|
||||||
: targetHtml;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let menu: Element | null = null;
|
|
||||||
|
|
||||||
const immediateValueChangedListener = (e: Event) => {
|
|
||||||
playbackSpeed =
|
|
||||||
(e as CustomEvent<{ value: number }>).detail.value || MIN_PLAYBACK_SPEED;
|
|
||||||
if (isNaN(playbackSpeed)) {
|
|
||||||
playbackSpeed = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayBackSpeed();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupSliderListener = singleton(() => {
|
|
||||||
document
|
|
||||||
.querySelector('#playback-speed-slider')
|
|
||||||
?.addEventListener(
|
|
||||||
'immediate-value-changed',
|
|
||||||
immediateValueChangedListener,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const observePopupContainer = () => {
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (!menu) {
|
|
||||||
menu = getSongMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menu && !menu.contains(slider)) {
|
|
||||||
menu.prepend(slider);
|
|
||||||
setupSliderListener();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const popupContainer = document.querySelector('ytmusic-popup-container');
|
|
||||||
if (popupContainer) {
|
|
||||||
observer.observe(popupContainer, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observeVideo = () => {
|
|
||||||
const video = document.querySelector<HTMLVideoElement>('video');
|
|
||||||
if (video) {
|
|
||||||
video.addEventListener('ratechange', forcePlaybackRate);
|
|
||||||
video.addEventListener('ytmd:src-changed', forcePlaybackRate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wheelEventListener = (e: WheelEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (isNaN(playbackSpeed)) {
|
|
||||||
playbackSpeed = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// E.deltaY < 0 means wheel-up
|
|
||||||
playbackSpeed = roundToTwo(
|
|
||||||
e.deltaY < 0
|
|
||||||
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
|
|
||||||
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
|
|
||||||
);
|
|
||||||
|
|
||||||
updatePlayBackSpeed();
|
|
||||||
// Update slider position
|
|
||||||
const playbackSpeedSilder = document.querySelector<
|
|
||||||
HTMLElement & { value: number }
|
|
||||||
>('#playback-speed-slider');
|
|
||||||
if (playbackSpeedSilder) {
|
|
||||||
playbackSpeedSilder.value = playbackSpeed;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupWheelListener = () => {
|
|
||||||
slider.addEventListener('wheel', wheelEventListener);
|
|
||||||
};
|
|
||||||
|
|
||||||
function forcePlaybackRate(e: Event) {
|
|
||||||
if (e.target instanceof HTMLVideoElement) {
|
|
||||||
const videoElement = e.target;
|
|
||||||
if (videoElement.playbackRate !== playbackSpeed) {
|
|
||||||
videoElement.playbackRate = playbackSpeed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const onPlayerApiReady = () => {
|
|
||||||
observePopupContainer();
|
|
||||||
observeVideo();
|
|
||||||
setupWheelListener();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onUnload = () => {
|
|
||||||
const video = document.querySelector<HTMLVideoElement>('video');
|
|
||||||
if (video) {
|
|
||||||
video.removeEventListener('ratechange', forcePlaybackRate);
|
|
||||||
video.removeEventListener('ytmd:src-changed', forcePlaybackRate);
|
|
||||||
}
|
|
||||||
slider.removeEventListener('wheel', wheelEventListener);
|
|
||||||
getSongMenu()?.removeChild(slider);
|
|
||||||
document
|
|
||||||
.querySelector('#playback-speed-slider')
|
|
||||||
?.removeEventListener(
|
|
||||||
'immediate-value-changed',
|
|
||||||
immediateValueChangedListener,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
119
src/plugins/playback-speed/renderer.tsx
Normal file
119
src/plugins/playback-speed/renderer.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { createSignal } from 'solid-js';
|
||||||
|
|
||||||
|
import { getSongMenu } from '@/providers/dom-elements';
|
||||||
|
|
||||||
|
import { PlaybackSpeedSlider } from './components/slider';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||||
|
|
||||||
|
const MIN_PLAYBACK_SPEED = 0.07;
|
||||||
|
const MAX_PLAYBACK_SPEED = 16;
|
||||||
|
|
||||||
|
const forcePlaybackRate = (e: Event) => {
|
||||||
|
if (e.target instanceof HTMLVideoElement) {
|
||||||
|
const videoElement = e.target;
|
||||||
|
if (videoElement.playbackRate !== speed()) {
|
||||||
|
videoElement.playbackRate = speed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
|
||||||
|
|
||||||
|
const [speed, setSpeed] = createSignal(1);
|
||||||
|
const sliderContainer = document.createElement('div');
|
||||||
|
|
||||||
|
export const onPlayerApiReady = () => {
|
||||||
|
const observePopupContainer = () => {
|
||||||
|
const updatePlayBackSpeed = () => {
|
||||||
|
const videoElement = document.querySelector<HTMLVideoElement>('video');
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.playbackRate = speed();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpeed(speed());
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<PlaybackSpeedSlider
|
||||||
|
speed={speed()}
|
||||||
|
title={t('plugins.playback-speed.templates.button')}
|
||||||
|
onImmediateValueChanged={(e) => {
|
||||||
|
let targetSpeed = Number(e.detail.value ?? MIN_PLAYBACK_SPEED);
|
||||||
|
|
||||||
|
if (isNaN(targetSpeed)) {
|
||||||
|
targetSpeed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSpeed = Math.min(
|
||||||
|
Math.max(MIN_PLAYBACK_SPEED, targetSpeed),
|
||||||
|
MAX_PLAYBACK_SPEED,
|
||||||
|
);
|
||||||
|
|
||||||
|
setSpeed(targetSpeed);
|
||||||
|
updatePlayBackSpeed();
|
||||||
|
}}
|
||||||
|
onWheel={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isNaN(speed())) {
|
||||||
|
setSpeed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// E.deltaY < 0 means wheel-up
|
||||||
|
setSpeed((prev) =>
|
||||||
|
roundToTwo(
|
||||||
|
e.deltaY < 0
|
||||||
|
? Math.min(prev + 0.01, MAX_PLAYBACK_SPEED)
|
||||||
|
: Math.max(prev - 0.01, MIN_PLAYBACK_SPEED),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
updatePlayBackSpeed();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
sliderContainer,
|
||||||
|
);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const menu = getSongMenu();
|
||||||
|
|
||||||
|
if (menu && !menu.contains(sliderContainer) && isMusicOrVideoTrack()) {
|
||||||
|
menu.prepend(sliderContainer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const popupContainer = document.querySelector('ytmusic-popup-container');
|
||||||
|
if (popupContainer) {
|
||||||
|
observer.observe(popupContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeVideo = () => {
|
||||||
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
|
if (video) {
|
||||||
|
video.addEventListener('ratechange', forcePlaybackRate);
|
||||||
|
video.addEventListener('ytmd:src-changed', forcePlaybackRate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
observePopupContainer();
|
||||||
|
observeVideo();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onUnload = () => {
|
||||||
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
|
if (video) {
|
||||||
|
video.removeEventListener('ratechange', forcePlaybackRate);
|
||||||
|
video.removeEventListener('ytmd:src-changed', forcePlaybackRate);
|
||||||
|
}
|
||||||
|
getSongMenu()?.removeChild(sliderContainer);
|
||||||
|
};
|
||||||
@ -1,90 +0,0 @@
|
|||||||
<div
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-selected="false"
|
|
||||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
|
||||||
role="option"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
id="navigation-endpoint"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<tp-yt-paper-slider
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-label="Playback speed"
|
|
||||||
aria-valuemax="2"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuenow="1"
|
|
||||||
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
|
||||||
dir="ltr"
|
|
||||||
id="playback-speed-slider"
|
|
||||||
max="2"
|
|
||||||
min="0"
|
|
||||||
role="slider"
|
|
||||||
step="0.125"
|
|
||||||
style="display: inherit !important"
|
|
||||||
tabindex="0"
|
|
||||||
title="Playback speed"
|
|
||||||
value="1"
|
|
||||||
><!--css-build:shady-->
|
|
||||||
<div class="style-scope tp-yt-paper-slider" id="sliderContainer">
|
|
||||||
<div class="bar-container style-scope tp-yt-paper-slider">
|
|
||||||
<tp-yt-paper-progress
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-valuemax="2"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuenow="1"
|
|
||||||
class="style-scope tp-yt-paper-slider"
|
|
||||||
id="sliderBar"
|
|
||||||
role="progressbar"
|
|
||||||
style="touch-action: none"
|
|
||||||
value="1"
|
|
||||||
><!--css-build:shady-->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="style-scope tp-yt-paper-progress"
|
|
||||||
id="progressContainer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="style-scope tp-yt-paper-progress"
|
|
||||||
hidden="true"
|
|
||||||
id="secondaryProgress"
|
|
||||||
style="transform: scaleX(0)"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="style-scope tp-yt-paper-progress"
|
|
||||||
id="primaryProgress"
|
|
||||||
style="transform: scaleX(0.5)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</tp-yt-paper-progress>
|
|
||||||
</div>
|
|
||||||
<dom-if class="style-scope tp-yt-paper-slider">
|
|
||||||
<template is="dom-if"></template>
|
|
||||||
</dom-if>
|
|
||||||
<div
|
|
||||||
class="slider-knob style-scope tp-yt-paper-slider"
|
|
||||||
id="sliderKnob"
|
|
||||||
style="left: 50%; touch-action: none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
|
||||||
value="1"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<dom-if class="style-scope tp-yt-paper-slider">
|
|
||||||
<template is="dom-if"></template>
|
|
||||||
</dom-if>
|
|
||||||
</tp-yt-paper-slider>
|
|
||||||
<div
|
|
||||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
|
||||||
id="ytmcustom-playback-speed"
|
|
||||||
>
|
|
||||||
<ytmd-trans key="plugins.playback-speed.templates.button"></ytmd-trans>
|
|
||||||
(<span id="playback-speed-value">1</span>)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
import QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { QualitySettingButton } from './templates/quality-setting-button';
|
||||||
|
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
@ -18,7 +19,7 @@ export default createPlugin({
|
|||||||
|
|
||||||
backend({ ipc, window }) {
|
backend({ ipc, window }) {
|
||||||
ipc.handle(
|
ipc.handle(
|
||||||
'qualityChanger',
|
'ytmd:quality-changer',
|
||||||
async (qualityLabels: string[], currentIndex: number) =>
|
async (qualityLabels: string[], currentIndex: number) =>
|
||||||
await dialog.showMessageBox(window, {
|
await dialog.showMessageBox(window, {
|
||||||
type: 'question',
|
type: 'question',
|
||||||
@ -42,40 +43,48 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderer: {
|
renderer: {
|
||||||
qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate),
|
qualitySettingsButtonContainer: document.createElement('div'),
|
||||||
onPlayerApiReady(api: YoutubePlayer, context) {
|
onPlayerApiReady(api: YoutubePlayer, context) {
|
||||||
const getPlayer = () =>
|
const chooseQuality = async (e: MouseEvent) => {
|
||||||
document.querySelector<HTMLVideoElement>('#player');
|
e.stopPropagation();
|
||||||
const chooseQuality = () => {
|
|
||||||
setTimeout(() => getPlayer()?.click());
|
|
||||||
|
|
||||||
const qualityLevels = api.getAvailableQualityLevels();
|
const qualityLevels = api.getAvailableQualityLevels();
|
||||||
|
|
||||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||||
|
|
||||||
(
|
const quality = (await context.ipc.invoke(
|
||||||
context.ipc.invoke(
|
'ytmd:quality-changer',
|
||||||
'qualityChanger',
|
api.getAvailableQualityLabels(),
|
||||||
api.getAvailableQualityLabels(),
|
currentIndex,
|
||||||
currentIndex,
|
)) as {
|
||||||
) as Promise<{ response: number }>
|
response: number;
|
||||||
).then((promise) => {
|
};
|
||||||
if (promise.response === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newQuality = qualityLevels[promise.response];
|
if (quality.response === -1) {
|
||||||
api.setPlaybackQualityRange(newQuality);
|
return;
|
||||||
api.setPlaybackQuality(newQuality);
|
}
|
||||||
});
|
|
||||||
|
const newQuality = qualityLevels[quality.response];
|
||||||
|
api.setPlaybackQualityRange(newQuality);
|
||||||
|
api.setPlaybackQuality(newQuality);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<QualitySettingButton
|
||||||
|
label={t(
|
||||||
|
'plugins.quality-changer.renderer.quality-settings-button.label',
|
||||||
|
)}
|
||||||
|
onClick={chooseQuality}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
this.qualitySettingsButtonContainer,
|
||||||
|
);
|
||||||
|
|
||||||
const setup = () => {
|
const setup = () => {
|
||||||
document
|
document
|
||||||
.querySelector('.top-row-buttons.ytmusic-player')
|
.querySelector('.top-row-buttons.ytmusic-player')
|
||||||
?.prepend(this.qualitySettingsButton);
|
?.prepend(this.qualitySettingsButtonContainer);
|
||||||
|
|
||||||
this.qualitySettingsButton.addEventListener('click', chooseQuality);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
@ -83,7 +92,7 @@ export default createPlugin({
|
|||||||
stop() {
|
stop() {
|
||||||
document
|
document
|
||||||
.querySelector('.top-row-buttons.ytmusic-player')
|
.querySelector('.top-row-buttons.ytmusic-player')
|
||||||
?.removeChild(this.qualitySettingsButton);
|
?.removeChild(this.qualitySettingsButtonContainer);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
export interface QualitySettingButtonProps {
|
||||||
|
label: string;
|
||||||
|
onClick: (event: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QualitySettingButton = (props: QualitySettingButtonProps) => (
|
||||||
|
<yt-icon-button
|
||||||
|
aria-disabled={false}
|
||||||
|
aria-label={props.label}
|
||||||
|
class="player-quality-button style-scope ytmusic-player"
|
||||||
|
icon={'yt-icons:settings'}
|
||||||
|
role={'button'}
|
||||||
|
tabindex={0}
|
||||||
|
title={props.label}
|
||||||
|
on:click={props.onClick}
|
||||||
|
>
|
||||||
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'block',
|
||||||
|
fill: 'currentcolor',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</yt-icon-button>
|
||||||
|
);
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<tp-yt-paper-icon-button
|
|
||||||
aria-disabled="false"
|
|
||||||
aria-label="Open player quality changer"
|
|
||||||
class="player-quality-button style-scope ytmusic-player"
|
|
||||||
icon="yt-icons:settings"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Open player quality changer"
|
|
||||||
>
|
|
||||||
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
|
|
||||||
<svg
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
focusable="false"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g class="style-scope yt-icon">
|
|
||||||
<path
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</tp-yt-iron-icon>
|
|
||||||
</tp-yt-paper-icon-button>
|
|
||||||
@ -36,7 +36,7 @@ export const onMainLoad = async ({
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
const songControls = getSongControls(window);
|
const songControls = getSongControls(window);
|
||||||
const { playPause, next, previous, search } = songControls;
|
const { playPause, next, previous } = songControls;
|
||||||
|
|
||||||
if (config.overrideMediaKeys) {
|
if (config.overrideMediaKeys) {
|
||||||
_registerGlobalShortcut(window.webContents, 'MediaPlayPause', playPause);
|
_registerGlobalShortcut(window.webContents, 'MediaPlayPause', playPause);
|
||||||
@ -44,9 +44,6 @@ export const onMainLoad = async ({
|
|||||||
_registerGlobalShortcut(window.webContents, 'MediaPreviousTrack', previous);
|
_registerGlobalShortcut(window.webContents, 'MediaPreviousTrack', previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerLocalShortcut(window, 'CommandOrControl+F', search);
|
|
||||||
_registerLocalShortcut(window, 'CommandOrControl+L', search);
|
|
||||||
|
|
||||||
if (is.linux()) {
|
if (is.linux()) {
|
||||||
registerMPRIS(window);
|
registerMPRIS(window);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,6 @@ const providerBias = (p: ProviderName) =>
|
|||||||
(lyricsStore.lyrics[p].data?.lines?.length && p === 'YTMusic' ? 1 : 0) +
|
(lyricsStore.lyrics[p].data?.lines?.length && p === 'YTMusic' ? 1 : 0) +
|
||||||
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const pickBestProvider = () => {
|
const pickBestProvider = () => {
|
||||||
const providers = Array.from(providerNames);
|
const providers = Array.from(providerNames);
|
||||||
|
|
||||||
@ -56,15 +55,17 @@ const pickBestProvider = () => {
|
|||||||
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
|
|
||||||
// prettier-ignore
|
export const LyricsPicker = (props: {
|
||||||
export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> }) => {
|
setStickRef: Setter<HTMLElement | null>;
|
||||||
|
}) => {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// fallback to the next source, if the current one has an error
|
// fallback to the next source, if the current one has an error
|
||||||
if (!hasManuallySwitchedProvider()
|
if (!hasManuallySwitchedProvider()) {
|
||||||
) {
|
|
||||||
const bestProvider = pickBestProvider();
|
const bestProvider = pickBestProvider();
|
||||||
|
|
||||||
const allProvidersFailed = providerNames.every((p) => shouldSwitchProvider(lyricsStore.lyrics[p]));
|
const allProvidersFailed = providerNames.every((p) =>
|
||||||
|
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
||||||
|
);
|
||||||
if (allProvidersFailed) return;
|
if (allProvidersFailed) return;
|
||||||
|
|
||||||
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
||||||
@ -80,7 +81,9 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
};
|
};
|
||||||
|
|
||||||
_ytAPI?.addEventListener('videodatachange', videoDataChangeHandler);
|
_ytAPI?.addEventListener('videodatachange', videoDataChangeHandler);
|
||||||
onCleanup(() => _ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler));
|
onCleanup(() =>
|
||||||
|
_ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
@ -95,7 +98,9 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
setHasManuallySwitchedProvider(true);
|
setHasManuallySwitchedProvider(true);
|
||||||
setLyricsStore('provider', (prevProvider) => {
|
setLyricsStore('provider', (prevProvider) => {
|
||||||
const idx = providerNames.indexOf(prevProvider);
|
const idx = providerNames.indexOf(prevProvider);
|
||||||
return providerNames[(idx + providerNames.length - 1) % providerNames.length];
|
return providerNames[
|
||||||
|
(idx + providerNames.length - 1) % providerNames.length
|
||||||
|
];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,7 +114,40 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
return (
|
return (
|
||||||
<div class="lyrics-picker" ref={props.setStickRef}>
|
<div class="lyrics-picker" ref={props.setStickRef}>
|
||||||
<div class="lyrics-picker-left">
|
<div class="lyrics-picker-left">
|
||||||
<tp-yt-paper-icon-button icon={chevronLeft} onClick={previous} />
|
<yt-icon-button
|
||||||
|
class="style-scope ytmusic-player-bar"
|
||||||
|
icon={chevronLeft}
|
||||||
|
onClick={previous}
|
||||||
|
role={'button'}
|
||||||
|
>
|
||||||
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
'display': 'flex',
|
||||||
|
'align-items': 'center',
|
||||||
|
'fill': 'currentcolor',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
height={'40px'}
|
||||||
|
width={'40px'}
|
||||||
|
fill="#FFFFFF"
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
d="M560.67-240 320-480.67l240.67-240.66L608-674 414.67-480.67 608-287.33 560.67-240Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</yt-icon-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lyrics-picker-content">
|
<div class="lyrics-picker-content">
|
||||||
@ -138,7 +176,7 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={currentLyrics().state === 'error'}>
|
<Match when={currentLyrics().state === 'error'}>
|
||||||
<tp-yt-paper-icon-button
|
<yt-icon-button
|
||||||
icon={errorIcon}
|
icon={errorIcon}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
@ -151,18 +189,20 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
currentLyrics().data?.lyrics)
|
currentLyrics().data?.lyrics)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<tp-yt-paper-icon-button
|
<yt-icon-button
|
||||||
icon={successIcon}
|
icon={successIcon}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={
|
<Match
|
||||||
currentLyrics().state === 'done'
|
when={
|
||||||
&& !currentLyrics().data?.lines
|
currentLyrics().state === 'done' &&
|
||||||
&& !currentLyrics().data?.lyrics
|
!currentLyrics().data?.lines &&
|
||||||
}>
|
!currentLyrics().data?.lyrics
|
||||||
<tp-yt-paper-icon-button
|
}
|
||||||
|
>
|
||||||
|
<yt-icon-button
|
||||||
icon={notFoundIcon}
|
icon={notFoundIcon}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
@ -194,7 +234,40 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lyrics-picker-right">
|
<div class="lyrics-picker-right">
|
||||||
<tp-yt-paper-icon-button icon={chevronRight} onClick={next} />
|
<yt-icon-button
|
||||||
|
class="style-scope ytmusic-player-bar"
|
||||||
|
icon={chevronRight}
|
||||||
|
onClick={next}
|
||||||
|
role={'button'}
|
||||||
|
>
|
||||||
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
'width': '100%',
|
||||||
|
'height': '100%',
|
||||||
|
'display': 'flex',
|
||||||
|
'align-items': 'center',
|
||||||
|
'fill': 'currentcolor',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
height={'40px'}
|
||||||
|
width={'40px'}
|
||||||
|
fill="#FFFFFF"
|
||||||
|
>
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path
|
||||||
|
class="style-scope yt-icon"
|
||||||
|
d="M521.33-480.67 328-674l47.33-47.33L616-480.67 375.33-240 328-287.33l193.33-193.34Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</yt-icon-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
20
src/plugins/utils/renderer/check.ts
Normal file
20
src/plugins/utils/renderer/check.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const isMusicOrVideoTrack = () => {
|
||||||
|
let menuUrl = document.querySelector<HTMLAnchorElement>(
|
||||||
|
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
||||||
|
)?.href;
|
||||||
|
|
||||||
|
if (!menuUrl?.includes('watch?')) {
|
||||||
|
menuUrl = undefined;
|
||||||
|
// check for podcast
|
||||||
|
for (const it of document.querySelectorAll(
|
||||||
|
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
||||||
|
)) {
|
||||||
|
if (it.getAttribute('href')?.includes('podcast/')) {
|
||||||
|
menuUrl = it.getAttribute('href')!;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!menuUrl;
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a DOM element from an HTML string
|
* Creates a DOM element from an HTML string
|
||||||
@ -6,13 +6,8 @@ import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
|||||||
* @returns The DOM element
|
* @returns The DOM element
|
||||||
*/
|
*/
|
||||||
export const ElementFromHtml = (html: string): HTMLElement => {
|
export const ElementFromHtml = (html: string): HTMLElement => {
|
||||||
const template = document.createElement('template');
|
return (domParser.parseFromString(html, 'text/html') as HTMLDocument).body
|
||||||
html = html.trim(); // Never return a text node of whitespace as the result
|
.firstElementChild as HTMLElement;
|
||||||
(template.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
|
||||||
? defaultTrustedTypePolicy.createHTML(html)
|
|
||||||
: html;
|
|
||||||
|
|
||||||
return template.content.firstElementChild as HTMLElement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-toggle-custom-mode .video-switch-button:before {
|
.video-toggle-custom-mode .video-switch-button:before {
|
||||||
content: 'Video';
|
content: attr(data-video-button-text);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import buttonTemplate from './templates/button_template.html?raw';
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
import forceHideStyle from './force-hide.css?inline';
|
import forceHideStyle from './force-hide.css?inline';
|
||||||
import buttonSwitcherStyle from './button-switcher.css?inline';
|
import buttonSwitcherStyle from './button-switcher.css?inline';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
import { ThumbnailElement } from '@/types/get-player-response';
|
import { ThumbnailElement } from '@/types/get-player-response';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { MenuTemplate } from '@/menu';
|
import { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
|
import { VideoSwitchButton } from './templates/video-switch-button';
|
||||||
|
|
||||||
export type VideoTogglePluginConfig = {
|
export type VideoTogglePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
hideVideo: boolean;
|
hideVideo: boolean;
|
||||||
@ -150,6 +154,8 @@ export default createPlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onPlayerApiReady(api, { getConfig }) {
|
async onPlayerApiReady(api, { getConfig }) {
|
||||||
|
const [showButton, setShowButton] = createSignal(true);
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
@ -164,7 +170,25 @@ export default createPlugin({
|
|||||||
>('ytmusic-player');
|
>('ytmusic-player');
|
||||||
const video = document.querySelector<HTMLVideoElement>('video');
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
|
|
||||||
const switchButtonDiv = ElementFromHtml(buttonTemplate);
|
const switchButtonContainer = document.createElement('div');
|
||||||
|
switchButtonContainer.style.display = 'flex';
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<Show when={showButton()}>
|
||||||
|
<VideoSwitchButton
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
|
||||||
|
setVideoState(target.checked);
|
||||||
|
}}
|
||||||
|
songButtonText={t('plugins.video-toggle.templates.button-song')}
|
||||||
|
videoButtonText={t('plugins.video-toggle.templates.button-video',)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
switchButtonContainer,
|
||||||
|
);
|
||||||
|
|
||||||
const forceThumbnail = (img: HTMLImageElement) => {
|
const forceThumbnail = (img: HTMLImageElement) => {
|
||||||
const thumbnails: ThumbnailElement[] =
|
const thumbnails: ThumbnailElement[] =
|
||||||
@ -184,7 +208,7 @@ export default createPlugin({
|
|||||||
const checkbox = document.querySelector<HTMLInputElement>(
|
const checkbox = document.querySelector<HTMLInputElement>(
|
||||||
'.video-switch-button-checkbox',
|
'.video-switch-button-checkbox',
|
||||||
); // custom mode
|
); // custom mode
|
||||||
if (checkbox) checkbox.checked = !this.config.hideVideo;
|
if (checkbox) checkbox.checked = !this.config?.hideVideo;
|
||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
player.style.margin = showVideo ? '' : 'auto 0px';
|
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||||
@ -217,17 +241,18 @@ export default createPlugin({
|
|||||||
// Video doesn't exist -> switch to song mode
|
// Video doesn't exist -> switch to song mode
|
||||||
setVideoState(false);
|
setVideoState(false);
|
||||||
// Hide toggle button
|
// Hide toggle button
|
||||||
switchButtonDiv.style.display = 'none';
|
setShowButton(false);
|
||||||
} else {
|
} else {
|
||||||
const songImage =
|
const songImage = document.querySelector<HTMLImageElement>(
|
||||||
document.querySelector<HTMLImageElement>('#song-image img');
|
'#song-image #img.style-scope.yt-img-shadow',
|
||||||
|
);
|
||||||
if (!songImage) {
|
if (!songImage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Switch to high-res thumbnail
|
// Switch to high-res thumbnail
|
||||||
forceThumbnail(songImage);
|
forceThumbnail(songImage);
|
||||||
// Show toggle button
|
// Show toggle button
|
||||||
switchButtonDiv.style.display = 'initial';
|
setShowButton(true);
|
||||||
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
||||||
if (
|
if (
|
||||||
!this.config?.hideVideo &&
|
!this.config?.hideVideo &&
|
||||||
@ -282,7 +307,7 @@ export default createPlugin({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
playbackModeObserver.observe(
|
playbackModeObserver.observe(
|
||||||
document.querySelector('#song-image img')!,
|
document.querySelector('#song-image #img.style-scope.yt-img-shadow')!,
|
||||||
{ attributeFilter: ['src'] },
|
{ attributeFilter: ['src'] },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -290,7 +315,7 @@ export default createPlugin({
|
|||||||
if (config.mode !== 'native' && config.mode != 'disabled') {
|
if (config.mode !== 'native' && config.mode != 'disabled') {
|
||||||
document
|
document
|
||||||
.querySelector<HTMLVideoElement>('#player')
|
.querySelector<HTMLVideoElement>('#player')
|
||||||
?.prepend(switchButtonDiv);
|
?.prepend(switchButtonContainer);
|
||||||
|
|
||||||
setVideoState(!config.hideVideo);
|
setVideoState(!config.hideVideo);
|
||||||
forcePlaybackMode();
|
forcePlaybackMode();
|
||||||
@ -299,36 +324,25 @@ export default createPlugin({
|
|||||||
video.style.height = 'auto';
|
video.style.height = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
//Prevents bubbling to the player which causes it to stop or resume
|
|
||||||
switchButtonDiv.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Button checked = show video
|
|
||||||
switchButtonDiv.addEventListener('change', (e) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
|
|
||||||
setVideoState(target.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
video?.addEventListener('ytmd:src-changed', videoStarted);
|
video?.addEventListener('ytmd:src-changed', videoStarted);
|
||||||
|
|
||||||
observeThumbnail();
|
observeThumbnail();
|
||||||
|
videoStarted();
|
||||||
|
|
||||||
switch (config.align) {
|
switch (config.align) {
|
||||||
case 'right': {
|
case 'right': {
|
||||||
switchButtonDiv.style.left = 'calc(100% - 240px)';
|
switchButtonContainer.style.justifyContent = 'flex-end';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'middle': {
|
case 'middle': {
|
||||||
switchButtonDiv.style.left = 'calc(50% - 120px)';
|
switchButtonContainer.style.justifyContent = 'center';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
case 'left': {
|
case 'left': {
|
||||||
switchButtonDiv.style.left = '0px';
|
switchButtonContainer.style.justifyContent = 'flex-start';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<div class="video-switch-button">
|
|
||||||
<input checked="true" class="video-switch-button-checkbox" type="checkbox" />
|
|
||||||
<label class="video-switch-button-label" for="">
|
|
||||||
<span class="video-switch-button-label-span">
|
|
||||||
<ytmd-trans key="plugins.video-toggle.templates.button"></ytmd-trans>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
25
src/plugins/video-toggle/templates/video-switch-button.tsx
Normal file
25
src/plugins/video-toggle/templates/video-switch-button.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export interface VideoSwitchButtonProps {
|
||||||
|
onClick?: (event: MouseEvent) => void;
|
||||||
|
onChange?: (event: Event) => void;
|
||||||
|
songButtonText: string;
|
||||||
|
videoButtonText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoSwitchButton = (props: VideoSwitchButtonProps) => (
|
||||||
|
<div
|
||||||
|
class="video-switch-button"
|
||||||
|
data-video-button-text={props.videoButtonText}
|
||||||
|
on:click={props.onClick}
|
||||||
|
onChange={props.onChange}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={true}
|
||||||
|
id="video-toggle-video-switch-button-checkbox"
|
||||||
|
class="video-switch-button-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="video-switch-button-label" for="video-toggle-video-switch-button-checkbox">
|
||||||
|
<span class="video-switch-button-label-span">{props.songButtonText}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,4 +1,6 @@
|
|||||||
export const getSongMenu = () =>
|
export const getSongMenu = () =>
|
||||||
document.querySelector('ytmusic-menu-popup-renderer tp-yt-paper-listbox');
|
document.querySelector<HTMLElement>(
|
||||||
|
'ytmusic-menu-popup-renderer tp-yt-paper-listbox',
|
||||||
|
);
|
||||||
|
|
||||||
export default { getSongMenu };
|
export default { getSongMenu };
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import getSongControls from './song-controls';
|
|||||||
|
|
||||||
export const APP_PROTOCOL = 'youtubemusic';
|
export const APP_PROTOCOL = 'youtubemusic';
|
||||||
|
|
||||||
let protocolHandler:
|
let protocolHandler: ((cmd: string, ...args: string[]) => void) | undefined;
|
||||||
| ((cmd: string, args: string[] | undefined) => void)
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
export function setupProtocolHandler(win: BrowserWindow) {
|
export function setupProtocolHandler(win: BrowserWindow) {
|
||||||
if (process.defaultApp && process.argv.length >= 2) {
|
if (process.defaultApp && process.argv.length >= 2) {
|
||||||
@ -21,22 +19,20 @@ export function setupProtocolHandler(win: BrowserWindow) {
|
|||||||
|
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
|
|
||||||
protocolHandler = ((
|
protocolHandler = ((cmd: keyof typeof songControls, ...args) => {
|
||||||
cmd: keyof typeof songControls,
|
|
||||||
args: string[] | undefined = undefined,
|
|
||||||
) => {
|
|
||||||
if (Object.keys(songControls).includes(cmd)) {
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
songControls[cmd](args as never);
|
// @ts-expect-error: cmd is a key of songControls
|
||||||
|
songControls[cmd](...args);
|
||||||
}
|
}
|
||||||
}) as (cmd: string) => void;
|
}) as (cmd: string, ...args: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleProtocol(cmd: string, args: string[] | undefined) {
|
export function handleProtocol(cmd: string, ...args: string[]) {
|
||||||
protocolHandler?.(cmd, args);
|
protocolHandler?.(cmd, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeProtocolHandler(
|
export function changeProtocolHandler(
|
||||||
f: (cmd: string, args: string[] | undefined) => void,
|
f: (cmd: string, ...args: string[]) => void,
|
||||||
) {
|
) {
|
||||||
protocolHandler = f;
|
protocolHandler = f;
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/utils/custom-element.ts
Normal file
6
src/utils/custom-element.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { customElement, type ComponentType } from 'solid-element';
|
||||||
|
|
||||||
|
export const anonymousCustomElement = <T extends object>(
|
||||||
|
ComponentType: ComponentType<T>,
|
||||||
|
): CustomElementConstructor =>
|
||||||
|
customElement(`ytmd-${crypto.randomUUID()}`, ComponentType);
|
||||||
21
src/yt-web-components.d.ts
vendored
21
src/yt-web-components.d.ts
vendored
@ -34,14 +34,35 @@ declare module 'solid-js' {
|
|||||||
icon: Icons;
|
icon: Icons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface YtmdTransProps {
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
center: ComponentProps<'div'>;
|
center: ComponentProps<'div'>;
|
||||||
|
'ytmd-trans': ComponentProps<'span'> & YtmdTransProps;
|
||||||
'yt-formatted-string': ComponentProps<'span'> & YtFormattedStringProps;
|
'yt-formatted-string': ComponentProps<'span'> & YtFormattedStringProps;
|
||||||
'yt-button-renderer': ComponentProps<'button'> & YtButtonRendererProps;
|
'yt-button-renderer': ComponentProps<'button'> & YtButtonRendererProps;
|
||||||
|
'yt-touch-feedback-shape': ComponentProps<'div'>;
|
||||||
'tp-yt-paper-spinner-lite': ComponentProps<'div'> &
|
'tp-yt-paper-spinner-lite': ComponentProps<'div'> &
|
||||||
YpYtPaperSpinnerLiteProps;
|
YpYtPaperSpinnerLiteProps;
|
||||||
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
||||||
TpYtPaperIconButtonProps;
|
TpYtPaperIconButtonProps;
|
||||||
|
'yt-icon-button': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
|
'tp-yt-iron-icon': ComponentProps<'div'>;
|
||||||
|
'yt-icon': ComponentProps<'div'>;
|
||||||
|
// input type="range" slider component
|
||||||
|
'tp-yt-paper-slider': ComponentProps<'input'> & {
|
||||||
|
value?: number | string;
|
||||||
|
min?: number | string;
|
||||||
|
max?: number | string;
|
||||||
|
step?: number | string;
|
||||||
|
disabled?: boolean;
|
||||||
|
'on:immediate-value-changed'?: (
|
||||||
|
event: CustomEvent<{ value: number }>,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
'tp-yt-paper-progress': ComponentProps<'input'>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,13 +20,18 @@ export const pluginVirtualModuleGenerator = (
|
|||||||
) => {
|
) => {
|
||||||
const srcPath = resolve(__dirname, '..', 'src');
|
const srcPath = resolve(__dirname, '..', 'src');
|
||||||
const plugins = globSync([
|
const plugins = globSync([
|
||||||
'src/plugins/*/index.{js,ts}',
|
'src/plugins/*/index.{js,ts,jsx,tsx}',
|
||||||
'src/plugins/*.{js,ts}',
|
'src/plugins/*.{js,ts,jsx,tsx}',
|
||||||
'!src/plugins/utils/**/*',
|
'!src/plugins/utils/**/*',
|
||||||
'!src/plugins/utils/*',
|
'!src/plugins/utils/*',
|
||||||
]).map((path) => {
|
]).map((path) => {
|
||||||
let name = basename(path);
|
let name = basename(path);
|
||||||
if (name === 'index.ts' || name === 'index.js') {
|
if (
|
||||||
|
name === 'index.ts' ||
|
||||||
|
name === 'index.js' ||
|
||||||
|
name === 'index.jsx' ||
|
||||||
|
name === 'index.tsx'
|
||||||
|
) {
|
||||||
name = basename(resolve(path, '..'));
|
name = basename(resolve(path, '..'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user