mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b843825f72 | |||
| b66d3bc3d4 | |||
| 9adabd41d9 | |||
| 3f3df09819 | |||
| 1f5f597561 | |||
| 91e4433aba | |||
| 2d3ce4a8b3 | |||
| 971b7f05c5 | |||
| bb6115fec1 | |||
| 2a6dc30366 | |||
| 5e2d843742 | |||
| 7aaef26cc8 | |||
| 0a08eaaa3c | |||
| 0d22446f20 | |||
| a0543d15a6 | |||
| e62ee35b42 | |||
| ef6fb402bf | |||
| a8301f44be | |||
| 1ead86a220 | |||
| 03e716fe17 | |||
| f0bb328981 | |||
| f40183f0ca | |||
| 5b004acdc1 | |||
| 0f96da9928 | |||
| dfba3d9c2d | |||
| d9c51063f4 | |||
| cd9012691a | |||
| 2499f574ef | |||
| e7e873866d | |||
| 4ccbc741b8 | |||
| 8ec965a1a4 | |||
| 0936e9a258 | |||
| 32a5597573 | |||
| 9932fd7647 | |||
| 68429be1ce | |||
| d7ac493337 | |||
| 686a0a340e | |||
| bba499044b | |||
| 6e1c50ede1 | |||
| 6e739e2723 | |||
| 86029a0a73 | |||
| b458925aa6 | |||
| 86a1c3c850 | |||
| 8666f934cd | |||
| 59a93916a8 | |||
| f06a3c8c70 | |||
| 7fe937b21e | |||
| a4aa22aae9 | |||
| 96b2aab683 | |||
| 54d25a26c7 | |||
| f5622970c6 | |||
| ea09825ece | |||
| 247764b64b | |||
| 5e187b47d8 | |||
| 1194befa48 | |||
| 74d3358487 | |||
| 769a613ea5 | |||
| 7280e02709 | |||
| 7b3a767003 | |||
| 96b0d4e367 | |||
| ae8365f721 | |||
| 8d85bbf5ec | |||
| 61cd2ef9dc | |||
| 3394d647a1 | |||
| 882ad63fa8 | |||
| 5fd88ce522 | |||
| de280195c5 | |||
| 7bd69e447a | |||
| 357f12c4d1 | |||
| d164cd6fb9 | |||
| 5d3dc6442f | |||
| cb7c9bda16 | |||
| 6f2552814f | |||
| 9beebd3772 | |||
| 7cd9506122 | |||
| f6de5c7c22 | |||
| 2ac3df0455 | |||
| 2dfe098521 | |||
| 77d4e9cb84 | |||
| b420998458 | |||
| feb06b015e | |||
| 09ba760aff | |||
| 0f192aab2b | |||
| c992ec4607 | |||
| 30840804fa | |||
| 8000a8326f | |||
| d23bfe9368 | |||
| 047085e72b | |||
| 768ec7bda7 | |||
| c25a6f9d2a | |||
| 23058729f3 | |||
| a1c6dfb199 | |||
| 89ebc230e0 | |||
| b4b785d773 | |||
| 57ec0a463d | |||
| 6be9b76550 | |||
| ebe3baf4bc | |||
| 648d540ca9 | |||
| cb910a6fd7 | |||
| e071f768b4 | |||
| 05b6435a5c | |||
| 71e9f280a1 | |||
| dbc34e6d0d | |||
| d0532d691e | |||
| 2f218ef108 | |||
| 14326d2440 | |||
| d37e557f79 | |||
| 5ca0c6b8a9 | |||
| e58a580b2b | |||
| f3641f5072 | |||
| 296ecb6740 | |||
| 28b5645a56 | |||
| 742a949680 | |||
| 57290c4164 | |||
| 6d5fe9561e | |||
| 735901095f | |||
| 27454ab527 | |||
| c345d2cb34 | |||
| 1da297a356 | |||
| 8ebdaf6fa0 | |||
| d4d82867f5 | |||
| 2e99d6b9bb | |||
| a1e740b881 | |||
| de14d64927 | |||
| 6c93d635d0 | |||
| f4df6fceee | |||
| d69c8a754e | |||
| c3d90d8b27 | |||
| 10681e4e99 | |||
| 48aa3ba0d8 | |||
| f98e4ea749 | |||
| dc500efb79 | |||
| 8d9dafb149 | |||
| 4ddd2f339b | |||
| d2265b59d7 | |||
| d47b03c23d | |||
| 4c857cb9e9 | |||
| c31f6cc797 | |||
| 0d3fa261a7 | |||
| b6ee861166 | |||
| f9cf12b7d3 | |||
| bed8d0a7f2 | |||
| afac520ff8 | |||
| 1332c66050 | |||
| 7f08579671 | |||
| d5e4f3af46 | |||
| bdceb4d462 | |||
| 2758a44965 | |||
| 704fba9aba | |||
| 5cffb6f062 | |||
| 407887254f | |||
| 47729130c9 | |||
| 7088941179 | |||
| ff39ddb277 | |||
| 1eeaf1dd0a | |||
| 9abf7a77d8 | |||
| 5bd97685b9 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -13,18 +13,18 @@ jobs:
|
|||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "14.x"
|
node-version: "16.x"
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v3
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
@ -91,6 +91,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: GabrielBB/xvfb-action@v1
|
uses: GabrielBB/xvfb-action@v1
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
with:
|
with:
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.4 KiB |
61
changelog.md
61
changelog.md
@ -2,8 +2,67 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||||
|
|
||||||
|
#### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0)
|
||||||
|
|
||||||
|
- Bump ytdl-core (bug fix) [`#816`](https://github.com/th-ch/youtube-music/pull/816)
|
||||||
|
- Bump electron and fix tests in CI [`#813`](https://github.com/th-ch/youtube-music/pull/813)
|
||||||
|
- Allow user to pass custom CSS file [`#800`](https://github.com/th-ch/youtube-music/pull/800)
|
||||||
|
- [Snyk] Upgrade html-to-text from 8.2.0 to 8.2.1 [`#799`](https://github.com/th-ch/youtube-music/pull/799)
|
||||||
|
- [Snyk] Upgrade electron-store from 8.0.1 to 8.0.2 [`#772`](https://github.com/th-ch/youtube-music/pull/772)
|
||||||
|
- Bump jpeg-js from 0.4.3 to 0.4.4 [`#756`](https://github.com/th-ch/youtube-music/pull/756)
|
||||||
|
- Support MPRIS loop and volume change [`#749`](https://github.com/th-ch/youtube-music/pull/749)
|
||||||
|
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8 [`#742`](https://github.com/th-ch/youtube-music/pull/742)
|
||||||
|
- Use ; instead of space for play/pause. [`#745`](https://github.com/th-ch/youtube-music/pull/745)
|
||||||
|
- Update readme.md [`#750`](https://github.com/th-ch/youtube-music/pull/750)
|
||||||
|
- fix lyrics font size [`#753`](https://github.com/th-ch/youtube-music/pull/753)
|
||||||
|
- fix top gap between nav-bar and browse-page [`#734`](https://github.com/th-ch/youtube-music/pull/734)
|
||||||
|
- migrate from remote to ipc + fix restart in portable app [`#605`](https://github.com/th-ch/youtube-music/pull/605)
|
||||||
|
- [Snyk] Upgrade custom-electron-prompt from 1.4.2 to 1.5.0 [`#717`](https://github.com/th-ch/youtube-music/pull/717)
|
||||||
|
- Picture in Picture v2 [`#685`](https://github.com/th-ch/youtube-music/pull/685)
|
||||||
|
- Add MPRIS volume control [`#776`](https://github.com/th-ch/youtube-music/issues/776)
|
||||||
|
- Remove jest [`bb6115f`](https://github.com/th-ch/youtube-music/commit/bb6115fec1a18a416edb365a442eb0b0ee330768)
|
||||||
|
- migrate from remote to ipc [`5bd9768`](https://github.com/th-ch/youtube-music/commit/5bd97685b9e07c656e0b57a9e02819afc70af1b1)
|
||||||
|
- v3 [`d23bfe9`](https://github.com/th-ch/youtube-music/commit/d23bfe936840b947ca101fd304464f65d36e88cc)
|
||||||
|
|
||||||
|
#### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0)
|
||||||
|
|
||||||
|
> 16 May 2022
|
||||||
|
|
||||||
|
- Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712)
|
||||||
|
- fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693)
|
||||||
|
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to 1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689)
|
||||||
|
- [Snyk] Upgrade custom-electron-prompt from 1.4.1 to 1.4.2 [`#686`](https://github.com/th-ch/youtube-music/pull/686)
|
||||||
|
- [Snyk] Upgrade @electron/remote from 2.0.7 to 2.0.8 [`#684`](https://github.com/th-ch/youtube-music/pull/684)
|
||||||
|
- Improve plugin submenu ux [`#699`](https://github.com/th-ch/youtube-music/pull/699)
|
||||||
|
- update build action [`#702`](https://github.com/th-ch/youtube-music/pull/702)
|
||||||
|
- add different modes to video-toggle plugin [`#700`](https://github.com/th-ch/youtube-music/pull/700)
|
||||||
|
- lint [`#701`](https://github.com/th-ch/youtube-music/pull/701)
|
||||||
|
- [ImgBot] Optimize images [`#703`](https://github.com/th-ch/youtube-music/pull/703)
|
||||||
|
- add album to lastfm if available [`#695`](https://github.com/th-ch/youtube-music/pull/695)
|
||||||
|
- [in-app-menu] add hide icon option [`#680`](https://github.com/th-ch/youtube-music/pull/680)
|
||||||
|
- Add plugin to bypass age restrictions [`#682`](https://github.com/th-ch/youtube-music/pull/682)
|
||||||
|
- Add "Picture in picture" plugin [`#674`](https://github.com/th-ch/youtube-music/pull/674)
|
||||||
|
- Set lyrics metadata from Genius [`#679`](https://github.com/th-ch/youtube-music/pull/679)
|
||||||
|
- MacOS: bring back the app in dock when using tray + app hidden [`#677`](https://github.com/th-ch/youtube-music/pull/677)
|
||||||
|
- [Snyk] Upgrade @electron/remote from 2.0.4 to 2.0.5 [`#644`](https://github.com/th-ch/youtube-music/pull/644)
|
||||||
|
- [Snyk] Upgrade ytpl from 2.2.3 to 2.3.0 [`#660`](https://github.com/th-ch/youtube-music/pull/660)
|
||||||
|
- [Snyk] Upgrade ytdl-core from 4.10.1 to 4.11.0 [`#659`](https://github.com/th-ch/youtube-music/pull/659)
|
||||||
|
- Bump plist from 3.0.2 to 3.0.5 [`#678`](https://github.com/th-ch/youtube-music/pull/678)
|
||||||
|
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.4 to 1.23.5 [`#624`](https://github.com/th-ch/youtube-music/pull/624)
|
||||||
|
- [Precise-Volume] fix volumeHud position in miniplayer [`#645`](https://github.com/th-ch/youtube-music/pull/645)
|
||||||
|
- add always-on-top option [`#655`](https://github.com/th-ch/youtube-music/pull/655)
|
||||||
|
- [precise-volume] fix expand-volume-slider not updating its value [`#670`](https://github.com/th-ch/youtube-music/pull/670)
|
||||||
|
- Fix lyrics genius missing parts [`#671`](https://github.com/th-ch/youtube-music/pull/671)
|
||||||
|
- feat: option to force show like buttons [`#673`](https://github.com/th-ch/youtube-music/pull/673)
|
||||||
|
- fix custom titlebar in prompt options [`#619`](https://github.com/th-ch/youtube-music/pull/619)
|
||||||
|
- Process lyrics HTML in Genius util [`d0532d6`](https://github.com/th-ch/youtube-music/commit/d0532d691e56f955ef0b41f5fe2efe6295dddf9e)
|
||||||
|
- Create first version of picture in picture plugin [`d2265b5`](https://github.com/th-ch/youtube-music/commit/d2265b59d78143cf51fe4dc3d5dee9da66873cc1)
|
||||||
|
- Bump electron-builder to fix Mac build script [`ae8365f`](https://github.com/th-ch/youtube-music/commit/ae8365f721eafda6c502d02eee86d098f2b9e2a1)
|
||||||
|
|
||||||
#### [v1.16.0](https://github.com/th-ch/youtube-music/compare/v1.15.0...v1.16.0)
|
#### [v1.16.0](https://github.com/th-ch/youtube-music/compare/v1.15.0...v1.16.0)
|
||||||
|
|
||||||
|
> 20 February 2022
|
||||||
|
|
||||||
- update in-app-menu [`#596`](https://github.com/th-ch/youtube-music/pull/596)
|
- update in-app-menu [`#596`](https://github.com/th-ch/youtube-music/pull/596)
|
||||||
- Fix clientID [`#602`](https://github.com/th-ch/youtube-music/pull/602)
|
- Fix clientID [`#602`](https://github.com/th-ch/youtube-music/pull/602)
|
||||||
- Add snoretoast custom compile script [`#600`](https://github.com/th-ch/youtube-music/pull/600)
|
- Add snoretoast custom compile script [`#600`](https://github.com/th-ch/youtube-music/pull/600)
|
||||||
@ -33,7 +92,7 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
- xesam:artist should be a list [`#539`](https://github.com/th-ch/youtube-music/pull/539)
|
- xesam:artist should be a list [`#539`](https://github.com/th-ch/youtube-music/pull/539)
|
||||||
- fix notifications showing thumbnail of last song [`#537`](https://github.com/th-ch/youtube-music/pull/537)
|
- fix notifications showing thumbnail of last song [`#537`](https://github.com/th-ch/youtube-music/pull/537)
|
||||||
- Fix https://github.com/th-ch/youtube-music/pull/578#issuecomment-1035517531 [`#578`](https://github.com/th-ch/youtube-music/pull/578)
|
- Fix https://github.com/th-ch/youtube-music/pull/578#issuecomment-1035517531 [`#578`](https://github.com/th-ch/youtube-music/pull/578)
|
||||||
- Add automatic changelog [`06245fe`](https://github.com/th-ch/youtube-music/commit/06245fe120d92b2f3e94ecb2ea14cfb1f6c8bbb9)
|
- Add automatic changelog [`1d9bfe8`](https://github.com/th-ch/youtube-music/commit/1d9bfe8ac8869cde648164979986964baa52c2f9)
|
||||||
- update electron to v17.0.0 [`fef7115`](https://github.com/th-ch/youtube-music/commit/fef711549fa9862f8ea23301edde747c5802e352)
|
- update electron to v17.0.0 [`fef7115`](https://github.com/th-ch/youtube-music/commit/fef711549fa9862f8ea23301edde747c5802e352)
|
||||||
- update dependencies [`8be07bc`](https://github.com/th-ch/youtube-music/commit/8be07bcb7ad8b727d97c36aa0760aed4e2fc481f)
|
- update dependencies [`8be07bc`](https://github.com/th-ch/youtube-music/commit/8be07bcb7ad8b727d97c36aa0760aed4e2fc481f)
|
||||||
|
|
||||||
|
|||||||
@ -80,8 +80,16 @@ const defaultConfig = {
|
|||||||
},
|
},
|
||||||
"video-toggle": {
|
"video-toggle": {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
mode: "custom",
|
||||||
forceHide: false,
|
forceHide: false,
|
||||||
},
|
},
|
||||||
|
"picture-in-picture": {
|
||||||
|
"enabled": false,
|
||||||
|
"alwaysOnTop": true,
|
||||||
|
"savePosition": true,
|
||||||
|
"saveSize": false,
|
||||||
|
"hotkey": "P"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,20 @@ const Store = require("electron-store");
|
|||||||
|
|
||||||
const defaults = require("./defaults");
|
const defaults = require("./defaults");
|
||||||
|
|
||||||
|
const setDefaultPluginOptions = (store, plugin) => {
|
||||||
|
if (!store.get(`plugins.${plugin}`)) {
|
||||||
|
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const migrations = {
|
const migrations = {
|
||||||
|
">=1.17.0": (store) => {
|
||||||
|
setDefaultPluginOptions(store, "picture-in-picture");
|
||||||
|
|
||||||
|
if (store.get("plugins.video-toggle.mode") === undefined) {
|
||||||
|
store.set("plugins.video-toggle.mode", "custom");
|
||||||
|
}
|
||||||
|
},
|
||||||
">=1.14.0": (store) => {
|
">=1.14.0": (store) => {
|
||||||
if (
|
if (
|
||||||
typeof store.get("plugins.precise-volume.globalShortcuts") !== "object"
|
typeof store.get("plugins.precise-volume.globalShortcuts") !== "object"
|
||||||
|
|||||||
78
index.js
78
index.js
@ -2,8 +2,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
const remote = require('@electron/remote/main');
|
|
||||||
remote.initialize();
|
|
||||||
const enhanceWebRequest = require("electron-better-web-request").default;
|
const enhanceWebRequest = require("electron-better-web-request").default;
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const unhandled = require("electron-unhandled");
|
const unhandled = require("electron-unhandled");
|
||||||
@ -15,6 +13,7 @@ const { fileExists, injectCSS } = require("./plugins/utils");
|
|||||||
const { isTesting } = require("./utils/testing");
|
const { isTesting } = require("./utils/testing");
|
||||||
const { setUpTray } = require("./tray");
|
const { setUpTray } = require("./tray");
|
||||||
const { setupSongInfo } = require("./providers/song-info");
|
const { setupSongInfo } = require("./providers/song-info");
|
||||||
|
const { setupAppControls, restart } = require("./providers/app-controls");
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -85,6 +84,22 @@ function onClosed() {
|
|||||||
|
|
||||||
function loadPlugins(win) {
|
function loadPlugins(win) {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||||
|
// Load user CSS
|
||||||
|
const themes = config.get("options.themes");
|
||||||
|
if (Array.isArray(themes)) {
|
||||||
|
themes.forEach((cssFile) => {
|
||||||
|
fileExists(
|
||||||
|
cssFile,
|
||||||
|
() => {
|
||||||
|
injectCSS(win.webContents, cssFile);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
win.webContents.once("did-finish-load", () => {
|
win.webContents.once("did-finish-load", () => {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log("did finish load");
|
console.log("did finish load");
|
||||||
@ -120,14 +135,13 @@ function createMainWindow() {
|
|||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
|
||||||
affinity: "main-window", // main window, and addition windows should work in one process
|
affinity: "main-window", // main window, and addition windows should work in one process
|
||||||
...(isTesting()
|
...(!isTesting()
|
||||||
? {
|
? {
|
||||||
// Only necessary when testing with Spectron
|
// Sandbox is only enabled in tests for now
|
||||||
contextIsolation: false,
|
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
||||||
nodeIntegration: true,
|
sandbox: false,
|
||||||
}
|
}
|
||||||
: undefined),
|
: undefined),
|
||||||
},
|
},
|
||||||
frame: !is.macOS() && !useInlineMenu,
|
frame: !is.macOS() && !useInlineMenu,
|
||||||
@ -138,7 +152,6 @@ function createMainWindow() {
|
|||||||
: "default",
|
: "default",
|
||||||
autoHideMenuBar: config.get("options.hideMenu"),
|
autoHideMenuBar: config.get("options.hideMenu"),
|
||||||
});
|
});
|
||||||
remote.enable(win.webContents);
|
|
||||||
|
|
||||||
if (windowPosition) {
|
if (windowPosition) {
|
||||||
const { x, y } = windowPosition;
|
const { x, y } = windowPosition;
|
||||||
@ -165,44 +178,67 @@ function createMainWindow() {
|
|||||||
win.maximize();
|
win.maximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(config.get("options.alwaysOnTop")){
|
||||||
|
win.setAlwaysOnTop(true);
|
||||||
|
}
|
||||||
|
|
||||||
const urlToLoad = config.get("options.resumeOnStart")
|
const urlToLoad = config.get("options.resumeOnStart")
|
||||||
? config.get("url")
|
? config.get("url")
|
||||||
: config.defaultConfig.url;
|
: config.defaultConfig.url;
|
||||||
win.webContents.loadURL(urlToLoad);
|
win.webContents.loadURL(urlToLoad);
|
||||||
win.on("closed", onClosed);
|
win.on("closed", onClosed);
|
||||||
|
|
||||||
|
const setPiPOptions = config.plugins.isEnabled("picture-in-picture")
|
||||||
|
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value })
|
||||||
|
: () => {};
|
||||||
|
|
||||||
win.on("move", () => {
|
win.on("move", () => {
|
||||||
if (win.isMaximized()) return;
|
if (win.isMaximized()) return;
|
||||||
let position = win.getPosition();
|
let position = win.getPosition();
|
||||||
lateSave("window-position", { x: position[0], y: position[1] });
|
const isPiPEnabled =
|
||||||
|
config.plugins.isEnabled("picture-in-picture") &&
|
||||||
|
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||||
|
if (!isPiPEnabled) {
|
||||||
|
lateSave("window-position", { x: position[0], y: position[1] });
|
||||||
|
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) {
|
||||||
|
lateSave("pip-position", position, setPiPOptions);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let winWasMaximized;
|
let winWasMaximized;
|
||||||
|
|
||||||
win.on("resize", () => {
|
win.on("resize", () => {
|
||||||
const windowSize = win.getSize();
|
const windowSize = win.getSize();
|
||||||
|
|
||||||
const isMaximized = win.isMaximized();
|
const isMaximized = win.isMaximized();
|
||||||
if (winWasMaximized !== isMaximized) {
|
|
||||||
|
const isPiPEnabled =
|
||||||
|
config.plugins.isEnabled("picture-in-picture") &&
|
||||||
|
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||||
|
|
||||||
|
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
||||||
winWasMaximized = isMaximized;
|
winWasMaximized = isMaximized;
|
||||||
config.set("window-maximized", isMaximized);
|
config.set("window-maximized", isMaximized);
|
||||||
}
|
}
|
||||||
if (!isMaximized) {
|
if (isMaximized) return;
|
||||||
|
|
||||||
|
if (!isPiPEnabled) {
|
||||||
lateSave("window-size", {
|
lateSave("window-size", {
|
||||||
width: windowSize[0],
|
width: windowSize[0],
|
||||||
height: windowSize[1],
|
height: windowSize[1],
|
||||||
});
|
});
|
||||||
|
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) {
|
||||||
|
lateSave("pip-size", windowSize, setPiPOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let savedTimeouts = {};
|
let savedTimeouts = {};
|
||||||
function lateSave(key, value) {
|
function lateSave(key, value, fn = config.set) {
|
||||||
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
|
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
|
||||||
|
|
||||||
savedTimeouts[key] = setTimeout(() => {
|
savedTimeouts[key] = setTimeout(() => {
|
||||||
config.set(key, value);
|
fn(key, value);
|
||||||
savedTimeouts[key] = undefined;
|
savedTimeouts[key] = undefined;
|
||||||
}, 1000)
|
}, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
win.webContents.on("render-process-gone", (event, webContents, details) => {
|
win.webContents.on("render-process-gone", (event, webContents, details) => {
|
||||||
@ -249,6 +285,7 @@ app.once("browser-window-created", (event, win) => {
|
|||||||
|
|
||||||
setupSongInfo(win);
|
setupSongInfo(win);
|
||||||
loadPlugins(win);
|
loadPlugins(win);
|
||||||
|
setupAppControls();
|
||||||
|
|
||||||
win.webContents.on("did-fail-load", (
|
win.webContents.on("did-fail-load", (
|
||||||
_event,
|
_event,
|
||||||
@ -436,13 +473,8 @@ function showUnresponsiveDialog(win, details) {
|
|||||||
cancelId: 0
|
cancelId: 0
|
||||||
}).then( result => {
|
}).then( result => {
|
||||||
switch (result.response) {
|
switch (result.response) {
|
||||||
case 1: //if relaunch - relaunch+exit
|
case 1: restart(); break;
|
||||||
app.relaunch();
|
case 2: app.quit(); break;
|
||||||
case 2:
|
|
||||||
app.quit();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
69
menu.js
69
menu.js
@ -3,6 +3,7 @@ const path = require("path");
|
|||||||
|
|
||||||
const { app, Menu, dialog } = require("electron");
|
const { app, Menu, dialog } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
const { restart } = require("./providers/app-controls");
|
||||||
|
|
||||||
const { getAllPlugins } = require("./plugins/utils");
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
@ -51,6 +52,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
label: plugin,
|
label: plugin,
|
||||||
submenu: [
|
submenu: [
|
||||||
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu),
|
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu),
|
||||||
|
{ type: "separator" },
|
||||||
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -80,12 +82,53 @@ const mainMenuTemplate = (win) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Remove upgrade button",
|
label: "Visual Tweaks",
|
||||||
type: "checkbox",
|
submenu: [
|
||||||
checked: config.get("options.removeUpgradeButton"),
|
{
|
||||||
click: (item) => {
|
label: "Remove upgrade button",
|
||||||
config.setMenuOption("options.removeUpgradeButton", item.checked);
|
type: "checkbox",
|
||||||
},
|
checked: config.get("options.removeUpgradeButton"),
|
||||||
|
click: (item) => {
|
||||||
|
config.setMenuOption("options.removeUpgradeButton", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Force show like buttons",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.ForceShowLikeButtons"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.ForceShowLikeButtons", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Theme",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "No theme",
|
||||||
|
type: "radio",
|
||||||
|
checked: !config.get("options.themes"), // todo rename "themes"
|
||||||
|
click: () => {
|
||||||
|
config.set("options.themes", []);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Import custom CSS file",
|
||||||
|
type: "radio",
|
||||||
|
checked: false,
|
||||||
|
click: async () => {
|
||||||
|
const { filePaths } = await dialog.showOpenDialog({
|
||||||
|
filters: [{ name: "CSS Files", extensions: ["css"] }],
|
||||||
|
properties: ["openFile", "multiSelections"],
|
||||||
|
});
|
||||||
|
if (filePaths) {
|
||||||
|
config.set("options.themes", filePaths);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Single instance lock",
|
label: "Single instance lock",
|
||||||
@ -100,6 +143,15 @@ const mainMenuTemplate = (win) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Always on top",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.alwaysOnTop"),
|
||||||
|
click: (item) => {
|
||||||
|
config.setMenuOption("options.alwaysOnTop", item.checked);
|
||||||
|
win.setAlwaysOnTop(item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
...(is.windows() || is.linux()
|
...(is.windows() || is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -279,10 +331,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Restart App",
|
label: "Restart App",
|
||||||
click: () => {
|
click: restart
|
||||||
app.relaunch();
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ role: "quit" },
|
{ role: "quit" },
|
||||||
],
|
],
|
||||||
|
|||||||
33
package.json
33
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.16.0",
|
"version": "1.18.0",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "th-ch/youtube-music",
|
"repository": "th-ch/youtube-music",
|
||||||
@ -67,7 +67,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "playwright test",
|
||||||
|
"test:debug": "DEBUG=pw:browser* playwright test",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
||||||
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
||||||
@ -92,52 +93,54 @@
|
|||||||
"npm": "Please use yarn and not npm"
|
"npm": "Please use yarn and not npm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "^1.23.4",
|
"@cliqz/adblocker-electron": "^1.23.8",
|
||||||
"@electron/remote": "^2.0.4",
|
|
||||||
"@ffmpeg/core": "^0.10.0",
|
"@ffmpeg/core": "^0.10.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.10.1",
|
"@ffmpeg/ffmpeg": "^0.10.1",
|
||||||
|
"Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.4.6",
|
||||||
"async-mutex": "^0.3.2",
|
"async-mutex": "^0.3.2",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"custom-electron-prompt": "^1.4.1",
|
"custom-electron-prompt": "^1.5.0",
|
||||||
"custom-electron-titlebar": "^4.1.0",
|
"custom-electron-titlebar": "^4.1.0",
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"electron-better-web-request": "^1.0.1",
|
"electron-better-web-request": "^1.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-store": "^8.0.1",
|
"electron-store": "^8.0.2",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^4.0.1",
|
||||||
"electron-updater": "^4.6.3",
|
"electron-updater": "^4.6.3",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"hark": "^1.2.3",
|
"hark": "^1.2.3",
|
||||||
|
"html-to-text": "^8.2.1",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"node-notifier": "^10.0.1",
|
"node-notifier": "^10.0.1",
|
||||||
"ytdl-core": "^4.10.1",
|
"ytdl-core": "^4.11.1",
|
||||||
"ytpl": "^2.2.3"
|
"ytpl": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.25.1",
|
||||||
"auto-changelog": "^2.4.0",
|
"auto-changelog": "^2.4.0",
|
||||||
"electron": "^17.0.0",
|
"electron": "^20.1.1",
|
||||||
"electron-builder": "^22.14.5",
|
"electron-builder": "^23.0.3",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-icon-maker": "0.0.5",
|
"electron-icon-maker": "0.0.5",
|
||||||
"jest": "^27.3.1",
|
"playwright": "^1.25.1",
|
||||||
"playwright": "^1.17.1",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"xo": "^0.45.0"
|
"xo": "^0.45.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"glob-parent": "5.1.2",
|
"glob-parent": "5.1.2",
|
||||||
"minimist": "1.2.5",
|
"minimist": "1.2.6",
|
||||||
"yargs-parser": "18.1.3"
|
"yargs-parser": "18.1.3"
|
||||||
},
|
},
|
||||||
"auto-changelog": {
|
"auto-changelog": {
|
||||||
"hideCredit": true,
|
"hideCredit": true,
|
||||||
"package": true,
|
"package": true,
|
||||||
"output": "changelog.md"
|
"unreleased": true,
|
||||||
|
"output": "changelog.md"
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
"envs": [
|
"envs": [
|
||||||
|
|||||||
4
plugins/bypass-age-restrictions/front.js
Normal file
4
plugins/bypass-age-restrictions/front.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = () => {
|
||||||
|
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||||
|
require("simple-youtube-age-restriction-bypass/Simple-YouTube-Age-Restriction-Bypass.user.js");
|
||||||
|
};
|
||||||
@ -8,7 +8,9 @@ const registerCallback = require("../../providers/song-info");
|
|||||||
const { injectCSS, listenAction } = require("../utils");
|
const { injectCSS, listenAction } = require("../utils");
|
||||||
const { cropMaxWidth } = require("./utils");
|
const { cropMaxWidth } = require("./utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
const { isEnabled } = require("../../config/plugins");
|
||||||
const { getImage } = require("../../providers/song-info");
|
const { getImage } = require("../../providers/song-info");
|
||||||
|
const { fetchFromGenius } = require("../lyrics-genius/back");
|
||||||
|
|
||||||
const sendError = (win, error) => {
|
const sendError = (win, error) => {
|
||||||
win.setProgressBar(-1); // close progress bar
|
win.setProgressBar(-1); // close progress bar
|
||||||
@ -71,6 +73,15 @@ function handle(win) {
|
|||||||
description: ""
|
description: ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isEnabled("lyrics-genius")) {
|
||||||
|
const lyrics = await fetchFromGenius(songMetadata);
|
||||||
|
if (lyrics) {
|
||||||
|
writer.setFrame("USLT", {
|
||||||
|
description: lyrics,
|
||||||
|
lyrics: lyrics,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
writer.addTag();
|
writer.addTag();
|
||||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ const { join } = require("path");
|
|||||||
|
|
||||||
const Mutex = require("async-mutex").Mutex;
|
const Mutex = require("async-mutex").Mutex;
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
const remote = require('@electron/remote');
|
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const filenamify = require("filenamify");
|
const filenamify = require("filenamify");
|
||||||
|
|
||||||
@ -137,7 +136,7 @@ const toMP3 = async (
|
|||||||
safeVideoName + "." + extension
|
safeVideoName + "." + extension
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = options.downloadFolder || remote.app.getPath("downloads");
|
const folder = options.downloadFolder || await ipcRenderer.invoke('getDownloadsFolder');
|
||||||
const name = metadata.title
|
const name = metadata.title
|
||||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||||
: videoName;
|
: videoName;
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
const config = require("../../config");
|
const config = require("../../config");
|
||||||
const { Titlebar, Color } = require("custom-electron-titlebar");
|
const { Titlebar, Color } = require("custom-electron-titlebar");
|
||||||
|
const { isEnabled } = require("../../config/plugins");
|
||||||
function $(selector) { return document.querySelector(selector); }
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (options) => {
|
||||||
let visible = !config.get("options.hideMenu");
|
let visible = !config.get("options.hideMenu");
|
||||||
const bar = new Titlebar({
|
const bar = new Titlebar({
|
||||||
backgroundColor: Color.fromHex("#050505"),
|
backgroundColor: Color.fromHex("#050505"),
|
||||||
@ -14,6 +15,10 @@ module.exports = () => {
|
|||||||
bar.updateTitle(" ");
|
bar.updateTitle(" ");
|
||||||
document.title = "Youtube Music";
|
document.title = "Youtube Music";
|
||||||
|
|
||||||
|
const hideIcon = hide => $('.cet-window-icon').style.display = hide ? 'none' : 'flex';
|
||||||
|
|
||||||
|
if (options.hideIcon) hideIcon(true);
|
||||||
|
|
||||||
ipcRenderer.on("refreshMenu", (_, showMenu) => {
|
ipcRenderer.on("refreshMenu", (_, showMenu) => {
|
||||||
if (showMenu === undefined && !visible) return;
|
if (showMenu === undefined && !visible) return;
|
||||||
if (showMenu === false) {
|
if (showMenu === false) {
|
||||||
@ -25,6 +30,14 @@ module.exports = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isEnabled("picture-in-picture")) {
|
||||||
|
ipcRenderer.on("pip-toggle", (_, pipEnabled) => {
|
||||||
|
bar.refreshMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
|
||||||
|
|
||||||
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
// 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', () => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
setNavbarMargin();
|
setNavbarMargin();
|
||||||
|
|||||||
14
plugins/in-app-menu/menu.js
Normal file
14
plugins/in-app-menu/menu.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
|
||||||
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Hide Icon",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.hideIcon,
|
||||||
|
click: (item) => {
|
||||||
|
win.webContents.send("hideIcon", item.checked);
|
||||||
|
options.hideIcon = item.checked;
|
||||||
|
setOptions("in-app-menu", options);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
@ -13,6 +13,11 @@
|
|||||||
height: 75px !important;
|
height: 75px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fixes top gap between nav-bar and browse-page */
|
||||||
|
#browse-page {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||||
ytmusic-nav-bar,
|
ytmusic-nav-bar,
|
||||||
.tab-titleiron-icon,
|
.tab-titleiron-icon,
|
||||||
@ -57,10 +62,10 @@ yt-page-navigation-progress,
|
|||||||
|
|
||||||
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
||||||
::-webkit-scrollbar-thumb:vertical {
|
::-webkit-scrollbar-thumb:vertical {
|
||||||
background-clip: padding-box;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0);
|
border: 2px solid rgba(0, 0, 0, 0);
|
||||||
|
|
||||||
background: #3a3a3a;
|
background: #3a3a3a;
|
||||||
|
background-clip: padding-box;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
-moz-border-radius: 100px;
|
-moz-border-radius: 100px;
|
||||||
-webkit-border-radius: 100px;
|
-webkit-border-radius: 100px;
|
||||||
@ -71,3 +76,7 @@ yt-page-navigation-progress,
|
|||||||
-moz-border-radius: 100px;
|
-moz-border-radius: 100px;
|
||||||
-webkit-border-radius: 100px;
|
-webkit-border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cet-menubar-menu-container .cet-action-item {
|
||||||
|
background-color: inherit
|
||||||
|
}
|
||||||
|
|||||||
@ -89,6 +89,7 @@ const postSongDataToAPI = async (songInfo, config, data) => {
|
|||||||
track: songInfo.title,
|
track: songInfo.title,
|
||||||
duration: songInfo.songDuration,
|
duration: songInfo.songDuration,
|
||||||
artist: songInfo.artist,
|
artist: songInfo.artist,
|
||||||
|
...(songInfo.album ? { album: songInfo.album } : undefined), // will be undefined if current song is a video
|
||||||
api_key: config.api_key,
|
api_key: config.api_key,
|
||||||
sk: config.session_key,
|
sk: config.session_key,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
@ -157,4 +158,4 @@ const lastfm = async (_win, config) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = lastfm;
|
module.exports = lastfm;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const { join } = require("path");
|
|||||||
|
|
||||||
const { ipcMain } = require("electron");
|
const { ipcMain } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
const { convert } = require("html-to-text");
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
const { cleanupName } = require("../../providers/song-info");
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
@ -12,15 +13,14 @@ module.exports = async (win) => {
|
|||||||
|
|
||||||
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
||||||
const metadata = JSON.parse(extractedSongInfo);
|
const metadata = JSON.parse(extractedSongInfo);
|
||||||
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
|
event.returnValue = await fetchFromGenius(metadata);
|
||||||
metadata.title
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
event.returnValue = await fetchFromGenius(queryString);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFromGenius = async (queryString) => {
|
const fetchFromGenius = async (metadata) => {
|
||||||
|
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
|
||||||
|
metadata.title
|
||||||
|
)}`;
|
||||||
let response = await fetch(
|
let response = await fetch(
|
||||||
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(queryString)}`
|
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(queryString)}`
|
||||||
);
|
);
|
||||||
@ -46,5 +46,26 @@ const fetchFromGenius = async (queryString) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.text();
|
const html = await response.text();
|
||||||
|
const lyrics = convert(html, {
|
||||||
|
baseElements: {
|
||||||
|
selectors: ['[class^="Lyrics__Container"]', ".lyrics"],
|
||||||
|
},
|
||||||
|
selectors: [
|
||||||
|
{
|
||||||
|
selector: "a",
|
||||||
|
format: "linkFormatter",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
formatters: {
|
||||||
|
// Remove links by keeping only the content
|
||||||
|
linkFormatter: (elem, walk, builder) => {
|
||||||
|
walk(elem.children, builder);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return lyrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.fetchFromGenius = fetchFromGenius;
|
||||||
|
|||||||
@ -17,11 +17,11 @@ module.exports = () => {
|
|||||||
|
|
||||||
let hasLyrics = true;
|
let hasLyrics = true;
|
||||||
|
|
||||||
const html = ipcRenderer.sendSync(
|
const lyrics = ipcRenderer.sendSync(
|
||||||
"search-genius-lyrics",
|
"search-genius-lyrics",
|
||||||
extractedSongInfo
|
extractedSongInfo
|
||||||
);
|
);
|
||||||
if (!html) {
|
if (!lyrics) {
|
||||||
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
||||||
checkLyricsContainer(() => {
|
checkLyricsContainer(() => {
|
||||||
hasLyrics = false;
|
hasLyrics = false;
|
||||||
@ -34,21 +34,6 @@ module.exports = () => {
|
|||||||
console.log("Fetched lyrics from Genius");
|
console.log("Fetched lyrics from Genius");
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
wrapper.innerHTML = html;
|
|
||||||
const lyricsSelector1 = wrapper.querySelector(".lyrics");
|
|
||||||
const lyricsSelector2 = wrapper.querySelector(
|
|
||||||
'[class^="Lyrics__Container"]'
|
|
||||||
);
|
|
||||||
const lyrics = lyricsSelector1
|
|
||||||
? lyricsSelector1.innerHTML
|
|
||||||
: lyricsSelector2
|
|
||||||
? lyricsSelector2.innerHTML
|
|
||||||
: null;
|
|
||||||
if (!lyrics) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
enableLyricsTab();
|
enableLyricsTab();
|
||||||
|
|
||||||
setTabsOnclick(enableLyricsTab);
|
setTabsOnclick(enableLyricsTab);
|
||||||
@ -78,9 +63,12 @@ module.exports = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLyrics(lyricsContainer) {
|
function setLyrics(lyricsContainer) {
|
||||||
lyricsContainer.innerHTML =
|
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
||||||
`<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
${
|
||||||
${hasLyrics ? lyrics : 'Could not retrieve lyrics from genius'}
|
hasLyrics
|
||||||
|
? lyrics.replace(/(?:\r\n|\r|\n)/g, "<br/>")
|
||||||
|
: "Could not retrieve lyrics from genius"
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
|
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
|
||||||
|
|||||||
@ -7,6 +7,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 1.1vw !important;
|
font-size: clamp(1.4rem, 1.1vmax, 3rem) !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: var(--ytmusic-title-1_-_line-height);
|
line-height: var(--ytmusic-title-1_-_line-height);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
|
||||||
--yt-endpoint-color: #fff;
|
--yt-endpoint-color: #fff;
|
||||||
--yt-endpoint-hover-color: #fff;
|
--yt-endpoint-hover-color: #fff;
|
||||||
--yt-endpoint-visited-color: #fff;
|
--yt-endpoint-visited-color: #fff;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"] {
|
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"],
|
||||||
|
.sign-in-link {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
37
plugins/picture-in-picture/adaptors/in-app-menu.js
Normal file
37
plugins/picture-in-picture/adaptors/in-app-menu.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const { Menu, app } = require("electron");
|
||||||
|
const { setApplicationMenu } = require("../../../menu");
|
||||||
|
|
||||||
|
module.exports = (win, options, setOptions, togglePip, isInPip) => {
|
||||||
|
if (isInPip) {
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "App",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Exit Picture in Picture",
|
||||||
|
click: togglePip,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Always on top",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.alwaysOnTop,
|
||||||
|
click: (item) => {
|
||||||
|
setOptions({ alwaysOnTop: item.checked });
|
||||||
|
win.setAlwaysOnTop(item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restart",
|
||||||
|
click: () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ role: "quit" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
setApplicationMenu(win);
|
||||||
|
}
|
||||||
|
};
|
||||||
109
plugins/picture-in-picture/back.js
Normal file
109
plugins/picture-in-picture/back.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const { app, ipcMain } = require("electron");
|
||||||
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
|
const { setOptions, isEnabled } = require("../../config/plugins");
|
||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
|
let isInPiP = false;
|
||||||
|
let originalPosition;
|
||||||
|
let originalSize;
|
||||||
|
let originalFullScreen;
|
||||||
|
let originalMaximized;
|
||||||
|
|
||||||
|
let win;
|
||||||
|
let options;
|
||||||
|
|
||||||
|
const pipPosition = () => (options.savePosition && options["pip-position"]) || [10, 10];
|
||||||
|
const pipSize = () => (options.saveSize && options["pip-size"]) || [450, 275];
|
||||||
|
|
||||||
|
const setLocalOptions = (_options) => {
|
||||||
|
options = { ...options, ..._options };
|
||||||
|
setOptions("picture-in-picture", _options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const adaptors = [];
|
||||||
|
const runAdaptors = () => adaptors.forEach(a => a());
|
||||||
|
|
||||||
|
if (isEnabled("in-app-menu")) {
|
||||||
|
let adaptor = require("./adaptors/in-app-menu");
|
||||||
|
adaptors.push(() => adaptor(win, options, setLocalOptions, togglePiP, isInPiP));
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePiP = async () => {
|
||||||
|
isInPiP = !isInPiP;
|
||||||
|
setLocalOptions({ isInPiP });
|
||||||
|
|
||||||
|
if (isInPiP) {
|
||||||
|
originalFullScreen = win.isFullScreen();
|
||||||
|
if (originalFullScreen) win.setFullScreen(false);
|
||||||
|
originalMaximized = win.isMaximized();
|
||||||
|
if (originalMaximized) win.unmaximize();
|
||||||
|
|
||||||
|
originalPosition = win.getPosition();
|
||||||
|
originalSize = win.getSize();
|
||||||
|
|
||||||
|
win.webContents.on("before-input-event", blockShortcutsInPiP);
|
||||||
|
|
||||||
|
win.setFullScreenable(false);
|
||||||
|
|
||||||
|
runAdaptors();
|
||||||
|
win.webContents.send("pip-toggle", true);
|
||||||
|
|
||||||
|
app.dock?.hide();
|
||||||
|
win.setVisibleOnAllWorkspaces(true, {
|
||||||
|
visibleOnFullScreen: true,
|
||||||
|
});
|
||||||
|
app.dock?.show();
|
||||||
|
if (options.alwaysOnTop) {
|
||||||
|
win.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
|
||||||
|
win.setFullScreenable(true);
|
||||||
|
|
||||||
|
runAdaptors();
|
||||||
|
win.webContents.send("pip-toggle", false);
|
||||||
|
|
||||||
|
win.setVisibleOnAllWorkspaces(false);
|
||||||
|
win.setAlwaysOnTop(false);
|
||||||
|
|
||||||
|
if (originalFullScreen) win.setFullScreen(true);
|
||||||
|
if (originalMaximized) win.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = isInPiP ? pipPosition() : originalPosition;
|
||||||
|
const [w, h] = isInPiP ? pipSize() : originalSize;
|
||||||
|
win.setPosition(x, y);
|
||||||
|
win.setSize(w, h);
|
||||||
|
|
||||||
|
win.setWindowButtonVisibility?.(!isInPiP);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockShortcutsInPiP = (event, input) => {
|
||||||
|
const key = input.key.toLowerCase();
|
||||||
|
|
||||||
|
if (key === "f") {
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (key === 'escape') {
|
||||||
|
togglePiP();
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (_win, _options) => {
|
||||||
|
options ??= _options;
|
||||||
|
win ??= _win;
|
||||||
|
setLocalOptions({ isInPiP });
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
|
ipcMain.on("picture-in-picture", async () => {
|
||||||
|
await togglePiP();
|
||||||
|
});
|
||||||
|
if (options.hotkey) {
|
||||||
|
electronLocalshortcut.register(win, options.hotkey, togglePiP);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.setOptions = setLocalOptions;
|
||||||
90
plugins/picture-in-picture/front.js
Normal file
90
plugins/picture-in-picture/front.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const { getSongMenu } = require("../../providers/dom-elements");
|
||||||
|
const { ElementFromFile, templatePath } = require("../utils");
|
||||||
|
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
|
let menu = null;
|
||||||
|
const pipButton = ElementFromFile(
|
||||||
|
templatePath(__dirname, "picture-in-picture.html")
|
||||||
|
);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (!menu) {
|
||||||
|
menu = getSongMenu();
|
||||||
|
if (!menu) return;
|
||||||
|
}
|
||||||
|
if (menu.contains(pipButton)) return;
|
||||||
|
const menuUrl = $(
|
||||||
|
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
|
||||||
|
)?.href;
|
||||||
|
if (menuUrl && !menuUrl.includes("watch?")) return;
|
||||||
|
|
||||||
|
menu.prepend(pipButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
global.togglePictureInPicture = () => {
|
||||||
|
ipcRenderer.send("picture-in-picture");
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenForToggle = () => {
|
||||||
|
const originalExitButton = $(".exit-fullscreen-button");
|
||||||
|
const clonedExitButton = originalExitButton.cloneNode(true);
|
||||||
|
clonedExitButton.onclick = () => togglePictureInPicture();
|
||||||
|
|
||||||
|
const appLayout = $("ytmusic-app-layout");
|
||||||
|
const expandMenu = $('#expanding-menu');
|
||||||
|
const middleControls = $('.middle-controls');
|
||||||
|
const playerPage = $("ytmusic-player-page");
|
||||||
|
const togglePlayerPageButton = $(".toggle-player-page-button");
|
||||||
|
const fullScreenButton = $(".fullscreen-button");
|
||||||
|
const player = $('#player');
|
||||||
|
const onPlayerDblClick = player.onDoubleClick_;
|
||||||
|
|
||||||
|
ipcRenderer.on('pip-toggle', (_, isPip) => {
|
||||||
|
if (isPip) {
|
||||||
|
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
|
||||||
|
player.onDoubleClick_ = () => {};
|
||||||
|
expandMenu.onmouseleave = () => middleControls.click();
|
||||||
|
if (!playerPage.playerPageOpen_) {
|
||||||
|
togglePlayerPageButton.click();
|
||||||
|
}
|
||||||
|
fullScreenButton.click();
|
||||||
|
appLayout.classList.add("pip");
|
||||||
|
} else {
|
||||||
|
$(".exit-fullscreen-button").replaceWith(originalExitButton);
|
||||||
|
player.onDoubleClick_ = onPlayerDblClick;
|
||||||
|
expandMenu.onmouseleave = undefined;
|
||||||
|
originalExitButton.click();
|
||||||
|
appLayout.classList.remove("pip");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeMenu(options) {
|
||||||
|
document.addEventListener(
|
||||||
|
"apiLoaded",
|
||||||
|
() => {
|
||||||
|
listenForToggle();
|
||||||
|
const minButton = $(".player-minimize-button");
|
||||||
|
// remove native listeners
|
||||||
|
minButton.replaceWith(minButton.cloneNode(true));
|
||||||
|
$(".player-minimize-button").onclick = () => {
|
||||||
|
global.togglePictureInPicture();
|
||||||
|
setTimeout(() => $('#player').click());
|
||||||
|
};
|
||||||
|
|
||||||
|
// allows easily closing the menu by programmatically clicking outside of it
|
||||||
|
$("#expanding-menu").removeAttribute("no-cancel-on-outside-click");
|
||||||
|
// TODO: think about wether an additional button in songMenu is needed
|
||||||
|
observer.observe($("ytmusic-popup-container"), {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ once: true, passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = observeMenu;
|
||||||
60
plugins/picture-in-picture/menu.js
Normal file
60
plugins/picture-in-picture/menu.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const prompt = require("custom-electron-prompt");
|
||||||
|
|
||||||
|
const promptOptions = require("../../providers/prompt-options");
|
||||||
|
const { setOptions } = require("./back.js");
|
||||||
|
|
||||||
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Always on top",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.alwaysOnTop,
|
||||||
|
click: (item) => {
|
||||||
|
setOptions({ alwaysOnTop: item.checked });
|
||||||
|
win.setAlwaysOnTop(item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Save window position",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.savePosition,
|
||||||
|
click: (item) => {
|
||||||
|
setOptions({ savePosition: item.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Save window size",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.saveSize,
|
||||||
|
click: (item) => {
|
||||||
|
setOptions({ saveSize: item.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hotkey",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.hotkey,
|
||||||
|
click: async (item) => {
|
||||||
|
const output = await prompt({
|
||||||
|
title: "Picture in Picture Hotkey",
|
||||||
|
label: "Choose a hotkey for toggling Picture in Picture",
|
||||||
|
type: "keybind",
|
||||||
|
keybindOptions: [{
|
||||||
|
value: "hotkey",
|
||||||
|
label: "Hotkey",
|
||||||
|
default: options.hotkey
|
||||||
|
}],
|
||||||
|
...promptOptions()
|
||||||
|
}, win)
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
const { value, accelerator } = output[0];
|
||||||
|
setOptions({ [value]: accelerator });
|
||||||
|
|
||||||
|
item.checked = !!accelerator;
|
||||||
|
} else {
|
||||||
|
// Reset checkbox if prompt was canceled
|
||||||
|
item.checked = !item.checked;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
26
plugins/picture-in-picture/style.css
Normal file
26
plugins/picture-in-picture/style.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* improve visibility of the player bar elements */
|
||||||
|
ytmusic-app-layout.pip ytmusic-player-bar svg,
|
||||||
|
ytmusic-app-layout.pip ytmusic-player-bar .time-info,
|
||||||
|
ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
|
||||||
|
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
|
||||||
|
filter: drop-shadow(2px 4px 6px black);
|
||||||
|
color: white !important;
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* improve the style of the player bar expanding menu */
|
||||||
|
ytmusic-app-layout.pip ytmusic-player-expanding-menu {
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(5px) brightness(20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fix volumeHud position when both in-app-menu and PiP are active */
|
||||||
|
.cet-container ytmusic-app-layout.pip #volumeHud {
|
||||||
|
top: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable the video-toggle button when in PiP mode */
|
||||||
|
ytmusic-app-layout.pip .video-switch-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
51
plugins/picture-in-picture/templates/picture-in-picture.html
Normal file
51
plugins/picture-in-picture/templates/picture-in-picture.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<div
|
||||||
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
|
role="option"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-selected="false"
|
||||||
|
onclick="togglePictureInPicture()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="navigation-endpoint"
|
||||||
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="enable-background: new 0 0 512 512"
|
||||||
|
xml:space="preserve"
|
||||||
|
>
|
||||||
|
<style type="text/css">
|
||||||
|
.st0 {
|
||||||
|
fill: #aaaaaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g id="XMLID_6_">
|
||||||
|
<path
|
||||||
|
id="XMLID_11_"
|
||||||
|
class="st0"
|
||||||
|
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||||
|
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||||
|
v326.8H464.8z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="ytmcustom-pip"
|
||||||
|
>
|
||||||
|
Picture in picture
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -85,7 +85,7 @@ function forcePlaybackRate(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
document.addEventListener('apiLoaded', e => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
observePopupContainer();
|
observePopupContainer();
|
||||||
observeVideo();
|
observeVideo();
|
||||||
setupWheelListener();
|
setupWheelListener();
|
||||||
|
|||||||
@ -1,9 +1,24 @@
|
|||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is used to determine if plugin is actually active
|
This is used to determine if plugin is actually active
|
||||||
(not if its only enabled in options)
|
(not if its only enabled in options)
|
||||||
*/
|
*/
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
|
|
||||||
module.exports = () => enabled = true;
|
const { globalShortcut } = require('electron');
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
enabled = true;
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "volume-hud.css"));
|
||||||
|
|
||||||
|
if (options.globalShortcuts?.volumeUp) {
|
||||||
|
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
|
||||||
|
}
|
||||||
|
if (options.globalShortcuts?.volumeDown) {
|
||||||
|
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.enabled = () => enabled;
|
module.exports.enabled = () => enabled;
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
const { globalShortcut } = require('@electron/remote');
|
|
||||||
|
|
||||||
const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins");
|
const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins");
|
||||||
|
|
||||||
function $(selector) { return document.querySelector(selector); }
|
function $(selector) { return document.querySelector(selector); }
|
||||||
let api;
|
|
||||||
|
|
||||||
module.exports = (options) => {
|
let api, options;
|
||||||
|
|
||||||
|
module.exports = (_options) => {
|
||||||
|
options = _options;
|
||||||
document.addEventListener('apiLoaded', e => {
|
document.addEventListener('apiLoaded', e => {
|
||||||
api = e.detail;
|
api = e.detail;
|
||||||
firstRun(options);
|
ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease));
|
||||||
|
ipcRenderer.on('setVolume', (_, value) => setVolume(value));
|
||||||
|
firstRun();
|
||||||
}, { once: true, passive: true })
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.moveVolumeHud = moveVolumeHud;
|
module.exports.moveVolumeHud = moveVolumeHud;
|
||||||
|
|
||||||
/** Restore saved volume and setup tooltip */
|
/** Restore saved volume and setup tooltip */
|
||||||
function firstRun(options) {
|
function firstRun() {
|
||||||
if (typeof options.savedVolume === "number") {
|
if (typeof options.savedVolume === "number") {
|
||||||
// Set saved volume as tooltip
|
// Set saved volume as tooltip
|
||||||
setTooltip(options.savedVolume);
|
setTooltip(options.savedVolume);
|
||||||
@ -26,16 +29,14 @@ function firstRun(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPlaybar(options);
|
setupPlaybar();
|
||||||
|
|
||||||
setupLocalArrowShortcuts(options);
|
setupLocalArrowShortcuts();
|
||||||
|
|
||||||
setupGlobalShortcuts(options);
|
|
||||||
|
|
||||||
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
|
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
|
||||||
injectVolumeHud(noVid);
|
injectVolumeHud(noVid);
|
||||||
if (!noVid) {
|
if (!noVid) {
|
||||||
setupVideoPlayerOnwheel(options);
|
setupVideoPlayerOnwheel();
|
||||||
if (!isEnabled('video-toggle')) {
|
if (!isEnabled('video-toggle')) {
|
||||||
//video-toggle handles hud positioning on its own
|
//video-toggle handles hud positioning on its own
|
||||||
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
||||||
@ -45,23 +46,21 @@ function firstRun(options) {
|
|||||||
|
|
||||||
// Change options from renderer to keep sync
|
// Change options from renderer to keep sync
|
||||||
ipcRenderer.on("setOptions", (_event, newOptions = {}) => {
|
ipcRenderer.on("setOptions", (_event, newOptions = {}) => {
|
||||||
for (option in newOptions) {
|
Object.assign(options, newOptions)
|
||||||
options[option] = newOptions[option];
|
|
||||||
}
|
|
||||||
setMenuOptions("precise-volume", options);
|
setMenuOptions("precise-volume", options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectVolumeHud(noVid) {
|
function injectVolumeHud(noVid) {
|
||||||
if (noVid) {
|
if (noVid) {
|
||||||
const position = "top: 18px; right: 60px; z-index: 999; position: absolute;";
|
const position = "top: 18px; right: 60px;";
|
||||||
const mainStyle = "font-size: xx-large; padding: 10px; transition: opacity 1s; pointer-events: none;";
|
const mainStyle = "font-size: xx-large;";
|
||||||
|
|
||||||
$(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend",
|
$(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend",
|
||||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
||||||
} else {
|
} else {
|
||||||
const position = `top: 10px; left: 10px; z-index: 999; position: absolute;`;
|
const position = `top: 10px; left: 10px;`;
|
||||||
const mainStyle = "font-size: xxx-large; padding: 10px; transition: opacity 0.6s; webkit-text-stroke: 1px black; font-weight: 600; pointer-events: none;";
|
const mainStyle = "font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;";
|
||||||
|
|
||||||
$("#song-video").insertAdjacentHTML('afterend',
|
$("#song-video").insertAdjacentHTML('afterend',
|
||||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
||||||
@ -98,22 +97,22 @@ function showVolumeHud(volume) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Add onwheel event to video player */
|
/** Add onwheel event to video player */
|
||||||
function setupVideoPlayerOnwheel(options) {
|
function setupVideoPlayerOnwheel() {
|
||||||
$("#main-panel").addEventListener("wheel", event => {
|
$("#main-panel").addEventListener("wheel", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Event.deltaY < 0 means wheel-up
|
// Event.deltaY < 0 means wheel-up
|
||||||
changeVolume(event.deltaY < 0, options);
|
changeVolume(event.deltaY < 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveVolume(volume, options) {
|
function saveVolume(volume) {
|
||||||
options.savedVolume = volume;
|
options.savedVolume = volume;
|
||||||
writeOptions(options);
|
writeOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
//without this function it would rewrite config 20 time when volume change by 20
|
//without this function it would rewrite config 20 time when volume change by 20
|
||||||
let writeTimeout;
|
let writeTimeout;
|
||||||
function writeOptions(options) {
|
function writeOptions() {
|
||||||
if (writeTimeout) clearTimeout(writeTimeout);
|
if (writeTimeout) clearTimeout(writeTimeout);
|
||||||
|
|
||||||
writeTimeout = setTimeout(() => {
|
writeTimeout = setTimeout(() => {
|
||||||
@ -123,13 +122,13 @@ function writeOptions(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Add onwheel event to play bar and also track if play bar is hovered*/
|
/** Add onwheel event to play bar and also track if play bar is hovered*/
|
||||||
function setupPlaybar(options) {
|
function setupPlaybar() {
|
||||||
const playerbar = $("ytmusic-player-bar");
|
const playerbar = $("ytmusic-player-bar");
|
||||||
|
|
||||||
playerbar.addEventListener("wheel", event => {
|
playerbar.addEventListener("wheel", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Event.deltaY < 0 means wheel-up
|
// Event.deltaY < 0 means wheel-up
|
||||||
changeVolume(event.deltaY < 0, options);
|
changeVolume(event.deltaY < 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep track of mouse position for showVolumeSlider()
|
// Keep track of mouse position for showVolumeSlider()
|
||||||
@ -141,11 +140,11 @@ function setupPlaybar(options) {
|
|||||||
playerbar.classList.remove("on-hover");
|
playerbar.classList.remove("on-hover");
|
||||||
});
|
});
|
||||||
|
|
||||||
setupSliderObserver(options);
|
setupSliderObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
||||||
function setupSliderObserver(options) {
|
function setupSliderObserver() {
|
||||||
const sliderObserver = new MutationObserver(mutations => {
|
const sliderObserver = new MutationObserver(mutations => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
// This checks that volume-slider was manually set
|
// This checks that volume-slider was manually set
|
||||||
@ -153,7 +152,7 @@ function setupSliderObserver(options) {
|
|||||||
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
||||||
// Diff>4 means it was manually set
|
// Diff>4 means it was manually set
|
||||||
setTooltip(mutation.target.value);
|
setTooltip(mutation.target.value);
|
||||||
saveVolume(mutation.target.value, options);
|
saveVolume(mutation.target.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -165,32 +164,39 @@ function setupSliderObserver(options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** if (toIncrease = false) then volume decrease */
|
function setVolume(value) {
|
||||||
function changeVolume(toIncrease, options) {
|
api.setVolume(value);
|
||||||
// Apply volume change if valid
|
|
||||||
const steps = Number(options.steps || 1);
|
|
||||||
api.setVolume(toIncrease ?
|
|
||||||
Math.min(api.getVolume() + steps, 100) :
|
|
||||||
Math.max(api.getVolume() - steps, 0));
|
|
||||||
|
|
||||||
// Save the new volume
|
// Save the new volume
|
||||||
saveVolume(api.getVolume(), options);
|
saveVolume(value);
|
||||||
|
|
||||||
// change slider position (important)
|
// change slider position (important)
|
||||||
updateVolumeSlider(options);
|
updateVolumeSlider();
|
||||||
|
|
||||||
// Change tooltips to new value
|
// Change tooltips to new value
|
||||||
setTooltip(options.savedVolume);
|
setTooltip(value);
|
||||||
// Show volume slider
|
// Show volume slider
|
||||||
showVolumeSlider();
|
showVolumeSlider();
|
||||||
// Show volume HUD
|
// Show volume HUD
|
||||||
showVolumeHud(options.savedVolume);
|
showVolumeHud(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVolumeSlider(options) {
|
/** if (toIncrease = false) then volume decrease */
|
||||||
|
function changeVolume(toIncrease) {
|
||||||
|
// Apply volume change if valid
|
||||||
|
const steps = Number(options.steps || 1);
|
||||||
|
setVolume(toIncrease ?
|
||||||
|
Math.min(api.getVolume() + steps, 100) :
|
||||||
|
Math.max(api.getVolume() - steps, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolumeSlider() {
|
||||||
// Slider value automatically rounds to multiples of 5
|
// Slider value automatically rounds to multiples of 5
|
||||||
$("#volume-slider").value = options.savedVolume > 0 && options.savedVolume < 5 ?
|
for (const slider of ["#volume-slider", "#expand-volume-slider"]) {
|
||||||
5 : options.savedVolume;
|
$(slider).value =
|
||||||
|
options.savedVolume > 0 && options.savedVolume < 5
|
||||||
|
? 5
|
||||||
|
: options.savedVolume;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let volumeHoverTimeoutID;
|
let volumeHoverTimeoutID;
|
||||||
@ -226,26 +232,17 @@ function setTooltip(volume) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupGlobalShortcuts(options) {
|
function setupLocalArrowShortcuts() {
|
||||||
if (options.globalShortcuts.volumeUp) {
|
|
||||||
globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
|
|
||||||
}
|
|
||||||
if (options.globalShortcuts.volumeDown) {
|
|
||||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLocalArrowShortcuts(options) {
|
|
||||||
if (options.arrowsShortcut) {
|
if (options.arrowsShortcut) {
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
changeVolume(true, options);
|
changeVolume(true);
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
changeVolume(false, options);
|
changeVolume(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
|
||||||
let ignored = {
|
let ignored = {
|
||||||
|
|||||||
11
plugins/precise-volume/volume-hud.css
Normal file
11
plugins/precise-volume/volume-hud.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#volumeHud {
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.6s;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ytmusic-player[player-ui-state_="MINIPLAYER"] #volumeHud {
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
15
plugins/quality-changer/back.js
Normal file
15
plugins/quality-changer/back.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const { ipcMain, dialog } = require("electron");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => {
|
||||||
|
return await dialog.showMessageBox({
|
||||||
|
type: "question",
|
||||||
|
buttons: qualityLabels,
|
||||||
|
defaultId: currentIndex,
|
||||||
|
title: "Choose Video Quality",
|
||||||
|
message: "Choose Video Quality:",
|
||||||
|
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||||
|
cancelId: -1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
const { ElementFromFile, templatePath } = require("../utils");
|
const { ElementFromFile, templatePath } = require("../utils");
|
||||||
const { dialog } = require('@electron/remote');
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
function $(selector) { return document.querySelector(selector); }
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
@ -18,24 +18,17 @@ function setup(event) {
|
|||||||
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
|
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
|
||||||
|
|
||||||
qualitySettingsButton.onclick = function chooseQuality() {
|
qualitySettingsButton.onclick = function chooseQuality() {
|
||||||
if (api.getPlayerState() === 2) api.playVideo();
|
setTimeout(() => $('#player').click());
|
||||||
else if (api.getPlayerState() === 1) api.pauseVideo();
|
|
||||||
|
|
||||||
const currentIndex = api.getAvailableQualityLevels().indexOf(api.getPlaybackQuality())
|
const qualityLevels = api.getAvailableQualityLevels();
|
||||||
|
|
||||||
dialog.showMessageBox({
|
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||||
type: "question",
|
|
||||||
buttons: api.getAvailableQualityLabels(),
|
ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then(promise => {
|
||||||
defaultId: currentIndex,
|
|
||||||
title: "Choose Video Quality",
|
|
||||||
message: "Choose Video Quality:",
|
|
||||||
detail: `Current Quality: ${api.getAvailableQualityLabels()[currentIndex]}`,
|
|
||||||
cancelId: -1
|
|
||||||
}).then((promise) => {
|
|
||||||
if (promise.response === -1) return;
|
if (promise.response === -1) return;
|
||||||
const newQuality = api.getAvailableQualityLevels()[promise.response];
|
const newQuality = qualityLevels[promise.response];
|
||||||
api.setPlaybackQualityRange(newQuality);
|
api.setPlaybackQualityRange(newQuality);
|
||||||
api.setPlaybackQuality(newQuality)
|
api.setPlaybackQuality(newQuality)
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const mpris = require("mpris-service");
|
|||||||
const { ipcMain } = require("electron");
|
const { ipcMain } = require("electron");
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
const config = require("../../config");
|
||||||
|
|
||||||
function setupMPRIS() {
|
function setupMPRIS() {
|
||||||
const player = mpris({
|
const player = mpris({
|
||||||
@ -19,7 +20,7 @@ function setupMPRIS() {
|
|||||||
|
|
||||||
function registerMPRIS(win) {
|
function registerMPRIS(win) {
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const { playPause, next, previous } = songControls;
|
const { playPause, next, previous, volumeMinus10, volumePlus10 } = songControls;
|
||||||
try {
|
try {
|
||||||
const secToMicro = n => Math.round(Number(n) * 1e6);
|
const secToMicro = n => Math.round(Number(n) * 1e6);
|
||||||
const microToSec = n => Math.round(Number(n) / 1e6);
|
const microToSec = n => Math.round(Number(n) / 1e6);
|
||||||
@ -34,6 +35,35 @@ function registerMPRIS(win) {
|
|||||||
let currentSeconds = 0;
|
let currentSeconds = 0;
|
||||||
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||||
|
|
||||||
|
let currentLoopStatus = undefined;
|
||||||
|
let manuallySwitchingStatus = false;
|
||||||
|
ipcMain.on("repeatChanged", (_, mode) => {
|
||||||
|
if (manuallySwitchingStatus)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mode === "Repeat off")
|
||||||
|
currentLoopStatus = "None";
|
||||||
|
else if (mode === "Repeat one")
|
||||||
|
currentLoopStatus = "Track";
|
||||||
|
else if (mode === "Repeat all")
|
||||||
|
currentLoopStatus = "Playlist";
|
||||||
|
|
||||||
|
player.loopStatus = currentLoopStatus;
|
||||||
|
});
|
||||||
|
player.on("loopStatus", (status) => {
|
||||||
|
// switchRepeat cycles between states in that order
|
||||||
|
const switches = ["None", "Playlist", "Track"];
|
||||||
|
const currentIndex = switches.indexOf(currentLoopStatus);
|
||||||
|
const targetIndex = switches.indexOf(status);
|
||||||
|
|
||||||
|
// Get a delta in the range [0,2]
|
||||||
|
const delta = (targetIndex - currentIndex + 3) % 3;
|
||||||
|
|
||||||
|
manuallySwitchingStatus = true;
|
||||||
|
songControls.switchRepeat(delta);
|
||||||
|
manuallySwitchingStatus = false;
|
||||||
|
})
|
||||||
|
|
||||||
player.getPosition = () => secToMicro(currentSeconds)
|
player.getPosition = () => secToMicro(currentSeconds)
|
||||||
|
|
||||||
player.on("raise", () => {
|
player.on("raise", () => {
|
||||||
@ -53,21 +83,45 @@ function registerMPRIS(win) {
|
|||||||
playPause()
|
playPause()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
player.on("playpause", () => {
|
||||||
|
player.playbackStatus = player.playbackStatus === 'Playing' ? "Paused" : "Playing";
|
||||||
|
playPause();
|
||||||
|
});
|
||||||
|
|
||||||
player.on("playpause", playPause);
|
|
||||||
player.on("next", next);
|
player.on("next", next);
|
||||||
player.on("previous", previous);
|
player.on("previous", previous);
|
||||||
|
|
||||||
player.on('seek', seekBy);
|
player.on('seek', seekBy);
|
||||||
player.on('position', seekTo);
|
player.on('position', seekTo);
|
||||||
|
|
||||||
registerCallback(songInfo => {
|
ipcMain.on('volumeChanged', (_, value) => {
|
||||||
if (player) {
|
player.volume = value;
|
||||||
const data = {
|
});
|
||||||
'mpris:length': secToMicro(songInfo.songDuration),
|
player.on('volume', (newVolume) => {
|
||||||
'mpris:artUrl': songInfo.imageSrc,
|
if (config.plugins.isEnabled('precise-volume')) {
|
||||||
'xesam:title': songInfo.title,
|
// With precise volume we can set the volume to the exact value.
|
||||||
'xesam:artist': [songInfo.artist],
|
win.webContents.send('setVolume', newVolume)
|
||||||
|
} else {
|
||||||
|
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
|
||||||
|
const deltaVolume = Math.round((newVolume - player.volume) / 10);
|
||||||
|
|
||||||
|
if (deltaVolume > 0) {
|
||||||
|
for (let i = 0; i < deltaVolume; i++)
|
||||||
|
volumePlus10();
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < -deltaVolume; i++)
|
||||||
|
volumeMinus10();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCallback(songInfo => {
|
||||||
|
if (player) {
|
||||||
|
const data = {
|
||||||
|
'mpris:length': secToMicro(songInfo.songDuration),
|
||||||
|
'mpris:artUrl': songInfo.imageSrc,
|
||||||
|
'xesam:title': songInfo.title,
|
||||||
|
'xesam:artist': [songInfo.artist],
|
||||||
'mpris:trackid': '/'
|
'mpris:trackid': '/'
|
||||||
};
|
};
|
||||||
if (songInfo.album) data['xesam:album'] = songInfo.album;
|
if (songInfo.album) data['xesam:album'] = songInfo.album;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const hark = require("hark/hark.bundle.js");
|
|||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
let isSilent = false;
|
let isSilent = false;
|
||||||
|
|
||||||
document.addEventListener("apiLoaded", (apiEvent) => {
|
document.addEventListener("apiLoaded", () => {
|
||||||
const video = document.querySelector("video");
|
const video = document.querySelector("video");
|
||||||
const speechEvents = hark(video, {
|
const speechEvents = hark(video, {
|
||||||
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
|
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
const { sortSegments } = require("../segments");
|
const { sortSegments } = require("../segments");
|
||||||
|
|
||||||
test("Segment sorting", () => {
|
test("Segment sorting", () => {
|
||||||
|
|||||||
@ -32,9 +32,16 @@ module.exports.listenAction = (channel, callback) => {
|
|||||||
return ipcMain.on(channel, callback);
|
return ipcMain.on(channel, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.fileExists = (path, callbackIfExists) => {
|
module.exports.fileExists = (
|
||||||
|
path,
|
||||||
|
callbackIfExists,
|
||||||
|
callbackIfError = undefined
|
||||||
|
) => {
|
||||||
fs.access(path, fs.F_OK, (err) => {
|
fs.access(path, fs.F_OK, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
if (callbackIfError) {
|
||||||
|
callbackIfError();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,15 +49,22 @@ module.exports.fileExists = (path, callbackIfExists) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cssToInject = new Map();
|
||||||
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
||||||
webContents.on("did-finish-load", async () => {
|
if (!cssToInject.size) setupCssInjection(webContents);
|
||||||
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
|
||||||
if (cb) {
|
cssToInject.set(filepath, cb);
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupCssInjection = (webContents) => {
|
||||||
|
webContents.on("did-finish-load", () => {
|
||||||
|
cssToInject.forEach(async (cb, filepath) => {
|
||||||
|
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||||
|
cb?.();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.getAllPlugins = () => {
|
module.exports.getAllPlugins = () => {
|
||||||
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
|
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
|
||||||
return fs
|
return fs
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const path = require("path");
|
|||||||
module.exports = (win, options) => {
|
module.exports = (win, options) => {
|
||||||
if (options.forceHide) {
|
if (options.forceHide) {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "force-hide.css"));
|
injectCSS(win.webContents, path.join(__dirname, "force-hide.css"));
|
||||||
} else {
|
} else if (!options.mode || options.mode === "custom") {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "button-switcher.css"));
|
injectCSS(win.webContents, path.join(__dirname, "button-switcher.css"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -75,3 +75,8 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 300ms;
|
transition: transform 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* disable the native toggler */
|
||||||
|
#av-id {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,9 +14,24 @@ const switchButtonDiv = ElementFromFile(
|
|||||||
|
|
||||||
module.exports = (_options) => {
|
module.exports = (_options) => {
|
||||||
if (_options.forceHide) return;
|
if (_options.forceHide) return;
|
||||||
options = _options;
|
switch (_options.mode) {
|
||||||
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
case "native": {
|
||||||
}
|
$("ytmusic-player-page").setAttribute("has-av-switcher");
|
||||||
|
$("ytmusic-player").setAttribute("has-av-switcher");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "disabled": {
|
||||||
|
$("ytmusic-player-page").removeAttribute("has-av-switcher");
|
||||||
|
$("ytmusic-player").removeAttribute("has-av-switcher");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
case "custom": {
|
||||||
|
options = _options;
|
||||||
|
document.addEventListener("apiLoaded", setup, { once: true, passive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function setup(e) {
|
function setup(e) {
|
||||||
api = e.detail;
|
api = e.detail;
|
||||||
@ -25,8 +40,6 @@ function setup(e) {
|
|||||||
|
|
||||||
$('ytmusic-player-page').prepend(switchButtonDiv);
|
$('ytmusic-player-page').prepend(switchButtonDiv);
|
||||||
|
|
||||||
$('#song-image.ytmusic-player').style.display = "block";
|
|
||||||
|
|
||||||
if (options.hideVideo) {
|
if (options.hideVideo) {
|
||||||
$('.video-switch-button-checkbox').checked = false;
|
$('.video-switch-button-checkbox').checked = false;
|
||||||
changeDisplay(false);
|
changeDisplay(false);
|
||||||
@ -50,7 +63,10 @@ function setup(e) {
|
|||||||
function changeDisplay(showVideo) {
|
function changeDisplay(showVideo) {
|
||||||
player.style.margin = showVideo ? '' : 'auto 0px';
|
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||||
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
||||||
$('#song-video.ytmusic-player').style.display = showVideo ? 'unset' : 'none';
|
|
||||||
|
$('#song-video.ytmusic-player').style.display = showVideo ? 'block' : 'none';
|
||||||
|
$('#song-image').style.display = showVideo ? 'none' : 'block';
|
||||||
|
|
||||||
if (showVideo && !video.style.top) {
|
if (showVideo && !video.style.top) {
|
||||||
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,38 @@
|
|||||||
const { setMenuOptions } = require("../../config/plugins");
|
const { setMenuOptions } = require("../../config/plugins");
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Mode",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Custom toggle",
|
||||||
|
type: "radio",
|
||||||
|
checked: options.mode === 'custom',
|
||||||
|
click: () => {
|
||||||
|
options.mode = 'custom';
|
||||||
|
setMenuOptions("video-toggle", options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Native toggle",
|
||||||
|
type: "radio",
|
||||||
|
checked: options.mode === 'native',
|
||||||
|
click: () => {
|
||||||
|
options.mode = 'native';
|
||||||
|
setMenuOptions("video-toggle", options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Disabled",
|
||||||
|
type: "radio",
|
||||||
|
checked: options.mode === 'disabled',
|
||||||
|
click: () => {
|
||||||
|
options.mode = 'disabled';
|
||||||
|
setMenuOptions("video-toggle", options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Force Remove Video Tab",
|
label: "Force Remove Video Tab",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
|
|||||||
55
preload.js
55
preload.js
@ -1,25 +1,34 @@
|
|||||||
const path = require("path");
|
require("./providers/front-logger")();
|
||||||
|
|
||||||
const remote = require('@electron/remote');
|
|
||||||
|
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
const setupFrontLogger = require("./providers/front-logger");
|
|
||||||
const setupSongInfo = require("./providers/song-info-front");
|
const setupSongInfo = require("./providers/song-info-front");
|
||||||
const { setupSongControls } = require("./providers/song-controls-front");
|
const { setupSongControls } = require("./providers/song-controls-front");
|
||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
let api;
|
let api;
|
||||||
|
|
||||||
plugins.forEach(([plugin, options]) => {
|
plugins.forEach(async ([plugin, options]) => {
|
||||||
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
|
const preloadPath = await ipcRenderer.invoke(
|
||||||
|
"getPath",
|
||||||
|
__dirname,
|
||||||
|
"plugins",
|
||||||
|
plugin,
|
||||||
|
"preload.js"
|
||||||
|
);
|
||||||
fileExists(preloadPath, () => {
|
fileExists(preloadPath, () => {
|
||||||
const run = require(preloadPath);
|
const run = require(preloadPath);
|
||||||
run(options);
|
run(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
const actionPath = await ipcRenderer.invoke(
|
||||||
|
"getPath",
|
||||||
|
__dirname,
|
||||||
|
"plugins",
|
||||||
|
plugin,
|
||||||
|
"actions.js"
|
||||||
|
);
|
||||||
fileExists(actionPath, () => {
|
fileExists(actionPath, () => {
|
||||||
const actions = require(actionPath).actions || {};
|
const actions = require(actionPath).actions || {};
|
||||||
|
|
||||||
@ -32,8 +41,14 @@ plugins.forEach(([plugin, options]) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
plugins.forEach(([plugin, options]) => {
|
plugins.forEach(async ([plugin, options]) => {
|
||||||
const pluginPath = path.join(__dirname, "plugins", plugin, "front.js");
|
const pluginPath = await ipcRenderer.invoke(
|
||||||
|
"getPath",
|
||||||
|
__dirname,
|
||||||
|
"plugins",
|
||||||
|
plugin,
|
||||||
|
"front.js"
|
||||||
|
);
|
||||||
fileExists(pluginPath, () => {
|
fileExists(pluginPath, () => {
|
||||||
const run = require(pluginPath);
|
const run = require(pluginPath);
|
||||||
run(options);
|
run(options);
|
||||||
@ -49,12 +64,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// inject song-controls
|
// inject song-controls
|
||||||
setupSongControls();
|
setupSongControls();
|
||||||
|
|
||||||
// inject front logger
|
|
||||||
setupFrontLogger();
|
|
||||||
|
|
||||||
// Add action for reloading
|
// Add action for reloading
|
||||||
global.reload = () =>
|
global.reload = () => ipcRenderer.send('reload');
|
||||||
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
|
||||||
|
|
||||||
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||||
setInterval(() => window._lact = Date.now(), 900000);
|
setInterval(() => window._lact = Date.now(), 900000);
|
||||||
@ -83,9 +94,17 @@ function onApiLoaded() {
|
|||||||
|
|
||||||
// Remove upgrade button
|
// Remove upgrade button
|
||||||
if (config.get("options.removeUpgradeButton")) {
|
if (config.get("options.removeUpgradeButton")) {
|
||||||
const upgradeButtton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')
|
const upgradeButton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')
|
||||||
if (upgradeButtton) {
|
if (upgradeButton) {
|
||||||
upgradeButtton.style.display = "none";
|
upgradeButton.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force show like buttons
|
||||||
|
if (config.get("options.ForceShowLikeButtons")) {
|
||||||
|
const likeButtons = document.querySelector('ytmusic-like-button-renderer')
|
||||||
|
if (likeButtons) {
|
||||||
|
likeButtons.style.display = 'inherit';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,23 @@
|
|||||||
const app = require("electron").app || require('@electron/remote').app;
|
const path = require("path");
|
||||||
|
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
const { app, BrowserWindow, ipcMain, ipcRenderer } = require("electron");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
module.exports.restart = () => {
|
module.exports.restart = () => {
|
||||||
app.relaunch();
|
is.main() ? restart() : ipcRenderer.send('restart');
|
||||||
app.exit();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.setupAppControls = () => {
|
||||||
|
ipcMain.on('restart', restart);
|
||||||
|
ipcMain.handle('getDownloadsFolder', () => app.getPath("downloads"));
|
||||||
|
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get("url")));
|
||||||
|
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
||||||
|
// execPath will be undefined if not running portable app, resulting in default behavior
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
const { isEnabled } = require("../config/plugins");
|
||||||
|
|
||||||
const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
|
const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
|
||||||
const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
|
const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
|
||||||
|
|
||||||
const promptOptions = is.macOS() ? {
|
const promptOptions = !is.macOS() && isEnabled("in-app-menu") ? {
|
||||||
customStylesheet: "dark",
|
|
||||||
icon: iconPath
|
|
||||||
} : {
|
|
||||||
customStylesheet: "dark",
|
customStylesheet: "dark",
|
||||||
// The following are used for custom titlebar
|
// The following are used for custom titlebar
|
||||||
frame: false,
|
frame: false,
|
||||||
customScript: customTitlebarPath,
|
customScript: customTitlebarPath,
|
||||||
|
} : {
|
||||||
|
customStylesheet: "dark",
|
||||||
|
icon: iconPath
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => promptOptions;
|
module.exports = () => promptOptions;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ module.exports = (win) => {
|
|||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, "k"),
|
previous: () => pressKey(win, "k"),
|
||||||
next: () => pressKey(win, "j"),
|
next: () => pressKey(win, "j"),
|
||||||
playPause: () => pressKey(win, "space"),
|
playPause: () => pressKey(win, ";"),
|
||||||
like: () => pressKey(win, "+"),
|
like: () => pressKey(win, "+"),
|
||||||
dislike: () => pressKey(win, "_"),
|
dislike: () => pressKey(win, "_"),
|
||||||
go10sBack: () => pressKey(win, "h"),
|
go10sBack: () => pressKey(win, "h"),
|
||||||
@ -20,7 +20,10 @@ module.exports = (win) => {
|
|||||||
go1sBack: () => pressKey(win, "h", ["shift"]),
|
go1sBack: () => pressKey(win, "h", ["shift"]),
|
||||||
go1sForward: () => pressKey(win, "l", ["shift"]),
|
go1sForward: () => pressKey(win, "l", ["shift"]),
|
||||||
shuffle: () => pressKey(win, "s"),
|
shuffle: () => pressKey(win, "s"),
|
||||||
switchRepeat: () => pressKey(win, "r"),
|
switchRepeat: (n = 1) => {
|
||||||
|
for (let i = 0; i < n; i++)
|
||||||
|
pressKey(win, "r");
|
||||||
|
},
|
||||||
// General
|
// General
|
||||||
volumeMinus10: () => pressKey(win, "-"),
|
volumeMinus10: () => pressKey(win, "-"),
|
||||||
volumePlus10: () => pressKey(win, "="),
|
volumePlus10: () => pressKey(win, "="),
|
||||||
|
|||||||
@ -22,6 +22,8 @@ module.exports = () => {
|
|||||||
if (config.plugins.isEnabled('tuna-obs') ||
|
if (config.plugins.isEnabled('tuna-obs') ||
|
||||||
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
||||||
setupTimeChangeListener();
|
setupTimeChangeListener();
|
||||||
|
setupRepeatChangeListener();
|
||||||
|
setupVolumeChangeListener(apiEvent.detail);
|
||||||
}
|
}
|
||||||
const video = $('video');
|
const video = $('video');
|
||||||
// name = "dataloaded" and abit later "dataupdated"
|
// name = "dataloaded" and abit later "dataupdated"
|
||||||
@ -63,3 +65,21 @@ function setupTimeChangeListener() {
|
|||||||
});
|
});
|
||||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
|
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupRepeatChangeListener() {
|
||||||
|
const repeatObserver = new MutationObserver(mutations => {
|
||||||
|
ipcRenderer.send('repeatChanged', mutations[0].target.title);
|
||||||
|
});
|
||||||
|
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||||
|
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
ipcRenderer.send('repeatChanged', $('#right-controls .repeat').title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVolumeChangeListener(api) {
|
||||||
|
$('video').addEventListener('volumechange', (_) => {
|
||||||
|
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||||
|
});
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||||
|
}
|
||||||
|
|||||||
16
readme.md
16
readme.md
@ -33,6 +33,10 @@ You can check out the [latest release](https://github.com/th-ch/youtube-music/re
|
|||||||
|
|
||||||
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
If you get an error "is damaged and can’t be opened." when launching the app, run `xattr -cr /Applications/YouTube\ Music.app` in Terminal.
|
||||||
|
|
||||||
## Available plugins:
|
## Available plugins:
|
||||||
|
|
||||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||||
@ -41,6 +45,8 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
|||||||
|
|
||||||
- **Blur Nav Bar**: makes navigation bar transparent and blurry
|
- **Blur Nav Bar**: makes navigation bar transparent and blurry
|
||||||
|
|
||||||
|
- **Bypass age restrictions**: bypass YouTube's age verification
|
||||||
|
|
||||||
- **Disable Autoplay**: Makes every song start in "paused" mode
|
- **Disable Autoplay**: Makes every song start in "paused" mode
|
||||||
|
|
||||||
- [**Discord**](https://discord.com/): Show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
- [**Discord**](https://discord.com/): Show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||||
@ -63,6 +69,8 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
|||||||
|
|
||||||
- **Notifications**: Display a notification when a song starts playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) are available on windows)
|
- **Notifications**: Display a notification when a song starts playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) are available on windows)
|
||||||
|
|
||||||
|
- **Picture in picture**: allows to switch the app to picture-in-picture mode
|
||||||
|
|
||||||
- **Playback Speed**: Listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
- **Playback Speed**: Listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||||
|
|
||||||
- **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume steps
|
- **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume steps
|
||||||
@ -81,7 +89,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
|||||||
|
|
||||||
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||||
|
|
||||||
- **Video Toggle**: Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab
|
- **Video Toggle**: Adds a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to switch between Video/Song mode. can also optionally remove the whole video tab
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -89,6 +97,12 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
|||||||
|
|
||||||
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
|
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
|
||||||
|
|
||||||
|
Some predefined themes are available in https://github.com/OceanicSquirrel/themes-for-ytmdesktop-player.
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
const NodeEnvironment = require("jest-environment-node");
|
|
||||||
const { _electron: electron } = require("playwright");
|
|
||||||
|
|
||||||
class TestEnvironment extends NodeEnvironment {
|
|
||||||
constructor(config) {
|
|
||||||
super(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setup() {
|
|
||||||
await super.setup();
|
|
||||||
|
|
||||||
const appPath = path.resolve(__dirname, "..");
|
|
||||||
this.global.__APP__ = await electron.launch({ args: [appPath] });
|
|
||||||
}
|
|
||||||
|
|
||||||
async teardown() {
|
|
||||||
if (this.global.__APP__) {
|
|
||||||
await this.global.__APP__.close();
|
|
||||||
}
|
|
||||||
await super.teardown();
|
|
||||||
}
|
|
||||||
|
|
||||||
runScript(script) {
|
|
||||||
return super.runScript(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TestEnvironment;
|
|
||||||
@ -1,16 +1,29 @@
|
|||||||
/**
|
const path = require("path");
|
||||||
* @jest-environment ./tests/environment
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe("YouTube Music App", () => {
|
const { _electron: electron } = require("playwright");
|
||||||
const app = global.__APP__;
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
test("With default settings, app is launched and visible", async () => {
|
process.env.NODE_ENV = "test";
|
||||||
const window = await app.firstWindow();
|
|
||||||
const title = await window.title();
|
|
||||||
expect(title).toEqual("YouTube Music");
|
|
||||||
|
|
||||||
const url = window.url();
|
const appPath = path.resolve(__dirname, "..");
|
||||||
expect(url.startsWith("https://music.youtube.com")).toBe(true);
|
|
||||||
|
test("YouTube Music App - With default settings, app is launched and visible", async () => {
|
||||||
|
const app = await electron.launch({
|
||||||
|
args: [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--whitelisted-ips=",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
appPath,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const window = await app.firstWindow();
|
||||||
|
const title = await window.title();
|
||||||
|
expect(title).toEqual("YouTube Music");
|
||||||
|
|
||||||
|
const url = window.url();
|
||||||
|
expect(url.startsWith("https://music.youtube.com")).toBe(true);
|
||||||
|
|
||||||
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|||||||
19
tray.js
19
tray.js
@ -1,7 +1,8 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { Menu, nativeImage, Tray } = require("electron");
|
const { app, Menu, nativeImage, Tray } = require("electron");
|
||||||
|
|
||||||
|
const { restart } = require("./providers/app-controls");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const getSongControls = require("./providers/song-controls");
|
const getSongControls = require("./providers/song-controls");
|
||||||
|
|
||||||
@ -27,7 +28,13 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
if (config.get("options.trayClickPlayPause")) {
|
if (config.get("options.trayClickPlayPause")) {
|
||||||
playPause();
|
playPause();
|
||||||
} else {
|
} else {
|
||||||
win.isVisible() ? win.hide() : win.show();
|
if (win.isVisible()) {
|
||||||
|
win.hide();
|
||||||
|
app.dock?.hide();
|
||||||
|
} else {
|
||||||
|
win.show();
|
||||||
|
app.dock?.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,16 +61,14 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
label: "Show",
|
label: "Show",
|
||||||
click: () => {
|
click: () => {
|
||||||
win.show();
|
win.show();
|
||||||
|
app.dock?.show();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Restart App",
|
label: "Restart App",
|
||||||
click: () => {
|
click: restart
|
||||||
app.relaunch();
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ role: "quit" }
|
{ role: "quit" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const trayMenu = Menu.buildFromTemplate(template);
|
const trayMenu = Menu.buildFromTemplate(template);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 816 KiB After Width: | Height: | Size: 726 KiB |
Reference in New Issue
Block a user