mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5dbfaf686 | |||
| 6b7fd5ba63 | |||
| 73a049a7bc | |||
| ef0c30e23a | |||
| 59ed2326d9 | |||
| 07a02c8c82 | |||
| f1050cb676 | |||
| 7131893f1c | |||
| e4dfb2ff33 | |||
| 187fad6834 | |||
| 26df435db0 | |||
| 0bee281d1d | |||
| 26de5802a0 | |||
| c258a4855e | |||
| b7b6d50ba2 | |||
| 0376a30fbb | |||
| ca92031e89 | |||
| 986d2ad5b1 | |||
| d9b8d8c48d | |||
| 0ef34d7c71 | |||
| f87607d25d | |||
| cc0bfae067 | |||
| e7d2d04f5a | |||
| f4319ebc6b |
10
changelog.md
10
changelog.md
@ -2,7 +2,15 @@
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
#### [v2.0.0](https://github.com/th-ch/youtube-music/compare/v1.20.0...v2.0.0)
|
#### [v2.0.1](https://github.com/th-ch/youtube-music/compare/v2.0.0...v2.0.1)
|
||||||
|
|
||||||
|
- Update changelog for v2.0.0 [`2d69dfd`](https://github.com/th-ch/youtube-music/commit/2d69dfd333c3223ecc7de13a0abc98fd99aa3a2b)
|
||||||
|
- hotfix: hotfix for #1267 [`c002263`](https://github.com/th-ch/youtube-music/commit/c002263c3bdd51890b8ffb431283afb60405d8fe)
|
||||||
|
- Bump version to 2.0.1 [`a1f025e`](https://github.com/th-ch/youtube-music/commit/a1f025e23c599fe5eb63b32ea38ee81200d232d6)
|
||||||
|
|
||||||
|
### [v2.0.0](https://github.com/th-ch/youtube-music/compare/v1.20.0...v2.0.0)
|
||||||
|
|
||||||
|
> 7 October 2023
|
||||||
|
|
||||||
- Bump version to 2.0.0 [`#1257`](https://github.com/th-ch/youtube-music/pull/1257)
|
- Bump version to 2.0.0 [`#1257`](https://github.com/th-ch/youtube-music/pull/1257)
|
||||||
- feat(GitHub): add issue template [`#1264`](https://github.com/th-ch/youtube-music/pull/1264)
|
- feat(GitHub): add issue template [`#1264`](https://github.com/th-ch/youtube-music/pull/1264)
|
||||||
|
|||||||
32
index.ts
32
index.ts
@ -226,15 +226,15 @@ function createMainWindow() {
|
|||||||
loadPlugins(win);
|
loadPlugins(win);
|
||||||
|
|
||||||
if (windowPosition) {
|
if (windowPosition) {
|
||||||
const { x, y } = windowPosition;
|
const { x: windowX, y: windowY } = windowPosition;
|
||||||
const winSize = win.getSize();
|
const winSize = win.getSize();
|
||||||
const displaySize
|
const displaySize
|
||||||
= screen.getDisplayNearestPoint(windowPosition).bounds;
|
= screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||||
if (
|
if (
|
||||||
x + winSize[0] < displaySize.x - 8
|
windowX + winSize[0] < displaySize.x - 8
|
||||||
|| x - winSize[0] > displaySize.x + displaySize.width
|
|| windowX - winSize[0] > displaySize.x + displaySize.width
|
||||||
|| y < displaySize.y - 8
|
|| windowY < displaySize.y - 8
|
||||||
|| y > displaySize.y + displaySize.height
|
|| windowY > displaySize.y + displaySize.height
|
||||||
) {
|
) {
|
||||||
// Window is offscreen
|
// Window is offscreen
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
@ -243,7 +243,7 @@ function createMainWindow() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
win.setPosition(x, y);
|
win.setPosition(windowX, windowY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,26 +261,6 @@ function createMainWindow() {
|
|||||||
win.webContents.loadURL(urlToLoad);
|
win.webContents.loadURL(urlToLoad);
|
||||||
win.on('closed', onClosed);
|
win.on('closed', onClosed);
|
||||||
|
|
||||||
const scaleFactor = screen.getAllDisplays().length > 1 ? screen.getPrimaryDisplay().scaleFactor : 1;
|
|
||||||
const size = config.get('window-size');
|
|
||||||
const position = config.get('window-position');
|
|
||||||
|
|
||||||
if (size && size.width && size.height) {
|
|
||||||
const scaledSize = {
|
|
||||||
width: size.width / scaleFactor,
|
|
||||||
height: size.height / scaleFactor,
|
|
||||||
};
|
|
||||||
win.setSize(scaledSize.width, scaledSize.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position && position.x && position.y) {
|
|
||||||
const scaledPosition = {
|
|
||||||
x: position.x / scaleFactor,
|
|
||||||
y: position.y / scaleFactor,
|
|
||||||
};
|
|
||||||
win.setPosition(scaledPosition.x, scaledPosition.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
|
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
|
||||||
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"icon": "assets/generated/icons/win/icon.ico",
|
"icon": "assets/generated/icons/win/icon.ico",
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis-web",
|
||||||
"arch": [
|
"arch": [
|
||||||
"x64",
|
"x64",
|
||||||
"ia32",
|
"ia32",
|
||||||
@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsisWeb": {
|
||||||
"runAfterFinish": false
|
"runAfterFinish": false
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
@ -119,7 +119,7 @@
|
|||||||
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
|
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
|
||||||
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
|
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
|
||||||
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
|
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
|
||||||
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis:x64 -p never",
|
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis-web:x64 -p never",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"changelog": "auto-changelog",
|
"changelog": "auto-changelog",
|
||||||
"plugins": "npm run plugin:bypass-age-restrictions",
|
"plugins": "npm run plugin:bypass-age-restrictions",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { dev } from 'electron-is';
|
|||||||
|
|
||||||
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||||
|
|
||||||
import registerCallback from '../../providers/song-info';
|
import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
|
||||||
|
|
||||||
import type { ConfigType } from '../../config/dynamic';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export interface Info {
|
|||||||
rpc: DiscordClient;
|
rpc: DiscordClient;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
autoReconnect: boolean;
|
autoReconnect: boolean;
|
||||||
lastSongInfo?: import('../../providers/song-info').SongInfo;
|
lastSongInfo?: SongInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
@ -44,31 +44,6 @@ const resetInfo = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info.rpc.on('connected', () => {
|
|
||||||
if (dev()) {
|
|
||||||
console.log('discord connected');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const cb of refreshCallbacks) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info.rpc.on('ready', () => {
|
|
||||||
info.ready = true;
|
|
||||||
if (info.lastSongInfo) {
|
|
||||||
updateActivity(info.lastSongInfo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info.rpc.on('disconnected', () => {
|
|
||||||
resetInfo();
|
|
||||||
|
|
||||||
if (info.autoReconnect) {
|
|
||||||
connectTimeout();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
|
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
|
||||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
if (!info.autoReconnect || info.rpc.isConnected) {
|
||||||
return;
|
return;
|
||||||
@ -117,7 +92,7 @@ export const connect = (showError = false) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let clearActivity: NodeJS.Timeout | undefined;
|
let clearActivity: NodeJS.Timeout | undefined;
|
||||||
let updateActivity: import('../../providers/song-info').SongInfoCallback;
|
let updateActivity: SongInfoCallback;
|
||||||
|
|
||||||
type DiscordOptions = ConfigType<'discord'>;
|
type DiscordOptions = ConfigType<'discord'>;
|
||||||
|
|
||||||
@ -125,6 +100,31 @@ export default (
|
|||||||
win: Electron.BrowserWindow,
|
win: Electron.BrowserWindow,
|
||||||
options: DiscordOptions,
|
options: DiscordOptions,
|
||||||
) => {
|
) => {
|
||||||
|
info.rpc.on('connected', () => {
|
||||||
|
if (dev()) {
|
||||||
|
console.log('discord connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cb of refreshCallbacks) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
info.rpc.on('ready', () => {
|
||||||
|
info.ready = true;
|
||||||
|
if (info.lastSongInfo) {
|
||||||
|
updateActivity(info.lastSongInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
info.rpc.on('disconnected', () => {
|
||||||
|
resetInfo();
|
||||||
|
|
||||||
|
if (info.autoReconnect) {
|
||||||
|
connectTimeout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
info.autoReconnect = options.autoReconnect;
|
info.autoReconnect = options.autoReconnect;
|
||||||
|
|
||||||
window = win;
|
window = win;
|
||||||
|
|||||||
3
plugins/in-app-menu/assets/close.svg
Normal file
3
plugins/in-app-menu/assets/close.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#ffffff" d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 392 B |
3
plugins/in-app-menu/assets/maximize.svg
Normal file
3
plugins/in-app-menu/assets/maximize.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#ffffff" d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
3
plugins/in-app-menu/assets/minimize.svg
Normal file
3
plugins/in-app-menu/assets/minimize.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#ffffff" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 174 B |
3
plugins/in-app-menu/assets/unmaximize.svg
Normal file
3
plugins/in-app-menu/assets/unmaximize.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#ffffff" d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 546 B |
@ -1,5 +1,3 @@
|
|||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import { register } from 'electron-localshortcut';
|
import { register } from 'electron-localshortcut';
|
||||||
|
|
||||||
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
|
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
|
||||||
@ -25,7 +23,7 @@ export default (win: BrowserWindow) => {
|
|||||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getMenuItemById = (commandId: number): MenuItem | null => {
|
const getMenuItemById = (commandId: number): MenuItem | null => {
|
||||||
const menu = Menu.getApplicationMenu();
|
const menu = Menu.getApplicationMenu();
|
||||||
|
|
||||||
@ -40,7 +38,7 @@ export default (win: BrowserWindow) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,4 +55,11 @@ export default (win: BrowserWindow) => {
|
|||||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-is-maximized', () => win.isMaximized());
|
||||||
|
|
||||||
|
ipcMain.handle('window-close', () => win.close());
|
||||||
|
ipcMain.handle('window-minimize', () => win.minimize());
|
||||||
|
ipcMain.handle('window-maximize', () => win.maximize());
|
||||||
|
ipcMain.handle('window-unmaximize', () => win.unmaximize());
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import { ipcRenderer, Menu } from 'electron';
|
|||||||
|
|
||||||
import { createPanel } from './menu/panel';
|
import { createPanel } from './menu/panel';
|
||||||
|
|
||||||
import logo from '../../assets/menu.svg';
|
import logo from './assets/menu.svg';
|
||||||
|
import close from './assets/close.svg';
|
||||||
|
import minimize from './assets/minimize.svg';
|
||||||
|
import maximize from './assets/maximize.svg';
|
||||||
|
import unmaximize from './assets/unmaximize.svg';
|
||||||
|
|
||||||
import { isEnabled } from '../../config/plugins';
|
import { isEnabled } from '../../config/plugins';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
|
||||||
@ -11,8 +16,9 @@ function $<E extends Element = Element>(selector: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMacOS = navigator.userAgent.includes('Macintosh');
|
const isMacOS = navigator.userAgent.includes('Macintosh');
|
||||||
|
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
|
||||||
|
|
||||||
export default () => {
|
export default async () => {
|
||||||
let hideMenu = config.get('options.hideMenu');
|
let hideMenu = config.get('options.hideMenu');
|
||||||
const titleBar = document.createElement('title-bar');
|
const titleBar = document.createElement('title-bar');
|
||||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||||
@ -39,6 +45,60 @@ export default () => {
|
|||||||
if (!isMacOS) titleBar.appendChild(logo);
|
if (!isMacOS) titleBar.appendChild(logo);
|
||||||
document.body.appendChild(titleBar);
|
document.body.appendChild(titleBar);
|
||||||
|
|
||||||
|
titleBar.appendChild(logo);
|
||||||
|
|
||||||
|
const addWindowControls = async () => {
|
||||||
|
|
||||||
|
// Create window control buttons
|
||||||
|
const minimizeButton = document.createElement('button');
|
||||||
|
minimizeButton.classList.add('window-control');
|
||||||
|
minimizeButton.appendChild(minimize);
|
||||||
|
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
|
||||||
|
|
||||||
|
const maximizeButton = document.createElement('button');
|
||||||
|
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||||
|
maximizeButton.classList.add('window-control');
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
} else {
|
||||||
|
maximizeButton.classList.add('window-control');
|
||||||
|
maximizeButton.appendChild(maximize);
|
||||||
|
}
|
||||||
|
maximizeButton.onclick = async () => {
|
||||||
|
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||||
|
// change icon to maximize
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||||
|
maximizeButton.appendChild(maximize);
|
||||||
|
|
||||||
|
// call unmaximize
|
||||||
|
await ipcRenderer.invoke('window-unmaximize');
|
||||||
|
} else {
|
||||||
|
// change icon to unmaximize
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
|
||||||
|
// call maximize
|
||||||
|
await ipcRenderer.invoke('window-maximize');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeButton = document.createElement('button');
|
||||||
|
closeButton.classList.add('window-control');
|
||||||
|
closeButton.appendChild(close);
|
||||||
|
closeButton.onclick = () => ipcRenderer.invoke('window-close');
|
||||||
|
|
||||||
|
// Create a container div for the window control buttons
|
||||||
|
const windowControlsContainer = document.createElement('div');
|
||||||
|
windowControlsContainer.classList.add('window-controls-container');
|
||||||
|
windowControlsContainer.appendChild(minimizeButton);
|
||||||
|
windowControlsContainer.appendChild(maximizeButton);
|
||||||
|
windowControlsContainer.appendChild(closeButton);
|
||||||
|
|
||||||
|
// Add window control buttons to the title bar
|
||||||
|
titleBar.appendChild(windowControlsContainer);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||||
|
|
||||||
if (navBar) {
|
if (navBar) {
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach(() => {
|
mutations.forEach(() => {
|
||||||
@ -69,8 +129,9 @@ export default () => {
|
|||||||
menu.style.visibility = 'hidden';
|
menu.style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||||
};
|
};
|
||||||
updateMenu();
|
await updateMenu();
|
||||||
|
|
||||||
document.title = 'Youtube Music';
|
document.title = 'Youtube Music';
|
||||||
|
|
||||||
|
|||||||
@ -101,6 +101,15 @@ export const createPanel = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
panel.setAttribute('open', 'true');
|
panel.setAttribute('open', 'true');
|
||||||
|
|
||||||
|
// Children are placed below their parent item, which can cause
|
||||||
|
// long lists to squeeze their children at the bottom of the screen
|
||||||
|
// (This needs to be done *after* setAttribute)
|
||||||
|
panel.classList.remove('position-by-bottom');
|
||||||
|
if (options.placement === 'right' && panel.scrollHeight > panel.clientHeight ) {
|
||||||
|
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
|
||||||
|
panel.classList.add('position-by-bottom');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
anchor.addEventListener('click', () => {
|
anchor.addEventListener('click', () => {
|
||||||
|
|||||||
@ -80,6 +80,11 @@ menu-panel[open="true"] {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
menu-panel.position-by-bottom {
|
||||||
|
top: unset;
|
||||||
|
bottom: calc(100vh - var(--y, 100%));
|
||||||
|
max-height: calc(var(--y, 0) - var(--menu-bar-height, 36px) - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
menu-item {
|
menu-item {
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
@ -121,6 +126,33 @@ menu-separator {
|
|||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Window control container */
|
||||||
|
|
||||||
|
.window-controls-container {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end; /* Align to the right end of the title-bar */
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px; /* Add spacing between the window control buttons */
|
||||||
|
position: absolute; /* Position it absolutely within title-bar */
|
||||||
|
right: 4px; /* Adjust the right position as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window control buttons */
|
||||||
|
|
||||||
|
.window-control {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #f1f1f1;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* youtube-music style */
|
/* youtube-music style */
|
||||||
|
|
||||||
ytmusic-app-layout {
|
ytmusic-app-layout {
|
||||||
|
|||||||
@ -15,11 +15,24 @@ interface LastFmData {
|
|||||||
timestamp?: number,
|
timestamp?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFormData = (parameters: Record<string, unknown>) => {
|
interface LastFmSongData {
|
||||||
|
track?: string,
|
||||||
|
duration?: number,
|
||||||
|
artist?: string,
|
||||||
|
album?: string,
|
||||||
|
api_key: string,
|
||||||
|
sk?: string,
|
||||||
|
format: string,
|
||||||
|
method: string,
|
||||||
|
timestamp?: number,
|
||||||
|
api_sig?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFormData = (parameters: LastFmSongData) => {
|
||||||
// Creates the body for in the post request
|
// Creates the body for in the post request
|
||||||
const formData = new URLSearchParams();
|
const formData = new URLSearchParams();
|
||||||
for (const key in parameters) {
|
for (const key in parameters) {
|
||||||
formData.append(key, String(parameters[key]));
|
formData.append(key, String(parameters[key as keyof LastFmSongData]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
@ -36,7 +49,7 @@ const createQueryString = (parameters: Record<string, unknown>, apiSignature: st
|
|||||||
return '?' + queryData.join('&');
|
return '?' + queryData.join('&');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createApiSig = (parameters: Record<string, unknown>, secret: string) => {
|
const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
||||||
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
||||||
const keys = Object.keys(parameters);
|
const keys = Object.keys(parameters);
|
||||||
|
|
||||||
@ -47,7 +60,7 @@ const createApiSig = (parameters: Record<string, unknown>, secret: string) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
sig += `${key}${String(parameters[key])}`;
|
sig += `${key}${parameters[key as keyof LastFmSongData]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sig += secret;
|
sig += secret;
|
||||||
@ -59,7 +72,7 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
|
|||||||
// Creates and stores the auth token
|
// Creates and stores the auth token
|
||||||
const data = {
|
const data = {
|
||||||
method: 'auth.gettoken',
|
method: 'auth.gettoken',
|
||||||
apiKey,
|
api_key: apiKey,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
};
|
};
|
||||||
const apiSigature = createApiSig(data, secret);
|
const apiSigature = createApiSig(data, secret);
|
||||||
@ -68,10 +81,9 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
|
|||||||
return json?.token;
|
return json?.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticateAndGetToken = async (config: LastFMOptions) => {
|
const authenticate = async (config: LastFMOptions) => {
|
||||||
// Asks the user for authentication
|
// Asks the user for authentication
|
||||||
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||||
return await createToken(config);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAndSetSessionKey = async (config: LastFMOptions) => {
|
const getAndSetSessionKey = async (config: LastFMOptions) => {
|
||||||
@ -86,18 +98,19 @@ const getAndSetSessionKey = async (config: LastFMOptions) => {
|
|||||||
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
|
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
|
||||||
const json = await response.json() as {
|
const json = await response.json() as {
|
||||||
error?: string,
|
error?: string,
|
||||||
session?: {
|
session?: {
|
||||||
key: string,
|
key: string,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (json.error) {
|
if (json.error) {
|
||||||
config.token = await authenticateAndGetToken(config);
|
config.token = await createToken(config);
|
||||||
|
await authenticate(config);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
}
|
}
|
||||||
if (json.session) {
|
if (json.session) {
|
||||||
config.session_key = json?.session?.key;
|
config.session_key = json.session.key;
|
||||||
setOptions('last-fm', config);
|
|
||||||
}
|
}
|
||||||
|
setOptions('last-fm', config);
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,20 +120,20 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data
|
|||||||
await getAndSetSessionKey(config);
|
await getAndSetSessionKey(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
const postData = {
|
const postData: LastFmSongData = {
|
||||||
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
|
...(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,
|
||||||
api_sig: '',
|
|
||||||
sk: config.session_key,
|
sk: config.session_key,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
postData.api_sig = createApiSig(postData, config.secret);
|
postData.api_sig = createApiSig(postData, config.secret);
|
||||||
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) })
|
const formData = createFormData(postData);
|
||||||
|
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: formData })
|
||||||
.catch(async (error: {
|
.catch(async (error: {
|
||||||
response?: {
|
response?: {
|
||||||
data?: {
|
data?: {
|
||||||
@ -131,7 +144,8 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data
|
|||||||
if (error?.response?.data?.error === 9) {
|
if (error?.response?.data?.error === 9) {
|
||||||
// Session key is invalid, so remove it from the config and reauthenticate
|
// Session key is invalid, so remove it from the config and reauthenticate
|
||||||
config.session_key = undefined;
|
config.session_key = undefined;
|
||||||
config.token = await authenticateAndGetToken(config);
|
config.token = await createToken(config);
|
||||||
|
await authenticate(config);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
10
readme.md
10
readme.md
@ -65,6 +65,16 @@ file).*
|
|||||||
winget install th-ch.YouTubeMusic
|
winget install th-ch.YouTubeMusic
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### How to install without a network connection? (in Windows)
|
||||||
|
|
||||||
|
- Download the `*.nsis.7z` file for _your device architecture_ in [release page](https://github.com/th-ch/youtube-music/releases/latest).
|
||||||
|
- `x64` for 64-bit Windows
|
||||||
|
- `ia32` for 32-bit Windows
|
||||||
|
- `arm64` for ARM64 Windows
|
||||||
|
- Download installer in release page. (`*-Setup.exe`)
|
||||||
|
- Place them in the **same directory**.
|
||||||
|
- Run the installer.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@ -30,6 +30,9 @@ test('YouTube Music App - With default settings, app is launched and visible', a
|
|||||||
await consentForm.click('button');
|
await consentForm.click('button');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = await window.title();
|
||||||
|
expect(title.replaceAll(/\s/g, ' ')).toEqual('YouTube Music');
|
||||||
|
|
||||||
const url = window.url();
|
const url = window.url();
|
||||||
expect(url.startsWith('https://music.youtube.com')).toBe(true);
|
expect(url.startsWith('https://music.youtube.com')).toBe(true);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user