QOL: Move source code under the src directory. (#1318)

This commit is contained in:
Angelos Bouklis
2023-10-15 15:52:48 +03:00
committed by GitHub
parent 30c8dcf730
commit 7625a3aa52
159 changed files with 102 additions and 71 deletions

View File

@ -0,0 +1,111 @@
import { app, BrowserWindow, ipcMain } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
import { setOptions as setPluginOptions } from '../../config/plugins';
import type { ConfigType } from '../../config/dynamic';
let isInPiP = false;
let originalPosition: number[];
let originalSize: number[];
let originalFullScreen: boolean;
let originalMaximized: boolean;
let win: BrowserWindow;
type PiPOptions = ConfigType<'picture-in-picture'>;
let options: Partial<PiPOptions>;
const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
const setLocalOptions = (_options: Partial<PiPOptions>) => {
options = { ...options, ..._options };
setPluginOptions('picture-in-picture', _options);
};
const togglePiP = () => {
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.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);
}
} 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;

View File

@ -0,0 +1,179 @@
import { ipcRenderer } from 'electron';
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
import keyEventAreEqual from 'keyboardevents-areequal';
import pipHTML from './templates/picture-in-picture.html';
import { getSongMenu } from '../../providers/dom-elements';
import { ElementFromHtml } from '../utils';
import type { ConfigType } from '../../config/dynamic';
type PiPOptions = ConfigType<'picture-in-picture'>;
function $<E extends Element = Element>(selector: string) {
return document.querySelector<E>(selector);
}
let useNativePiP = false;
let menu: Element | null = null;
const pipButton = ElementFromHtml(pipHTML);
// Will also clone
function replaceButton(query: string, button: Element) {
const svg = button.querySelector('#icon svg')?.cloneNode(true);
if (svg) {
button.replaceWith(button.cloneNode(true));
button.remove();
const newButton = $(query);
if (newButton) {
newButton.querySelector('#icon')?.append(svg);
}
return newButton;
}
return null;
}
function cloneButton(query: string) {
const button = $(query);
if (button) {
replaceButton(query, button);
}
return $(query);
}
const observer = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
if (!menu) {
return;
}
}
if (
menu.contains(pipButton) ||
!(menu.parentElement as (HTMLElement & { eventSink_: Element }) | null)
?.eventSink_
?.matches('ytmusic-menu-renderer.ytmusic-player-bar')
) {
return;
}
const menuUrl = $<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
if (!menuUrl?.includes('watch?')) {
return;
}
menu.prepend(pipButton);
});
const togglePictureInPicture = async () => {
if (useNativePiP) {
const isInPiP = document.pictureInPictureElement !== null;
const video = $<HTMLVideoElement>('video');
const togglePiP = () =>
isInPiP
? document.exitPictureInPicture.call(document)
: video?.requestPictureInPicture?.call(video);
try {
await togglePiP();
$<HTMLButtonElement>('#icon')?.click(); // Close the menu
return true;
} catch {
}
}
ipcRenderer.send('picture-in-picture');
return false;
};
// For UI (HTML)
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
(global as any).togglePictureInPicture = togglePictureInPicture;
const listenForToggle = () => {
const originalExitButton = $<HTMLButtonElement>('.exit-fullscreen-button');
const appLayout = $<HTMLElement>('ytmusic-app-layout');
const expandMenu = $<HTMLElement>('#expanding-menu');
const middleControls = $<HTMLButtonElement>('.middle-controls');
const playerPage = $<HTMLElement & { playerPageOpen_: boolean }>('ytmusic-player-page');
const togglePlayerPageButton = $<HTMLButtonElement>('.toggle-player-page-button');
const fullScreenButton = $<HTMLButtonElement>('.fullscreen-button');
const player = $<HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }>('#player');
const onPlayerDblClick = player?.onDoubleClick_;
const mouseLeaveEventListener = () => middleControls?.click();
const titlebar = $<HTMLElement>('.cet-titlebar');
ipcRenderer.on('pip-toggle', (_, isPip: boolean) => {
if (originalExitButton && player) {
if (isPip) {
replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture());
player.onDoubleClick_ = () => {
};
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
if (!playerPage?.playerPageOpen_) {
togglePlayerPageButton?.click();
}
fullScreenButton?.click();
appLayout?.classList.add('pip');
if (titlebar) {
titlebar.style.display = 'none';
}
} else {
$('.exit-fullscreen-button')?.replaceWith(originalExitButton);
player.onDoubleClick_ = onPlayerDblClick;
expandMenu?.removeEventListener('mouseleave', mouseLeaveEventListener);
originalExitButton.click();
appLayout?.classList.remove('pip');
if (titlebar) {
titlebar.style.display = 'flex';
}
}
}
});
};
function observeMenu(options: PiPOptions) {
useNativePiP = options.useNativePiP;
document.addEventListener(
'apiLoaded',
() => {
listenForToggle();
cloneButton('.player-minimize-button')?.addEventListener('click', async () => {
await togglePictureInPicture();
setTimeout(() => $<HTMLButtonElement>('#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
const popupContainer = $('ytmusic-popup-container');
if (popupContainer) observer.observe(popupContainer, {
childList: true,
subtree: true,
});
},
{ once: true, passive: true },
);
}
export default (options: PiPOptions) => {
observeMenu(options);
if (options.hotkey) {
const hotkeyEvent = toKeyEvent(options.hotkey);
window.addEventListener('keydown', (event) => {
if (
keyEventAreEqual(event, hotkeyEvent)
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) {
togglePictureInPicture();
}
});
}
};

View File

@ -0,0 +1,12 @@
declare module 'keyboardevent-from-electron-accelerator' {
interface KeyboardEvent {
key?: string;
code?: string;
metaKey?: boolean;
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
}
export const toKeyEvent: (accelerator: string) => KeyboardEvent;
}

View File

@ -0,0 +1,14 @@
declare module 'keyboardevents-areequal' {
interface KeyboardEvent {
key?: string;
code?: string;
metaKey?: boolean;
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
}
const areEqual: (event1: KeyboardEvent, event2: KeyboardEvent) => boolean;
export default areEqual;
}

View File

@ -0,0 +1,75 @@
import prompt from 'custom-electron-prompt';
import { BrowserWindow } from 'electron';
import { setOptions } from './back';
import promptOptions from '../../providers/prompt-options';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic';
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);
},
},
{
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,
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);
if (output) {
const { value, accelerator } = output[0];
setOptions({ [value]: accelerator });
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 });
},
},
];

View File

@ -0,0 +1,43 @@
/* 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;
}
/* make player-bar not draggable if in-app-menu is enabled */
.cet-container ytmusic-app-layout.pip ytmusic-player-bar {
-webkit-app-region: no-drag !important;
}
/* make player draggable if in-app-menu is enabled */
.cet-container ytmusic-app-layout.pip #player {
-webkit-app-region: drag !important;
}
/* remove info, thumbnail and menu from player-bar */
ytmusic-app-layout.pip ytmusic-player-bar .content-info-wrapper,
ytmusic-app-layout.pip ytmusic-player-bar .thumbnail-image-wrapper,
ytmusic-app-layout.pip ytmusic-player-bar ytmusic-menu-renderer {
display: none !important;
}
/* disable the video-toggle button when in PiP mode */
ytmusic-app-layout.pip .video-switch-button {
display: none !important;
}

View File

@ -0,0 +1,50 @@
<div
aria-disabled="false"
aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
onclick="togglePictureInPicture()"
role="option"
tabindex="-1"
>
<div
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
id="navigation-endpoint"
tabindex="-1"
>
<div
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
>
<svg
id="Layer_1"
style="enable-background: new 0 0 512 512"
version="1.1"
viewBox="0 0 512 512"
x="0px"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
y="0px"
>
<style type="text/css">
.st0 {
fill: #aaaaaa;
}
</style>
<g id="XMLID_6_">
<path
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"
id="XMLID_11_"
/>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-pip"
>
Picture in picture
</div>
</div>
</div>