mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 04:11:47 +00:00
feat: migrate to new plugin api
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
40
src/plugins/picture-in-picture/index.ts
Normal file
40
src/plugins/picture-in-picture/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
|
||||
export type PictureInPicturePluginConfig = {
|
||||
'enabled': boolean;
|
||||
'alwaysOnTop': boolean;
|
||||
'savePosition': boolean;
|
||||
'saveSize': boolean;
|
||||
'hotkey': 'P',
|
||||
'pip-position': [number, number];
|
||||
'pip-size': [number, number];
|
||||
'isInPiP': boolean;
|
||||
'useNativePiP': boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('picture-in-picture', {
|
||||
name: 'Picture In Picture',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
'enabled': false,
|
||||
'alwaysOnTop': true,
|
||||
'savePosition': true,
|
||||
'saveSize': false,
|
||||
'hotkey': 'P',
|
||||
'pip-position': [10, 10],
|
||||
'pip-size': [450, 275],
|
||||
'isInPiP': false,
|
||||
'useNativePiP': true,
|
||||
} as PictureInPicturePluginConfig,
|
||||
styles: [style],
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
@ -1,111 +1,121 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
import style from './style.css?inline';
|
||||
|
||||
import builder, { PictureInPicturePluginConfig } from './index';
|
||||
|
||||
import { injectCSS } from '../utils/main';
|
||||
import { setOptions as setPluginOptions } from '../../config/plugins';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
export default builder.createMain(({ getConfig, setConfig, send, handle }) => {
|
||||
let isInPiP = false;
|
||||
let originalPosition: number[];
|
||||
let originalSize: number[];
|
||||
let originalFullScreen: boolean;
|
||||
let originalMaximized: boolean;
|
||||
|
||||
let isInPiP = false;
|
||||
let originalPosition: number[];
|
||||
let originalSize: number[];
|
||||
let originalFullScreen: boolean;
|
||||
let originalMaximized: boolean;
|
||||
let win: BrowserWindow;
|
||||
|
||||
let win: BrowserWindow;
|
||||
let config: PictureInPicturePluginConfig;
|
||||
|
||||
type PiPOptions = ConfigType<'picture-in-picture'>;
|
||||
const pipPosition = () => (config.savePosition && config['pip-position']) || [10, 10];
|
||||
const pipSize = () => (config.saveSize && config['pip-size']) || [450, 275];
|
||||
|
||||
let options: Partial<PiPOptions>;
|
||||
const togglePiP = () => {
|
||||
isInPiP = !isInPiP;
|
||||
setConfig({ isInPiP });
|
||||
|
||||
const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
|
||||
const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(false);
|
||||
}
|
||||
|
||||
const setLocalOptions = (_options: Partial<PiPOptions>) => {
|
||||
options = { ...options, ..._options };
|
||||
setPluginOptions('picture-in-picture', _options);
|
||||
};
|
||||
originalMaximized = win.isMaximized();
|
||||
if (originalMaximized) {
|
||||
win.unmaximize();
|
||||
}
|
||||
|
||||
const togglePiP = () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(false);
|
||||
handle('before-input-event', blockShortcutsInPiP);
|
||||
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
|
||||
send('pip-toggle', true);
|
||||
|
||||
app.dock?.hide();
|
||||
win.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
app.dock?.show();
|
||||
if (config.alwaysOnTop) {
|
||||
win.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
send('pip-toggle', false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
win.setAlwaysOnTop(false);
|
||||
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
}
|
||||
|
||||
if (originalMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
originalMaximized = win.isMaximized();
|
||||
if (originalMaximized) {
|
||||
win.unmaximize();
|
||||
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: Electron.Event, input: Electron.Input) => {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === 'f') {
|
||||
event.preventDefault();
|
||||
} else if (key === 'escape') {
|
||||
togglePiP();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
return ({
|
||||
async onLoad(window) {
|
||||
config ??= await getConfig();
|
||||
win ??= window;
|
||||
setConfig({ isInPiP });
|
||||
injectCSS(win.webContents, style);
|
||||
ipcMain.on('picture-in-picture', () => {
|
||||
togglePiP();
|
||||
});
|
||||
|
||||
win.webContents.on('before-input-event', blockShortcutsInPiP);
|
||||
window.on('move', () => {
|
||||
if (config.isInPiP && !config.useNativePiP) {
|
||||
setConfig({ 'pip-position': window.getPosition() as [number, number] });
|
||||
}
|
||||
});
|
||||
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
|
||||
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);
|
||||
window.on('resize', () => {
|
||||
if (config.isInPiP && !config.useNativePiP) {
|
||||
setConfig({ 'pip-size': window.getSize() as [number, number] });
|
||||
}
|
||||
});
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
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: Electron.Event, input: Electron.Input) => {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === 'f') {
|
||||
event.preventDefault();
|
||||
} else if (key === 'escape') {
|
||||
togglePiP();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
export default (_win: BrowserWindow, _options: PiPOptions) => {
|
||||
options ??= _options;
|
||||
win ??= _win;
|
||||
setLocalOptions({ isInPiP });
|
||||
injectCSS(win.webContents, style);
|
||||
ipcMain.on('picture-in-picture', () => {
|
||||
togglePiP();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export const setOptions = setLocalOptions;
|
||||
|
||||
@ -1,75 +1,74 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { setOptions } from './main';
|
||||
import builder from './index';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
export default builder.createMenu(async ({ window, getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
export default (win: BrowserWindow, options: ConfigType<'picture-in-picture'>): MenuTemplate => [
|
||||
{
|
||||
label: 'Always on top',
|
||||
type: 'checkbox',
|
||||
checked: options.alwaysOnTop,
|
||||
click(item) {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
return [
|
||||
{
|
||||
label: 'Always on top',
|
||||
type: 'checkbox',
|
||||
checked: config.alwaysOnTop,
|
||||
click(item) {
|
||||
setConfig({ alwaysOnTop: item.checked });
|
||||
window.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save window position',
|
||||
type: 'checkbox',
|
||||
checked: options.savePosition,
|
||||
click(item) {
|
||||
setOptions({ savePosition: item.checked });
|
||||
{
|
||||
label: 'Save window position',
|
||||
type: 'checkbox',
|
||||
checked: config.savePosition,
|
||||
click(item) {
|
||||
setConfig({ savePosition: item.checked });
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save window size',
|
||||
type: 'checkbox',
|
||||
checked: options.saveSize,
|
||||
click(item) {
|
||||
setOptions({ saveSize: item.checked });
|
||||
{
|
||||
label: 'Save window size',
|
||||
type: 'checkbox',
|
||||
checked: config.saveSize,
|
||||
click(item) {
|
||||
setConfig({ saveSize: item.checked });
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hotkey',
|
||||
type: 'checkbox',
|
||||
checked: !!options.hotkey,
|
||||
async click(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);
|
||||
{
|
||||
label: 'Hotkey',
|
||||
type: 'checkbox',
|
||||
checked: !!config.hotkey,
|
||||
async click(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: config.hotkey,
|
||||
}],
|
||||
...promptOptions(),
|
||||
}, window);
|
||||
|
||||
if (output) {
|
||||
const { value, accelerator } = output[0];
|
||||
setOptions({ [value]: accelerator });
|
||||
if (output) {
|
||||
const { value, accelerator } = output[0];
|
||||
setConfig({ [value]: accelerator });
|
||||
|
||||
item.checked = !!accelerator;
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
item.checked = !!accelerator;
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Use native PiP',
|
||||
type: 'checkbox',
|
||||
checked: options.useNativePiP,
|
||||
click(item) {
|
||||
setOptions({ useNativePiP: item.checked });
|
||||
{
|
||||
label: 'Use native PiP',
|
||||
type: 'checkbox',
|
||||
checked: config.useNativePiP,
|
||||
click(item) {
|
||||
setConfig({ useNativePiP: item.checked });
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
});
|
||||
|
||||
@ -3,14 +3,12 @@ import keyEventAreEqual from 'keyboardevents-areequal';
|
||||
|
||||
import pipHTML from './templates/picture-in-picture.html?raw';
|
||||
|
||||
import builder, { PictureInPicturePluginConfig } from './index';
|
||||
|
||||
import { getSongMenu } from '../../providers/dom-elements';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
type PiPOptions = ConfigType<'picture-in-picture'>;
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
@ -135,7 +133,7 @@ const listenForToggle = () => {
|
||||
});
|
||||
};
|
||||
|
||||
function observeMenu(options: PiPOptions) {
|
||||
function observeMenu(options: PictureInPicturePluginConfig) {
|
||||
useNativePiP = options.useNativePiP;
|
||||
document.addEventListener(
|
||||
'apiLoaded',
|
||||
@ -160,18 +158,24 @@ function observeMenu(options: PiPOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
export default (options: PiPOptions) => {
|
||||
observeMenu(options);
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
return {
|
||||
async onLoad() {
|
||||
const config = await getConfig();
|
||||
|
||||
if (options.hotkey) {
|
||||
const hotkeyEvent = toKeyEvent(options.hotkey);
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (
|
||||
keyEventAreEqual(event, hotkeyEvent)
|
||||
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
|
||||
) {
|
||||
togglePictureInPicture();
|
||||
observeMenu(config);
|
||||
|
||||
if (config.hotkey) {
|
||||
const hotkeyEvent = toKeyEvent(config.hotkey);
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (
|
||||
keyEventAreEqual(event, hotkeyEvent)
|
||||
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
|
||||
) {
|
||||
togglePictureInPicture();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user