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,69 @@
import { BrowserWindow, globalShortcut } from 'electron';
import is from 'electron-is';
import electronLocalshortcut from 'electron-localshortcut';
import registerMPRIS from './mpris';
import getSongControls from '../../providers/song-controls';
import type { ConfigType } from '../../config/dynamic';
function _registerGlobalShortcut(webContents: Electron.WebContents, shortcut: string, action: (webContents: Electron.WebContents) => void) {
globalShortcut.register(shortcut, () => {
action(webContents);
});
}
function _registerLocalShortcut(win: BrowserWindow, shortcut: string, action: (webContents: Electron.WebContents) => void) {
electronLocalshortcut.register(win, shortcut, () => {
action(win.webContents);
});
}
function registerShortcuts(win: BrowserWindow, options: ConfigType<'shortcuts'>) {
const songControls = getSongControls(win);
const { playPause, next, previous, search } = songControls;
if (options.overrideMediaKeys) {
_registerGlobalShortcut(win.webContents, 'MediaPlayPause', playPause);
_registerGlobalShortcut(win.webContents, 'MediaNextTrack', next);
_registerGlobalShortcut(win.webContents, 'MediaPreviousTrack', previous);
}
_registerLocalShortcut(win, 'CommandOrControl+F', search);
_registerLocalShortcut(win, 'CommandOrControl+L', search);
if (is.linux()) {
registerMPRIS(win);
}
const { global, local } = options;
const shortcutOptions = { global, local };
for (const optionType in shortcutOptions) {
registerAllShortcuts(shortcutOptions[optionType as 'global' | 'local'], optionType);
}
function registerAllShortcuts(container: Record<string, string>, type: string) {
for (const action in container) {
if (!container[action]) {
continue; // Action accelerator is empty
}
console.debug(`Registering ${type} shortcut`, container[action], ':', action);
const actionCallback: () => void = songControls[action as keyof typeof songControls];
if (typeof actionCallback !== 'function') {
console.warn('Invalid action', action);
continue;
}
if (type === 'global') {
_registerGlobalShortcut(win.webContents, container[action], actionCallback);
} else { // Type === "local"
_registerLocalShortcut(win, local[action], actionCallback);
}
}
}
}
export default registerShortcuts;

View File

@ -0,0 +1,67 @@
import prompt, { KeybindOptions } from 'custom-electron-prompt';
import { BrowserWindow } from 'electron';
import { setMenuOptions } from '../../config/plugins';
import promptOptions from '../../providers/prompt-options';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic';
export default (win: BrowserWindow, options: ConfigType<'shortcuts'>): MenuTemplate => [
{
label: 'Set Global Song Controls',
click: () => promptKeybind(options, win),
},
{
label: 'Override MediaKeys',
type: 'checkbox',
checked: options.overrideMediaKeys,
click: (item) => setOption(options, 'overrideMediaKeys', item.checked),
},
];
function setOption<Key extends keyof ConfigType<'shortcuts'> = keyof ConfigType<'shortcuts'>>(
options: ConfigType<'shortcuts'>,
key: Key | null = null,
newValue: ConfigType<'shortcuts'>[Key] | null = null,
) {
if (key && newValue !== null) {
options[key] = newValue;
}
setMenuOptions('shortcuts', options);
}
// Helper function for keybind prompt
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ value: value_, label: label_, default: default_ });
async function promptKeybind(options: ConfigType<'shortcuts'>, win: BrowserWindow) {
const output = await prompt({
title: 'Global Keybinds',
label: 'Choose Global Keybinds for Songs Control:',
type: 'keybind',
keybindOptions: [ // If default=undefined then no default is used
kb('Previous', 'previous', options.global?.previous),
kb('Play / Pause', 'playPause', options.global?.playPause),
kb('Next', 'next', options.global?.next),
],
height: 270,
...promptOptions(),
}, win);
if (output) {
if (!options.global) {
options.global = {};
}
for (const { value, accelerator } of output) {
options.global[value] = accelerator;
}
setOption(options);
}
// Else -> pressed cancel
}

131
src/plugins/shortcuts/mpris-service.d.ts vendored Normal file
View File

@ -0,0 +1,131 @@
declare module '@jellybrick/mpris-service' {
import { EventEmitter } from 'events';
import dbus from 'dbus-next';
interface RootInterfaceOptions {
identity: string;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
desktopEntry: string;
}
export interface Track {
'mpris:trackid'?: string;
'mpris:length'?: number;
'mpris:artUrl'?: string;
'xesam:album'?: string;
'xesam:albumArtist'?: string[];
'xesam:artist'?: string[];
'xesam:asText'?: string;
'xesam:audioBPM'?: number;
'xesam:autoRating'?: number;
'xesam:comment'?: string[];
'xesam:composer'?: string[];
'xesam:contentCreated'?: string;
'xesam:discNumber'?: number;
'xesam:firstUsed'?: string;
'xesam:genre'?: string[];
'xesam:lastUsed'?: string;
'xesam:lyricist'?: string[];
'xesam:title'?: string;
'xesam:trackNumber'?: number;
'xesam:url'?: string;
'xesam:useCount'?: number;
'xesam:userRating'?: number;
}
declare class Player extends EventEmitter {
constructor(opts: {
name: string;
identity: string;
supportedMimeTypes?: string[];
supportedInterfaces?: string[];
});
name: string;
identity: string;
fullscreen: boolean;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
canQuit: boolean;
canRaise: boolean;
canSetFullscreen: boolean;
hasTrackList: boolean;
desktopEntry: string;
playbackStatus: string;
loopStatus: string;
shuffle: boolean;
metadata: object;
volume: number;
canControl: boolean;
canPause: boolean;
canPlay: boolean;
canSeek: boolean;
canGoNext: boolean;
canGoPrevious: boolean;
rate: number;
minimumRate: number;
maximumRate: number;
playlists: unknown[];
activePlaylist: string;
init(opts: RootInterfaceOptions): void;
objectPath(subpath?: string): string;
getPosition(): number;
seeked(position: number): void;
getTrackIndex(trackId: string): number;
getTrack(trackId: string): Track;
addTrack(track: Track): void;
removeTrack(trackId: string): void;
getPlaylistIndex(playlistId: string): number;
setPlaylists(playlists: Track[]): void;
setActivePlaylist(playlistId: string): void;
static PLAYBACK_STATUS_PLAYING: 'Playing';
static PLAYBACK_STATUS_PAUSED: 'Paused';
static PLAYBACK_STATUS_STOPPED: 'Stopped';
static LOOP_STATUS_NONE: 'None';
static LOOP_STATUS_TRACK: 'Track';
static LOOP_STATUS_PLAYLIST: 'Playlist';
}
interface MprisInterface extends dbus.interface.Interface {
setProperty(property: string, valuePlain: unknown): void;
}
interface RootInterface {
}
interface PlayerInterface {
}
interface TracklistInterface {
TrackListReplaced(tracks: Track[]): void;
TrackAdded(afterTrack: string): void;
TrackRemoved(trackId: string): void;
}
interface PlaylistsInterface {
PlaylistChanged(playlist: unknown[]): void;
setActivePlaylistId(playlistId: string): void;
}
export default Player;
}

View File

@ -0,0 +1,177 @@
import { BrowserWindow, ipcMain } from 'electron';
import mpris, { Track } from '@jellybrick/mpris-service';
import registerCallback from '../../providers/song-info';
import getSongControls from '../../providers/song-controls';
import config from '../../config';
function setupMPRIS() {
const instance = new mpris({
name: 'youtube-music',
identity: 'YouTube Music',
supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ['player'],
});
instance.canRaise = true;
instance.supportedUriSchemes = ['https'];
instance.desktopEntry = 'youtube-music';
return instance;
}
function registerMPRIS(win: BrowserWindow) {
const songControls = getSongControls(win);
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
try {
// TODO: "Typing" for this arguments
const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6);
const microToSec = (n: unknown) => Math.round(Number(n) / 1e6);
const seekTo = (e: { position: unknown }) => win.webContents.send('seekTo', microToSec(e.position));
const seekBy = (o: unknown) => win.webContents.send('seekBy', microToSec(o));
const player = setupMPRIS();
ipcMain.on('apiLoaded', () => {
win.webContents.send('setupSeekedListener', 'mpris');
win.webContents.send('setupTimeChangedListener', 'mpris');
win.webContents.send('setupRepeatChangedListener', 'mpris');
win.webContents.send('setupVolumeChangedListener', 'mpris');
});
ipcMain.on('seeked', (_, t: number) => player.seeked(secToMicro(t)));
let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
ipcMain.on('repeatChanged', (_, mode: string) => {
switch (mode) {
case 'NONE': {
player.loopStatus = mpris.LOOP_STATUS_NONE;
break;
}
case 'ONE': {
player.loopStatus = mpris.LOOP_STATUS_TRACK;
break;
}
case 'ALL': {
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
// No default
break;
}
}
});
player.on('loopStatus', (status: string) => {
// SwitchRepeat cycles between states in that order
const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK];
const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status);
// Get a delta in the range [0,2]
const delta = (targetIndex - currentIndex + 3) % 3;
songControls.switchRepeat(delta);
});
player.getPosition = () => secToMicro(currentSeconds);
player.on('raise', () => {
win.setSkipTaskbar(false);
win.show();
});
player.on('play', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
playPause();
}
});
player.on('pause', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
playPause();
}
});
player.on('playpause', () => {
player.playbackStatus = player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
playPause();
});
player.on('next', next);
player.on('previous', previous);
player.on('seek', seekBy);
player.on('position', seekTo);
player.on('shuffle', (enableShuffle) => {
if (enableShuffle) {
shuffle();
}
});
player.on('open', (args: { uri: string }) => { win.loadURL(args.uri); });
let mprisVolNewer = false;
let autoUpdate = false;
ipcMain.on('volumeChanged', (_, newVol) => {
if (~~(player.volume * 100) !== newVol) {
if (mprisVolNewer) {
mprisVolNewer = false;
autoUpdate = false;
} else {
autoUpdate = true;
player.volume = Number.parseFloat((newVol / 100).toFixed(2));
mprisVolNewer = false;
autoUpdate = false;
}
}
});
player.on('volume', (newVolume) => {
if (config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value.
const newVol = ~~(newVolume * 100);
if (~~(player.volume * 100) !== newVol && !autoUpdate) {
mprisVolNewer = true;
autoUpdate = false;
win.webContents.send('setVolume', newVol);
}
} else {
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
let deltaVolume = Math.round((newVolume - player.volume) * 10);
while (deltaVolume !== 0 && deltaVolume > 0) {
volumePlus10();
player.volume += 0.1;
deltaVolume--;
}
while (deltaVolume !== 0 && deltaVolume < 0) {
volumeMinus10();
player.volume -= 0.1;
deltaVolume++;
}
}
});
registerCallback((songInfo) => {
if (player) {
const data: Track = {
'mpris:length': secToMicro(songInfo.songDuration),
'mpris:artUrl': songInfo.imageSrc ?? undefined,
'xesam:title': songInfo.title,
'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist],
'mpris:trackid': '/',
};
if (songInfo.album) {
data['xesam:album'] = songInfo.album;
}
player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds));
player.playbackStatus = songInfo.isPaused ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
}
});
} catch (error) {
console.warn('Error in MPRIS', error);
}
}
export default registerMPRIS;