mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Merge branch 'master' of https://github.com/th-ch/youtube-music
This commit is contained in:
@ -1 +1,3 @@
|
||||
.eslintrc.js
|
||||
rollup.main.config.ts
|
||||
rollup.preload.config.ts
|
||||
|
||||
73
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
name: Bug Report
|
||||
description: Report a YouTube Music bug
|
||||
title: "[Bug]: "
|
||||
labels: "bug :beetle:"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I use the latest version of YouTube Music (Application).
|
||||
required: true
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: YouTube Music (Application) Version
|
||||
description: |
|
||||
What version of YouTube Music Application are you using?
|
||||
|
||||
Note: Please check if this issue is reproducible with the latest stable release.
|
||||
placeholder: 2.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What operating system are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Ubuntu
|
||||
- Other Linux
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What arch are you using?
|
||||
options:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64 (including Apple Silicon)
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Last Known Working YouTube Music (Application) version
|
||||
description: (If applicable) What is the last version of YouTube Music this worked in?
|
||||
placeholder: 1.20.0
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen. (Add a replication step if applicable)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for YouTube Music
|
||||
title: "[Feature Request]: "
|
||||
labels: "enhancement :sparkles:"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I use the latest version of YouTube Music (Application).
|
||||
required: true
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a feature request that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe the solution you'd like in a clear and concise manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -40,21 +40,21 @@ jobs:
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:mac
|
||||
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:win
|
||||
|
||||
@ -107,7 +107,7 @@ jobs:
|
||||
uses: cardinalby/git-get-release-action@v1
|
||||
id: get_draft_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
draft: true
|
||||
@ -117,7 +117,7 @@ jobs:
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: ${{ steps.get_draft_release.outputs.id }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Commit changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Update changelog for ${{ env.VERSION_TAG }}
|
||||
file_pattern: "changelog.md"
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
3
assets/menu.svg
Normal file
3
assets/menu.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
6
assets/youtube-music.svg
Normal file
6
assets/youtube-music.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32">
|
||||
<circle fill="red" cx="88" cy="88" r="88"/>
|
||||
<path fill="#FFF"
|
||||
d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/>
|
||||
<path fill="#FFF" d="M72 111l39-24-39-22z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@ -65,24 +65,107 @@ const defaultConfig = {
|
||||
proxy: '',
|
||||
startingPage: '',
|
||||
overrideUserAgent: false,
|
||||
themes: {} as string[],
|
||||
themes: [] as string[],
|
||||
},
|
||||
/** please order alphabetically */
|
||||
'plugins': {
|
||||
// Enabled plugins
|
||||
'navigation': {
|
||||
enabled: true,
|
||||
},
|
||||
'adblocker': {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
blocker: 'With blocklists',
|
||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||
disableDefaultLists: [],
|
||||
disableDefaultLists: false,
|
||||
},
|
||||
'album-color-theme': {},
|
||||
'ambient-mode': {},
|
||||
'audio-compressor': {},
|
||||
'blur-nav-bar': {},
|
||||
'bypass-age-restrictions': {},
|
||||
'captions-selector': {
|
||||
enabled: false,
|
||||
disableCaptions: false,
|
||||
autoload: false,
|
||||
lastCaptionsCode: '',
|
||||
},
|
||||
'compact-sidebar': {},
|
||||
'crossfade': {
|
||||
enabled: false,
|
||||
fadeInDuration: 1500, // Ms
|
||||
fadeOutDuration: 5000, // Ms
|
||||
secondsBeforeEnd: 10, // S
|
||||
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
|
||||
},
|
||||
'disable-autoplay': {
|
||||
applyOnce: false,
|
||||
},
|
||||
'discord': {
|
||||
enabled: false,
|
||||
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
|
||||
listenAlong: true, // Add a "listen along" button to rich presence
|
||||
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
|
||||
},
|
||||
'downloader': {
|
||||
enabled: false,
|
||||
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||
preset: 'mp3',
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined as number | undefined,
|
||||
},
|
||||
'exponential-volume': {},
|
||||
'in-app-menu': {},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
token: undefined as string | undefined, // Token used for authentication
|
||||
session_key: undefined as string | undefined, // Session key used for scrobbling
|
||||
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
||||
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
|
||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||
},
|
||||
'lumiastream': {},
|
||||
// Disabled plugins
|
||||
'lyrics-genius': {
|
||||
romanizedLyrics: false,
|
||||
},
|
||||
'navigation': {
|
||||
enabled: true,
|
||||
},
|
||||
'no-google-login': {},
|
||||
'notifications': {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: 'normal', // Has effect only on Linux
|
||||
// the following has effect only on Windows
|
||||
interactive: true,
|
||||
toastStyle: 1, // See plugins/notifications/utils for more info
|
||||
refreshOnPlayPause: false,
|
||||
trayControls: true,
|
||||
hideButtonText: false,
|
||||
},
|
||||
'picture-in-picture': {
|
||||
'enabled': false,
|
||||
'alwaysOnTop': true,
|
||||
'savePosition': true,
|
||||
'saveSize': false,
|
||||
'hotkey': 'P',
|
||||
'pip-position': [10, 10],
|
||||
'pip-size': [450, 275],
|
||||
'isInPiP': false,
|
||||
'useNativePiP': false,
|
||||
},
|
||||
'playback-speed': {},
|
||||
'precise-volume': {
|
||||
enabled: false,
|
||||
steps: 1, // Percentage of volume to change
|
||||
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
|
||||
globalShortcuts: {
|
||||
volumeUp: '',
|
||||
volumeDown: '',
|
||||
},
|
||||
savedVolume: undefined as number | undefined, // Plugin save volume between session here
|
||||
},
|
||||
'quality-changer': {},
|
||||
'shortcuts': {
|
||||
enabled: false,
|
||||
overrideMediaKeys: false,
|
||||
@ -97,53 +180,8 @@ const defaultConfig = {
|
||||
next: '',
|
||||
} as Record<string, string>,
|
||||
},
|
||||
'downloader': {
|
||||
enabled: false,
|
||||
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||
preset: 'mp3',
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined as number | undefined,
|
||||
},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
token: undefined as string | undefined, // Token used for authentication
|
||||
session_key: undefined as string | undefined, // Session key used for scrobbling
|
||||
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
||||
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
|
||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||
},
|
||||
'lyric-genius': {
|
||||
romanizedLyrics: false,
|
||||
},
|
||||
'discord': {
|
||||
enabled: false,
|
||||
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
|
||||
listenAlong: true, // Add a "listen along" button to rich presence
|
||||
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
|
||||
},
|
||||
'notifications': {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: 'normal', // Has effect only on Linux
|
||||
// the following has effect only on Windows
|
||||
interactive: true,
|
||||
toastStyle: 1, // See plugins/notifications/utils for more info
|
||||
refreshOnPlayPause: false,
|
||||
trayControls: true,
|
||||
hideButtonText: false,
|
||||
},
|
||||
'precise-volume': {
|
||||
enabled: false,
|
||||
steps: 1, // Percentage of volume to change
|
||||
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
|
||||
globalShortcuts: {
|
||||
volumeUp: '',
|
||||
volumeDown: '',
|
||||
},
|
||||
savedVolume: undefined as number | undefined, // Plugin save volume between session here
|
||||
'skip-silences': {
|
||||
onlySkipBeginning: false,
|
||||
},
|
||||
'sponsorblock': {
|
||||
enabled: false,
|
||||
@ -157,6 +195,9 @@ const defaultConfig = {
|
||||
'music_offtopic',
|
||||
],
|
||||
},
|
||||
'taskbar-mediacontrol': {},
|
||||
'touchbar': {},
|
||||
'tuna-obs': {},
|
||||
'video-toggle': {
|
||||
enabled: false,
|
||||
hideVideo: false,
|
||||
@ -164,34 +205,6 @@ const defaultConfig = {
|
||||
forceHide: false,
|
||||
align: '',
|
||||
},
|
||||
'picture-in-picture': {
|
||||
'enabled': false,
|
||||
'alwaysOnTop': true,
|
||||
'savePosition': true,
|
||||
'saveSize': false,
|
||||
'hotkey': 'P',
|
||||
'pip-position': [10, 10],
|
||||
'pip-size': [450, 275],
|
||||
'isInPiP': false,
|
||||
'useNativePiP': false,
|
||||
},
|
||||
'captions-selector': {
|
||||
enabled: false,
|
||||
disableCaptions: false,
|
||||
autoload: false,
|
||||
lastCaptionsCode: '',
|
||||
disabledCaptions: false,
|
||||
},
|
||||
'skip-silences': {
|
||||
onlySkipBeginning: false,
|
||||
},
|
||||
'crossfade': {
|
||||
enabled: false,
|
||||
fadeInDuration: 1500, // Ms
|
||||
fadeOutDuration: 5000, // Ms
|
||||
secondsBeforeEnd: 10, // S
|
||||
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
|
||||
},
|
||||
'visualizer': {
|
||||
enabled: false,
|
||||
type: 'butterchurn',
|
||||
|
||||
@ -10,9 +10,9 @@ import { getOptions, setMenuOptions, setOptions } from './plugins';
|
||||
import { sendToFront } from '../providers/app-controls';
|
||||
import { Entries } from '../utils/type-utils';
|
||||
|
||||
type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||
type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
|
||||
type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
|
||||
export type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||
export type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
|
||||
export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {};
|
||||
|
||||
134
index.ts
134
index.ts
@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import electron, { BrowserWindow } from 'electron';
|
||||
import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
|
||||
import enhanceWebRequest from 'electron-better-web-request';
|
||||
import is from 'electron-is';
|
||||
import unhandled from 'electron-unhandled';
|
||||
@ -11,13 +11,40 @@ 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';
|
||||
import { restart, setupAppControls } from './providers/app-controls';
|
||||
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
|
||||
|
||||
import adblocker from './plugins/adblocker/back';
|
||||
import albumColorTheme from './plugins/album-color-theme/back';
|
||||
import ambientMode from './plugins/ambient-mode/back';
|
||||
import blurNavigationBar from './plugins/blur-nav-bar/back';
|
||||
import captionsSelector from './plugins/captions-selector/back';
|
||||
import crossfade from './plugins/crossfade/back';
|
||||
import discord from './plugins/discord/back';
|
||||
import downloader from './plugins/downloader/back';
|
||||
import inAppMenu from './plugins/in-app-menu/back';
|
||||
import lastFm from './plugins/last-fm/back';
|
||||
import lumiaStream from './plugins/lumiastream/back';
|
||||
import lyricsGenius from './plugins/lyrics-genius/back';
|
||||
import navigation from './plugins/navigation/back';
|
||||
import noGoogleLogin from './plugins/no-google-login/back';
|
||||
import notifications from './plugins/notifications/back';
|
||||
import pictureInPicture, { setOptions as pipSetOptions } from './plugins/picture-in-picture/back';
|
||||
import preciseVolume from './plugins/precise-volume/back';
|
||||
import qualityChanger from './plugins/quality-changer/back';
|
||||
import shortcuts from './plugins/shortcuts/back';
|
||||
import sponsorBlock from './plugins/sponsorblock/back';
|
||||
import taskbarMediaControl from './plugins/taskbar-mediacontrol/back';
|
||||
import touchbar from './plugins/touchbar/back';
|
||||
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({
|
||||
@ -28,7 +55,6 @@ unhandled({
|
||||
// Disable Node options if the env var is set
|
||||
process.env.NODE_OPTIONS = '';
|
||||
|
||||
const { app } = electron;
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow: Electron.BrowserWindow | null;
|
||||
autoUpdater.autoDownload = false;
|
||||
@ -38,7 +64,9 @@ if (!gotTheLock) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); // Required for downloader
|
||||
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
|
||||
// OverlayScrollbar: Required for overlay scrollbars
|
||||
app.commandLine.appendSwitch('enable-features', 'OverlayScrollbar,SharedArrayBuffer');
|
||||
if (config.get('options.disableHardwareAcceleration')) {
|
||||
if (is.dev()) {
|
||||
console.log('Disabling hardware acceleration');
|
||||
@ -74,8 +102,50 @@ function onClosed() {
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
const mainPlugins = {
|
||||
'adblocker': adblocker,
|
||||
'album-color-theme': albumColorTheme,
|
||||
'ambient-mode': ambientMode,
|
||||
'blur-nav-bar': blurNavigationBar,
|
||||
'captions-selector': captionsSelector,
|
||||
'crossfade': crossfade,
|
||||
'discord': discord,
|
||||
'downloader': downloader,
|
||||
'in-app-menu': inAppMenu,
|
||||
'last-fm': lastFm,
|
||||
'lumiastream': lumiaStream,
|
||||
'lyrics-genius': lyricsGenius,
|
||||
'navigation': navigation,
|
||||
'no-google-login': noGoogleLogin,
|
||||
'notifications': notifications,
|
||||
'picture-in-picture': pictureInPicture,
|
||||
'precise-volume': preciseVolume,
|
||||
'quality-changer': qualityChanger,
|
||||
'shortcuts': shortcuts,
|
||||
'sponsorblock': sponsorBlock,
|
||||
'taskbar-mediacontrol': undefined as typeof taskbarMediaControl | undefined,
|
||||
'touchbar': undefined as typeof touchbar | undefined,
|
||||
'tuna-obs': tunaObs,
|
||||
'video-toggle': videoToggle,
|
||||
'visualizer': visualizer,
|
||||
};
|
||||
export const mainPluginNames = Object.keys(mainPlugins);
|
||||
|
||||
if (is.windows()) {
|
||||
mainPlugins['taskbar-mediacontrol'] = taskbarMediaControl;
|
||||
delete mainPlugins['touchbar'];
|
||||
} else if (is.macOS()) {
|
||||
mainPlugins['touchbar'] = touchbar;
|
||||
delete mainPlugins['taskbar-mediacontrol'];
|
||||
} else {
|
||||
delete mainPlugins['touchbar'];
|
||||
delete mainPlugins['taskbar-mediacontrol'];
|
||||
}
|
||||
|
||||
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)) {
|
||||
@ -83,7 +153,7 @@ function loadPlugins(win: BrowserWindow) {
|
||||
fileExists(
|
||||
cssFile,
|
||||
() => {
|
||||
injectCSS(win.webContents, cssFile);
|
||||
injectCSSAsFile(win.webContents, cssFile);
|
||||
},
|
||||
() => {
|
||||
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
|
||||
@ -100,13 +170,17 @@ function loadPlugins(win: BrowserWindow) {
|
||||
});
|
||||
|
||||
for (const [plugin, options] of config.plugins.getEnabled()) {
|
||||
try {
|
||||
if (Object.hasOwn(mainPlugins, plugin)) {
|
||||
console.log('Loaded plugin - ' + plugin);
|
||||
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
|
||||
fileExists(pluginPath, () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
|
||||
const handle = require(pluginPath).default as (window: BrowserWindow, option: typeof options) => void;
|
||||
handle(win, options);
|
||||
});
|
||||
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
|
||||
if (handler) {
|
||||
handler(win, options as never);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin "${plugin}"`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +211,11 @@ function createMainWindow() {
|
||||
}),
|
||||
},
|
||||
frame: !is.macOS() && !useInlineMenu,
|
||||
titleBarOverlay: {
|
||||
color: '#00000000',
|
||||
symbolColor: '#ffffff',
|
||||
height: 36,
|
||||
},
|
||||
titleBarStyle: useInlineMenu
|
||||
? 'hidden'
|
||||
: (is.macOS()
|
||||
@ -150,7 +229,7 @@ function createMainWindow() {
|
||||
const { x, y } = windowPosition;
|
||||
const winSize = win.getSize();
|
||||
const displaySize
|
||||
= electron.screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||
= screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||
if (
|
||||
x + winSize[0] < displaySize.x - 8
|
||||
|| x - winSize[0] > displaySize.x + displaySize.width
|
||||
@ -182,7 +261,7 @@ function createMainWindow() {
|
||||
win.webContents.loadURL(urlToLoad);
|
||||
win.on('closed', onClosed);
|
||||
|
||||
const scaleFactor = electron.screen.getAllDisplays().length > 1 ? electron.screen.getPrimaryDisplay().scaleFactor : 1;
|
||||
const scaleFactor = screen.getAllDisplays().length > 1 ? screen.getPrimaryDisplay().scaleFactor : 1;
|
||||
const size = config.get('window-size');
|
||||
const position = config.get('window-position');
|
||||
|
||||
@ -205,8 +284,7 @@ function createMainWindow() {
|
||||
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
|
||||
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
? (key: string, value: unknown) => (require('./plugins/picture-in-picture/back') as typeof import('./plugins/picture-in-picture/back'))
|
||||
.setOptions({ [key]: value })
|
||||
? (key: string, value: unknown) => pipSetOptions({ [key]: value })
|
||||
: () => {};
|
||||
|
||||
win.on('move', () => {
|
||||
@ -353,7 +431,7 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
|
||||
// Unregister all shortcuts.
|
||||
electron.globalShortcut.unregisterAll();
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
@ -374,7 +452,7 @@ app.on('ready', () => {
|
||||
console.log('Clearing app cache.');
|
||||
}
|
||||
|
||||
electron.session.defaultSession.clearCache();
|
||||
session.defaultSession.clearCache();
|
||||
clearTimeout(clearCacheTimeout);
|
||||
}, 20_000);
|
||||
}
|
||||
@ -389,7 +467,7 @@ app.on('ready', () => {
|
||||
if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
|
||||
const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
|
||||
try { // Check if shortcut is registered and valid
|
||||
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
||||
if (
|
||||
shortcutDetails.target !== appLocation
|
||||
|| shortcutDetails.appUserModelId !== appID
|
||||
@ -397,7 +475,7 @@ app.on('ready', () => {
|
||||
throw 'needUpdate';
|
||||
}
|
||||
} catch (error) { // If not valid -> Register shortcut
|
||||
electron.shell.writeShortcutLink(
|
||||
shell.writeShortcutLink(
|
||||
shortcutPath,
|
||||
error === 'needUpdate' ? 'update' : 'create',
|
||||
{
|
||||
@ -466,11 +544,11 @@ app.on('ready', () => {
|
||||
message: 'A new version is available',
|
||||
detail: `A new version is available and can be downloaded at ${downloadLink}`,
|
||||
};
|
||||
electron.dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
|
||||
dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
|
||||
switch (dialogOutput.response) {
|
||||
// Download
|
||||
case 1: {
|
||||
electron.shell.openExternal(downloadLink);
|
||||
shell.openExternal(downloadLink);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -489,7 +567,7 @@ app.on('ready', () => {
|
||||
}
|
||||
|
||||
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
||||
electron.dialog.showMessageBox(mainWindow, {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info', title: 'Hide Menu Enabled',
|
||||
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
|
||||
});
|
||||
@ -522,7 +600,7 @@ function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProc
|
||||
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
||||
}
|
||||
|
||||
electron.dialog.showMessageBox(win, {
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'error',
|
||||
title: 'Window Unresponsive',
|
||||
message: 'The Application is Unresponsive',
|
||||
@ -547,15 +625,15 @@ function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProc
|
||||
// HACK: electron-better-web-request's typing is wrong
|
||||
type BetterSession = Omit<Electron.Session, 'webRequest'> & { webRequest: BetterWebRequest & Electron.WebRequest };
|
||||
function removeContentSecurityPolicy(
|
||||
session: BetterSession = electron.session.defaultSession as BetterSession,
|
||||
betterSession: BetterSession = session.defaultSession as BetterSession,
|
||||
) {
|
||||
// Allows defining multiple "onHeadersReceived" listeners
|
||||
// by enhancing the session.
|
||||
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
|
||||
enhanceWebRequest(session);
|
||||
enhanceWebRequest(betterSession);
|
||||
|
||||
// Custom listener to tweak the content security policy
|
||||
session.webRequest.onHeadersReceived((details, callback) => {
|
||||
betterSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
details.responseHeaders ??= {};
|
||||
|
||||
// Remove the content security policy
|
||||
@ -567,7 +645,7 @@ function removeContentSecurityPolicy(
|
||||
|
||||
type ResolverListener = { apply: () => Promise<Record<string, unknown>>; context: unknown };
|
||||
// When multiple listeners are defined, apply them all
|
||||
session.webRequest.setResolver('onHeadersReceived', async (listeners: ResolverListener[]) => {
|
||||
betterSession.webRequest.setResolver('onHeadersReceived', async (listeners: ResolverListener[]) => {
|
||||
return listeners.reduce<Promise<Record<string, unknown>>>(
|
||||
async (accumulator: Promise<Record<string, unknown>>, listener: ResolverListener) => {
|
||||
const acc = await accumulator;
|
||||
|
||||
91
menu.ts
91
menu.ts
@ -1,23 +1,50 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import is from 'electron-is';
|
||||
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import { getAllPlugins } from './plugins/utils';
|
||||
import config from './config';
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
import promptOptions from './providers/prompt-options';
|
||||
|
||||
export type MenuTemplate = (Electron.MenuItemConstructorOptions | Electron.MenuItem)[];
|
||||
import adblockerMenu from './plugins/adblocker/menu';
|
||||
import captionsSelectorMenu from './plugins/captions-selector/menu';
|
||||
import crossfadeMenu from './plugins/crossfade/menu';
|
||||
import disableAutoplayMenu from './plugins/disable-autoplay/menu';
|
||||
import discordMenu from './plugins/discord/menu';
|
||||
import downloaderMenu from './plugins/downloader/menu';
|
||||
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
|
||||
import notificationsMenu from './plugins/notifications/menu';
|
||||
import pictureInPictureMenu from './plugins/picture-in-picture/menu';
|
||||
import preciseVolumeMenu from './plugins/precise-volume/menu';
|
||||
import shortcutsMenu from './plugins/shortcuts/menu';
|
||||
import videoToggleMenu from './plugins/video-toggle/menu';
|
||||
import visualizerMenu from './plugins/visualizer/menu';
|
||||
import { getAvailablePluginNames } from './plugins/utils';
|
||||
|
||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||
|
||||
// True only if in-app-menu was loaded on launch
|
||||
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
||||
|
||||
const betaPlugins = ['crossfade', 'lumiastream'];
|
||||
|
||||
const pluginMenus = {
|
||||
'adblocker': adblockerMenu,
|
||||
'disable-autoplay': disableAutoplayMenu,
|
||||
'captions-selector': captionsSelectorMenu,
|
||||
'crossfade': crossfadeMenu,
|
||||
'discord': discordMenu,
|
||||
'downloader': downloaderMenu,
|
||||
'lyrics-genius': lyricsGeniusMenu,
|
||||
'notifications': notificationsMenu,
|
||||
'picture-in-picture': pictureInPictureMenu,
|
||||
'precise-volume': preciseVolumeMenu,
|
||||
'shortcuts': shortcutsMenu,
|
||||
'video-toggle': videoToggleMenu,
|
||||
'visualizer': visualizerMenu,
|
||||
};
|
||||
|
||||
const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refreshMenu: (() => void ) | undefined = undefined): Electron.MenuItemConstructorOptions => ({
|
||||
label: label || plugin,
|
||||
type: 'checkbox',
|
||||
@ -47,33 +74,30 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
{
|
||||
label: 'Plugins',
|
||||
submenu:
|
||||
getAllPlugins().map((plugin) => {
|
||||
let pluginLabel = plugin;
|
||||
if (betaPlugins.includes(plugin)) {
|
||||
getAvailablePluginNames().map((pluginName) => {
|
||||
let pluginLabel = pluginName;
|
||||
if (betaPlugins.includes(pluginLabel)) {
|
||||
pluginLabel += ' [beta]';
|
||||
}
|
||||
|
||||
const pluginPath = path.join(__dirname, 'plugins', plugin, 'menu.js');
|
||||
if (existsSync(pluginPath)) {
|
||||
if (!config.plugins.isEnabled(plugin)) {
|
||||
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
|
||||
if (Object.hasOwn(pluginMenus, pluginName)) {
|
||||
const getPluginMenu = pluginMenus[pluginName as keyof typeof pluginMenus];
|
||||
|
||||
if (!config.plugins.isEnabled(pluginName)) {
|
||||
return pluginEnabledMenu(pluginName, pluginLabel, true, refreshMenu);
|
||||
}
|
||||
|
||||
type PluginType = (window: BrowserWindow, plugins: string, func: () => void) => Electron.MenuItemConstructorOptions[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
|
||||
const getPluginMenu = require(pluginPath).default as PluginType;
|
||||
return {
|
||||
label: pluginLabel,
|
||||
submenu: [
|
||||
pluginEnabledMenu(plugin, 'Enabled', true, refreshMenu),
|
||||
pluginEnabledMenu(pluginName, 'Enabled', true, refreshMenu),
|
||||
{ type: 'separator' },
|
||||
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||
...getPluginMenu(win, config.plugins.getOptions(pluginName), refreshMenu),
|
||||
],
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
}
|
||||
|
||||
return pluginEnabledMenu(plugin, pluginLabel);
|
||||
return pluginEnabledMenu(pluginName, pluginLabel);
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -97,14 +121,25 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
},
|
||||
{
|
||||
label: 'Starting page',
|
||||
submenu: Object.keys(startingPages).map((name) => ({
|
||||
submenu: (() => {
|
||||
const subMenuArray: Electron.MenuItemConstructorOptions[] = Object.keys(startingPages).map((name) => ({
|
||||
label: name,
|
||||
type: 'radio',
|
||||
checked: config.get('options.startingPage') === name,
|
||||
click() {
|
||||
config.set('options.startingPage', name);
|
||||
},
|
||||
})),
|
||||
}));
|
||||
subMenuArray.unshift({
|
||||
label: 'Unset',
|
||||
type: 'radio',
|
||||
checked: config.get('options.startingPage') === '',
|
||||
click() {
|
||||
config.set('options.startingPage', '');
|
||||
},
|
||||
});
|
||||
return subMenuArray;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: 'Visual Tweaks',
|
||||
@ -152,7 +187,7 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
{
|
||||
label: 'No theme',
|
||||
type: 'radio',
|
||||
checked: !config.get('options.themes'), // Todo rename "themes"
|
||||
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
|
||||
click() {
|
||||
config.set('options.themes', []);
|
||||
},
|
||||
@ -160,8 +195,7 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Import custom CSS file',
|
||||
type: 'radio',
|
||||
checked: false,
|
||||
type: 'normal',
|
||||
async click() {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
filters: [{ name: 'CSS Files', extensions: ['css'] }],
|
||||
@ -275,11 +309,10 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
label: 'Advanced options',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Proxy',
|
||||
type: 'checkbox',
|
||||
checked: !!(config.get('options.proxy')),
|
||||
click(item) {
|
||||
setProxy(item, win);
|
||||
label: 'Set Proxy',
|
||||
type: 'normal',
|
||||
async click(item) {
|
||||
await setProxy(item, win);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
3038
package-lock.json
generated
3038
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
@ -14,17 +14,36 @@
|
||||
"build": {
|
||||
"appId": "com.github.th-ch.youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"mac": {
|
||||
"identity": null,
|
||||
"files": [
|
||||
"!*",
|
||||
"dist",
|
||||
"!dist/plugins/taskbar-mediacontrol${/*}",
|
||||
"license",
|
||||
"node_modules",
|
||||
"package.json",
|
||||
"tests"
|
||||
"!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/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,
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
@ -38,15 +57,6 @@
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/generated/icons/win/icon.ico",
|
||||
"files": [
|
||||
"!*",
|
||||
"dist",
|
||||
"!dist/plugins/touchbar${/*}",
|
||||
"license",
|
||||
"node_modules",
|
||||
"package.json",
|
||||
"tests"
|
||||
],
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
@ -71,15 +81,6 @@
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/generated/icons/png",
|
||||
"files": [
|
||||
"!*",
|
||||
"dist",
|
||||
"!dist/plugins/{touchbar,taskbar-mediacontrol}${/*}",
|
||||
"license",
|
||||
"node_modules",
|
||||
"package.json",
|
||||
"tests"
|
||||
],
|
||||
"category": "AudioVideo",
|
||||
"target": [
|
||||
"AppImage",
|
||||
@ -103,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": {
|
||||
@ -144,7 +143,6 @@
|
||||
"butterchurn-presets": "2.4.7",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"custom-electron-titlebar": "4.1.6",
|
||||
"electron-better-web-request": "1.0.1",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-is": "3.0.0",
|
||||
@ -152,8 +150,8 @@
|
||||
"electron-store": "8.1.0",
|
||||
"electron-unhandled": "4.0.1",
|
||||
"electron-updater": "6.1.4",
|
||||
"fast-average-color-node": "^2.6.0",
|
||||
"filenamify": "4.3.0",
|
||||
"fast-average-color": "9.4.0",
|
||||
"filenamify": "6.0.0",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
@ -162,33 +160,47 @@
|
||||
"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": {
|
||||
"rollup": "4.0.2",
|
||||
"node-gyp": "9.4.0",
|
||||
"xml2js": "0.6.2",
|
||||
"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.5",
|
||||
"@rollup/plugin-image": "3.0.3",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-node-resolve": "15.2.2",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@rollup/plugin-typescript": "11.1.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@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",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"eslint": "8.50.0",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-prettier": "5.0.0",
|
||||
"node-gyp": "9.4.0",
|
||||
"playwright": "1.38.1",
|
||||
"rollup": "4.0.2",
|
||||
"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": {
|
||||
|
||||
@ -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',
|
||||
@ -20,19 +21,31 @@ export const loadAdBlockerEngine = (
|
||||
session: Electron.Session | undefined = undefined,
|
||||
cache = true,
|
||||
additionalBlockLists = [],
|
||||
disableDefaultLists: boolean | string[] = false,
|
||||
disableDefaultLists: boolean | unknown[] = false,
|
||||
) => {
|
||||
// Only use cache if no additional blocklists are passed
|
||||
let cacheDirectory: string;
|
||||
if (app.isPackaged) {
|
||||
cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
|
||||
} else {
|
||||
cacheDirectory = path.resolve(__dirname, 'adblock_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,
|
||||
}
|
||||
: undefined;
|
||||
const lists = [
|
||||
...(disableDefaultLists ? [] : SOURCES),
|
||||
...(
|
||||
(disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
|
||||
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) ? [] : SOURCES
|
||||
),
|
||||
...additionalBlockLists,
|
||||
];
|
||||
|
||||
@ -58,6 +71,3 @@ export const loadAdBlockerEngine = (
|
||||
};
|
||||
|
||||
export default { loadAdBlockerEngine };
|
||||
if (require.main === module) {
|
||||
loadAdBlockerEngine(); // Generate the engine without enabling it
|
||||
}
|
||||
|
||||
4
plugins/adblocker/inject-cliqz-preload.ts
Normal file
4
plugins/adblocker/inject-cliqz-preload.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default () => {
|
||||
const path = '@cliqz/adblocker-electron-preload'; // prevent require hoisting
|
||||
require(path);
|
||||
};
|
||||
3
plugins/adblocker/inject.d.ts
vendored
Normal file
3
plugins/adblocker/inject.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
const inject: () => void;
|
||||
|
||||
export default inject;
|
||||
@ -7,7 +7,7 @@
|
||||
Parts of this code is derived from set-constant.js:
|
||||
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
|
||||
*/
|
||||
|
||||
module.exports = () => {
|
||||
{
|
||||
const pruner = function (o) {
|
||||
delete o.playerAds;
|
||||
@ -432,3 +432,4 @@
|
||||
//
|
||||
trapChain(window, chain);
|
||||
})();
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import config, { blockers } from './config';
|
||||
|
||||
export default () => {
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (): MenuTemplate => {
|
||||
return [
|
||||
{
|
||||
label: 'Blocker',
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import config from './config';
|
||||
import config, { blockers } from './config';
|
||||
import inject from './inject';
|
||||
import injectCliqzPreload from './inject-cliqz-preload';
|
||||
|
||||
export default async () => {
|
||||
if (await config.shouldUseBlocklists()) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
require('@cliqz/adblocker-electron-preload');
|
||||
injectCliqzPreload();
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
} else if ((await config.get('blocker')) === config.blockers.InPlayer) {
|
||||
require('./inject.js');
|
||||
} else if ((await config.get('blocker')) === blockers.InPlayer) {
|
||||
inject();
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -14,7 +14,7 @@ export default (): MenuTemplate => [
|
||||
{
|
||||
label: 'No captions by default',
|
||||
type: 'checkbox',
|
||||
checked: config.get('disabledCaptions'),
|
||||
checked: config.get('disableCaptions'),
|
||||
click(item) {
|
||||
config.set('disableCaptions', item.checked);
|
||||
},
|
||||
|
||||
@ -7,11 +7,13 @@ import config from './config';
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import configOptions from '../../config/defaults';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const defaultOptions = configOptions.plugins.crossfade;
|
||||
|
||||
export default (win: BrowserWindow) => [
|
||||
export default (win: BrowserWindow): MenuTemplate => [
|
||||
{
|
||||
label: 'Advanced',
|
||||
async click() {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export default () => {
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (options: ConfigType<'disable-autoplay'>) => {
|
||||
const timeUpdateListener = (e: Event) => {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
e.target.pause();
|
||||
@ -6,13 +8,16 @@ export default () => {
|
||||
};
|
||||
|
||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
||||
const eventListener = (name: string) => {
|
||||
if (options.applyOnce) {
|
||||
apiEvent.detail.removeEventListener('videodatachange', eventListener);
|
||||
}
|
||||
|
||||
if (name === 'dataloaded') {
|
||||
apiEvent.detail.pauseVideo();
|
||||
document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', timeUpdateListener);
|
||||
} else {
|
||||
document.querySelector<HTMLVideoElement>('video')?.removeEventListener('timeupdate', timeUpdateListener);
|
||||
document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', timeUpdateListener, { once: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
apiEvent.detail.addEventListener('videodatachange', eventListener);
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
20
plugins/disable-autoplay/menu.ts
Normal file
20
plugins/disable-autoplay/menu.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (_: BrowserWindow, options: ConfigType<'disable-autoplay'>): MenuTemplate => [
|
||||
{
|
||||
label: 'Applies only on startup',
|
||||
type: 'checkbox',
|
||||
checked: options.applyOnce,
|
||||
click() {
|
||||
setMenuOptions('disable-autoplay', {
|
||||
applyOnce: !options.applyOnce,
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
@ -1,12 +1,11 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { Electron } from 'playwright';
|
||||
|
||||
import { clear, connect, isConnected, registerRefresh } from './back';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import { singleton } from '../../providers/decorators';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
@ -16,14 +15,14 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
|
||||
type DiscordOptions = ConfigType<'discord'>;
|
||||
|
||||
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => {
|
||||
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void): MenuTemplate => {
|
||||
registerRefreshOnce(refreshMenu);
|
||||
|
||||
return [
|
||||
{
|
||||
label: isConnected() ? 'Connected' : 'Reconnect',
|
||||
enabled: !isConnected(),
|
||||
click: connect,
|
||||
click: () => connect(),
|
||||
},
|
||||
{
|
||||
label: 'Auto reconnect',
|
||||
|
||||
@ -2,7 +2,7 @@ import { createWriteStream, existsSync, mkdirSync, writeFileSync, } from 'node:f
|
||||
import { join } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
|
||||
import { ClientType, Innertube, UniversalCache, Utils } from 'youtubei.js';
|
||||
import is from 'electron-is';
|
||||
import ytpl from 'ytpl';
|
||||
@ -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 + ';'
|
||||
@ -77,6 +80,24 @@ export default async (win_: BrowserWindow) => {
|
||||
cache: new UniversalCache(false),
|
||||
cookie,
|
||||
generate_session_locally: true,
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === 'string' ?
|
||||
new URL(input) :
|
||||
input instanceof URL ?
|
||||
input : new URL(input.url);
|
||||
|
||||
if (init?.body && !init.method) {
|
||||
init.method = 'POST';
|
||||
}
|
||||
|
||||
const request = new Request(
|
||||
url,
|
||||
input instanceof Request ? input : undefined,
|
||||
);
|
||||
|
||||
return net.fetch(request, init);
|
||||
}
|
||||
});
|
||||
ipcMain.on('download-song', (_, url: string) => downloadSong(url));
|
||||
ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => {
|
||||
@ -113,8 +134,7 @@ async function downloadSongUnsafe(
|
||||
setName: (name: string) => void,
|
||||
playlistFolder: string | undefined = undefined,
|
||||
trackId: string | undefined = undefined,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
},
|
||||
increasePlaylistProgress: (value: number) => void = () => {},
|
||||
) {
|
||||
const sendFeedback = (message: unknown, progress?: number) => {
|
||||
if (!playlistFolder) {
|
||||
@ -540,11 +560,7 @@ const getPlaylistID = (aURL: URL) => {
|
||||
};
|
||||
|
||||
const getVideoId = (url: URL | string): string | null => {
|
||||
if (typeof url === 'string') {
|
||||
url = new URL(url);
|
||||
}
|
||||
|
||||
return url.searchParams.get('v');
|
||||
return (new URL(url)).searchParams.get('v');
|
||||
};
|
||||
|
||||
const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1,27 +1,60 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { register } from 'electron-localshortcut';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { attachTitlebarToWindow, setupTitlebar } from 'custom-electron-titlebar/main';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
|
||||
|
||||
import titlebarStyle from './titlebar.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
|
||||
setupTitlebar();
|
||||
|
||||
// Tracks menu visibility
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
// Css for custom scrollbar + disable drag area(was causing bugs)
|
||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||
injectCSS(win.webContents, titlebarStyle);
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
attachTitlebarToWindow(win);
|
||||
|
||||
register(win, '`', () => {
|
||||
win.webContents.send('toggleMenu');
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'get-menu',
|
||||
() => JSON.parse(JSON.stringify(
|
||||
Menu.getApplicationMenu(),
|
||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||
),
|
||||
);
|
||||
|
||||
const getMenuItemById = (commandId: number): MenuItem | null => {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
|
||||
let target: MenuItem | null = null;
|
||||
const stack = [...menu?.items ?? []];
|
||||
while (stack.length > 0) {
|
||||
const now = stack.shift();
|
||||
now?.submenu?.items.forEach((item) => stack.push(item));
|
||||
|
||||
if (now?.commandId === commandId) {
|
||||
target = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
ipcMain.handle('menu-event', (event, commandId: number) => {
|
||||
const target = getMenuItemById(commandId);
|
||||
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-menu-by-id', (_, commandId: number) => {
|
||||
const result = getMenuItemById(commandId);
|
||||
|
||||
return JSON.parse(JSON.stringify(
|
||||
result,
|
||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
declare module 'custom-electron-titlebar' {
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import OriginalTitlebar from 'custom-electron-titlebar/dist/titlebar';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { Color as OriginalColor } from 'custom-electron-titlebar/dist/vs/base/common/color';
|
||||
|
||||
export const Color: typeof OriginalColor;
|
||||
export const Titlebar: typeof OriginalTitlebar;
|
||||
}
|
||||
@ -1,104 +1,95 @@
|
||||
import { ipcRenderer, Menu } from 'electron';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { Color, Titlebar } from 'custom-electron-titlebar';
|
||||
|
||||
import config from '../../config';
|
||||
import { createPanel } from './menu/panel';
|
||||
|
||||
import logo from '../../assets/menu.svg';
|
||||
import { isEnabled } from '../../config/plugins';
|
||||
|
||||
type ElectronCSSStyleDeclaration = CSSStyleDeclaration & { webkitAppRegion: 'drag' | 'no-drag' };
|
||||
type ElectronHTMLElement = HTMLElement & { style: ElectronCSSStyleDeclaration };
|
||||
import config from '../../config';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
const isMacOS = navigator.userAgent.includes('Macintosh');
|
||||
|
||||
export default () => {
|
||||
const visible = () => !!($('.cet-menubar')?.firstChild);
|
||||
const bar = new Titlebar({
|
||||
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
|
||||
backgroundColor: Color.fromHex('#050505'),
|
||||
itemBackgroundColor: Color.fromHex('#1d1d1d') ,
|
||||
svgColor: Color.WHITE,
|
||||
menu: config.get('options.hideMenu') ? null as unknown as Menu : undefined,
|
||||
let hideMenu = config.get('options.hideMenu');
|
||||
const titleBar = document.createElement('title-bar');
|
||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
||||
|
||||
logo.classList.add('title-bar-icon');
|
||||
const logoClick = () => {
|
||||
hideMenu = !hideMenu;
|
||||
let visibilityStyle: string;
|
||||
if (hideMenu) {
|
||||
visibilityStyle = 'hidden';
|
||||
} else {
|
||||
visibilityStyle = 'visible';
|
||||
}
|
||||
const menus = document.querySelectorAll<HTMLElement>('menu-button');
|
||||
menus.forEach((menu) => {
|
||||
menu.style.visibility = visibilityStyle;
|
||||
});
|
||||
bar.updateTitle(' ');
|
||||
};
|
||||
logo.onclick = logoClick;
|
||||
|
||||
ipcRenderer.on('toggleMenu', logoClick);
|
||||
|
||||
if (!isMacOS) titleBar.appendChild(logo);
|
||||
document.body.appendChild(titleBar);
|
||||
|
||||
if (navBar) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(() => {
|
||||
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(navBar, { attributes : true, attributeFilter : ['style'] });
|
||||
}
|
||||
|
||||
const updateMenu = async () => {
|
||||
const children = [...titleBar.children];
|
||||
children.forEach((child) => {
|
||||
if (child !== logo) child.remove();
|
||||
});
|
||||
|
||||
const menu = await ipcRenderer.invoke('get-menu') as Menu | null;
|
||||
if (!menu) return;
|
||||
|
||||
menu.items.forEach((menuItem) => {
|
||||
const menu = document.createElement('menu-button');
|
||||
createPanel(titleBar, menu, menuItem.submenu?.items ?? []);
|
||||
|
||||
menu.append(menuItem.label);
|
||||
titleBar.appendChild(menu);
|
||||
if (hideMenu) {
|
||||
menu.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
};
|
||||
updateMenu();
|
||||
|
||||
document.title = 'Youtube Music';
|
||||
|
||||
const toggleMenu = () => {
|
||||
if (visible()) {
|
||||
bar.updateMenu(null as unknown as Menu);
|
||||
} else {
|
||||
bar.refreshMenu();
|
||||
}
|
||||
};
|
||||
|
||||
$('.cet-window-icon')?.addEventListener('click', toggleMenu);
|
||||
ipcRenderer.on('toggleMenu', toggleMenu);
|
||||
|
||||
ipcRenderer.on('refreshMenu', () => {
|
||||
if (visible()) {
|
||||
bar.refreshMenu();
|
||||
}
|
||||
updateMenu();
|
||||
});
|
||||
|
||||
if (isEnabled('picture-in-picture')) {
|
||||
ipcRenderer.on('pip-toggle', () => {
|
||||
bar.refreshMenu();
|
||||
updateMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
setNavbarMargin();
|
||||
const playPageObserver = new MutationObserver(setNavbarMargin);
|
||||
const appLayout = $('ytmusic-app-layout');
|
||||
if (appLayout) {
|
||||
playPageObserver.observe(appLayout, { attributeFilter: ['player-page-open_', 'playerPageOpen_'] });
|
||||
const htmlHeadStyle = $('head > div > style');
|
||||
if (htmlHeadStyle) {
|
||||
// HACK: This is a hack to remove the scrollbar width
|
||||
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {');
|
||||
}
|
||||
setupSearchOpenObserver();
|
||||
setupMenuOpenObserver();
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
function setupSearchOpenObserver() {
|
||||
const searchOpenObserver = new MutationObserver((mutations) => {
|
||||
const navBarBackground = $<ElectronHTMLElement>('#nav-bar-background');
|
||||
if (navBarBackground) {
|
||||
navBarBackground.style.webkitAppRegion = (mutations[0].target as HTMLElement & { opened: boolean }).opened ? 'no-drag' : 'drag';
|
||||
}
|
||||
});
|
||||
const searchBox = $('ytmusic-search-box');
|
||||
if (searchBox) {
|
||||
searchOpenObserver.observe(searchBox, { attributeFilter: ['opened'] });
|
||||
}
|
||||
}
|
||||
|
||||
function setupMenuOpenObserver() {
|
||||
const cetMenubar = $('.cet-menubar');
|
||||
if (cetMenubar) {
|
||||
const menuOpenObserver = new MutationObserver(() => {
|
||||
let isOpen = false;
|
||||
for (const child of cetMenubar.children) {
|
||||
if (child.classList.contains('open')) {
|
||||
isOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const navBarBackground = $<ElectronHTMLElement>('#nav-bar-background');
|
||||
if (navBarBackground) {
|
||||
navBarBackground.style.webkitAppRegion = isOpen ? 'no-drag' : 'drag';
|
||||
}
|
||||
});
|
||||
menuOpenObserver.observe(cetMenubar, { subtree: true, attributeFilter: ['class'] });
|
||||
}
|
||||
}
|
||||
|
||||
function setNavbarMargin() {
|
||||
const navBarBackground = $<HTMLElement>('#nav-bar-background');
|
||||
if (navBarBackground) {
|
||||
navBarBackground.style.right
|
||||
= $<HTMLElement & { playerPageOpen_: boolean }>('ytmusic-app-layout')?.playerPageOpen_
|
||||
? '0px'
|
||||
: '12px';
|
||||
}
|
||||
}
|
||||
|
||||
10
plugins/in-app-menu/menu/icons.ts
Normal file
10
plugins/in-app-menu/menu/icons.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const Icons = {
|
||||
submenu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
|
||||
checkbox: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
|
||||
radio: {
|
||||
checked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
unchecked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Icons;
|
||||
125
plugins/in-app-menu/menu/panel.ts
Normal file
125
plugins/in-app-menu/menu/panel.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron';
|
||||
|
||||
import Icons from './icons';
|
||||
|
||||
import { ElementFromHtml } from '../../utils';
|
||||
|
||||
interface PanelOptions {
|
||||
placement?: 'bottom' | 'right';
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export const createPanel = (
|
||||
parent: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
items: MenuItem[],
|
||||
options: PanelOptions = { placement: 'bottom', order: 0 },
|
||||
) => {
|
||||
const childPanels: HTMLElement[] = [];
|
||||
const panel = document.createElement('menu-panel');
|
||||
panel.style.zIndex = `${options.order}`;
|
||||
|
||||
const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => {
|
||||
if (item.type === 'checkbox') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.checkbox;
|
||||
else iconWrapper.innerHTML = '';
|
||||
} else if (item.type === 'radio') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.radio.checked;
|
||||
else iconWrapper.innerHTML = Icons.radio.unchecked;
|
||||
} else {
|
||||
const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon;
|
||||
const iconURL = nativeImageIcon?.toDataURL();
|
||||
|
||||
if (iconURL) iconWrapper.style.background = `url(${iconURL})`;
|
||||
}
|
||||
};
|
||||
|
||||
const radioGroups: [MenuItem, HTMLElement][] = [];
|
||||
items.map((item) => {
|
||||
if (item.type === 'separator') return panel.appendChild(document.createElement('menu-separator'));
|
||||
|
||||
const menu = document.createElement('menu-item');
|
||||
const iconWrapper = document.createElement('menu-icon');
|
||||
|
||||
updateIconState(iconWrapper, item);
|
||||
menu.appendChild(iconWrapper);
|
||||
menu.append(item.label);
|
||||
|
||||
menu.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('menu-event', item.commandId);
|
||||
const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (menuItem) {
|
||||
updateIconState(iconWrapper, menuItem);
|
||||
|
||||
if (menuItem.type === 'radio') {
|
||||
await Promise.all(
|
||||
radioGroups.map(async ([item, iconWrapper]) => {
|
||||
if (item.commandId === menuItem.commandId) return;
|
||||
const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (newItem) updateIconState(iconWrapper, newItem);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (item.type === 'radio') {
|
||||
radioGroups.push([item, iconWrapper]);
|
||||
}
|
||||
|
||||
if (item.type === 'submenu') {
|
||||
const subMenuIcon = document.createElement('menu-icon');
|
||||
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
|
||||
menu.appendChild(subMenuIcon);
|
||||
|
||||
const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], {
|
||||
placement: 'right',
|
||||
order: (options?.order ?? 0) + 1,
|
||||
});
|
||||
|
||||
childPanels.push(child);
|
||||
children.push(...children);
|
||||
}
|
||||
|
||||
panel.appendChild(menu);
|
||||
});
|
||||
|
||||
/* methods */
|
||||
const isOpened = () => panel.getAttribute('open') === 'true';
|
||||
const close = () => panel.setAttribute('open', 'false');
|
||||
const open = () => {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
|
||||
if (options.placement === 'bottom') {
|
||||
panel.style.setProperty('--x', `${rect.x}px`);
|
||||
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
|
||||
} else {
|
||||
panel.style.setProperty('--x', `${rect.x + rect.width}px`);
|
||||
panel.style.setProperty('--y', `${rect.y}px`);
|
||||
}
|
||||
|
||||
panel.setAttribute('open', 'true');
|
||||
};
|
||||
|
||||
anchor.addEventListener('click', () => {
|
||||
if (isOpened()) close();
|
||||
else open();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', (event) => {
|
||||
const path = event.composedPath();
|
||||
const isInside = path.some((it) => it === panel || it === anchor || childPanels.includes(it as HTMLElement));
|
||||
|
||||
if (!isInside) close();
|
||||
});
|
||||
|
||||
parent.appendChild(panel);
|
||||
|
||||
return [
|
||||
panel,
|
||||
{ isOpened, close, open },
|
||||
childPanels,
|
||||
] as const;
|
||||
};
|
||||
@ -1,113 +0,0 @@
|
||||
/* increase font size for menu and menuItems */
|
||||
.titlebar,
|
||||
.menubar-menu-container .action-label {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */
|
||||
#nav-bar-background {
|
||||
opacity: 1 !important;
|
||||
pointer-events: none !important;
|
||||
top: 30px !important;
|
||||
height: 75px !important;
|
||||
}
|
||||
|
||||
/* fix top gap between nav-bar and browse-page */
|
||||
#browse-page {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* fix navbar hiding library items */
|
||||
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"],
|
||||
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] {
|
||||
top: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||
ytmusic-nav-bar,
|
||||
.tab-titleiron-icon,
|
||||
ytmusic-pivot-bar-item-renderer {
|
||||
-webkit-app-region: unset !important;
|
||||
}
|
||||
|
||||
/* move up item selection renderers */
|
||||
ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer,
|
||||
ytmusic-tabs.stuck {
|
||||
top: calc(var(--ytmusic-nav-bar-height) - 15px) !important;
|
||||
}
|
||||
|
||||
/* fix weird positioning in search screen*/
|
||||
ytmusic-header-renderer.ytmusic-search-page {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
/* Move navBar downwards */
|
||||
ytmusic-nav-bar[slot="nav-bar"] {
|
||||
top: 17px !important;
|
||||
}
|
||||
|
||||
/* fix page progress bar position*/
|
||||
yt-page-navigation-progress,
|
||||
#progress.yt-page-navigation-progress {
|
||||
top: 30px !important;
|
||||
}
|
||||
|
||||
/* custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
background-color: #030303;
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
|
||||
/* hover effect for both scrollbar area, and scrollbar 'thumb' */
|
||||
::-webkit-scrollbar:hover {
|
||||
background-color: rgba(15, 15, 15, 0.699);
|
||||
}
|
||||
|
||||
/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
||||
::-webkit-scrollbar-thumb:vertical {
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
|
||||
background: #3a3a3a;
|
||||
background-clip: padding-box;
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:vertical:active {
|
||||
background: #4d4c4c; /* some darker color when you click it */
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
|
||||
.cet-menubar-menu-container .cet-action-item {
|
||||
background-color: inherit
|
||||
}
|
||||
|
||||
/** hideMenu toggler **/
|
||||
.cet-window-icon {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.cet-window-icon img {
|
||||
-webkit-user-drag: none;
|
||||
filter: invert(50%);
|
||||
}
|
||||
|
||||
/** make navbar draggable **/
|
||||
#nav-bar-background {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
ytmusic-nav-bar input,
|
||||
ytmusic-nav-bar span,
|
||||
ytmusic-nav-bar [role="button"],
|
||||
ytmusic-nav-bar yt-icon,
|
||||
tp-yt-iron-dropdown {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
148
plugins/in-app-menu/titlebar.css
Normal file
148
plugins/in-app-menu/titlebar.css
Normal file
@ -0,0 +1,148 @@
|
||||
:root {
|
||||
--titlebar-background-color: #030303;
|
||||
--menu-bar-height: 36px;
|
||||
}
|
||||
|
||||
title-bar {
|
||||
-webkit-app-region: drag;
|
||||
box-sizing: border-box;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 10000000;
|
||||
|
||||
width: 100%;
|
||||
height: var(--menu-bar-height, 36px);
|
||||
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
padding-left: var(--offset-left, 12px);
|
||||
background-color: var(--titlebar-background-color, #030303);
|
||||
user-select: none;
|
||||
|
||||
transition: opacity 200ms ease 0s, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
|
||||
}
|
||||
|
||||
menu-button {
|
||||
-webkit-app-region: none;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
menu-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
menu-panel {
|
||||
position: fixed;
|
||||
top: var(--y, 0);
|
||||
left: var(--x, 0);
|
||||
|
||||
max-height: calc(100vh - var(--menu-bar-height, 36px) - 16px - var(--y, 0));
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
|
||||
overflow: auto;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
background-color: color-mix(in srgb, var(--titlebar-background-color, #030303) 50%, rgba(0, 0, 0, 0.1));
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transform-origin: top left;
|
||||
|
||||
transition: opacity 200ms ease 0s, transform 200ms ease 0s;
|
||||
}
|
||||
menu-panel[open="true"] {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
menu-item {
|
||||
-webkit-app-region: none;
|
||||
min-height: 32px;
|
||||
height: 32px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr minmax(32px, auto);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
menu-item > menu-icon {
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
menu-separator {
|
||||
min-height: 1px;
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* classes */
|
||||
|
||||
.title-bar-icon {
|
||||
height: calc(100% - 8px);
|
||||
object-fit: cover;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
/* youtube-music style */
|
||||
|
||||
ytmusic-app-layout {
|
||||
margin-top: var(--menu-bar-height, 36px) !important;
|
||||
}
|
||||
|
||||
ytmusic-app-layout>[slot=nav-bar], #nav-bar-background.ytmusic-app-layout {
|
||||
top: var(--menu-bar-height, 36px) !important;
|
||||
}
|
||||
#nav-bar-divider.ytmusic-app-layout {
|
||||
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important;
|
||||
}
|
||||
ytmusic-app[is-bauhaus-sidenav-enabled] #guide-spacer.ytmusic-app,
|
||||
ytmusic-app[is-bauhaus-sidenav-enabled] #mini-guide-spacer.ytmusic-app {
|
||||
margin-top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important;
|
||||
}
|
||||
|
||||
ytmusic-app-layout>[slot=player-page] {
|
||||
margin-top: var(--menu-bar-height);
|
||||
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height) - var(--ytmusic-player-bar-height)) !important;
|
||||
}
|
||||
|
||||
ytmusic-guide-renderer {
|
||||
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height)) !important;
|
||||
}
|
||||
@ -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';
|
||||
@ -15,14 +14,14 @@ import type { ConfigType } from '../../config/dynamic';
|
||||
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||
let revRomanized = false;
|
||||
|
||||
export type LyricGeniusType = ConfigType<'lyric-genius'>;
|
||||
export type LyricGeniusType = ConfigType<'lyrics-genius'>;
|
||||
|
||||
export default (win: BrowserWindow, options: LyricGeniusType) => {
|
||||
if (options.romanizedLyrics) {
|
||||
revRomanized = true;
|
||||
}
|
||||
|
||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||
injectCSS(win.webContents, style);
|
||||
|
||||
ipcMain.handle('search-genius-lyrics', async (_, extractedSongInfo: SongInfo) => {
|
||||
const metadata = extractedSongInfo;
|
||||
|
||||
@ -3,8 +3,9 @@ import { BrowserWindow, MenuItem } from 'electron';
|
||||
import { LyricGeniusType, toggleRomanized } from './back';
|
||||
|
||||
import { setOptions } from '../../config/plugins';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (_: BrowserWindow, options: LyricGeniusType) => [
|
||||
export default (_: BrowserWindow, options: LyricGeniusType): MenuTemplate => [
|
||||
{
|
||||
label: 'Romanized Lyrics',
|
||||
type: 'checkbox',
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { Actions, triggerAction } from '../utils';
|
||||
|
||||
export const CHANNEL = 'navigation';
|
||||
export const ACTIONS = Actions;
|
||||
|
||||
export function goToNextPage() {
|
||||
triggerAction(CHANNEL, Actions.NEXT);
|
||||
}
|
||||
// for HTML
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||
(global as any).goToNextPage = goToNextPage;
|
||||
|
||||
export function goToPreviousPage() {
|
||||
triggerAction(CHANNEL, Actions.BACK);
|
||||
}
|
||||
// for HTML
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||
(global as any).goToPreviousPage = goToPreviousPage;
|
||||
|
||||
export default {
|
||||
CHANNEL,
|
||||
ACTIONS,
|
||||
actions: {
|
||||
goToNextPage,
|
||||
goToPreviousPage,
|
||||
},
|
||||
};
|
||||
@ -1,39 +1,13 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { ACTIONS, CHANNEL } from './actions';
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS, listenAction } from '../utils';
|
||||
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');
|
||||
});
|
||||
|
||||
listenAction(CHANNEL, (_, action) => {
|
||||
switch (action) {
|
||||
case ACTIONS.NEXT: {
|
||||
if (win.webContents.canGoForward()) {
|
||||
win.webContents.goForward();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.BACK: {
|
||||
if (win.webContents.canGoBack()) {
|
||||
win.webContents.goBack();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log('Unknown action: ' + action);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default handle;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div
|
||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||
onclick="goToPreviousPage()"
|
||||
onclick="history.back()"
|
||||
role="tab"
|
||||
tab-id="FEmusic_back"
|
||||
>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div
|
||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||
onclick="goToNextPage()"
|
||||
onclick="history.forward()"
|
||||
role="tab"
|
||||
tab-id="FEmusic_next"
|
||||
>
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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<typeof getSongControls>;
|
||||
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`)}"
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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, () => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -55,7 +55,7 @@ function firstRun() {
|
||||
setTooltip(options.savedVolume);
|
||||
|
||||
if (api.getVolume() !== options.savedVolume) {
|
||||
api.setVolume(options.savedVolume);
|
||||
setVolume(options.savedVolume);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
},
|
||||
{
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { TouchBar, NativeImage, BrowserWindow } from 'electron';
|
||||
import registerCallback from '../../providers/song-info';
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
const {
|
||||
TouchBarButton,
|
||||
TouchBarLabel,
|
||||
@ -61,7 +62,7 @@ const touchBar = new TouchBar({
|
||||
],
|
||||
});
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
|
||||
const { playPause, next, previous, dislike, like } = getSongControls(win);
|
||||
|
||||
// If the page is ready, register the callback
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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<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?.();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,18 +56,12 @@ function setup(e: CustomEvent<YoutubePlayer>) {
|
||||
|
||||
$<HTMLVideoElement>('#player')?.prepend(switchButtonDiv);
|
||||
|
||||
if (options.hideVideo) {
|
||||
const checkbox = $<HTMLInputElement>('.video-switch-button-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
changeDisplay(false);
|
||||
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
|
||||
switchButtonDiv.addEventListener('click', (e) => {
|
||||
@ -77,9 +71,8 @@ function setup(e: CustomEvent<YoutubePlayer>) {
|
||||
// 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<YoutubePlayer>) {
|
||||
}
|
||||
}
|
||||
|
||||
function changeDisplay(showVideo: boolean) {
|
||||
function setVideoState(showVideo: boolean) {
|
||||
options.hideVideo = !showVideo;
|
||||
setOptions('video-toggle', options);
|
||||
|
||||
const checkbox = $<HTMLInputElement>('.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 && $<HTMLElement>('#song-video.ytmusic-player')?.style.display === 'none') {
|
||||
changeDisplay(true);
|
||||
setVideoState(true);
|
||||
} else {
|
||||
moveVolumeHud(!options.hideVideo);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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 => [
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
5
plugins/visualizer/visualizers/index.ts
Normal file
5
plugins/visualizer/visualizers/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import ButterchurnVisualizer from './butterchurn';
|
||||
import VudioVisualizer from './vudio';
|
||||
import WaveVisualizer from './wave';
|
||||
|
||||
export { ButterchurnVisualizer, VudioVisualizer, WaveVisualizer };
|
||||
@ -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(
|
||||
|
||||
@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
|
||||
import type { ConfigType } from '../../../config/dynamic';
|
||||
|
||||
class VudioVisualizer extends Visualizer<Vudio> {
|
||||
name = 'vudio';
|
||||
|
||||
visualizer: Vudio;
|
||||
|
||||
constructor(
|
||||
|
||||
@ -5,6 +5,8 @@ import { Visualizer } from './visualizer';
|
||||
import type { ConfigType } from '../../../config/dynamic';
|
||||
|
||||
class WaveVisualizer extends Visualizer<Wave> {
|
||||
name = 'wave';
|
||||
|
||||
visualizer: Wave;
|
||||
|
||||
constructor(
|
||||
|
||||
135
preload.ts
135
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';
|
||||
|
||||
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';
|
||||
|
||||
const plugins = config.plugins.getEnabled();
|
||||
import adblockerPreload from './plugins/adblocker/preload';
|
||||
import preciseVolumePreload from './plugins/precise-volume/preload';
|
||||
|
||||
import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic';
|
||||
|
||||
type PluginMapper<Type extends 'renderer' | 'preload' | 'backend'> = {
|
||||
[Key in OneOfDefaultConfigKey]?: (
|
||||
Type extends 'renderer' ? (options: ConfigType<Key>) => (Promise<void> | void) :
|
||||
Type extends 'preload' ? () => (Promise<void> | 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<string, string>,
|
||||
actions: Record<string, () => void>,
|
||||
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)}`);
|
||||
}
|
||||
|
||||
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<void>;
|
||||
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];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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<void>;
|
||||
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
|
||||
|
||||
@ -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<HTMLElement>('#container')?.style;
|
||||
if (mainStyle) {
|
||||
mainStyle.width = '100%';
|
||||
mainStyle.position = 'fixed';
|
||||
mainStyle.border = 'unset';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = customTitlebarFunction;
|
||||
export default customTitlebarFunction;
|
||||
@ -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,
|
||||
};
|
||||
|
||||
68
readme.md
68
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:
|
||||
|
||||
```node
|
||||
module.exports = win => {
|
||||
// win is the BrowserWindow object
|
||||
```typescript
|
||||
// file: back.ts
|
||||
export default (win: Electron.BrowserWindow, config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
|
||||
// something
|
||||
};
|
||||
```
|
||||
|
||||
- if you need to change the front, create a file `front.js` with the following template:
|
||||
then, register the plugin in `index.ts`:
|
||||
|
||||
```node
|
||||
module.exports = () => {
|
||||
```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 with the following template:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
65
rollup.main.config.ts
Normal file
65
rollup.main.config.ts
Normal file
@ -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,
|
||||
],
|
||||
});
|
||||
58
rollup.preload.config.ts
Normal file
58
rollup.preload.config.ts
Normal file
@ -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,
|
||||
],
|
||||
});
|
||||
@ -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);
|
||||
|
||||
|
||||
4
tray.ts
4
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,
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": [
|
||||
"*.config.ts",
|
||||
"./dist"
|
||||
],
|
||||
"paths": {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
37
youtube-music.d.ts
vendored
Normal file
37
youtube-music.d.ts
vendored
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user