mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-09 17:51: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",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"socks": "2.8.5",
|
||||
"solid-element": "1.9.1",
|
||||
"solid-floating-ui": "0.3.1",
|
||||
"solid-js": "1.9.7",
|
||||
"solid-styled-components": "0.28.5",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@ -208,6 +208,9 @@ importers:
|
||||
socks:
|
||||
specifier: 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:
|
||||
specifier: 0.3.1
|
||||
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==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
component-register@0.8.7:
|
||||
resolution: {integrity: sha512-clPS/o1RNfJw7L1/w4q+nkj6l7JV32kFHCx6vW5nSPOEly4B9olMeADNilEgpLV/DdeS7y8JXhHKx9YvSj8vqQ==}
|
||||
|
||||
compressible@2.0.18:
|
||||
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -4280,6 +4286,11 @@ packages:
|
||||
resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==}
|
||||
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:
|
||||
resolution: {integrity: sha512-o/QmGsWPS2Z3KidAxP0nDvN7alI7Kqy0kU+wd85Fz+au5SYcnYm7I6Fk3M60Za35azsPX0U+5fEtqfOuk6Ao0Q==}
|
||||
engines: {node: '>=10'}
|
||||
@ -6644,6 +6655,8 @@ snapshots:
|
||||
|
||||
compare-version@0.1.2: {}
|
||||
|
||||
component-register@0.8.7: {}
|
||||
|
||||
compressible@2.0.18:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@ -9218,6 +9231,11 @@ snapshots:
|
||||
ip-address: 9.0.5
|
||||
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):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.2
|
||||
|
||||
@ -5,7 +5,7 @@ import defaults from './defaults';
|
||||
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
||||
|
||||
// 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 = {
|
||||
'>=3.3.0'(store: IStore) {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "تفعيل الفيديو",
|
||||
"templates": {
|
||||
"button": "أغنية"
|
||||
"button-song": "أغنية"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -835,7 +835,7 @@
|
||||
},
|
||||
"name": "Превключване на видео",
|
||||
"templates": {
|
||||
"button": "Песен"
|
||||
"button-song": "Песен"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Botó de vídeo",
|
||||
"templates": {
|
||||
"button": "Cançó"
|
||||
"button-song": "Cançó"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -765,7 +765,7 @@
|
||||
},
|
||||
"name": "Přepínač videa",
|
||||
"templates": {
|
||||
"button": "Písnička"
|
||||
"button-song": "Písnička"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Videoumschalter",
|
||||
"templates": {
|
||||
"button": "Lied"
|
||||
"button-song": "Lied"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Εναλλαγή βίντεο",
|
||||
"templates": {
|
||||
"button": "Τραγούδι"
|
||||
"button-song": "Τραγούδι"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
},
|
||||
"templates": {
|
||||
"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": {
|
||||
@ -600,7 +605,15 @@
|
||||
},
|
||||
"navigation": {
|
||||
"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": {
|
||||
"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",
|
||||
"name": "Video Quality Changer"
|
||||
},
|
||||
@ -859,7 +877,8 @@
|
||||
},
|
||||
"name": "Video Toggle",
|
||||
"templates": {
|
||||
"button": "Song"
|
||||
"button-song": "Song",
|
||||
"button-video": "Video"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Alternador de vídeo",
|
||||
"templates": {
|
||||
"button": "Canción"
|
||||
"button-song": "Canción"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -831,7 +831,7 @@
|
||||
},
|
||||
"name": "ویدیو به آهنگ",
|
||||
"templates": {
|
||||
"button": "ترانه"
|
||||
"button-song": "ترانه"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -784,7 +784,7 @@
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"button": "Kanta"
|
||||
"button-song": "Kanta"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Basculer la vidéo",
|
||||
"templates": {
|
||||
"button": "Musique"
|
||||
"button-song": "Musique"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -831,7 +831,7 @@
|
||||
},
|
||||
"name": "Videó váltó",
|
||||
"templates": {
|
||||
"button": "Zeneszám"
|
||||
"button-song": "Zeneszám"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Peralih Video",
|
||||
"templates": {
|
||||
"button": "Lagu"
|
||||
"button-song": "Lagu"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -799,7 +799,7 @@
|
||||
},
|
||||
"name": "Myndbandsrofi",
|
||||
"templates": {
|
||||
"button": "Lag"
|
||||
"button-song": "Lag"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Selettore Brano/Video",
|
||||
"templates": {
|
||||
"button": "Brano"
|
||||
"button-song": "Brano"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -857,7 +857,7 @@
|
||||
},
|
||||
"name": "動画の切り替え",
|
||||
"templates": {
|
||||
"button": "曲"
|
||||
"button-song": "曲"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,8 @@
|
||||
},
|
||||
"name": "영상 전환",
|
||||
"templates": {
|
||||
"button": "노래"
|
||||
"button-song": "노래",
|
||||
"button-video": "영상"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -618,7 +618,7 @@
|
||||
},
|
||||
"name": "Vaizdo įrašo perjungimas",
|
||||
"templates": {
|
||||
"button": "Daina"
|
||||
"button-song": "Daina"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -381,7 +381,7 @@
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"button": "Lagu"
|
||||
"button-song": "Lagu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -577,7 +577,7 @@
|
||||
},
|
||||
"name": "Videoveksling",
|
||||
"templates": {
|
||||
"button": "Spor"
|
||||
"button-song": "Spor"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "भिडियो टगल",
|
||||
"templates": {
|
||||
"button": "गीत"
|
||||
"button-song": "गीत"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Videoschakelaar",
|
||||
"templates": {
|
||||
"button": "Nummer"
|
||||
"button-song": "Nummer"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Przełącznik wideo",
|
||||
"templates": {
|
||||
"button": "Utwór"
|
||||
"button-song": "Utwór"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Alternar vídeo",
|
||||
"templates": {
|
||||
"button": "Música"
|
||||
"button-song": "Música"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Botão de Alternar Vídeo",
|
||||
"templates": {
|
||||
"button": "Música"
|
||||
"button-song": "Música"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Comutator video",
|
||||
"templates": {
|
||||
"button": "Melodie"
|
||||
"button-song": "Melodie"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Переключатель видео",
|
||||
"templates": {
|
||||
"button": "Песня"
|
||||
"button-song": "Песня"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -222,7 +222,7 @@
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"button": "Låt"
|
||||
"button-song": "Låt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "வீடியோ மாற்று",
|
||||
"templates": {
|
||||
"button": "பாடல்"
|
||||
"button-song": "பாடல்"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "ปุ่มวิดีโอ",
|
||||
"templates": {
|
||||
"button": "เพลง"
|
||||
"button-song": "เพลง"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Video Geçiş",
|
||||
"templates": {
|
||||
"button": "Şarkı"
|
||||
"button-song": "Şarkı"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "Перемикач відео",
|
||||
"templates": {
|
||||
"button": "Пісня"
|
||||
"button-song": "Пісня"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -857,7 +857,7 @@
|
||||
},
|
||||
"name": "Chuyển đổi video",
|
||||
"templates": {
|
||||
"button": "Bài hát"
|
||||
"button-song": "Bài hát"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "视频切换开关",
|
||||
"templates": {
|
||||
"button": "歌曲"
|
||||
"button-song": "歌曲"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -859,7 +859,7 @@
|
||||
},
|
||||
"name": "歌曲/影片切換",
|
||||
"templates": {
|
||||
"button": "歌曲"
|
||||
"button-song": "歌曲"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -763,7 +763,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
const splited = decodeURIComponent(command).split(' ');
|
||||
|
||||
handleProtocol(splited.shift()!, splited);
|
||||
handleProtocol(splited.shift()!, ...splited);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { createSignal, Show } from 'solid-js';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
import undislikeHTML from './templates/undislike.html?raw';
|
||||
import dislikeHTML from './templates/dislike.html?raw';
|
||||
import likeHTML from './templates/like.html?raw';
|
||||
import unlikeHTML from './templates/unlike.html?raw';
|
||||
import {
|
||||
DislikeButton,
|
||||
LikeButton,
|
||||
UnDislikeButton,
|
||||
UnLikeButton,
|
||||
} from './templates';
|
||||
|
||||
export default createPlugin<
|
||||
unknown,
|
||||
@ -52,19 +57,69 @@ export default createPlugin<
|
||||
}
|
||||
const continuations = await waitForElement<HTMLElement>('#continuations');
|
||||
this.waiting = false;
|
||||
//Gets the for buttons
|
||||
const buttons: Array<HTMLElement> = [
|
||||
ElementFromHtml(undislikeHTML),
|
||||
ElementFromHtml(dislikeHTML),
|
||||
ElementFromHtml(likeHTML),
|
||||
ElementFromHtml(unlikeHTML),
|
||||
];
|
||||
|
||||
const [showUnDislike, setShowUnDislike] = createSignal(true);
|
||||
const [showDislike, setShowDislike] = createSignal(true);
|
||||
const [showLike, setShowLike] = createSignal(true);
|
||||
const [showUnLike, setShowUnLike] = createSignal(true);
|
||||
|
||||
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
|
||||
const playlist =
|
||||
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
|
||||
this.changeObserver?.disconnect();
|
||||
|
||||
this.changeObserver = new MutationObserver(() => {
|
||||
this.stop();
|
||||
this.start();
|
||||
@ -84,34 +139,57 @@ export default createPlugin<
|
||||
'#button-shape-dislike > button',
|
||||
).length;
|
||||
if (continuations.children.length == 0 && listsLength > 0) {
|
||||
const counts = [
|
||||
playlist?.querySelectorAll(
|
||||
const counts = {
|
||||
dislike: playlist?.querySelectorAll(
|
||||
'#button-shape-dislike > button[aria-pressed=true]',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
undislike: playlist?.querySelectorAll(
|
||||
'#button-shape-dislike > button[aria-pressed=false]',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
unlike: playlist?.querySelectorAll(
|
||||
'#button-shape-like > button[aria-pressed=false]',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
like: playlist?.querySelectorAll(
|
||||
'#button-shape-like > button[aria-pressed=true]',
|
||||
).length,
|
||||
];
|
||||
let i = 0;
|
||||
for (const count of counts) {
|
||||
if (count == 0) {
|
||||
buttons.splice(i, 1);
|
||||
i--;
|
||||
} else {
|
||||
(
|
||||
buttons[i].children[0].children[0] as HTMLElement
|
||||
).style.setProperty(
|
||||
'-webkit-mask-size',
|
||||
`100% ${100 - (count / listsLength) * 100}%`,
|
||||
);
|
||||
};
|
||||
for (const [name, size] of Object.entries(counts)) {
|
||||
switch (name) {
|
||||
case 'dislike':
|
||||
if (size > 0) {
|
||||
setShowDislike(true);
|
||||
setDislikeMaskSize(`100% ${100 - (size / listsLength) * 100}%`);
|
||||
} else {
|
||||
setShowDislike(false);
|
||||
}
|
||||
break;
|
||||
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 =
|
||||
@ -126,10 +204,7 @@ export default createPlugin<
|
||||
menu,
|
||||
menuParent.children[menuParent.children.length - 1],
|
||||
);
|
||||
for (const button of buttons) {
|
||||
menu.appendChild(button);
|
||||
button.addEventListener('click', this.loadFullList);
|
||||
}
|
||||
menu.appendChild(buttonContainer);
|
||||
}
|
||||
},
|
||||
loadFullList(event: MouseEvent) {
|
||||
@ -159,7 +234,7 @@ export default createPlugin<
|
||||
let playlistButtons: NodeListOf<HTMLElement> | undefined;
|
||||
const playlist =
|
||||
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) {
|
||||
case 'allundislike':
|
||||
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({
|
||||
start({ ipc: { handle }, window }) {
|
||||
handle(
|
||||
'captionsSelector',
|
||||
'ytmd:captions-selector',
|
||||
async (captionLabels: Record<string, string>, currentIndex: string) =>
|
||||
await prompt(
|
||||
{
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { YoutubePlayer } from '@/types/youtube-player';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import backend from './back';
|
||||
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
export default createPlugin<
|
||||
unknown,
|
||||
unknown,
|
||||
{
|
||||
captionsSettingsButton: HTMLElement;
|
||||
captionsSettingsButton?: HTMLElement;
|
||||
captionTrackList: LanguageOptions[] | null;
|
||||
api: YoutubePlayer | null;
|
||||
config: CaptionsSelectorConfig | null;
|
||||
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
|
||||
videoChangeListener: () => void;
|
||||
captionsButtonClickListener: () => void;
|
||||
},
|
||||
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 { getSongMenu } from '@/providers/dom-elements';
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import { LoggerPrefix } from '@/utils';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||
|
||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
import { DownloadButton } from './templates/download';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
import type { DownloaderPluginConfig } from './index';
|
||||
|
||||
let menu: Element | null = null;
|
||||
let progress: Element | null = null;
|
||||
const downloadButton = ElementFromHtml(downloadHTML);
|
||||
let menu: HTMLElement | null = null;
|
||||
let download: () => void;
|
||||
|
||||
let doneFirstLoad = false;
|
||||
const [downloadButtonText, setDownloadButtonText] = createSignal<string>('');
|
||||
|
||||
let buttonContainer: HTMLDivElement | null = null;
|
||||
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
@ -30,46 +28,24 @@ const menuObserver = new MutationObserver(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.contains(downloadButton)) {
|
||||
if (
|
||||
menu.contains(buttonContainer) ||
|
||||
!isMusicOrVideoTrack() ||
|
||||
!buttonContainer
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for video (or music)
|
||||
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);
|
||||
}
|
||||
menu.prepend(buttonContainer);
|
||||
});
|
||||
|
||||
export const onRendererLoad = ({
|
||||
ipc,
|
||||
}: RendererContext<DownloaderPluginConfig>) => {
|
||||
window.download = () => {
|
||||
download = () => {
|
||||
const songMenu = getSongMenu();
|
||||
|
||||
let videoUrl = songMenu
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector(
|
||||
'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
|
||||
)
|
||||
@ -108,21 +84,30 @@ export const onRendererLoad = ({
|
||||
};
|
||||
|
||||
ipc.on('downloader-feedback', (feedback: string) => {
|
||||
if (progress) {
|
||||
const targetHtml = feedback || t('plugins.downloader.templates.button');
|
||||
(progress.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
||||
: targetHtml;
|
||||
} else {
|
||||
console.warn(
|
||||
LoggerPrefix,
|
||||
t('plugins.downloader.renderer.can-not-update-progress'),
|
||||
);
|
||||
}
|
||||
const targetHtml = feedback || t('plugins.downloader.templates.button');
|
||||
setDownloadButtonText(targetHtml);
|
||||
});
|
||||
};
|
||||
|
||||
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')!, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
@ -1,24 +1,23 @@
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||
onclick="download()"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
>
|
||||
export const DownloadButton = (props: {
|
||||
onClick: () => void;
|
||||
text: string;
|
||||
}) => (
|
||||
<a
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="navigation-endpoint"
|
||||
tabindex="-1"
|
||||
tabindex={-1}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div
|
||||
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
|
||||
>
|
||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||
<svg
|
||||
class="style-scope yt-icon"
|
||||
focusable="false"
|
||||
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"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
@ -39,7 +38,7 @@
|
||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="ytmcustom-download"
|
||||
>
|
||||
<ytmd-trans key="plugins.downloader.templates.button"></ytmd-trans>
|
||||
{props.text}
|
||||
</div>
|
||||
</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();
|
||||
});
|
||||
|
||||
changeProtocolHandler((cmd, args) => {
|
||||
changeProtocolHandler((cmd, ...args) => {
|
||||
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 (
|
||||
config().refreshOnPlayPause &&
|
||||
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
|
||||
|
||||
@ -3,7 +3,7 @@ import { createPlugin } from '@/utils';
|
||||
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||
import { onPlayerApiReady } from './renderer';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
export type PictureInPicturePluginConfig = {
|
||||
@ -41,7 +41,6 @@ export default createPlugin({
|
||||
onConfigChange,
|
||||
},
|
||||
renderer: {
|
||||
start: onRendererLoad,
|
||||
onPlayerApiReady,
|
||||
},
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ export const onMainLoad = async ({
|
||||
window.setMaximizable(false);
|
||||
window.setFullScreenable(false);
|
||||
|
||||
send('pip-toggle', true);
|
||||
send('ytmd:pip-toggle', true);
|
||||
|
||||
app.dock?.hide();
|
||||
window.setVisibleOnAllWorkspaces(true, {
|
||||
@ -63,7 +63,7 @@ export const onMainLoad = async ({
|
||||
window.setMaximizable(true);
|
||||
window.setFullScreenable(true);
|
||||
|
||||
send('pip-toggle', false);
|
||||
send('ytmd:pip-toggle', false);
|
||||
|
||||
window.setVisibleOnAllWorkspaces(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 QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { QualitySettingButton } from './templates/quality-setting-button';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
export default createPlugin({
|
||||
@ -18,7 +19,7 @@ export default createPlugin({
|
||||
|
||||
backend({ ipc, window }) {
|
||||
ipc.handle(
|
||||
'qualityChanger',
|
||||
'ytmd:quality-changer',
|
||||
async (qualityLabels: string[], currentIndex: number) =>
|
||||
await dialog.showMessageBox(window, {
|
||||
type: 'question',
|
||||
@ -42,40 +43,48 @@ export default createPlugin({
|
||||
},
|
||||
|
||||
renderer: {
|
||||
qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate),
|
||||
qualitySettingsButtonContainer: document.createElement('div'),
|
||||
onPlayerApiReady(api: YoutubePlayer, context) {
|
||||
const getPlayer = () =>
|
||||
document.querySelector<HTMLVideoElement>('#player');
|
||||
const chooseQuality = () => {
|
||||
setTimeout(() => getPlayer()?.click());
|
||||
const chooseQuality = async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const qualityLevels = api.getAvailableQualityLevels();
|
||||
|
||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||
|
||||
(
|
||||
context.ipc.invoke(
|
||||
'qualityChanger',
|
||||
api.getAvailableQualityLabels(),
|
||||
currentIndex,
|
||||
) as Promise<{ response: number }>
|
||||
).then((promise) => {
|
||||
if (promise.response === -1) {
|
||||
return;
|
||||
}
|
||||
const quality = (await context.ipc.invoke(
|
||||
'ytmd:quality-changer',
|
||||
api.getAvailableQualityLabels(),
|
||||
currentIndex,
|
||||
)) as {
|
||||
response: number;
|
||||
};
|
||||
|
||||
const newQuality = qualityLevels[promise.response];
|
||||
api.setPlaybackQualityRange(newQuality);
|
||||
api.setPlaybackQuality(newQuality);
|
||||
});
|
||||
if (quality.response === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
document
|
||||
.querySelector('.top-row-buttons.ytmusic-player')
|
||||
?.prepend(this.qualitySettingsButton);
|
||||
|
||||
this.qualitySettingsButton.addEventListener('click', chooseQuality);
|
||||
?.prepend(this.qualitySettingsButtonContainer);
|
||||
};
|
||||
|
||||
setup();
|
||||
@ -83,7 +92,7 @@ export default createPlugin({
|
||||
stop() {
|
||||
document
|
||||
.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 songControls = getSongControls(window);
|
||||
const { playPause, next, previous, search } = songControls;
|
||||
const { playPause, next, previous } = songControls;
|
||||
|
||||
if (config.overrideMediaKeys) {
|
||||
_registerGlobalShortcut(window.webContents, 'MediaPlayPause', playPause);
|
||||
@ -44,9 +44,6 @@ export const onMainLoad = async ({
|
||||
_registerGlobalShortcut(window.webContents, 'MediaPreviousTrack', previous);
|
||||
}
|
||||
|
||||
_registerLocalShortcut(window, 'CommandOrControl+F', search);
|
||||
_registerLocalShortcut(window, 'CommandOrControl+L', search);
|
||||
|
||||
if (is.linux()) {
|
||||
registerMPRIS(window);
|
||||
}
|
||||
|
||||
@ -44,7 +44,6 @@ const providerBias = (p: ProviderName) =>
|
||||
(lyricsStore.lyrics[p].data?.lines?.length && p === 'YTMusic' ? 1 : 0) +
|
||||
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
||||
|
||||
// prettier-ignore
|
||||
const pickBestProvider = () => {
|
||||
const providers = Array.from(providerNames);
|
||||
|
||||
@ -56,15 +55,17 @@ const pickBestProvider = () => {
|
||||
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||
createSignal(false);
|
||||
|
||||
// prettier-ignore
|
||||
export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> }) => {
|
||||
export const LyricsPicker = (props: {
|
||||
setStickRef: Setter<HTMLElement | null>;
|
||||
}) => {
|
||||
createEffect(() => {
|
||||
// fallback to the next source, if the current one has an error
|
||||
if (!hasManuallySwitchedProvider()
|
||||
) {
|
||||
if (!hasManuallySwitchedProvider()) {
|
||||
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 (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
||||
@ -80,7 +81,9 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
||||
};
|
||||
|
||||
_ytAPI?.addEventListener('videodatachange', videoDataChangeHandler);
|
||||
onCleanup(() => _ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler));
|
||||
onCleanup(() =>
|
||||
_ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler),
|
||||
);
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
@ -95,7 +98,9 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
||||
setHasManuallySwitchedProvider(true);
|
||||
setLyricsStore('provider', (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 (
|
||||
<div class="lyrics-picker" ref={props.setStickRef}>
|
||||
<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 class="lyrics-picker-content">
|
||||
@ -138,7 +176,7 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
||||
/>
|
||||
</Match>
|
||||
<Match when={currentLyrics().state === 'error'}>
|
||||
<tp-yt-paper-icon-button
|
||||
<yt-icon-button
|
||||
icon={errorIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
@ -151,18 +189,20 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
||||
currentLyrics().data?.lyrics)
|
||||
}
|
||||
>
|
||||
<tp-yt-paper-icon-button
|
||||
<yt-icon-button
|
||||
icon={successIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={
|
||||
currentLyrics().state === 'done'
|
||||
&& !currentLyrics().data?.lines
|
||||
&& !currentLyrics().data?.lyrics
|
||||
}>
|
||||
<tp-yt-paper-icon-button
|
||||
<Match
|
||||
when={
|
||||
currentLyrics().state === 'done' &&
|
||||
!currentLyrics().data?.lines &&
|
||||
!currentLyrics().data?.lyrics
|
||||
}
|
||||
>
|
||||
<yt-icon-button
|
||||
icon={notFoundIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
@ -194,7 +234,40 @@ export const LyricsPicker = (props: { setStickRef: Setter<HTMLElement | null> })
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
|
||||
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
|
||||
@ -6,13 +6,8 @@ import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
||||
* @returns The DOM element
|
||||
*/
|
||||
export const ElementFromHtml = (html: string): HTMLElement => {
|
||||
const template = document.createElement('template');
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
(template.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
||||
? defaultTrustedTypePolicy.createHTML(html)
|
||||
: html;
|
||||
|
||||
return template.content.firstElementChild as HTMLElement;
|
||||
return (domParser.parseFromString(html, 'text/html') as HTMLDocument).body
|
||||
.firstElementChild as HTMLElement;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.video-toggle-custom-mode .video-switch-button:before {
|
||||
content: 'Video';
|
||||
content: attr(data-video-button-text);
|
||||
position: absolute;
|
||||
top: 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 buttonSwitcherStyle from './button-switcher.css?inline';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
import { ThumbnailElement } from '@/types/get-player-response';
|
||||
import { t } from '@/i18n';
|
||||
import { MenuTemplate } from '@/menu';
|
||||
|
||||
import { VideoSwitchButton } from './templates/video-switch-button';
|
||||
|
||||
export type VideoTogglePluginConfig = {
|
||||
enabled: boolean;
|
||||
hideVideo: boolean;
|
||||
@ -150,6 +154,8 @@ export default createPlugin({
|
||||
}
|
||||
},
|
||||
async onPlayerApiReady(api, { getConfig }) {
|
||||
const [showButton, setShowButton] = createSignal(true);
|
||||
|
||||
const config = await getConfig();
|
||||
this.config = config;
|
||||
|
||||
@ -164,7 +170,25 @@ export default createPlugin({
|
||||
>('ytmusic-player');
|
||||
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 thumbnails: ThumbnailElement[] =
|
||||
@ -184,7 +208,7 @@ export default createPlugin({
|
||||
const checkbox = document.querySelector<HTMLInputElement>(
|
||||
'.video-switch-button-checkbox',
|
||||
); // custom mode
|
||||
if (checkbox) checkbox.checked = !this.config.hideVideo;
|
||||
if (checkbox) checkbox.checked = !this.config?.hideVideo;
|
||||
|
||||
if (player) {
|
||||
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||
@ -217,17 +241,18 @@ export default createPlugin({
|
||||
// Video doesn't exist -> switch to song mode
|
||||
setVideoState(false);
|
||||
// Hide toggle button
|
||||
switchButtonDiv.style.display = 'none';
|
||||
setShowButton(false);
|
||||
} else {
|
||||
const songImage =
|
||||
document.querySelector<HTMLImageElement>('#song-image img');
|
||||
const songImage = document.querySelector<HTMLImageElement>(
|
||||
'#song-image #img.style-scope.yt-img-shadow',
|
||||
);
|
||||
if (!songImage) {
|
||||
return;
|
||||
}
|
||||
// Switch to high-res thumbnail
|
||||
forceThumbnail(songImage);
|
||||
// Show toggle button
|
||||
switchButtonDiv.style.display = 'initial';
|
||||
setShowButton(true);
|
||||
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
||||
if (
|
||||
!this.config?.hideVideo &&
|
||||
@ -282,7 +307,7 @@ export default createPlugin({
|
||||
}
|
||||
});
|
||||
playbackModeObserver.observe(
|
||||
document.querySelector('#song-image img')!,
|
||||
document.querySelector('#song-image #img.style-scope.yt-img-shadow')!,
|
||||
{ attributeFilter: ['src'] },
|
||||
);
|
||||
};
|
||||
@ -290,7 +315,7 @@ export default createPlugin({
|
||||
if (config.mode !== 'native' && config.mode != 'disabled') {
|
||||
document
|
||||
.querySelector<HTMLVideoElement>('#player')
|
||||
?.prepend(switchButtonDiv);
|
||||
?.prepend(switchButtonContainer);
|
||||
|
||||
setVideoState(!config.hideVideo);
|
||||
forcePlaybackMode();
|
||||
@ -299,36 +324,25 @@ export default createPlugin({
|
||||
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);
|
||||
|
||||
observeThumbnail();
|
||||
videoStarted();
|
||||
|
||||
switch (config.align) {
|
||||
case 'right': {
|
||||
switchButtonDiv.style.left = 'calc(100% - 240px)';
|
||||
switchButtonContainer.style.justifyContent = 'flex-end';
|
||||
return;
|
||||
}
|
||||
|
||||
case 'middle': {
|
||||
switchButtonDiv.style.left = 'calc(50% - 120px)';
|
||||
switchButtonContainer.style.justifyContent = 'center';
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
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 = () =>
|
||||
document.querySelector('ytmusic-menu-popup-renderer tp-yt-paper-listbox');
|
||||
document.querySelector<HTMLElement>(
|
||||
'ytmusic-menu-popup-renderer tp-yt-paper-listbox',
|
||||
);
|
||||
|
||||
export default { getSongMenu };
|
||||
|
||||
@ -6,9 +6,7 @@ import getSongControls from './song-controls';
|
||||
|
||||
export const APP_PROTOCOL = 'youtubemusic';
|
||||
|
||||
let protocolHandler:
|
||||
| ((cmd: string, args: string[] | undefined) => void)
|
||||
| undefined;
|
||||
let protocolHandler: ((cmd: string, ...args: string[]) => void) | undefined;
|
||||
|
||||
export function setupProtocolHandler(win: BrowserWindow) {
|
||||
if (process.defaultApp && process.argv.length >= 2) {
|
||||
@ -21,22 +19,20 @@ export function setupProtocolHandler(win: BrowserWindow) {
|
||||
|
||||
const songControls = getSongControls(win);
|
||||
|
||||
protocolHandler = ((
|
||||
cmd: keyof typeof songControls,
|
||||
args: string[] | undefined = undefined,
|
||||
) => {
|
||||
protocolHandler = ((cmd: keyof typeof songControls, ...args) => {
|
||||
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) {
|
||||
protocolHandler?.(cmd, args);
|
||||
export function handleProtocol(cmd: string, ...args: string[]) {
|
||||
protocolHandler?.(cmd, ...args);
|
||||
}
|
||||
|
||||
export function changeProtocolHandler(
|
||||
f: (cmd: string, args: string[] | undefined) => void,
|
||||
f: (cmd: string, ...args: string[]) => void,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
interface YtmdTransProps {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
center: ComponentProps<'div'>;
|
||||
'ytmd-trans': ComponentProps<'span'> & YtmdTransProps;
|
||||
'yt-formatted-string': ComponentProps<'span'> & YtFormattedStringProps;
|
||||
'yt-button-renderer': ComponentProps<'button'> & YtButtonRendererProps;
|
||||
'yt-touch-feedback-shape': ComponentProps<'div'>;
|
||||
'tp-yt-paper-spinner-lite': ComponentProps<'div'> &
|
||||
YpYtPaperSpinnerLiteProps;
|
||||
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
||||
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 plugins = globSync([
|
||||
'src/plugins/*/index.{js,ts}',
|
||||
'src/plugins/*.{js,ts}',
|
||||
'src/plugins/*/index.{js,ts,jsx,tsx}',
|
||||
'src/plugins/*.{js,ts,jsx,tsx}',
|
||||
'!src/plugins/utils/**/*',
|
||||
'!src/plugins/utils/*',
|
||||
]).map((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, '..'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user