feat: apply rollup 🚀 (#20)

Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2023-10-04 17:51:39 +09:00
committed by GitHub
parent 2c337953eb
commit 40745d3946
42 changed files with 1752 additions and 1339 deletions

View File

@ -1 +1,2 @@
.eslintrc.js
rollup.config.ts

View File

@ -62,17 +62,17 @@ jobs:
- name: Build on Mac
if: startsWith(matrix.os, 'macOS') && github.repository != 'organization/youtube-music-next'
run: |
npm run build:mac
npm run dist:mac
- name: Build on Linux
if: startsWith(matrix.os, 'ubuntu') && github.repository != 'organization/youtube-music-next'
run: |
npm run build:linux
npm run dist:linux
- name: Build on Windows
if: startsWith(matrix.os, 'windows') && github.repository != 'organization/youtube-music-next'
run: |
npm run build:win
npm run dist:win
release:
runs-on: ubuntu-latest

View File

@ -11,7 +11,7 @@ import { BetterWebRequest } from 'electron-better-web-request/lib/electron-bette
import config from './config';
import { setApplicationMenu } from './menu';
import { fileExists, injectCSS } from './plugins/utils';
import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils';
import { isTesting } from './utils/testing';
import { setUpTray } from './tray';
import { setupSongInfo } from './providers/song-info';
@ -42,6 +42,8 @@ import tunaObs from './plugins/tuna-obs/back';
import videoToggle from './plugins/video-toggle/back';
import visualizer from './plugins/visualizer/back';
import youtubeMusicCSS from './youtube-music.css';
// Catch errors and log them
unhandled({
logger: console.error,
@ -139,7 +141,7 @@ if (is.windows()) {
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
function loadPlugins(win: BrowserWindow) {
injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
injectCSS(win.webContents, youtubeMusicCSS);
// Load user CSS
const themes: string[] = config.get('options.themes');
if (Array.isArray(themes)) {
@ -147,7 +149,7 @@ function loadPlugins(win: BrowserWindow) {
fileExists(
cssFile,
() => {
injectCSS(win.webContents, cssFile);
injectCSSAsFile(win.webContents, cssFile);
},
() => {
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);

View File

@ -181,7 +181,7 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
{
label: 'No theme',
type: 'radio',
checked: config.get('options.themes').length === 0, // Todo rename "themes"
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
click() {
config.set('options.themes', []);
},

2588
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,20 +14,38 @@
"build": {
"appId": "com.github.th-ch.youtube-music",
"productName": "YouTube Music",
"files": [
"!*",
"dist",
"license",
"!node_modules",
"node_modules/custom-electron-prompt/**",
"node_modules/youtubei.js/**",
"node_modules/undici/**",
"node_modules/@fastify/busboy/**",
"node_modules/jintr/**",
"node_modules/acorn/**",
"node_modules/tslib/**",
"node_modules/sharp/**",
"!node_modules/sharp/vendor/*/win32-x64",
"node_modules/semver/**",
"node_modules/lru-cache/**",
"node_modules/detect-libc/**",
"node_modules/color/**",
"node_modules/color-convert/**",
"node_modules/color-string/**",
"node_modules/color-name/**",
"node_modules/simple-swizzle/**",
"node_modules/is-arrayish/**",
"node_modules/@cliqz/adblocker-electron-preload/**",
"node_modules/@cliqz/adblocker-content/**",
"node_modules/@cliqz/adblocker-extended-selectors/**",
"node_modules/@ffmpeg.wasm/core-mt/**",
"!node_modules/**/*.map",
"!node_modules/**/*.ts"
],
"mac": {
"identity": null,
"files": [
"!*",
"dist",
"!dist/plugins/taskbar-mediacontrol${/*}",
"license",
"node_modules",
"!node_modules/sharp/vendor",
"!node_modules/**/*.map",
"!node_modules/**/*.ts",
"package.json",
"tests"
],
"target": [
{
"target": "dmg",
@ -41,18 +59,6 @@
},
"win": {
"icon": "assets/generated/icons/win/icon.ico",
"files": [
"!*",
"dist",
"!dist/plugins/touchbar${/*}",
"license",
"node_modules",
"!node_modules/sharp/vendor",
"!node_modules/**/*.map",
"!node_modules/**/*.ts",
"package.json",
"tests"
],
"target": [
{
"target": "nsis",
@ -75,18 +81,6 @@
},
"linux": {
"icon": "assets/generated/icons/png",
"files": [
"!*",
"dist",
"!dist/plugins/{touchbar,taskbar-mediacontrol}${/*}",
"license",
"node_modules",
"!node_modules/sharp/vendor",
"!node_modules/**/*.map",
"!node_modules/**/*.ts",
"package.json",
"tests"
],
"category": "AudioVideo",
"target": [
"AppImage",
@ -110,31 +104,29 @@
}
},
"scripts": {
"test": "playwright test",
"test:debug": "DEBUG=pw:browser* playwright test",
"start": "npm run tsc-and-copy && electron ./dist/index.js",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron ./dist/index.js",
"test": "npm run build && playwright test",
"test:debug": "DEBUG=pw:browser* npm run build && playwright test",
"rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"build": "npm run rollup:preload && npm run rollup:main",
"start": "npm run build && electron ./dist/index.js",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 npm run start",
"generate:package": "node utils/generate-package-json.js",
"postinstall": "npm run plugins",
"postinstall": "npm run plugins && npm run clean",
"clean": "del-cli dist && del-cli pack",
"ytm-resource-copy-files": "copyfiles error.html youtube-music.css assets/**/* dist/",
"copy-files": "copyfiles -u 1 plugins/**/*.html plugins/**/*.css plugins/**/*.bin plugins/**/*.js dist/plugins/",
"tsc-and-copy": "tsc && npm run plugin:adblocker-without-tsc && npm run ytm-resource-copy-files && npm run copy-files",
"build": "npm run clean && npm run tsc-and-copy && electron-builder --win --mac --linux -p never",
"build:linux": "npm run clean && npm run tsc-and-copy && electron-builder --linux -p never",
"build:mac": "npm run clean && npm run tsc-and-copy && electron-builder --mac dmg:x64 -p never",
"build:mac:arm64": "npm run clean && npm run tsc-and-copy && electron-builder --mac dmg:arm64 -p never",
"build:win": "npm run clean && npm run tsc-and-copy && electron-builder --win -p never",
"build:win:x64": "npm run clean && npm run tsc-and-copy && electron-builder --win nsis:x64 -p never",
"dist": "npm run clean && npm run build && electron-builder --win --mac --linux -p never",
"dist:linux": "npm run clean && npm run build && electron-builder --linux -p never",
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis:x64 -p never",
"lint": "eslint .",
"changelog": "auto-changelog",
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
"plugin:adblocker-without-tsc": "del-cli plugins/adblocker/ad-blocker-engine.bin && node dist/plugins/adblocker/blocker.js",
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && tsc && node dist/plugins/adblocker/blocker.js",
"plugins": "npm run plugin:bypass-age-restrictions",
"plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass",
"release:linux": "npm run clean && npm run tsc-and-copy && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "npm run clean && npm run tsc-and-copy && electron-builder --mac -p always",
"release:win": "npm run clean && npm run tsc-and-copy && electron-builder --win -p always",
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "npm run clean && npm run build && electron-builder --mac -p always",
"release:win": "npm run clean && npm run build && electron-builder --win -p always",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"engines": {
@ -168,25 +160,34 @@
"node-id3": "0.2.6",
"simple-youtube-age-restriction-bypass": "git+https://github.com/MiepHD/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.5",
"vudio": "2.1.1",
"x11": "2.3.0",
"youtubei.js": "6.4.1",
"ytpl": "2.3.0"
},
"overrides": {
"node-gyp": "9.4.0",
"xml2js": "0.6.2",
"sharp": "0.32.6",
"dbus-next": "0.10.2",
"node-fetch": "2.7.0",
"@electron/universal": "1.4.2",
"electron": "27.0.0-beta.9"
},
"devDependencies": {
"@playwright/test": "1.38.1",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-image": "3.0.2",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-terser": "0.4.3",
"@rollup/plugin-typescript": "11.1.4",
"@rollup/plugin-wasm": "6.2.1",
"@total-typescript/ts-reset": "0.5.1",
"@types/electron-localshortcut": "3.1.1",
"@types/howler": "2.2.9",
"@types/html-to-text": "9.0.2",
"@typescript-eslint/eslint-plugin": "6.7.4",
"auto-changelog": "2.4.0",
"copyfiles": "2.4.1",
"del-cli": "5.1.0",
"electron": "27.0.0-beta.9",
"electron-builder": "24.6.4",
@ -196,6 +197,10 @@
"eslint-plugin-prettier": "5.0.0",
"node-gyp": "9.4.0",
"playwright": "1.38.1",
"rollup": "3.29.4",
"rollup-plugin-copy": "3.5.0",
"rollup-plugin-import-css": "3.3.4",
"rollup-plugin-string": "3.0.0",
"typescript": "5.2.2"
},
"auto-changelog": {

View File

@ -1,8 +1,9 @@
// Used for caching
import path from 'node:path';
import { promises } from 'node:fs';
import fs, { promises } from 'node:fs';
import { ElectronBlocker } from '@cliqz/adblocker-electron';
import { app } from 'electron';
const SOURCES = [
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
@ -23,10 +24,19 @@ export const loadAdBlockerEngine = (
disableDefaultLists: boolean | string[] = false,
) => {
// Only use cache if no additional blocklists are passed
let cacheDirectory: string;
if (app.isPackaged) {
cacheDirectory = path.join(app.getPath('userData'), 'cache');
} else {
cacheDirectory = path.resolve(__dirname, 'cache');
}
if (!fs.existsSync(cacheDirectory)) {
fs.mkdirSync(cacheDirectory);
}
const cachingOptions
= cache && additionalBlockLists.length === 0
? {
path: path.resolve(__dirname, 'ad-blocker-engine.bin'),
path: path.join(cacheDirectory, 'adblocker-engine.bin'),
read: promises.readFile,
write: promises.writeFile,
}

View File

@ -1,3 +1,4 @@
export default () => {
require('@cliqz/adblocker-electron-preload');
const path = require.resolve('@cliqz/adblocker-electron-preload'); // prevent require hoisting
require(path);
};

View File

@ -1,14 +1,13 @@
import path from 'node:path';
import { getAverageColor } from 'fast-average-color-node';
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
import registerCallback from '../../providers/song-info';
export default (win: BrowserWindow) => {
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
injectCSS(win.webContents, style);
registerCallback((songInfo) => {
const songTitle = songInfo.title;

View File

@ -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);
};

View File

@ -5,7 +5,9 @@ import { ipcRenderer } from 'electron';
import configProvider from './config';
import { ElementFromFile, templatePath } from '../utils';
import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html';
import { ElementFromHtml } from '../utils';
import { YoutubePlayer } from '../../types/youtube-player';
import type { ConfigType } from '../../config/dynamic';
@ -27,9 +29,7 @@ let config: ConfigType<'captions-selector'>;
const $ = <Element extends HTMLElement>(selector: string): Element => document.querySelector(selector)!;
const captionsSettingsButton = ElementFromFile(
templatePath(__dirname, 'captions-settings-template.html'),
);
const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML);
export default async () => {
// RENDERER

View File

@ -24,6 +24,8 @@ import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBad
import config from './config';
import style from './style.css';
import { fetchFromGenius } from '../lyrics-genius/back';
import { isEnabled } from '../../config/plugins';
import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
@ -32,6 +34,7 @@ import { cache } from '../../providers/decorators';
import type { GetPlayerResponse } from '../../types/get-player-response';
type CustomSongInfo = SongInfo & { trackId?: string };
const ffmpeg = createFFmpeg({
@ -68,7 +71,7 @@ const sendError = (error: Error, source?: string) => {
export default async (win_: BrowserWindow) => {
win = win_;
injectCSS(win.webContents, join(__dirname, 'style.css'));
injectCSS(win.webContents, style);
const cookie = (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
it.name + '=' + it.value + ';'
@ -104,6 +107,7 @@ export async function downloadSong(
increasePlaylistProgress,
);
} catch (error: unknown) {
console.log('maybe?????', error);
sendError(error as Error, resolvedName || url);
}
}
@ -316,6 +320,7 @@ async function iterableStreamToMP3(
ffmpeg.FS('unlink', `${safeVideoName}.mp3`);
}
} catch (error: unknown) {
console.log('maybe?', error);
sendError(error as Error, safeVideoName);
} finally {
releaseFFmpegMutex();
@ -368,6 +373,7 @@ async function writeID3(buffer: Buffer, metadata: CustomSongInfo, sendFeedback:
return NodeID3.write(tags, buffer);
} catch (error: unknown) {
console.log('fallback', error);
sendError(error as Error, `${metadata.artist} - ${metadata.title}`);
return null;
}
@ -482,6 +488,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
counter++;
}
} catch (error: unknown) {
console.log('also?', error);
sendError(error as Error);
} finally {
win.setProgressBar(-1); // Close progress bar
@ -507,6 +514,7 @@ async function ffmpegWriteTags(filePath: string, metadata: CustomSongInfo, prese
filePath,
);
} catch (error: unknown) {
console.log('ffmpeg?', error);
sendError(error as Error);
} finally {
releaseFFmpegMutex();

View File

@ -1,15 +1,15 @@
import { ipcRenderer } from 'electron';
import downloadHTML from './templates/download.html';
import defaultConfig from '../../config/defaults';
import { getSongMenu } from '../../providers/dom-elements';
import { ElementFromFile, templatePath } from '../utils';
import { ElementFromHtml } from '../utils';
import { getSongInfo } from '../../providers/song-info-front';
let menu: Element | null = null;
let progress: Element | null = null;
const downloadButton = ElementFromFile(
templatePath(__dirname, 'download.html'),
);
const downloadButton = ElementFromHtml(downloadHTML);
let doneFirstLoad = false;

View File

@ -4,11 +4,13 @@ import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
import titlebarStyle from './titlebar.css';
import { injectCSS } from '../utils';
// Tracks menu visibility
export default (win: BrowserWindow) => {
injectCSS(win.webContents, path.join(__dirname, 'titlebar.css'));
injectCSS(win.webContents, titlebarStyle);
win.once('ready-to-show', () => {
register(win, '`', () => {

View File

@ -1,10 +1,8 @@
import path from 'node:path';
import { ipcRenderer, Menu } from 'electron';
import { createPanel } from './menu/panel';
import { ElementFromFile } from '../utils';
import logo from '../../assets/youtube-music.svg';
import { isEnabled } from '../../config/plugins';
function $<E extends Element = Element>(selector: string) {
@ -18,7 +16,6 @@ export default () => {
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
const logo = ElementFromFile(path.join(__dirname, '..' , '..' , 'assets', 'youtube-music.svg'));
logo.classList.add('title-bar-icon');
if (!isMacOS) titleBar.appendChild(logo);

View File

@ -1,9 +1,8 @@
import { join } from 'node:path';
import { BrowserWindow, ipcMain, net } from 'electron';
import is from 'electron-is';
import { convert } from 'html-to-text';
import style from './style.css';
import { GetGeniusLyric } from './types';
import { cleanupName, SongInfo } from '../../providers/song-info';
@ -22,7 +21,7 @@ export default (win: BrowserWindow, options: LyricGeniusType) => {
revRomanized = true;
}
injectCSS(win.webContents, join(__dirname, 'style.css'));
injectCSS(win.webContents, style);
ipcMain.handle('search-genius-lyrics', async (_, extractedSongInfo: SongInfo) => {
const metadata = extractedSongInfo;

View File

@ -1,11 +1,11 @@
import path from 'node:path';
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
export function handle(win: BrowserWindow) {
injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => {
injectCSS(win.webContents, style, () => {
win.webContents.send('navigation-css-ready');
});
}

View File

@ -1,13 +1,14 @@
import { ipcRenderer } from 'electron';
import { ElementFromFile, templatePath } from '../utils';
import forwardHTML from './templates/forward.html';
import backHTML from './templates/back.html';
import { ElementFromHtml } from '../utils';
export function run() {
ipcRenderer.on('navigation-css-ready', () => {
const forwardButton = ElementFromFile(
templatePath(__dirname, 'forward.html'),
);
const backButton = ElementFromFile(templatePath(__dirname, 'back.html'));
const forwardButton = ElementFromHtml(forwardHTML);
const backButton = ElementFromHtml(backHTML);
const menu = document.querySelector('#right-content');
if (menu) {

View File

@ -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);
};

View File

@ -154,7 +154,7 @@ const getXml = (songInfo: SongInfo, iconSrc: string) => {
const iconLocation = app.isPackaged
? path.resolve(app.getPath('userData'), 'icons')
: path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
: path.resolve(__dirname, 'assets', 'media-icons-black');
const display = (kind: keyof typeof icons) => {
if (config.get('toastStyle') === ToastStyles.legacy) {

View File

@ -88,7 +88,7 @@ export const saveTempIcon = () => {
continue;
}
const iconPath = path.resolve(__dirname, '../../assets/media-icons-black', `${kind}.png`);
const iconPath = path.resolve(__dirname, 'assets', 'media-icons-black', `${kind}.png`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFile(iconPath, destinationPath, () => {
});

View File

@ -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();
});

View File

@ -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 $<E extends Element = Element>(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) {

View File

@ -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 $<E extends Element = Element>(selector: string) {
return document.querySelector<E>(selector);
}
const slider = ElementFromFile(templatePath(__dirname, 'slider.html'));
const slider = ElementFromHtml(sliderHTML);
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;

View File

@ -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));

View File

@ -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<YoutubePlayer>) {
const api = event.detail;

View File

@ -49,18 +49,32 @@ 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<string, (() => void) | undefined>();
const cssToInjectFile = new Map<string, (() => 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?.();
});

View File

@ -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);
}
};

View File

@ -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) {

View File

@ -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);
};

View File

@ -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';

View File

@ -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 => [
{

View File

@ -8,6 +8,8 @@ import { ConfigType } from '../../../config/dynamic';
const presets = ButterchurnPresets.getPresets();
class ButterchurnVisualizer extends Visualizer<Butterchurn> {
name = 'butterchurn';
visualizer: ReturnType<typeof Butterchurn.createVisualizer>;
private readonly renderingFrequencyInMs: number;

View File

@ -0,0 +1,5 @@
import ButterchurnVisualizer from './butterchurn';
import VudioVisualizer from './vudio';
import WaveVisualizer from './wave';
export { ButterchurnVisualizer, VudioVisualizer, WaveVisualizer };

View File

@ -1,6 +1,10 @@
import type { ConfigType } from '../../../config/dynamic';
export abstract class Visualizer<T> {
/**
* The name must be the same as the file name.
*/
abstract name: string;
abstract visualizer: T;
protected constructor(

View File

@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
import type { ConfigType } from '../../../config/dynamic';
class VudioVisualizer extends Visualizer<Vudio> {
name = 'vudio';
visualizer: Vudio;
constructor(

View File

@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
import type { ConfigType } from '../../../config/dynamic';
class WaveVisualizer extends Visualizer<Wave> {
name = 'wave';
visualizer: Wave;
constructor(

57
rollup.main.config.ts Normal file
View File

@ -0,0 +1,57 @@
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: 'assets', dest: 'dist/' },
],
}),
terser({
ecma: 2020,
}),
],
input: './index.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: [
'electron',
'sharp',
'custom-electron-prompt',
'youtubei.js', // https://github.com/LuanRT/YouTube.js/pull/509
...builtinModules,
],
});

51
rollup.preload.config.ts Normal file
View File

@ -0,0 +1,51 @@
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,
}),
],
input: './preload.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: [
'electron',
'sharp',
'custom-electron-prompt',
...builtinModules,
],
});

View File

@ -1,11 +1,11 @@
const path = require('node:path');
const { _electron: electron } = require('playwright');
const { test, expect } = require('@playwright/test');
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { _electron as electron } from 'playwright';
import { expect, test } from '@playwright/test';
process.env.NODE_ENV = 'test';
const appPath = path.resolve(__dirname, '..');
const appPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
test('YouTube Music App - With default settings, app is launched and visible', async () => {
const app = await electron.launch({
@ -16,6 +16,7 @@ test('YouTube Music App - With default settings, app is launched and visible', a
'--disable-gpu',
'--whitelisted-ips=',
'--disable-dev-shm-usage',
'dist/index.js',
],
});

View File

@ -15,7 +15,6 @@
"skipLibCheck": true
},
"exclude": [
"*.config.ts",
"./dist"
],
"paths": {

37
youtube-music.d.ts vendored Normal file
View File

@ -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;
}