diff --git a/plugins/navigation/templates/forward.html b/plugins/navigation/templates/forward.html
index 84c717be88..0c8631c149 100644
--- a/plugins/navigation/templates/forward.html
+++ b/plugins/navigation/templates/forward.html
@@ -1,6 +1,6 @@
diff --git a/plugins/no-google-login/back.ts b/plugins/no-google-login/back.ts
index 5634e5efbf..e82da6b89f 100644
--- a/plugins/no-google-login/back.ts
+++ b/plugins/no-google-login/back.ts
@@ -1,9 +1,9 @@
-import path from 'node:path';
-
import { BrowserWindow } from 'electron';
+import style from './style.css';
+
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
- injectCSS(win.webContents, path.join(__dirname, 'style.css'));
+ injectCSS(win.webContents, style);
};
diff --git a/plugins/notifications/interactive.ts b/plugins/notifications/interactive.ts
index ab724ba2c7..cfb2247b30 100644
--- a/plugins/notifications/interactive.ts
+++ b/plugins/notifications/interactive.ts
@@ -9,6 +9,7 @@ import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info';
import { changeProtocolHandler } from '../../providers/protocol-handler';
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
+import { getMediaIconLocation } from '../utils';
let songControls: ReturnType;
let savedNotification: Notification | undefined;
@@ -151,11 +152,6 @@ const getXml = (songInfo: SongInfo, iconSrc: string) => {
}
}
};
-
-const iconLocation = app.isPackaged
- ? path.resolve(app.getPath('userData'), 'icons')
- : path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
-
const display = (kind: keyof typeof icons) => {
if (config.get('toastStyle') === ToastStyles.legacy) {
return `content="${icons[kind]}"`;
@@ -163,7 +159,7 @@ const display = (kind: keyof typeof icons) => {
return `\
content="${config.get('hideButtonText') ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
- imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
+ imageUri="file:///${path.resolve(getMediaIconLocation(), `${kind}.png`)}"
`;
};
diff --git a/plugins/notifications/menu.ts b/plugins/notifications/menu.ts
index 127d287a36..9fd51a03d0 100644
--- a/plugins/notifications/menu.ts
+++ b/plugins/notifications/menu.ts
@@ -6,11 +6,13 @@ import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
import config from './config';
+import { MenuTemplate } from '../../menu';
+
import type { ConfigType } from '../../config/dynamic';
-export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
- ...(is.linux()
- ? [
+const getMenu = (options: ConfigType<'notifications'>): MenuTemplate => {
+ if (is.linux()) {
+ return [
{
label: 'Notification Priority',
submenu: urgencyLevels.map((level) => ({
@@ -19,11 +21,10 @@ export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
checked: options.urgency === level.value,
click: () => config.set('urgency', level.value),
})),
- },
- ]
- : []),
- ...(is.windows()
- ? [
+ }
+ ];
+ } else if (is.windows()) {
+ return [
{
label: 'Interactive Notifications',
type: 'checkbox',
@@ -59,8 +60,14 @@ export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
label: 'Style',
submenu: getToastStyleMenuItems(options),
},
- ]
- : []),
+ ];
+ } else {
+ return [];
+ }
+};
+
+export default (_win: BrowserWindow, options: ConfigType<'notifications'>): MenuTemplate => [
+ ...getMenu(options),
{
label: 'Show notification on unpause',
type: 'checkbox',
@@ -79,8 +86,8 @@ export function getToastStyleMenuItems(options: ConfigType<'notifications'>) {
type: 'radio',
checked: options.toastStyle === index,
click: () => config.set('toastStyle', index),
- };
+ } satisfies Electron.MenuItemConstructorOptions;
}
- return array;
+ return array as Electron.MenuItemConstructorOptions[];
}
diff --git a/plugins/notifications/utils.ts b/plugins/notifications/utils.ts
index 7870730c6e..7c9c8a3169 100644
--- a/plugins/notifications/utils.ts
+++ b/plugins/notifications/utils.ts
@@ -7,6 +7,7 @@ import config from './config';
import { cache } from '../../providers/decorators';
import { SongInfo } from '../../providers/song-info';
+import { getAssetsDirectoryLocation } from '../utils';
const icon = 'assets/youtube-music.png';
const userData = app.getPath('userData');
@@ -88,10 +89,9 @@ export const saveTempIcon = () => {
continue;
}
- const iconPath = path.resolve(__dirname, '../../assets/media-icons-black', `${kind}.png`);
+ const iconPath = path.resolve(path.resolve(getAssetsDirectoryLocation(), 'media-icons-black'), `${kind}.png`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
- fs.copyFile(iconPath, destinationPath, () => {
- });
+ fs.copyFile(iconPath, destinationPath, () => {});
}
};
diff --git a/plugins/picture-in-picture/back.ts b/plugins/picture-in-picture/back.ts
index 2041e404b2..11e59faf73 100644
--- a/plugins/picture-in-picture/back.ts
+++ b/plugins/picture-in-picture/back.ts
@@ -1,9 +1,9 @@
-import path from 'node:path';
-
import { app, BrowserWindow, ipcMain } from 'electron';
-import { setOptions as setPluginOptions } from '../../config/plugins';
+import style from './style.css';
+
import { injectCSS } from '../utils';
+import { setOptions as setPluginOptions } from '../../config/plugins';
import type { ConfigType } from '../../config/dynamic';
@@ -102,7 +102,7 @@ export default (_win: BrowserWindow, _options: PiPOptions) => {
options ??= _options;
win ??= _win;
setLocalOptions({ isInPiP });
- injectCSS(win.webContents, path.join(__dirname, 'style.css'));
+ injectCSS(win.webContents, style);
ipcMain.on('picture-in-picture', () => {
togglePiP();
});
diff --git a/plugins/picture-in-picture/front.ts b/plugins/picture-in-picture/front.ts
index 7bc9d2c5dc..96236eba51 100644
--- a/plugins/picture-in-picture/front.ts
+++ b/plugins/picture-in-picture/front.ts
@@ -2,9 +2,11 @@ import { ipcRenderer } from 'electron';
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
import keyEventAreEqual from 'keyboardevents-areequal';
+import pipHTML from './templates/picture-in-picture.html';
+
import { getSongMenu } from '../../providers/dom-elements';
-import { ElementFromFile, templatePath } from '../utils';
+import { ElementFromHtml } from '../utils';
import type { ConfigType } from '../../config/dynamic';
@@ -16,9 +18,7 @@ function $(selector: string) {
let useNativePiP = false;
let menu: Element | null = null;
-const pipButton = ElementFromFile(
- templatePath(__dirname, 'picture-in-picture.html'),
-);
+const pipButton = ElementFromHtml(pipHTML);
// Will also clone
function replaceButton(query: string, button: Element) {
diff --git a/plugins/playback-speed/front.ts b/plugins/playback-speed/front.ts
index 17c873c2c1..5c491a0337 100644
--- a/plugins/playback-speed/front.ts
+++ b/plugins/playback-speed/front.ts
@@ -1,5 +1,7 @@
+import sliderHTML from './templates/slider.html';
+
import { getSongMenu } from '../../providers/dom-elements';
-import { ElementFromFile, templatePath } from '../utils';
+import { ElementFromHtml } from '../utils';
import { singleton } from '../../providers/decorators';
@@ -7,7 +9,7 @@ function $(selector: string) {
return document.querySelector(selector);
}
-const slider = ElementFromFile(templatePath(__dirname, 'slider.html'));
+const slider = ElementFromHtml(sliderHTML);
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
diff --git a/plugins/precise-volume/back.ts b/plugins/precise-volume/back.ts
index 2e8ea70ad8..07077e8212 100644
--- a/plugins/precise-volume/back.ts
+++ b/plugins/precise-volume/back.ts
@@ -1,7 +1,7 @@
-import path from 'node:path';
-
import { globalShortcut, BrowserWindow } from 'electron';
+import volumeHudStyle from './volume-hud.css';
+
import { injectCSS } from '../utils';
import type { ConfigType } from '../../config/dynamic';
@@ -16,7 +16,7 @@ export const enabled = () => isEnabled;
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>) => {
isEnabled = true;
- injectCSS(win.webContents, path.join(__dirname, 'volume-hud.css'));
+ injectCSS(win.webContents, volumeHudStyle);
if (options.globalShortcuts?.volumeUp) {
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
diff --git a/plugins/precise-volume/front.ts b/plugins/precise-volume/front.ts
index 0acc7583aa..e5bd14935d 100644
--- a/plugins/precise-volume/front.ts
+++ b/plugins/precise-volume/front.ts
@@ -55,7 +55,7 @@ function firstRun() {
setTooltip(options.savedVolume);
if (api.getVolume() !== options.savedVolume) {
- api.setVolume(options.savedVolume);
+ setVolume(options.savedVolume);
}
}
diff --git a/plugins/precise-volume/menu.ts b/plugins/precise-volume/menu.ts
index de6dfceb29..708b27cbfd 100644
--- a/plugins/precise-volume/menu.ts
+++ b/plugins/precise-volume/menu.ts
@@ -35,7 +35,7 @@ export default (win: BrowserWindow, options: ConfigType<'precise-volume'>): Menu
{
label: 'Global Hotkeys',
type: 'checkbox',
- checked: Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown),
+ checked: Boolean(options.globalShortcuts?.volumeUp ?? options.globalShortcuts?.volumeDown),
click: (item) => promptGlobalShortcuts(win, options, item),
},
{
diff --git a/plugins/precise-volume/volume-hud.css b/plugins/precise-volume/volume-hud.css
index 71cc2c4e51..6517eb9487 100644
--- a/plugins/precise-volume/volume-hud.css
+++ b/plugins/precise-volume/volume-hud.css
@@ -4,6 +4,8 @@
transition: opacity 0.6s;
pointer-events: none;
padding: 10px;
+
+ text-shadow: rgba(0, 0, 0, 0.5) 0px 0px 12px;
}
ytmusic-player[player-ui-state_="MINIPLAYER"] #volumeHud {
diff --git a/plugins/quality-changer/front.ts b/plugins/quality-changer/front.ts
index dd47a87b45..38514113c8 100644
--- a/plugins/quality-changer/front.ts
+++ b/plugins/quality-changer/front.ts
@@ -1,15 +1,15 @@
import { ipcRenderer } from 'electron';
-import { ElementFromFile, templatePath } from '../utils';
+import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html';
+
+import { ElementFromHtml } from '../utils';
import { YoutubePlayer } from '../../types/youtube-player';
function $(selector: string): HTMLElement | null {
return document.querySelector(selector);
}
-const qualitySettingsButton = ElementFromFile(
- templatePath(__dirname, 'qualitySettingsTemplate.html'),
-);
+const qualitySettingsButton = ElementFromHtml(qualitySettingsTemplate);
function setup(event: CustomEvent) {
const api = event.detail;
diff --git a/plugins/sponsorblock/back.ts b/plugins/sponsorblock/back.ts
index cd3e49d0d6..ae9a444118 100644
--- a/plugins/sponsorblock/back.ts
+++ b/plugins/sponsorblock/back.ts
@@ -10,8 +10,6 @@ import defaultConfig from '../../config/defaults';
import type { GetPlayerResponse } from '../../types/get-player-response';
import type { ConfigType } from '../../config/dynamic';
-let videoID: string;
-
export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => {
const { apiURL, categories } = {
...defaultConfig.plugins.sponsorblock,
@@ -19,14 +17,13 @@ export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => {
};
ipcMain.on('video-src-changed', async (_, data: GetPlayerResponse) => {
- videoID = data?.videoDetails?.videoId;
- const segments = await fetchSegments(apiURL, categories);
+ const segments = await fetchSegments(apiURL, categories, data?.videoDetails?.videoId);
win.webContents.send('sponsorblock-skip', segments);
});
};
-const fetchSegments = async (apiURL: string, categories: string[]) => {
- const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
+const fetchSegments = async (apiURL: string, categories: string[], videoId: string) => {
+ const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoId}&categories=${JSON.stringify(
categories,
)}`;
try {
diff --git a/plugins/taskbar-mediacontrol/back.ts b/plugins/taskbar-mediacontrol/back.ts
index ea411b23ea..9579d50992 100644
--- a/plugins/taskbar-mediacontrol/back.ts
+++ b/plugins/taskbar-mediacontrol/back.ts
@@ -4,18 +4,48 @@ import { BrowserWindow, nativeImage } from 'electron';
import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info';
-
-
-let controls: {
- playPause: () => void;
- next: () => void;
- previous: () => void;
-};
-let currentSongInfo: SongInfo;
+import { getMediaIconLocation } from '../utils';
export default (win: BrowserWindow) => {
+ let currentSongInfo: SongInfo;
+
const { playPause, next, previous } = getSongControls(win);
- controls = { playPause, next, previous };
+
+ const setThumbar = (win: BrowserWindow, songInfo: SongInfo) => {
+ // Wait for song to start before setting thumbar
+ if (!songInfo?.title) {
+ return;
+ }
+
+ // Win32 require full rewrite of components
+ win.setThumbarButtons([
+ {
+ tooltip: 'Previous',
+ icon: nativeImage.createFromPath(get('previous')),
+ click() {
+ previous();
+ },
+ }, {
+ tooltip: 'Play/Pause',
+ // Update icon based on play state
+ icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
+ click() {
+ playPause();
+ },
+ }, {
+ tooltip: 'Next',
+ icon: nativeImage.createFromPath(get('next')),
+ click() {
+ next();
+ },
+ },
+ ]);
+ };
+
+ // Util
+ const get = (kind: string) => {
+ return path.join(getMediaIconLocation(), `${kind}.png`);
+ };
registerCallback((songInfo) => {
// Update currentsonginfo for win.on('show')
@@ -29,39 +59,3 @@ export default (win: BrowserWindow) => {
setThumbar(win, currentSongInfo);
});
};
-
-function setThumbar(win: BrowserWindow, songInfo: SongInfo) {
- // Wait for song to start before setting thumbar
- if (!songInfo?.title) {
- return;
- }
-
- // Win32 require full rewrite of components
- win.setThumbarButtons([
- {
- tooltip: 'Previous',
- icon: nativeImage.createFromPath(get('previous')),
- click() {
- controls.previous();
- },
- }, {
- tooltip: 'Play/Pause',
- // Update icon based on play state
- icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
- click() {
- controls.playPause();
- },
- }, {
- tooltip: 'Next',
- icon: nativeImage.createFromPath(get('next')),
- click() {
- controls.next();
- },
- },
- ]);
-}
-
-// Util
-function get(kind: string) {
- return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
-}
diff --git a/plugins/touchbar/back.ts b/plugins/touchbar/back.ts
index bbfea57880..e3a27f4bb5 100644
--- a/plugins/touchbar/back.ts
+++ b/plugins/touchbar/back.ts
@@ -3,65 +3,66 @@ import { TouchBar, NativeImage, BrowserWindow } from 'electron';
import registerCallback from '../../providers/song-info';
import getSongControls from '../../providers/song-controls';
-const {
- TouchBarButton,
- TouchBarLabel,
- TouchBarSpacer,
- TouchBarSegmentedControl,
- TouchBarScrubber,
-} = TouchBar;
+export default (win: BrowserWindow) => {
+ const {
+ TouchBarButton,
+ TouchBarLabel,
+ TouchBarSpacer,
+ TouchBarSegmentedControl,
+ TouchBarScrubber,
+ } = TouchBar;
+
+ // Songtitle label
+ const songTitle = new TouchBarLabel({
+ label: '',
+ });
+ // This will store the song controls once available
+ let controls: (() => void)[] = [];
-// Songtitle label
-const songTitle = new TouchBarLabel({
- label: '',
-});
-// This will store the song controls once available
-let controls: (() => void)[] = [];
+ // This will store the song image once available
+ const songImage: {
+ icon?: NativeImage;
+ } = {};
-// This will store the song image once available
-const songImage: {
- icon?: NativeImage;
-} = {};
+ // Pause/play button
+ const pausePlayButton = new TouchBarButton({});
-// Pause/play button
-const pausePlayButton = new TouchBarButton({});
+ // The song control buttons (control functions are in the same order)
+ const buttons = new TouchBarSegmentedControl({
+ mode: 'buttons',
+ segments: [
+ new TouchBarButton({
+ label: 'โฎ',
+ }),
+ pausePlayButton,
+ new TouchBarButton({
+ label: 'โญ',
+ }),
+ new TouchBarButton({
+ label: '๐',
+ }),
+ new TouchBarButton({
+ label: '๐',
+ }),
+ ],
+ change: (i) => controls[i](),
+ });
-// The song control buttons (control functions are in the same order)
-const buttons = new TouchBarSegmentedControl({
- mode: 'buttons',
- segments: [
- new TouchBarButton({
- label: 'โฎ',
- }),
- pausePlayButton,
- new TouchBarButton({
- label: 'โญ',
- }),
- new TouchBarButton({
- label: '๐',
- }),
- new TouchBarButton({
- label: '๐',
- }),
- ],
- change: (i) => controls[i](),
-});
+ // This is the touchbar object, this combines everything with proper layout
+ const touchBar = new TouchBar({
+ items: [
+ new TouchBarScrubber({
+ items: [songImage, songTitle],
+ continuous: false,
+ }),
+ new TouchBarSpacer({
+ size: 'flexible',
+ }),
+ buttons,
+ ],
+ });
-// This is the touchbar object, this combines everything with proper layout
-const touchBar = new TouchBar({
- items: [
- new TouchBarScrubber({
- items: [songImage, songTitle],
- continuous: false,
- }),
- new TouchBarSpacer({
- size: 'flexible',
- }),
- buttons,
- ],
-});
-export default (win: BrowserWindow) => {
const { playPause, next, previous, dislike, like } = getSongControls(win);
// If the page is ready, register the callback
diff --git a/plugins/tuna-obs/back.ts b/plugins/tuna-obs/back.ts
index c01b655741..f08ae22d7a 100644
--- a/plugins/tuna-obs/back.ts
+++ b/plugins/tuna-obs/back.ts
@@ -1,4 +1,5 @@
import { ipcMain, net, BrowserWindow } from 'electron';
+import is from 'electron-is';
import registerCallback from '../../providers/song-info';
@@ -41,7 +42,11 @@ const post = (data: Data) => {
method: 'POST',
headers,
body: JSON.stringify({ data }),
- }).catch((error: { code: number, errno: number }) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`));
+ }).catch((error: { code: number, errno: number }) => {
+ if (is.dev()) {
+ console.debug(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`);
+ }
+ });
};
export default (win: BrowserWindow) => {
diff --git a/plugins/utils.ts b/plugins/utils.ts
index 73df857a29..44beb152c2 100644
--- a/plugins/utils.ts
+++ b/plugins/utils.ts
@@ -1,9 +1,19 @@
import fs from 'node:fs';
import path from 'node:path';
-import { ipcMain, ipcRenderer } from 'electron';
+import { app, ipcMain, ipcRenderer } from 'electron';
+
+import is from 'electron-is';
import { ValueOf } from '../utils/type-utils';
+import defaultConfig from '../config/defaults';
+
+export const getAssetsDirectoryLocation = () => path.resolve(__dirname, 'assets');
+
+export const getMediaIconLocation = () =>
+ app.isPackaged
+ ? path.resolve(app.getPath('userData'), 'icons')
+ : path.resolve(getAssetsDirectoryLocation(), 'media-icons-black');
// Creates a DOM element from an HTML string
export const ElementFromHtml = (html: string): HTMLElement => {
@@ -46,29 +56,47 @@ export const fileExists = (
});
};
-const cssToInject = new Map();
-export const injectCSS = (webContents: Electron.WebContents, filepath: unknown, cb: (() => void) | undefined = undefined) => {
- if (cssToInject.size === 0) {
+const cssToInject = new Map void) | undefined>();
+const cssToInjectFile = new Map void) | undefined>();
+export const injectCSS = (webContents: Electron.WebContents, css: string, cb: (() => void) | undefined = undefined) => {
+ if (cssToInject.size === 0 && cssToInjectFile.size === 0) {
setupCssInjection(webContents);
}
- cssToInject.set(filepath, cb);
+ cssToInject.set(css, cb);
+};
+
+export const injectCSSAsFile = (webContents: Electron.WebContents, filepath: string, cb: (() => void) | undefined = undefined) => {
+ if (cssToInject.size === 0 && cssToInjectFile.size === 0) {
+ setupCssInjection(webContents);
+ }
+
+ cssToInjectFile.set(filepath, cb);
};
const setupCssInjection = (webContents: Electron.WebContents) => {
webContents.on('did-finish-load', () => {
- cssToInject.forEach(async (callback: () => void | undefined, filepath: fs.PathOrFileDescriptor) => {
+ cssToInject.forEach(async (callback, css) => {
+ await webContents.insertCSS(css);
+ callback?.();
+ });
+
+ cssToInjectFile.forEach(async (callback, filepath) => {
await webContents.insertCSS(fs.readFileSync(filepath, 'utf8'));
callback?.();
});
});
};
-export const getAllPlugins = () => {
- const isDirectory = (source: fs.PathLike) => fs.lstatSync(source).isDirectory();
- return fs
- .readdirSync(__dirname)
- .map((name) => path.join(__dirname, name))
- .filter(isDirectory)
- .map((name) => path.basename(name));
+export const getAvailablePluginNames = () => {
+ return Object.keys(defaultConfig.plugins).filter((name) => {
+ if (is.windows() && name === 'touchbar') {
+ return false;
+ } else if (is.macOS() && name === 'taskbar-mediacontrol') {
+ return false;
+ } else if (is.linux() && (name === 'taskbar-mediacontrol' || name === 'touchbar')) {
+ return false;
+ }
+ return true;
+ });
};
diff --git a/plugins/video-toggle/back.ts b/plugins/video-toggle/back.ts
index 1fe17d6c75..f4286eb191 100644
--- a/plugins/video-toggle/back.ts
+++ b/plugins/video-toggle/back.ts
@@ -1,15 +1,16 @@
-import path from 'node:path';
-
import { BrowserWindow } from 'electron';
+import forceHideStyle from './force-hide.css';
+import buttonSwitcherStyle from './button-switcher.css';
+
import { injectCSS } from '../utils';
import type { ConfigType } from '../../config/dynamic';
export default (win: BrowserWindow, options: ConfigType<'video-toggle'>) => {
if (options.forceHide) {
- injectCSS(win.webContents, path.join(__dirname, 'force-hide.css'));
+ injectCSS(win.webContents, forceHideStyle);
} else if (!options.mode || options.mode === 'custom') {
- injectCSS(win.webContents, path.join(__dirname, 'button-switcher.css'));
+ injectCSS(win.webContents, buttonSwitcherStyle);
}
};
diff --git a/plugins/video-toggle/button-switcher.css b/plugins/video-toggle/button-switcher.css
index 92c052e285..76d22f858b 100644
--- a/plugins/video-toggle/button-switcher.css
+++ b/plugins/video-toggle/button-switcher.css
@@ -15,12 +15,12 @@
background: rgba(33, 33, 33, 0.4);
border-radius: 30px;
overflow: hidden;
- width: 240px;
+ width: 20rem;
text-align: center;
font-size: 18px;
letter-spacing: 1px;
color: #fff;
- padding-right: 120px;
+ padding-right: 10rem;
position: absolute;
}
@@ -30,7 +30,7 @@
top: 0;
bottom: 0;
right: 0;
- width: 120px;
+ width: 10rem;
display: flex;
align-items: center;
justify-content: center;
@@ -55,7 +55,7 @@
}
.video-switch-button-checkbox:checked + .video-switch-button-label:before {
- transform: translateX(120px);
+ transform: translateX(10rem);
transition: transform 300ms linear;
}
diff --git a/plugins/video-toggle/front.ts b/plugins/video-toggle/front.ts
index 49a3ecd1e3..39e7f6e10d 100644
--- a/plugins/video-toggle/front.ts
+++ b/plugins/video-toggle/front.ts
@@ -1,4 +1,6 @@
-import { ElementFromFile, templatePath } from '../utils';
+import buttonTemplate from './templates/button_template.html';
+
+import { ElementFromHtml } from '../utils';
import { setOptions, isEnabled } from '../../config/plugins';
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/front';
@@ -19,9 +21,7 @@ let player: HTMLElement & { videoMode_: boolean } | null;
let video: HTMLVideoElement | null;
let api: YoutubePlayer;
-const switchButtonDiv = ElementFromFile(
- templatePath(__dirname, 'button_template.html'),
-);
+const switchButtonDiv = ElementFromHtml(buttonTemplate);
export default (_options: ConfigType<'video-toggle'>) => {
if (_options.forceHide) {
@@ -56,17 +56,11 @@ function setup(e: CustomEvent) {
$('#player')?.prepend(switchButtonDiv);
- if (options.hideVideo) {
- const checkbox = $('.video-switch-button-checkbox');
- if (checkbox) {
- checkbox.checked = false;
- }
- changeDisplay(false);
- forcePlaybackMode();
- // Fix black video
- if (video) {
- video.style.height = 'auto';
- }
+ setVideoState(!options.hideVideo);
+ forcePlaybackMode();
+ // Fix black video
+ if (video) {
+ video.style.height = 'auto';
}
//Prevents bubbling to the player which causes it to stop or resume
@@ -77,9 +71,8 @@ function setup(e: CustomEvent) {
// Button checked = show video
switchButtonDiv.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
- options.hideVideo = target.checked;
- changeDisplay(target.checked);
- setOptions('video-toggle', options);
+
+ setVideoState(target.checked);
});
video?.addEventListener('srcChanged', videoStarted);
@@ -104,7 +97,13 @@ function setup(e: CustomEvent) {
}
}
-function changeDisplay(showVideo: boolean) {
+function setVideoState(showVideo: boolean) {
+ options.hideVideo = !showVideo;
+ setOptions('video-toggle', options);
+
+ const checkbox = $('.video-switch-button-checkbox'); // custom mode
+ if (checkbox) checkbox.checked = !options.hideVideo;
+
if (player) {
player.style.margin = showVideo ? '' : 'auto 0px';
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
@@ -123,7 +122,7 @@ function changeDisplay(showVideo: boolean) {
function videoStarted() {
if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_ATV') {
// Video doesn't exist -> switch to song mode
- changeDisplay(false);
+ setVideoState(false);
// Hide toggle button
switchButtonDiv.style.display = 'none';
} else {
@@ -137,7 +136,7 @@ function videoStarted() {
switchButtonDiv.style.display = 'initial';
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
if (!options.hideVideo && $('#song-video.ytmusic-player')?.style.display === 'none') {
- changeDisplay(true);
+ setVideoState(true);
} else {
moveVolumeHud(!options.hideVideo);
}
diff --git a/plugins/visualizer/back.ts b/plugins/visualizer/back.ts
index a66e9ded76..99036b2099 100644
--- a/plugins/visualizer/back.ts
+++ b/plugins/visualizer/back.ts
@@ -1,9 +1,9 @@
-import path from 'node:path';
-
import { BrowserWindow } from 'electron';
+import emptyPlayerStyle from './empty-player.css';
+
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
- injectCSS(win.webContents, path.join(__dirname, 'empty-player.css'));
+ injectCSS(win.webContents, emptyPlayerStyle);
};
diff --git a/plugins/visualizer/front.ts b/plugins/visualizer/front.ts
index eeec5bd42c..1fdda444ac 100644
--- a/plugins/visualizer/front.ts
+++ b/plugins/visualizer/front.ts
@@ -1,9 +1,6 @@
+import { ButterchurnVisualizer as butterchurn, WaveVisualizer as wave, VudioVisualizer as vudio } from './visualizers';
import { Visualizer } from './visualizers/visualizer';
-import vudio from './visualizers/vudio';
-import wave from './visualizers/wave';
-import butterchurn from './visualizers/butterchurn';
-
import defaultConfig from '../../config/defaults';
import type { ConfigType } from '../../config/dynamic';
@@ -40,7 +37,7 @@ export default (options: ConfigType<'visualizer'>) => {
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = 'visualizer';
- visualizerContainer.prepend(canvas);
+ visualizerContainer?.prepend(canvas);
}
const resizeCanvas = () => {
diff --git a/plugins/visualizer/menu.ts b/plugins/visualizer/menu.ts
index 4a0f54b6a2..27b2407170 100644
--- a/plugins/visualizer/menu.ts
+++ b/plugins/visualizer/menu.ts
@@ -1,17 +1,11 @@
-import { readdirSync } from 'node:fs';
-import path from 'node:path';
-
import { BrowserWindow } from 'electron';
-import { setMenuOptions } from '../../config/plugins';
-
import { MenuTemplate } from '../../menu';
+import { setMenuOptions } from '../../config/plugins';
import type { ConfigType } from '../../config/dynamic';
-const visualizerTypes = readdirSync(path.join(__dirname, 'visualizers'))
- .map((filename) => path.parse(filename).name)
- .filter((filename) => filename !== 'visualizer');
+const visualizerTypes = ['butterchurn', 'vudio', 'wave']; // For bundling
export default (win: BrowserWindow, options: ConfigType<'visualizer'>): MenuTemplate => [
{
diff --git a/plugins/visualizer/visualizers/butterchurn.ts b/plugins/visualizer/visualizers/butterchurn.ts
index de9b80587b..d6e3662374 100644
--- a/plugins/visualizer/visualizers/butterchurn.ts
+++ b/plugins/visualizer/visualizers/butterchurn.ts
@@ -8,6 +8,8 @@ import { ConfigType } from '../../../config/dynamic';
const presets = ButterchurnPresets.getPresets();
class ButterchurnVisualizer extends Visualizer {
+ name = 'butterchurn';
+
visualizer: ReturnType;
private readonly renderingFrequencyInMs: number;
diff --git a/plugins/visualizer/visualizers/index.ts b/plugins/visualizer/visualizers/index.ts
new file mode 100644
index 0000000000..2b3deb1b1c
--- /dev/null
+++ b/plugins/visualizer/visualizers/index.ts
@@ -0,0 +1,5 @@
+import ButterchurnVisualizer from './butterchurn';
+import VudioVisualizer from './vudio';
+import WaveVisualizer from './wave';
+
+export { ButterchurnVisualizer, VudioVisualizer, WaveVisualizer };
diff --git a/plugins/visualizer/visualizers/visualizer.ts b/plugins/visualizer/visualizers/visualizer.ts
index 38555ca49f..3e40c18b8c 100644
--- a/plugins/visualizer/visualizers/visualizer.ts
+++ b/plugins/visualizer/visualizers/visualizer.ts
@@ -1,6 +1,10 @@
import type { ConfigType } from '../../../config/dynamic';
export abstract class Visualizer {
+ /**
+ * The name must be the same as the file name.
+ */
+ abstract name: string;
abstract visualizer: T;
protected constructor(
diff --git a/plugins/visualizer/visualizers/vudio.ts b/plugins/visualizer/visualizers/vudio.ts
index 32f5eab342..3b296eda4d 100644
--- a/plugins/visualizer/visualizers/vudio.ts
+++ b/plugins/visualizer/visualizers/vudio.ts
@@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
import type { ConfigType } from '../../../config/dynamic';
class VudioVisualizer extends Visualizer {
+ name = 'vudio';
+
visualizer: Vudio;
constructor(
diff --git a/plugins/visualizer/visualizers/wave.ts b/plugins/visualizer/visualizers/wave.ts
index 21a4005e37..fa6d2a70fb 100644
--- a/plugins/visualizer/visualizers/wave.ts
+++ b/plugins/visualizer/visualizers/wave.ts
@@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
import type { ConfigType } from '../../../config/dynamic';
class WaveVisualizer extends Visualizer {
+ name = 'wave';
+
visualizer: Wave;
constructor(
diff --git a/preload.ts b/preload.ts
index 9cbc11d71b..1886958bc3 100644
--- a/preload.ts
+++ b/preload.ts
@@ -2,75 +2,106 @@ import { ipcRenderer } from 'electron';
import is from 'electron-is';
import config from './config';
-import { fileExists } from './plugins/utils';
import setupSongInfo from './providers/song-info-front';
import { setupSongControls } from './providers/song-controls-front';
import { startingPages } from './providers/extracted-data';
-
-const plugins = config.plugins.getEnabled();
+import albumColorThemeRenderer from './plugins/album-color-theme/front';
+import ambientModeRenderer from './plugins/ambient-mode/front';
+import audioCompressorRenderer from './plugins/audio-compressor/front';
+import bypassAgeRestrictionsRenderer from './plugins/bypass-age-restrictions/front';
+import captionsSelectorRenderer from './plugins/captions-selector/front';
+import compactSidebarRenderer from './plugins/compact-sidebar/front';
+import crossfadeRenderer from './plugins/crossfade/front';
+import disableAutoplayRenderer from './plugins/disable-autoplay/front';
+import downloaderRenderer from './plugins/downloader/front';
+import exponentialVolumeRenderer from './plugins/exponential-volume/front';
+import inAppMenuRenderer from './plugins/in-app-menu/front';
+import lyricsGeniusRenderer from './plugins/lyrics-genius/front';
+import navigationRenderer from './plugins/navigation/front';
+import noGoogleLogin from './plugins/no-google-login/front';
+import pictureInPictureRenderer from './plugins/picture-in-picture/front';
+import playbackSpeedRenderer from './plugins/playback-speed/front';
+import preciseVolumeRenderer from './plugins/precise-volume/front';
+import qualityChangerRenderer from './plugins/quality-changer/front';
+import skipSilencesRenderer from './plugins/skip-silences/front';
+import sponsorblockRenderer from './plugins/sponsorblock/front';
+import videoToggleRenderer from './plugins/video-toggle/front';
+import visualizerRenderer from './plugins/visualizer/front';
+
+import adblockerPreload from './plugins/adblocker/preload';
+import preciseVolumePreload from './plugins/precise-volume/preload';
+
+import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic';
+
+type PluginMapper = {
+ [Key in OneOfDefaultConfigKey]?: (
+ Type extends 'renderer' ? (options: ConfigType) => (Promise | void) :
+ Type extends 'preload' ? () => (Promise | void) :
+ never
+ )
+};
+
+const rendererPlugins: PluginMapper<'renderer'> = {
+ 'album-color-theme': albumColorThemeRenderer,
+ 'ambient-mode': ambientModeRenderer,
+ 'audio-compressor': audioCompressorRenderer,
+ 'bypass-age-restrictions': bypassAgeRestrictionsRenderer,
+ 'captions-selector': captionsSelectorRenderer,
+ 'compact-sidebar': compactSidebarRenderer,
+ 'crossfade': crossfadeRenderer,
+ 'disable-autoplay': disableAutoplayRenderer,
+ 'downloader': downloaderRenderer,
+ 'exponential-volume': exponentialVolumeRenderer,
+ 'in-app-menu': inAppMenuRenderer,
+ 'lyrics-genius': lyricsGeniusRenderer,
+ 'navigation': navigationRenderer,
+ 'no-google-login': noGoogleLogin,
+ 'picture-in-picture': pictureInPictureRenderer,
+ 'playback-speed': playbackSpeedRenderer,
+ 'precise-volume': preciseVolumeRenderer,
+ 'quality-changer': qualityChangerRenderer,
+ 'skip-silences': skipSilencesRenderer,
+ 'sponsorblock': sponsorblockRenderer,
+ 'video-toggle': videoToggleRenderer,
+ 'visualizer': visualizerRenderer,
+};
+
+const preloadPlugins: PluginMapper<'preload'> = {
+ 'adblocker': adblockerPreload,
+ 'precise-volume': preciseVolumePreload,
+};
+
+const enabledPluginNameAndOptions = config.plugins.getEnabled();
const $ = document.querySelector.bind(document);
let api: Element | null = null;
-interface Actions {
- CHANNEL: string;
- ACTIONS: Record,
- actions: Record void>,
-}
-
-plugins.forEach(async ([plugin, options]) => {
- const preloadPath = await ipcRenderer.invoke(
- 'getPath',
- __dirname,
- 'plugins',
- plugin,
- 'preload.js',
- ) as string;
- fileExists(preloadPath, () => {
- // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
- const run = require(preloadPath).default as (config: typeof options) => Promise;
- run(options);
- });
-
- const actionPath = await ipcRenderer.invoke(
- 'getPath',
- __dirname,
- 'plugins',
- plugin,
- 'actions.js',
- ) as string;
- fileExists(actionPath, () => {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const actions = (require(actionPath) as Actions).actions ?? {};
-
- // TODO: re-enable once contextIsolation is set to true
- // contextBridge.exposeInMainWorld(plugin + "Actions", actions);
- for (const actionName of Object.keys(actions)) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
- (global as any)[actionName] = actions[actionName];
+enabledPluginNameAndOptions.forEach(async ([plugin, options]) => {
+ if (Object.hasOwn(preloadPlugins, plugin)) {
+ const handler = preloadPlugins[plugin];
+ try {
+ await handler?.();
+ } catch (error) {
+ console.error(`Error in plugin "${plugin}": ${String(error)}`);
}
- });
+ }
});
document.addEventListener('DOMContentLoaded', () => {
- plugins.forEach(async ([plugin, options]) => {
- const pluginPath = await ipcRenderer.invoke(
- 'getPath',
- __dirname,
- 'plugins',
- plugin,
- 'front.js',
- ) as string;
- fileExists(pluginPath, () => {
- // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
- const run = require(pluginPath).default as (config: typeof options) => Promise;
- run(options);
- });
+ enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => {
+ if (Object.hasOwn(rendererPlugins, pluginName)) {
+ const handler = rendererPlugins[pluginName];
+ try {
+ await handler?.(options as never);
+ } catch (error) {
+ console.error(`Error in plugin "${pluginName}": ${String(error)}`);
+ }
+ }
});
- // Wait for complete load of youtube api
+ // Wait for complete load of YouTube api
listenForApiLoad();
// Inject song-info provider
diff --git a/providers/prompt-custom-titlebar.ts b/providers/prompt-custom-titlebar.ts
deleted file mode 100644
index 5068745c60..0000000000
--- a/providers/prompt-custom-titlebar.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// eslint-disable-next-line import/no-unresolved
-import { Titlebar, Color } from 'custom-electron-titlebar';
-
-const customTitlebarFunction = () => {
- new Titlebar({
- backgroundColor: Color.fromHex('#050505'),
- minimizable: false,
- maximizable: false,
- menu: undefined,
- });
- const mainStyle = document.querySelector('#container')?.style;
- if (mainStyle) {
- mainStyle.width = '100%';
- mainStyle.position = 'fixed';
- mainStyle.border = 'unset';
- }
-};
-
-
-module.exports = customTitlebarFunction;
-export default customTitlebarFunction;
diff --git a/providers/prompt-options.ts b/providers/prompt-options.ts
index 87e27f13d5..895531f3c0 100644
--- a/providers/prompt-options.ts
+++ b/providers/prompt-options.ts
@@ -1,18 +1,10 @@
import path from 'node:path';
-import is from 'electron-is';
+import { getAssetsDirectoryLocation } from '../plugins/utils';
-import { isEnabled } from '../config/plugins';
+const iconPath = path.join(getAssetsDirectoryLocation(), 'youtube-music-tray.png');
-const iconPath = path.join(__dirname, '..', 'assets', 'youtube-music-tray.png');
-const customTitlebarPath = path.join(__dirname, 'prompt-custom-titlebar.js');
-
-const promptOptions = !is.macOS() && isEnabled('in-app-menu') ? {
- customStylesheet: 'dark',
- // The following are used for custom titlebar
- frame: false,
- customScript: customTitlebarPath,
-} : {
+const promptOptions = {
customStylesheet: 'dark',
icon: iconPath,
};
diff --git a/readme.md b/readme.md
index cc17bd3e16..1df1e06875 100644
--- a/readme.md
+++ b/readme.md
@@ -40,7 +40,7 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
If you get an error "is damaged and canโt be opened." when launching the app, run the following in the Terminal:
-```
+```bash
xattr -cr /Applications/YouTube\ Music.app
```
@@ -49,7 +49,7 @@ xattr -cr /Applications/YouTube\ Music.app
You can use the [Scoop package manager](https://scoop.sh) to install the `youtube-music` package from
the [`extras` bucket](https://github.com/ScoopInstaller/Extras).
-```
+```bash
scoop bucket add extras
scoop install extras/youtube-music
```
@@ -61,7 +61,7 @@ official CLI package manager to install the `th-ch.YouTubeMusic` package.
true for the manual installation when trying to run the executable(.exe) after a manual download here on github (same
file).*
-```
+```bash
winget install th-ch.YouTubeMusic
```
@@ -166,7 +166,7 @@ Some predefined themes are available in https://github.com/kerichdev/themes-for-
## Dev
-```sh
+```bash
git clone https://github.com/th-ch/youtube-music
cd youtube-music
npm
@@ -184,42 +184,68 @@ Using plugins, you can:
Create a folder in `plugins/YOUR-PLUGIN-NAME`:
-- if you need to manipulate the BrowserWindow, create a file `back.js` with the following template:
+- if you need to manipulate the BrowserWindow, create a file with the following template:
+
+```typescript
+// file: back.ts
+export default (win: Electron.BrowserWindow, config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
+ // something
+};
+```
+
+then, register the plugin in `index.ts`:
-```node
-module.exports = win => {
- // win is the BrowserWindow object
+```typescript
+import yourPlugin from './plugins/YOUR-PLUGIN-NAME/back';
+
+// ...
+
+const mainPlugins = {
+ // ...
+ 'YOUR-PLUGIN-NAME': yourPlugin,
};
```
-- if you need to change the front, create a file `front.js` with the following template:
+- if you need to change the front, create a file with the following template:
-```node
-module.exports = () => {
+```typescript
+// file: front.ts
+export default (config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
// This function will be called as a preload script
// So you can use front features like `document.querySelector`
};
```
+then, register the plugin in `preload.ts`:
+
+```typescript
+import yourPlugin from './plugins/YOUR-PLUGIN-NAME/front';
+
+const rendererPlugins: PluginMapper<'renderer'> = {
+ // ...
+ 'YOUR-PLUGIN-NAME': yourPlugin,
+};
+```
+
### Common use cases
- injecting custom CSS: create a `style.css` file in the same folder then:
-```node
-const path = require("path");
-const {injectCSS} = require("../utils");
+```typescript
+import path from 'node:path';
+import { injectCSS } from '../utils';
-// back.js
-module.exports = win => {
- injectCSS(win.webContents, path.join(__dirname, "style.css"));
+// back.ts
+export default (win: Electron.BrowserWindow) => {
+ injectCSS(win.webContents, path.join(__dirname, 'style.css'));
};
```
- changing the HTML:
-```node
-// front.js
-module.exports = () => {
+```typescript
+// front.ts
+export default () => {
// Remove the login button
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
};
@@ -243,7 +269,7 @@ using [electron-builder](https://github.com/electron-userland/electron-builder).
## Tests
-```sh
+```bash
npm run test
```
diff --git a/rollup.main.config.ts b/rollup.main.config.ts
new file mode 100644
index 0000000000..767c89a190
--- /dev/null
+++ b/rollup.main.config.ts
@@ -0,0 +1,65 @@
+import { defineConfig } from 'rollup';
+import builtinModules from 'builtin-modules';
+import typescript from '@rollup/plugin-typescript';
+import commonjs from '@rollup/plugin-commonjs';
+import nodeResolvePlugin from '@rollup/plugin-node-resolve';
+import json from '@rollup/plugin-json';
+import terser from '@rollup/plugin-terser';
+import { string } from 'rollup-plugin-string';
+import css from 'rollup-plugin-import-css';
+import wasmPlugin from '@rollup/plugin-wasm';
+import copy from 'rollup-plugin-copy';
+
+export default defineConfig({
+ plugins: [
+ typescript({
+ module: 'ESNext',
+ }),
+ nodeResolvePlugin({
+ browser: false,
+ preferBuiltins: true,
+ exportConditions: ['node', 'default', 'module', 'import'] ,
+ }),
+ commonjs({
+ ignoreDynamicRequires: true,
+ }),
+ wasmPlugin({
+ maxFileSize: 0,
+ targetEnv: 'browser',
+ }),
+ json(),
+ string({
+ include: '**/*.html',
+ }),
+ css(),
+ copy({
+ targets: [
+ { src: 'error.html', dest: 'dist/' },
+ { src: 'assets', dest: 'dist/' },
+ ],
+ }),
+ terser({
+ ecma: 2020,
+ }),
+ {
+ closeBundle() {
+ if (!process.env.ROLLUP_WATCH) {
+ setTimeout(() => process.exit(0));
+ }
+ },
+ name: 'force-close'
+ },
+ ],
+ input: './index.ts',
+ output: {
+ format: 'cjs',
+ name: '[name].js',
+ dir: './dist',
+ },
+ external: [
+ 'electron',
+ 'custom-electron-prompt',
+ 'youtubei.js', // https://github.com/LuanRT/YouTube.js/pull/509
+ ...builtinModules,
+ ],
+});
diff --git a/rollup.preload.config.ts b/rollup.preload.config.ts
new file mode 100644
index 0000000000..86114b6770
--- /dev/null
+++ b/rollup.preload.config.ts
@@ -0,0 +1,58 @@
+import { defineConfig } from 'rollup';
+import builtinModules from 'builtin-modules';
+import typescript from '@rollup/plugin-typescript';
+import commonjs from '@rollup/plugin-commonjs';
+import nodeResolvePlugin from '@rollup/plugin-node-resolve';
+import json from '@rollup/plugin-json';
+import terser from '@rollup/plugin-terser';
+import { string } from 'rollup-plugin-string';
+import css from 'rollup-plugin-import-css';
+import wasmPlugin from '@rollup/plugin-wasm';
+import image from '@rollup/plugin-image';
+
+export default defineConfig({
+ plugins: [
+ typescript({
+ module: 'ESNext',
+ }),
+ nodeResolvePlugin({
+ browser: false,
+ preferBuiltins: true,
+ }),
+ commonjs({
+ ignoreDynamicRequires: true,
+ }),
+ json(),
+ string({
+ include: '**/*.html',
+ }),
+ css(),
+ wasmPlugin({
+ maxFileSize: 0,
+ targetEnv: 'browser',
+ }),
+ image({ dom: true }),
+ terser({
+ ecma: 2020,
+ }),
+ {
+ closeBundle() {
+ if (!process.env.ROLLUP_WATCH) {
+ setTimeout(() => process.exit(0));
+ }
+ },
+ name: 'force-close'
+ },
+ ],
+ input: './preload.ts',
+ output: {
+ format: 'cjs',
+ name: '[name].js',
+ dir: './dist',
+ },
+ external: [
+ 'electron',
+ 'custom-electron-prompt',
+ ...builtinModules,
+ ],
+});
diff --git a/tests/index.test.js b/tests/index.test.js
index a08da6dc78..5a84fdff17 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
const path = require('node:path');
const { _electron: electron } = require('playwright');
@@ -28,9 +30,6 @@ test('YouTube Music App - With default settings, app is launched and visible', a
await consentForm.click('button');
}
- const title = await window.title();
- expect(title.replaceAll(/\s/g, ' ')).toEqual('YouTube Music');
-
const url = window.url();
expect(url.startsWith('https://music.youtube.com')).toBe(true);
diff --git a/tray.ts b/tray.ts
index f753705745..eedad40056 100644
--- a/tray.ts
+++ b/tray.ts
@@ -6,6 +6,8 @@ import { restart } from './providers/app-controls';
import config from './config';
import getSongControls from './providers/song-controls';
+import { getAssetsDirectoryLocation } from './plugins/utils';
+
import type { MenuTemplate } from './menu';
// Prevent tray being garbage collected
@@ -39,7 +41,7 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
}
const { playPause, next, previous } = getSongControls(win);
- const iconPath = path.join(__dirname, 'assets', 'youtube-music-tray.png');
+ const iconPath = path.join(getAssetsDirectoryLocation(), 'youtube-music-tray.png');
const trayIcon = nativeImage.createFromPath(iconPath).resize({
width: 16,
diff --git a/tsconfig.json b/tsconfig.json
index a53e985ab6..d9cb14b22b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,7 +15,6 @@
"skipLibCheck": true
},
"exclude": [
- "*.config.ts",
"./dist"
],
"paths": {
diff --git a/youtube-music.css b/youtube-music.css
index 686feb4f3f..cc60f28478 100644
--- a/youtube-music.css
+++ b/youtube-music.css
@@ -54,3 +54,8 @@ ytmusic-nav-bar > div.left-content > a,
ytmusic-nav-bar > div.left-content > a > picture > img {
-webkit-user-drag: none;
}
+
+/* yt-music bugs */
+tp-yt-paper-item.ytmusic-guide-entry-renderer::before {
+ border-radius: 8px !important;
+}
diff --git a/youtube-music.d.ts b/youtube-music.d.ts
new file mode 100644
index 0000000000..6a921443f6
--- /dev/null
+++ b/youtube-music.d.ts
@@ -0,0 +1,37 @@
+declare module '*.html' {
+ const html: string;
+
+ export default html;
+}
+declare module '*.svg' {
+ const element: SVGAElement;
+
+ export default element;
+}
+declare module '*.png' {
+ const element: HTMLImageElement;
+
+ export default element;
+}
+declare module '*.jpg' {
+ const element: HTMLImageElement;
+
+ export default element;
+}
+declare module '*.css' {
+ const css: string;
+
+ export default css;
+}
+
+declare module 'rollup-plugin-string' {
+ import type { Plugin } from 'rollup';
+
+ interface PluginOptions {
+ include?: string[] | string;
+ exclude?: string[] | string;
+ minifier?: unknown;
+ }
+
+ export function string(options?: PluginOptions): Plugin;
+}
\ No newline at end of file