feat: Add new plugin Album actions (#1515)

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Yazazuyo
2023-12-29 16:13:56 +01:00
committed by GitHub
parent 5e303c2ba8
commit 5e79e9e0f2
6 changed files with 478 additions and 0 deletions

View File

@ -593,6 +593,10 @@
"visualizer-type": "Visualizer Type"
},
"name": "Visualizer"
},
"album-actions": {
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album.",
"name": "Album actions"
}
}
}

View File

@ -0,0 +1,178 @@
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer';
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';
export default createPlugin({
name: () => t('plugins.album-actions.name'),
description: () => t('plugins.album-actions.description'),
restartNeeded: false,
config: {
enabled: true,
},
renderer: {
observer: null as MutationObserver | null,
loadObserver: null as MutationObserver | null,
changeObserver: null as MutationObserver | null,
waiting: false as boolean,
start() {
//Waits for pagechange
this.onPageChange();
this.observer = new MutationObserver(() => {
this.onPageChange();
});
this.observer.observe(document.querySelector('#browse-page'), {
attributes: false,
childList: true,
subtree: true,
});
},
onPageChange() {
if (this.waiting) {
return;
} else {
this.waiting = true;
}
this.waitForElem('#continuations').then((continuations: HTMLElement) => {
this.waiting = false;
//Gets the for buttons
let buttons: Array<HTMLElement> = [
ElementFromHtml(undislikeHTML),
ElementFromHtml(dislikeHTML),
ElementFromHtml(likeHTML),
ElementFromHtml(unlikeHTML),
];
//Finds the playlist
const playlist =
document.querySelector('ytmusic-shelf-renderer') ??
document.querySelector('ytmusic-playlist-shelf-renderer');
//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();
});
const allButtons = playlist.querySelectorAll(
'yt-button-shape.ytmusic-like-button-renderer',
);
for (const btn of allButtons)
this.changeObserver.observe(btn, {
attributes: true,
childList: false,
subtree: false,
});
//Determine if button is needed and colors the percentage
const listsLength = playlist.querySelectorAll(
'#button-shape-dislike > button',
).length;
if (continuations.children.length == 0 && listsLength > 0) {
const counts = [
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
).length,
];
let i = 0;
for (const count of counts) {
if (count == 0) {
buttons.splice(i, 1);
i--;
} else {
buttons[i].children[0].children[0].style.setProperty(
'-webkit-mask-size',
`100% ${100 - (count / listsLength) * 100}%`,
);
}
i++;
}
}
const menu = document.querySelector('.detail-page-menu');
if (menu && !document.querySelector('.like-menu')) {
for (const button of buttons) {
menu.appendChild(button);
button.addEventListener('click', this.loadFullList);
}
}
});
},
loadFullList(event) {
event.stopPropagation();
const id: string = event.currentTarget.id,
loader = document.getElementById('continuations');
this.loadObserver = new MutationObserver(() => {
this.applyToList(id, loader);
});
this.applyToList(id, loader);
this.loadObserver.observe(loader, {
attributes: true,
childList: true,
subtree: true,
});
loader?.style.setProperty('top', '0');
loader?.style.setProperty('left', '50%');
loader?.style.setProperty('position', 'absolute');
},
applyToList(id: string, loader: HTMLElement) {
if (loader.children.length != 0) return;
this.loadObserver?.disconnect();
let playlistbuttons: NodeListOf<Element> | undefined;
const playlist = document.querySelector('ytmusic-shelf-renderer')
? document.querySelector('ytmusic-shelf-renderer')
: document.querySelector('ytmusic-playlist-shelf-renderer');
switch (id) {
case 'allundislike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
);
break;
case 'alldislike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
);
break;
case 'alllike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
);
break;
case 'allunlike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
);
break;
default:
}
playlistButtons?.forEach((elem) => elem.click());
},
stop() {
this.observer?.disconnect();
this.changeObserver?.disconnect();
for (const button of document.querySelectorAll('.like-menu')) {
button.remove();
}
},
waitForElem(selector: string) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
},
});

View File

@ -0,0 +1,74 @@
<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>

View File

@ -0,0 +1,74 @@
<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>

View File

@ -0,0 +1,74 @@
<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>

View File

@ -0,0 +1,74 @@
<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>