mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
convert plugins
This commit is contained in:
@ -6,7 +6,7 @@ import defaultConfig from './defaults';
|
||||
import store from './store';
|
||||
import plugins from './plugins';
|
||||
|
||||
import { restart } from '../providers/app-controls';
|
||||
import { restart } from '@/providers/app-controls';
|
||||
|
||||
const set = (key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
|
||||
@ -5,7 +5,7 @@ import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
import defaults from './defaults';
|
||||
|
||||
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
||||
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
||||
|
||||
const setDefaultPluginOptions = (
|
||||
store: Conf<Record<string, unknown>>,
|
||||
|
||||
@ -10,7 +10,7 @@ import { startPlugin, stopPlugin } from '@/utils';
|
||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
||||
const loadedPluginMap: Record<string, PluginDef<unknown, unknown, unknown>> = {};
|
||||
|
||||
const createContext = <Config extends PluginConfig>(id: string): RendererContext<Config> => ({
|
||||
export const createContext = <Config extends PluginConfig>(id: string): RendererContext<Config> => ({
|
||||
getConfig: () => window.mainConfig.plugins.getOptions(id),
|
||||
setConfig: async (newConfig) => {
|
||||
await window.ipcRenderer.invoke('set-config', id, newConfig);
|
||||
|
||||
@ -9,15 +9,13 @@ import {
|
||||
} from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import config from './config';
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
import promptOptions from './providers/prompt-options';
|
||||
|
||||
/* eslint-disable import/order */
|
||||
import { allPlugins } from 'virtual:plugins';
|
||||
/* eslint-enable import/order */
|
||||
|
||||
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
|
||||
|
||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { blockers } from './types';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from '@/plugins/adblocker/blocker';
|
||||
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from './blocker';
|
||||
|
||||
import injectCliqzPreload from '@/plugins/adblocker/injectors/inject-cliqz-preload';
|
||||
import { inject, isInjected } from '@/plugins/adblocker/injectors/inject';
|
||||
import injectCliqzPreload from './injectors/inject-cliqz-preload';
|
||||
import { inject, isInjected } from './injectors/inject';
|
||||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { Howl } from 'howler';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { VolumeFader } from '@/plugins/crossfade/fader';
|
||||
import { VolumeFader } from './fader';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onLoad, onUnload } from '@/plugins/discord/main';
|
||||
import {onMenu} from "@/plugins/discord/menu";
|
||||
import { onLoad, onUnload } from './main';
|
||||
import { onMenu } from './menu';
|
||||
|
||||
export type DiscordPluginConfig = {
|
||||
enabled: boolean;
|
||||
|
||||
@ -4,7 +4,7 @@ import { dev } from 'electron-is';
|
||||
|
||||
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||
|
||||
import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
|
||||
import registerCallback, { type SongInfoCallback, type SongInfo } from '@/providers/song-info';
|
||||
|
||||
import type { DiscordPluginConfig } from './index';
|
||||
|
||||
|
||||
@ -3,12 +3,11 @@ import prompt from 'custom-electron-prompt';
|
||||
import { clear, connect, isConnected, registerRefresh } from './main';
|
||||
|
||||
import { singleton } from '@/providers/decorators';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
import { setMenuOptions } from '@/config/plugins';
|
||||
import { MenuContext } from '@/types/contexts';
|
||||
import { DiscordPluginConfig } from '@/plugins/discord/index';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
|
||||
@ -3,8 +3,8 @@ import { DefaultPresetList, Preset } from './types';
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onConfigChange, onMainLoad } from '@/plugins/downloader/main';
|
||||
import { onPlayerApiReady, onRendererLoad } from '@/plugins/downloader/renderer';
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||
|
||||
export type DownloaderPluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -15,17 +15,19 @@ export type DownloaderPluginConfig = {
|
||||
playlistMaxItems?: number;
|
||||
}
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Downloader',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
export const defaultConfig: DownloaderPluginConfig = {
|
||||
enabled: false,
|
||||
downloadFolder: undefined,
|
||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined,
|
||||
} as DownloaderPluginConfig,
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Downloader',
|
||||
restartNeeded: true,
|
||||
config: defaultConfig,
|
||||
stylesheets: [style],
|
||||
backend: {
|
||||
start: onMainLoad,
|
||||
|
||||
@ -37,7 +37,7 @@ import { BackendContext } from '@/types/contexts';
|
||||
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
|
||||
|
||||
import { DownloaderPluginConfig } from '../index';
|
||||
import { defaultConfig, type DownloaderPluginConfig } from '../index';
|
||||
|
||||
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
|
||||
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
|
||||
@ -90,7 +90,7 @@ export const getCookieFromWindow = async (win: BrowserWindow) => {
|
||||
.join(';');
|
||||
};
|
||||
|
||||
let config: DownloaderPluginConfig;
|
||||
let config: DownloaderPluginConfig = defaultConfig;
|
||||
|
||||
export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext<DownloaderPluginConfig>) => {
|
||||
win = _win;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import titlebarStyle from './titlebar.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onMainLoad } from '@/plugins/in-app-menu/main';
|
||||
import { onMenu } from '@/plugins/in-app-menu/menu';
|
||||
import { onPlayerApiReady, onRendererLoad } from '@/plugins/in-app-menu/renderer';
|
||||
import { onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||
|
||||
export interface InAppMenuConfig {
|
||||
enabled: boolean;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import { addScrobble, getAndSetSessionKey, setNowPlaying } from './main';
|
||||
|
||||
export interface LastFmPluginConfig {
|
||||
enabled: boolean;
|
||||
@ -30,7 +32,7 @@ export interface LastFmPluginConfig {
|
||||
secret: string;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('last-fm', {
|
||||
export default createPlugin({
|
||||
name: 'Last.fm',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -39,12 +41,34 @@ const builder = createPluginBuilder('last-fm', {
|
||||
api_key: '04d76faaac8726e60988e14c105d421a',
|
||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||
} as LastFmPluginConfig,
|
||||
async backend({ getConfig, setConfig }) {
|
||||
let config = await getConfig();
|
||||
// This will store the timeout that will trigger addScrobble
|
||||
let scrobbleTimer: number | undefined;
|
||||
|
||||
if (!config.api_root) {
|
||||
config.enabled = true;
|
||||
setConfig(config);
|
||||
}
|
||||
|
||||
if (!config.session_key) {
|
||||
// Not authenticated
|
||||
config = await getAndSetSessionKey(config, setConfig);
|
||||
}
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Set remove the old scrobble timer
|
||||
clearTimeout(scrobbleTimer);
|
||||
if (!songInfo.isPaused) {
|
||||
setNowPlaying(songInfo, config, setConfig);
|
||||
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
||||
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
||||
// Scrobble still needs to happen
|
||||
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
||||
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,10 +2,8 @@ import crypto from 'node:crypto';
|
||||
|
||||
import { net, shell } from 'electron';
|
||||
|
||||
import builder, { type LastFmPluginConfig } from './index';
|
||||
|
||||
import { setOptions } from '../../config/plugins';
|
||||
import registerCallback, { type SongInfo } from '../../providers/song-info';
|
||||
import type { LastFmPluginConfig } from './index';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
interface LastFmData {
|
||||
method: string,
|
||||
@ -53,7 +51,7 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
||||
keys.sort();
|
||||
let sig = '';
|
||||
for (const key of keys) {
|
||||
if (String(key) === 'format') {
|
||||
if (key === 'format') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -83,7 +81,9 @@ const authenticate = async (config: LastFmPluginConfig) => {
|
||||
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||
};
|
||||
|
||||
const getAndSetSessionKey = async (config: LastFmPluginConfig) => {
|
||||
type SetConfType = (conf: Partial<Omit<LastFmPluginConfig, 'enabled'>>) => (void | Promise<void>);
|
||||
|
||||
export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig: SetConfType) => {
|
||||
// Get and store the session key
|
||||
const data = {
|
||||
api_key: config.api_key,
|
||||
@ -102,19 +102,19 @@ const getAndSetSessionKey = async (config: LastFmPluginConfig) => {
|
||||
if (json.error) {
|
||||
config.token = await createToken(config);
|
||||
await authenticate(config);
|
||||
setOptions('last-fm', config);
|
||||
setConfig(config);
|
||||
}
|
||||
if (json.session) {
|
||||
config.session_key = json.session.key;
|
||||
}
|
||||
setOptions('last-fm', config);
|
||||
setConfig(config);
|
||||
return config;
|
||||
};
|
||||
|
||||
const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig, data: LastFmData) => {
|
||||
const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig, data: LastFmData, setConfig: SetConfType) => {
|
||||
// This sends a post request to the api, and adds the common data
|
||||
if (!config.session_key) {
|
||||
await getAndSetSessionKey(config);
|
||||
await getAndSetSessionKey(config, setConfig);
|
||||
}
|
||||
|
||||
const postData: LastFmSongData = {
|
||||
@ -143,58 +143,24 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig,
|
||||
config.session_key = undefined;
|
||||
config.token = await createToken(config);
|
||||
await authenticate(config);
|
||||
setOptions('last-fm', config);
|
||||
setConfig(config);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addScrobble = (songInfo: SongInfo, config: LastFmPluginConfig) => {
|
||||
export const addScrobble = (songInfo: SongInfo, config: LastFmPluginConfig, setConfig: SetConfType) => {
|
||||
// This adds one scrobbled song to last.fm
|
||||
const data = {
|
||||
method: 'track.scrobble',
|
||||
timestamp: Math.trunc((Date.now() - (songInfo.elapsedSeconds ?? 0)) / 1000),
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
postSongDataToAPI(songInfo, config, data, setConfig);
|
||||
};
|
||||
|
||||
const setNowPlaying = (songInfo: SongInfo, config: LastFmPluginConfig) => {
|
||||
export const setNowPlaying = (songInfo: SongInfo, config: LastFmPluginConfig, setConfig: SetConfType) => {
|
||||
// This sets the now playing status in last.fm
|
||||
const data = {
|
||||
method: 'track.updateNowPlaying',
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
postSongDataToAPI(songInfo, config, data, setConfig);
|
||||
};
|
||||
|
||||
// This will store the timeout that will trigger addScrobble
|
||||
let scrobbleTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
export default builder.createMain(({ getConfig, send }) => ({
|
||||
async onLoad(_win) {
|
||||
let config = await getConfig();
|
||||
|
||||
if (!config.api_root) {
|
||||
config.enabled = true;
|
||||
setOptions('last-fm', config);
|
||||
}
|
||||
|
||||
if (!config.session_key) {
|
||||
// Not authenticated
|
||||
config = await getAndSetSessionKey(config);
|
||||
}
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Set remove the old scrobble timer
|
||||
clearTimeout(scrobbleTimer);
|
||||
if (!songInfo.isPaused) {
|
||||
setNowPlaying(songInfo, config);
|
||||
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
||||
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
||||
// Scrobble still needs to happen
|
||||
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
||||
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
@ -1,17 +1,88 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { net } from 'electron';
|
||||
|
||||
const builder = createPluginBuilder('lumiastream', {
|
||||
import { createPlugin } from '@/utils';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
|
||||
type LumiaData = {
|
||||
origin: string;
|
||||
eventType: string;
|
||||
url?: string;
|
||||
videoId?: string;
|
||||
playlistId?: string;
|
||||
cover?: string|null;
|
||||
cover_url?: string|null;
|
||||
title?: string;
|
||||
artists?: string[];
|
||||
status?: string;
|
||||
progress?: number;
|
||||
duration?: number;
|
||||
album_url?: string|null;
|
||||
album?: string|null;
|
||||
views?: number;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Lumia Stream [beta]',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
backend() {
|
||||
const secToMilisec = (t?: number) => t ? Math.round(Number(t) * 1e3) : undefined;
|
||||
const previousStatePaused = null;
|
||||
|
||||
const data: LumiaData = {
|
||||
origin: 'youtubemusic',
|
||||
eventType: 'switchSong',
|
||||
};
|
||||
|
||||
const post = (data: LumiaData) => {
|
||||
const port = 39231;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
} as const;
|
||||
const url = `http://127.0.0.1:${port}/api/media`;
|
||||
|
||||
net.fetch(url, { method: 'POST', body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }), headers })
|
||||
.catch((error: { code: number, errno: number }) => {
|
||||
console.log(
|
||||
`Error: '${
|
||||
error.code || error.errno
|
||||
}' - when trying to access lumiastream webserver at port ${port}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default builder;
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
if (previousStatePaused === null) {
|
||||
data.eventType = 'switchSong';
|
||||
} else if (previousStatePaused !== songInfo.isPaused) {
|
||||
data.eventType = 'playPause';
|
||||
}
|
||||
|
||||
data.duration = secToMilisec(songInfo.songDuration);
|
||||
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
||||
data.url = songInfo.url;
|
||||
data.videoId = songInfo.videoId;
|
||||
data.playlistId = songInfo.playlistId;
|
||||
data.cover = songInfo.imageSrc;
|
||||
data.cover_url = songInfo.imageSrc;
|
||||
data.album_url = songInfo.imageSrc;
|
||||
data.title = songInfo.title;
|
||||
data.artists = [songInfo.artist];
|
||||
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||
data.isPaused = songInfo.isPaused;
|
||||
data.album = songInfo.album;
|
||||
data.views = songInfo.views;
|
||||
post(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import { net } from 'electron';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import registerCallback from '../../providers/song-info';
|
||||
|
||||
const secToMilisec = (t?: number) => t ? Math.round(Number(t) * 1e3) : undefined;
|
||||
const previousStatePaused = null;
|
||||
|
||||
type LumiaData = {
|
||||
origin: string;
|
||||
eventType: string;
|
||||
url?: string;
|
||||
videoId?: string;
|
||||
playlistId?: string;
|
||||
cover?: string|null;
|
||||
cover_url?: string|null;
|
||||
title?: string;
|
||||
artists?: string[];
|
||||
status?: string;
|
||||
progress?: number;
|
||||
duration?: number;
|
||||
album_url?: string|null;
|
||||
album?: string|null;
|
||||
views?: number;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
const data: LumiaData = {
|
||||
origin: 'youtubemusic',
|
||||
eventType: 'switchSong',
|
||||
};
|
||||
|
||||
|
||||
const post = (data: LumiaData) => {
|
||||
const port = 39231;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
} as const;
|
||||
const url = `http://127.0.0.1:${port}/api/media`;
|
||||
|
||||
net.fetch(url, { method: 'POST', body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }), headers })
|
||||
.catch((error: { code: number, errno: number }) => {
|
||||
console.log(
|
||||
`Error: '${
|
||||
error.code || error.errno
|
||||
}' - when trying to access lumiastream webserver at port ${port}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default builder.createMain(() => {
|
||||
return {
|
||||
onLoad() {
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousStatePaused === null) {
|
||||
data.eventType = 'switchSong';
|
||||
} else if (previousStatePaused !== songInfo.isPaused) {
|
||||
data.eventType = 'playPause';
|
||||
}
|
||||
|
||||
data.duration = secToMilisec(songInfo.songDuration);
|
||||
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
||||
data.url = songInfo.url;
|
||||
data.videoId = songInfo.videoId;
|
||||
data.playlistId = songInfo.playlistId;
|
||||
data.cover = songInfo.imageSrc;
|
||||
data.cover_url = songInfo.imageSrc;
|
||||
data.album_url = songInfo.imageSrc;
|
||||
data.title = songInfo.title;
|
||||
data.artists = [songInfo.artist];
|
||||
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||
data.isPaused = songInfo.isPaused;
|
||||
data.album = songInfo.album;
|
||||
data.views = songInfo.views;
|
||||
post(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -1,26 +1,41 @@
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onRendererLoad } from './renderer';
|
||||
|
||||
export type LyricsGeniusPluginConfig = {
|
||||
enabled: boolean;
|
||||
romanizedLyrics: boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('lyrics-genius', {
|
||||
export default createPlugin({
|
||||
name: 'Lyrics Genius',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
romanizedLyrics: false,
|
||||
} as LyricsGeniusPluginConfig,
|
||||
styles: [style],
|
||||
stylesheets: [style],
|
||||
async menu({ getConfig, setConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Romanized Lyrics',
|
||||
type: 'checkbox',
|
||||
checked: config.romanizedLyrics,
|
||||
click(item) {
|
||||
setConfig({
|
||||
romanizedLyrics: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
backend: {
|
||||
start: onMainLoad,
|
||||
onConfigChange,
|
||||
},
|
||||
renderer: onRendererLoad,
|
||||
});
|
||||
|
||||
@ -3,33 +3,31 @@ import is from 'electron-is';
|
||||
import { convert } from 'html-to-text';
|
||||
|
||||
import { GetGeniusLyric } from './types';
|
||||
import { cleanupName, type SongInfo } from '@/providers/song-info';
|
||||
|
||||
import builder from './index';
|
||||
import type { LyricsGeniusPluginConfig } from './index';
|
||||
|
||||
import { cleanupName, type SongInfo } from '../../providers/song-info';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||
let revRomanized = false;
|
||||
|
||||
export default builder.createMain(({ handle, getConfig }) =>{
|
||||
return {
|
||||
async onLoad() {
|
||||
export const onMainLoad = async ({ ipc, getConfig }: BackendContext<LyricsGeniusPluginConfig>) => {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.romanizedLyrics) {
|
||||
revRomanized = true;
|
||||
}
|
||||
|
||||
handle('search-genius-lyrics', async (extractedSongInfo: SongInfo) => {
|
||||
ipc.handle('search-genius-lyrics', async (extractedSongInfo: SongInfo) => {
|
||||
const metadata = extractedSongInfo;
|
||||
return await fetchFromGenius(metadata);
|
||||
});
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
revRomanized = newConfig.romanizedLyrics;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const onConfigChange = (newConfig: LyricsGeniusPluginConfig) => {
|
||||
revRomanized = newConfig.romanizedLyrics;
|
||||
};
|
||||
|
||||
export const fetchFromGenius = async (metadata: SongInfo) => {
|
||||
const songTitle = `${cleanupName(metadata.title)}`;
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Romanized Lyrics',
|
||||
type: 'checkbox',
|
||||
checked: config.romanizedLyrics,
|
||||
click(item) {
|
||||
setConfig({
|
||||
romanizedLyrics: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,9 +1,8 @@
|
||||
import builder from './index';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
|
||||
|
||||
import type { SongInfo } from '../../providers/song-info';
|
||||
|
||||
export default builder.createRenderer(({ on, invoke }) => ({
|
||||
onLoad() {
|
||||
export const onRendererLoad = ({ ipc: { invoke, on } }: RendererContext<LyricsGeniusPluginConfig>) => {
|
||||
const setLyrics = (lyricsContainer: Element, lyrics: string | null) => {
|
||||
lyricsContainer.innerHTML = `
|
||||
<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
||||
@ -38,10 +37,10 @@ export default builder.createRenderer(({ on, invoke }) => ({
|
||||
// Check if disabled
|
||||
if (!tabs.lyrics?.hasAttribute('disabled')) return;
|
||||
|
||||
const lyrics = await invoke<string | null>(
|
||||
const lyrics = await invoke(
|
||||
'search-genius-lyrics',
|
||||
extractedSongInfo,
|
||||
);
|
||||
) as string | null;
|
||||
|
||||
if (!lyrics) {
|
||||
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
||||
@ -105,5 +104,4 @@ export default builder.createRenderer(({ on, invoke }) => ({
|
||||
};
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
import style from './style.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import forwardHTML from './templates/forward.html?raw';
|
||||
import backHTML from './templates/back.html?raw';
|
||||
|
||||
const builder = createPluginBuilder('navigation', {
|
||||
export default createPlugin({
|
||||
name: 'Navigation',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
styles: [style],
|
||||
stylesheets: [style],
|
||||
renderer() {
|
||||
const forwardButton = ElementFromHtml(forwardHTML);
|
||||
const backButton = ElementFromHtml(backHTML);
|
||||
const menu = document.querySelector('#right-content');
|
||||
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import forwardHTML from './templates/forward.html?raw';
|
||||
import backHTML from './templates/back.html?raw';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
export default builder.createRenderer(() => {
|
||||
return {
|
||||
onLoad() {
|
||||
const forwardButton = ElementFromHtml(forwardHTML);
|
||||
const backButton = ElementFromHtml(backHTML);
|
||||
const menu = document.querySelector('#right-content');
|
||||
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,20 +1,46 @@
|
||||
import style from './style.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
|
||||
const builder = createPluginBuilder('no-google-login', {
|
||||
export default createPlugin({
|
||||
name: 'Remove Google Login',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
styles: [style],
|
||||
stylesheets: [style],
|
||||
renderer() {
|
||||
const elementsToRemove = [
|
||||
'.sign-in-link.ytmusic-nav-bar',
|
||||
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]',
|
||||
];
|
||||
|
||||
for (const selector of elementsToRemove) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the library button
|
||||
const libraryIconPath
|
||||
= 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
|
||||
const observer = new MutationObserver(() => {
|
||||
const menuEntries = document.querySelectorAll(
|
||||
'#items ytmusic-guide-entry-renderer',
|
||||
);
|
||||
menuEntries.forEach((item) => {
|
||||
const icon = item.querySelector('path');
|
||||
if (icon) {
|
||||
observer.disconnect();
|
||||
if (icon.getAttribute('d') === libraryIconPath) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { injectCSS } from '../utils/main';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,39 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createRenderer(() => ({
|
||||
onLoad() {
|
||||
const elementsToRemove = [
|
||||
'.sign-in-link.ytmusic-nav-bar',
|
||||
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]',
|
||||
];
|
||||
|
||||
for (const selector of elementsToRemove) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the library button
|
||||
const libraryIconPath
|
||||
= 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
|
||||
const observer = new MutationObserver(() => {
|
||||
const menuEntries = document.querySelectorAll(
|
||||
'#items ytmusic-guide-entry-renderer',
|
||||
);
|
||||
menuEntries.forEach((item) => {
|
||||
const icon = item.querySelector('path');
|
||||
if (icon) {
|
||||
observer.disconnect();
|
||||
if (icon.getAttribute('d') === libraryIconPath) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}));
|
||||
@ -1,36 +1,46 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
|
||||
export interface NotificationsPluginConfig {
|
||||
enabled: boolean;
|
||||
unpauseNotification: boolean;
|
||||
/**
|
||||
* Has effect only on Linux
|
||||
*/
|
||||
urgency: 'low' | 'normal' | 'critical';
|
||||
/**
|
||||
* the following has effect only on Windows
|
||||
*/
|
||||
interactive: boolean;
|
||||
/**
|
||||
* See plugins/notifications/utils for more info
|
||||
*/
|
||||
toastStyle: number;
|
||||
refreshOnPlayPause: boolean;
|
||||
trayControls: boolean;
|
||||
hideButtonText: boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('notifications', {
|
||||
name: 'Notifications',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
export const defaultConfig: NotificationsPluginConfig = {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: 'normal', // Has effect only on Linux
|
||||
// the following has effect only on Windows
|
||||
urgency: 'normal',
|
||||
interactive: true,
|
||||
toastStyle: 1, // See plugins/notifications/utils for more info
|
||||
toastStyle: 1,
|
||||
refreshOnPlayPause: false,
|
||||
trayControls: true,
|
||||
hideButtonText: false,
|
||||
} as NotificationsPluginConfig,
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Notifications',
|
||||
restartNeeded: true,
|
||||
config: defaultConfig,
|
||||
menu: onMenu,
|
||||
backend: {
|
||||
start: onMainLoad,
|
||||
onConfigChange,
|
||||
},
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
import { app, BrowserWindow, Notification } from 'electron';
|
||||
|
||||
import playIcon from '@assets/media-icons-black/play.png?asset&asarUnpack';
|
||||
import pauseIcon from '@assets/media-icons-black/pause.png?asset&asarUnpack';
|
||||
import nextIcon from '@assets/media-icons-black/next.png?asset&asarUnpack';
|
||||
import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||
|
||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import { changeProtocolHandler } from '../../providers/protocol-handler';
|
||||
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
|
||||
import { mediaIcons } from '../../types/media-icons';
|
||||
|
||||
import playIcon from '../../../assets/media-icons-black/play.png?asset&asarUnpack';
|
||||
import pauseIcon from '../../../assets/media-icons-black/pause.png?asset&asarUnpack';
|
||||
import nextIcon from '../../../assets/media-icons-black/next.png?asset&asarUnpack';
|
||||
import previousIcon from '../../../assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||
|
||||
import { MainPluginContext } from '../utils/builder';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import registerCallback, { SongInfo } from '@/providers/song-info';
|
||||
import { changeProtocolHandler } from '@/providers/protocol-handler';
|
||||
import { setTrayOnClick, setTrayOnDoubleClick } from '@/tray';
|
||||
import { mediaIcons } from '@/types/media-icons';
|
||||
|
||||
import type { NotificationsPluginConfig } from './index';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
let songControls: ReturnType<typeof getSongControls>;
|
||||
let savedNotification: Notification | undefined;
|
||||
@ -25,7 +24,7 @@ type Accessor<T> = () => T;
|
||||
export default (
|
||||
win: BrowserWindow,
|
||||
config: Accessor<NotificationsPluginConfig>,
|
||||
{ on, send }: MainPluginContext<NotificationsPluginConfig>,
|
||||
{ ipc: { on, send } }: BackendContext<NotificationsPluginConfig>,
|
||||
) => {
|
||||
const sendNotification = (songInfo: SongInfo) => {
|
||||
const iconSrc = notificationImage(songInfo, config());
|
||||
|
||||
@ -5,11 +5,12 @@ import is from 'electron-is';
|
||||
import { notificationImage } from './utils';
|
||||
import interactive from './interactive';
|
||||
|
||||
import builder, { NotificationsPluginConfig } from './index';
|
||||
import { defaultConfig, type NotificationsPluginConfig } from './index';
|
||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
let config: NotificationsPluginConfig = builder.config;
|
||||
let config: NotificationsPluginConfig = defaultConfig;
|
||||
|
||||
const notify = (info: SongInfo) => {
|
||||
// Send the notification
|
||||
@ -42,17 +43,14 @@ const setup = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export default builder.createMain((context) => {
|
||||
return {
|
||||
async onLoad(win) {
|
||||
export const onMainLoad = async (context: BackendContext<NotificationsPluginConfig>) => {
|
||||
config = await context.getConfig();
|
||||
|
||||
// Register the callback for new song information
|
||||
if (is.windows() && config.interactive) interactive(win, () => config, context);
|
||||
if (is.windows() && config.interactive) interactive(context.window, () => config, context);
|
||||
else setup();
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const onConfigChange = (newConfig: NotificationsPluginConfig) => {
|
||||
config = newConfig;
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import is from 'electron-is';
|
||||
|
||||
import { MenuItem } from 'electron';
|
||||
|
||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||
|
||||
import builder, { NotificationsPluginConfig } from './index';
|
||||
import type { NotificationsPluginConfig } from './index';
|
||||
|
||||
import type { MenuTemplate } from '../../menu';
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
export const onMenu = async ({ getConfig, setConfig }: MenuContext<NotificationsPluginConfig>): Promise<MenuTemplate> => {
|
||||
const config = await getConfig();
|
||||
|
||||
const getToastStyleMenuItems = (options: NotificationsPluginConfig) => {
|
||||
@ -25,7 +25,7 @@ export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
}
|
||||
|
||||
return array as Electron.MenuItemConstructorOptions[];
|
||||
}
|
||||
};
|
||||
|
||||
const getMenu = (): MenuTemplate => {
|
||||
if (is.linux()) {
|
||||
@ -92,4 +92,4 @@ export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
click: (item) => setConfig({ unpauseNotification: item.checked }),
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,12 +3,12 @@ import fs from 'node:fs';
|
||||
|
||||
import { app, NativeImage } from 'electron';
|
||||
|
||||
import { cache } from '../../providers/decorators';
|
||||
import { SongInfo } from '../../providers/song-info';
|
||||
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||
|
||||
import youtubeMusicIcon from '../../../assets/youtube-music.png?asset&asarUnpack';
|
||||
import {NotificationsPluginConfig} from "./index";
|
||||
import { cache } from '@/providers/decorators';
|
||||
import { SongInfo } from '@/providers/song-info';
|
||||
|
||||
import type { NotificationsPluginConfig } from './index';
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
const temporaryIcon = path.join(userData, 'tempIcon.png');
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import style from './style.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||
|
||||
export type PictureInPicturePluginConfig = {
|
||||
'enabled': boolean;
|
||||
@ -14,7 +17,7 @@ export type PictureInPicturePluginConfig = {
|
||||
'useNativePiP': boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('picture-in-picture', {
|
||||
export default createPlugin({
|
||||
name: 'Picture In Picture',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -28,13 +31,15 @@ const builder = createPluginBuilder('picture-in-picture', {
|
||||
'isInPiP': false,
|
||||
'useNativePiP': true,
|
||||
} as PictureInPicturePluginConfig,
|
||||
styles: [style],
|
||||
stylesheets: [style],
|
||||
menu: onMenu,
|
||||
|
||||
backend: {
|
||||
start: onMainLoad,
|
||||
onConfigChange,
|
||||
},
|
||||
renderer: {
|
||||
start: onRendererLoad,
|
||||
onPlayerApiReady,
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,18 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import { app } from 'electron';
|
||||
|
||||
import style from './style.css?inline';
|
||||
import type { PictureInPicturePluginConfig } from './index';
|
||||
|
||||
import builder, { PictureInPicturePluginConfig } from './index';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
import { injectCSS } from '../utils/main';
|
||||
let config: PictureInPicturePluginConfig;
|
||||
|
||||
export default builder.createMain(({ getConfig, setConfig, send, handle, on }) => {
|
||||
export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, handle, on } }: BackendContext<PictureInPicturePluginConfig>) => {
|
||||
let isInPiP = false;
|
||||
let originalPosition: number[];
|
||||
let originalSize: number[];
|
||||
let originalFullScreen: boolean;
|
||||
let originalMaximized: boolean;
|
||||
|
||||
let win: BrowserWindow;
|
||||
|
||||
let config: PictureInPicturePluginConfig;
|
||||
|
||||
const pipPosition = () => (config.savePosition && config['pip-position']) || [10, 10];
|
||||
const pipSize = () => (config.saveSize && config['pip-size']) || [450, 275];
|
||||
|
||||
@ -25,59 +21,59 @@ export default builder.createMain(({ getConfig, setConfig, send, handle, on }) =
|
||||
setConfig({ isInPiP });
|
||||
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
originalFullScreen = window.isFullScreen();
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(false);
|
||||
window.setFullScreen(false);
|
||||
}
|
||||
|
||||
originalMaximized = win.isMaximized();
|
||||
originalMaximized = window.isMaximized();
|
||||
if (originalMaximized) {
|
||||
win.unmaximize();
|
||||
window.unmaximize();
|
||||
}
|
||||
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
originalPosition = window.getPosition();
|
||||
originalSize = window.getSize();
|
||||
|
||||
handle('before-input-event', blockShortcutsInPiP);
|
||||
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
window.setMaximizable(false);
|
||||
window.setFullScreenable(false);
|
||||
|
||||
send('pip-toggle', true);
|
||||
|
||||
app.dock?.hide();
|
||||
win.setVisibleOnAllWorkspaces(true, {
|
||||
window.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
app.dock?.show();
|
||||
if (config.alwaysOnTop) {
|
||||
win.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
window.webContents.removeListener('before-input-event', blockShortcutsInPiP);
|
||||
window.setMaximizable(true);
|
||||
window.setFullScreenable(true);
|
||||
|
||||
send('pip-toggle', false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
win.setAlwaysOnTop(false);
|
||||
window.setVisibleOnAllWorkspaces(false);
|
||||
window.setAlwaysOnTop(false);
|
||||
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
window.setFullScreen(true);
|
||||
}
|
||||
|
||||
if (originalMaximized) {
|
||||
win.maximize();
|
||||
window.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
const [x, y] = isInPiP ? pipPosition() : originalPosition;
|
||||
const [w, h] = isInPiP ? pipSize() : originalSize;
|
||||
win.setPosition(x, y);
|
||||
win.setSize(w, h);
|
||||
window.setPosition(x, y);
|
||||
window.setSize(w, h);
|
||||
|
||||
win.setWindowButtonVisibility?.(!isInPiP);
|
||||
window.setWindowButtonVisibility?.(!isInPiP);
|
||||
};
|
||||
|
||||
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => {
|
||||
@ -91,10 +87,7 @@ export default builder.createMain(({ getConfig, setConfig, send, handle, on }) =
|
||||
}
|
||||
};
|
||||
|
||||
return ({
|
||||
async onLoad(window) {
|
||||
config ??= await getConfig();
|
||||
win ??= window;
|
||||
setConfig({ isInPiP });
|
||||
on('picture-in-picture', () => {
|
||||
togglePiP();
|
||||
@ -111,10 +104,8 @@ export default builder.createMain(({ getConfig, setConfig, send, handle, on }) =
|
||||
setConfig({ 'pip-size': window.getSize() as [number, number] });
|
||||
}
|
||||
});
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const onConfigChange = (newConfig: PictureInPicturePluginConfig) => {
|
||||
config = newConfig;
|
||||
};
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import builder from './index';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import type { PictureInPicturePluginConfig } from './index';
|
||||
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
|
||||
export default builder.createMenu(async ({ window, getConfig, setConfig }) => {
|
||||
export const onMenu = async ({ window, getConfig, setConfig }: MenuContext<PictureInPicturePluginConfig>): Promise<MenuTemplate> => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
@ -71,4 +74,4 @@ export default builder.createMenu(async ({ window, getConfig, setConfig }) => {
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,12 +3,13 @@ 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 { getSongMenu } from '@/providers/dom-elements';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import type { PictureInPicturePluginConfig } from './index';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
@ -133,10 +134,7 @@ const listenForToggle = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
return {
|
||||
async onLoad() {
|
||||
export const onRendererLoad = async ({ getConfig }: RendererContext<PictureInPicturePluginConfig>) => {
|
||||
const config = await getConfig();
|
||||
|
||||
useNativePiP = config.useNativePiP;
|
||||
@ -152,8 +150,9 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onPlayerApiReady() {
|
||||
};
|
||||
|
||||
export const onPlayerApiReady = () => {
|
||||
listenForToggle();
|
||||
|
||||
cloneButton('.player-minimize-button')?.addEventListener('click', async () => {
|
||||
@ -169,6 +168,4 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onPlayerApiReady, onUnload } from './renderer';
|
||||
|
||||
const builder = createPluginBuilder('playback-speed', {
|
||||
export default createPlugin({
|
||||
name: 'Playback Speed',
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
renderer: {
|
||||
stop: onUnload,
|
||||
onPlayerApiReady,
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
import sliderHTML from './templates/slider.html?raw';
|
||||
|
||||
import builder from './index';
|
||||
import { getSongMenu } from '@/providers/dom-elements';
|
||||
import { singleton } from '@/providers/decorators';
|
||||
|
||||
import { getSongMenu } from '../../providers/dom-elements';
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
import { singleton } from '../../providers/decorators';
|
||||
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
const slider = ElementFromHtml(sliderHTML);
|
||||
|
||||
@ -21,12 +15,12 @@ const MAX_PLAYBACK_SPEED = 16;
|
||||
let playbackSpeed = 1;
|
||||
|
||||
const updatePlayBackSpeed = () => {
|
||||
const videoElement = $<HTMLVideoElement>('video');
|
||||
const videoElement = document.querySelector<HTMLVideoElement>('video');
|
||||
if (videoElement) {
|
||||
videoElement.playbackRate = playbackSpeed;
|
||||
}
|
||||
|
||||
const playbackSpeedElement = $('#playback-speed-value');
|
||||
const playbackSpeedElement = document.querySelector('#playback-speed-value');
|
||||
if (playbackSpeedElement) {
|
||||
playbackSpeedElement.innerHTML = String(playbackSpeed);
|
||||
}
|
||||
@ -44,7 +38,7 @@ const immediateValueChangedListener = (e: Event) => {
|
||||
};
|
||||
|
||||
const setupSliderListener = singleton(() => {
|
||||
$('#playback-speed-slider')?.addEventListener('immediate-value-changed', immediateValueChangedListener);
|
||||
document.querySelector('#playback-speed-slider')?.addEventListener('immediate-value-changed', immediateValueChangedListener);
|
||||
});
|
||||
|
||||
const observePopupContainer = () => {
|
||||
@ -64,7 +58,7 @@ const observePopupContainer = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const popupContainer = $('ytmusic-popup-container');
|
||||
const popupContainer = document.querySelector('ytmusic-popup-container');
|
||||
if (popupContainer) {
|
||||
observer.observe(popupContainer, {
|
||||
childList: true,
|
||||
@ -74,7 +68,7 @@ const observePopupContainer = () => {
|
||||
};
|
||||
|
||||
const observeVideo = () => {
|
||||
const video = $<HTMLVideoElement>('video');
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (video) {
|
||||
video.addEventListener('ratechange', forcePlaybackRate);
|
||||
video.addEventListener('srcChanged', forcePlaybackRate);
|
||||
@ -95,7 +89,7 @@ const wheelEventListener = (e: WheelEvent) => {
|
||||
|
||||
updatePlayBackSpeed();
|
||||
// Update slider position
|
||||
const playbackSpeedSilder = $<HTMLElement & { value: number }>('#playback-speed-slider');
|
||||
const playbackSpeedSilder = document.querySelector<HTMLElement & { value: number }>('#playback-speed-slider');
|
||||
if (playbackSpeedSilder) {
|
||||
playbackSpeedSilder.value = playbackSpeed;
|
||||
}
|
||||
@ -114,22 +108,19 @@ function forcePlaybackRate(e: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
export default builder.createRenderer(() => {
|
||||
return {
|
||||
onPlayerApiReady() {
|
||||
export const onPlayerApiReady = () => {
|
||||
observePopupContainer();
|
||||
observeVideo();
|
||||
setupWheelListener();
|
||||
},
|
||||
onUnload() {
|
||||
const video = $<HTMLVideoElement>('video');
|
||||
};
|
||||
|
||||
export const onUnload = () => {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (video) {
|
||||
video.removeEventListener('ratechange', forcePlaybackRate);
|
||||
video.removeEventListener('srcChanged', forcePlaybackRate);
|
||||
}
|
||||
slider.removeEventListener('wheel', wheelEventListener);
|
||||
getSongMenu()?.removeChild(slider);
|
||||
$('#playback-speed-slider')?.removeEventListener('immediate-value-changed', immediateValueChangedListener);
|
||||
}
|
||||
document.querySelector('#playback-speed-slider')?.removeEventListener('immediate-value-changed', immediateValueChangedListener);
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import hudStyle from './volume-hud.css?inline';
|
||||
import { globalShortcut, MenuItem } from 'electron';
|
||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import hudStyle from './volume-hud.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
import { overrideListener } from './override';
|
||||
import { onConfigChange, onPlayerApiReady } from './renderer';
|
||||
|
||||
export type PreciseVolumePluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -13,7 +19,7 @@ export type PreciseVolumePluginConfig = {
|
||||
savedVolume: number | undefined;
|
||||
};
|
||||
|
||||
const builder = createPluginBuilder('precise-volume', {
|
||||
export default createPlugin({
|
||||
name: 'Precise Volume',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -26,13 +32,107 @@ const builder = createPluginBuilder('precise-volume', {
|
||||
},
|
||||
savedVolume: undefined, // Plugin save volume between session here
|
||||
} as PreciseVolumePluginConfig,
|
||||
styles: [hudStyle],
|
||||
stylesheets: [hudStyle],
|
||||
menu: async ({ setConfig, getConfig, window }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig) {
|
||||
for (const option in changedOptions) {
|
||||
// HACK: Weird TypeScript error
|
||||
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option];
|
||||
}
|
||||
|
||||
setConfig(options);
|
||||
}
|
||||
|
||||
// Helper function for globalShortcuts prompt
|
||||
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
|
||||
|
||||
async function promptVolumeSteps(options: PreciseVolumePluginConfig) {
|
||||
const output = await prompt({
|
||||
title: 'Volume Steps',
|
||||
label: 'Choose Volume Increase/Decrease Steps',
|
||||
value: options.steps || 1,
|
||||
type: 'counter',
|
||||
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||
width: 380,
|
||||
...promptOptions(),
|
||||
}, window);
|
||||
|
||||
if (output || output === 0) { // 0 is somewhat valid
|
||||
changeOptions({ steps: output }, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptGlobalShortcuts(options: PreciseVolumePluginConfig, item: MenuItem) {
|
||||
const output = await prompt({
|
||||
title: 'Global Volume Keybinds',
|
||||
label: 'Choose Global Volume Keybinds:',
|
||||
type: 'keybind',
|
||||
keybindOptions: [
|
||||
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
|
||||
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
|
||||
],
|
||||
...promptOptions(),
|
||||
}, window);
|
||||
|
||||
if (output) {
|
||||
const newGlobalShortcuts: {
|
||||
volumeUp: string;
|
||||
volumeDown: string;
|
||||
} = { volumeUp: '', volumeDown: '' };
|
||||
for (const { value, accelerator } of output) {
|
||||
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
||||
}
|
||||
|
||||
changeOptions({ globalShortcuts: newGlobalShortcuts }, options);
|
||||
|
||||
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Local Arrowkeys Controls',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.arrowsShortcut),
|
||||
click(item) {
|
||||
changeOptions({ arrowsShortcut: item.checked }, config);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Global Hotkeys',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.globalShortcuts?.volumeUp ?? config.globalShortcuts?.volumeDown),
|
||||
click: (item) => promptGlobalShortcuts(config, item),
|
||||
},
|
||||
{
|
||||
label: 'Set Custom Volume Steps',
|
||||
click: () => promptVolumeSteps(config),
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
async backend({ getConfig, ipc }) {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.globalShortcuts?.volumeUp) {
|
||||
globalShortcut.register(config.globalShortcuts.volumeUp, () => ipc.send('changeVolume', true));
|
||||
}
|
||||
|
||||
if (config.globalShortcuts?.volumeDown) {
|
||||
globalShortcut.register(config.globalShortcuts.volumeDown, () => ipc.send('changeVolume', false));
|
||||
}
|
||||
},
|
||||
|
||||
renderer: {
|
||||
start() {
|
||||
overrideListener();
|
||||
},
|
||||
onPlayerApiReady,
|
||||
onConfigChange,
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { globalShortcut } from 'electron';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createMain(({ getConfig, send }) => ({
|
||||
async onLoad() {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.globalShortcuts?.volumeUp) {
|
||||
globalShortcut.register(config.globalShortcuts.volumeUp, () => send('changeVolume', true));
|
||||
}
|
||||
|
||||
if (config.globalShortcuts?.volumeDown) {
|
||||
globalShortcut.register(config.globalShortcuts.volumeDown, () => send('changeVolume', false));
|
||||
}
|
||||
},
|
||||
}));
|
||||
@ -1,90 +0,0 @@
|
||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow, MenuItem } from 'electron';
|
||||
|
||||
import builder, { PreciseVolumePluginConfig } from './index';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
|
||||
export default builder.createMenu(async ({ setConfig, getConfig, window }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig, win: BrowserWindow) {
|
||||
for (const option in changedOptions) {
|
||||
// HACK: Weird TypeScript error
|
||||
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option];
|
||||
}
|
||||
|
||||
setConfig(options);
|
||||
}
|
||||
|
||||
// Helper function for globalShortcuts prompt
|
||||
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
|
||||
|
||||
async function promptVolumeSteps(win: BrowserWindow, options: PreciseVolumePluginConfig) {
|
||||
const output = await prompt({
|
||||
title: 'Volume Steps',
|
||||
label: 'Choose Volume Increase/Decrease Steps',
|
||||
value: options.steps || 1,
|
||||
type: 'counter',
|
||||
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||
width: 380,
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output || output === 0) { // 0 is somewhat valid
|
||||
changeOptions({ steps: output }, options, win);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptGlobalShortcuts(win: BrowserWindow, options: PreciseVolumePluginConfig, item: MenuItem) {
|
||||
const output = await prompt({
|
||||
title: 'Global Volume Keybinds',
|
||||
label: 'Choose Global Volume Keybinds:',
|
||||
type: 'keybind',
|
||||
keybindOptions: [
|
||||
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
|
||||
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
|
||||
],
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output) {
|
||||
const newGlobalShortcuts: {
|
||||
volumeUp: string;
|
||||
volumeDown: string;
|
||||
} = { volumeUp: '', volumeDown: '' };
|
||||
for (const { value, accelerator } of output) {
|
||||
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
||||
}
|
||||
|
||||
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
||||
|
||||
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Local Arrowkeys Controls',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.arrowsShortcut),
|
||||
click(item) {
|
||||
changeOptions({ arrowsShortcut: item.checked }, config, window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Global Hotkeys',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.globalShortcuts?.volumeUp ?? config.globalShortcuts?.volumeDown),
|
||||
click: (item) => promptGlobalShortcuts(window, config, item),
|
||||
},
|
||||
{
|
||||
label: 'Set Custom Volume Steps',
|
||||
click: () => promptVolumeSteps(window, config),
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,5 +1,4 @@
|
||||
/* what */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
const ignored = {
|
||||
id: ['volume-slider', 'expand-volume-slider'],
|
||||
@ -9,7 +8,8 @@ const ignored = {
|
||||
function overrideAddEventListener() {
|
||||
// YO WHAT ARE YOU DOING NOW?!?!
|
||||
// Save native addEventListener
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error - We know what we're doing
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
||||
// Override addEventListener to Ignore specific events in volume-slider
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { overrideListener } from './override';
|
||||
import { type PreciseVolumePluginConfig } from './index';
|
||||
|
||||
import builder, { type PreciseVolumePluginConfig } from './index';
|
||||
import { debounce } from '@/providers/decorators';
|
||||
|
||||
import { debounce } from '../../providers/decorators';
|
||||
|
||||
import type { YoutubePlayer } from '../../types/youtube-player';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
@ -23,12 +22,14 @@ export const moveVolumeHud = debounce((showVideo: boolean) => {
|
||||
: '0';
|
||||
}, 250);
|
||||
|
||||
export default builder.createRenderer(async ({ on, getConfig, setConfig }) => {
|
||||
let options: PreciseVolumePluginConfig = await getConfig();
|
||||
let options: PreciseVolumePluginConfig;
|
||||
|
||||
export const onPlayerApiReady = (playerApi: YoutubePlayer, context: RendererContext<PreciseVolumePluginConfig>) => {
|
||||
api = playerApi;
|
||||
|
||||
// Without this function it would rewrite config 20 time when volume change by 20
|
||||
const writeOptions = debounce(() => {
|
||||
setConfig(options);
|
||||
context.setConfig(options);
|
||||
}, 1000);
|
||||
|
||||
const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
|
||||
@ -254,20 +255,12 @@ export default builder.createRenderer(async ({ on, getConfig, setConfig }) => {
|
||||
}
|
||||
}
|
||||
|
||||
context.ipc.on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease));
|
||||
context.ipc.on('setVolume', (value: number) => setVolume(value));
|
||||
|
||||
return {
|
||||
onLoad() {
|
||||
overrideListener();
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
api = playerApi;
|
||||
|
||||
on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease));
|
||||
on('setVolume', (value: number) => setVolume(value));
|
||||
firstRun();
|
||||
},
|
||||
onConfigChange(config) {
|
||||
options = config;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const onConfigChange = (config: PreciseVolumePluginConfig) => {
|
||||
options = config;
|
||||
};
|
||||
|
||||
@ -1,17 +1,65 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { dialog } from 'electron';
|
||||
|
||||
const builder = createPluginBuilder('quality-changer', {
|
||||
import QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Video Quality Changer',
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
backend({ ipc, window }) {
|
||||
ipc.handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(window, {
|
||||
type: 'question',
|
||||
buttons: qualityLabels,
|
||||
defaultId: currentIndex,
|
||||
title: 'Choose Video Quality',
|
||||
message: 'Choose Video Quality:',
|
||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||
cancelId: -1,
|
||||
}));
|
||||
},
|
||||
|
||||
renderer: {
|
||||
qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate),
|
||||
onPlayerApiReady(api: YoutubePlayer, context) {
|
||||
const getPlayer = () => document.querySelector<HTMLVideoElement>('#player');
|
||||
const chooseQuality = () => {
|
||||
setTimeout(() => getPlayer()?.click());
|
||||
|
||||
const qualityLevels = api.getAvailableQualityLevels();
|
||||
|
||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||
|
||||
(context.ipc.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex) as Promise<{ response: number }>)
|
||||
.then((promise) => {
|
||||
if (promise.response === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuality = qualityLevels[promise.response];
|
||||
api.setPlaybackQualityRange(newQuality);
|
||||
api.setPlaybackQuality(newQuality);
|
||||
});
|
||||
};
|
||||
|
||||
const setup = () => {
|
||||
document.querySelector('.top-row-buttons.ytmusic-player')?.prepend(this.qualitySettingsButton);
|
||||
|
||||
this.qualitySettingsButton.addEventListener('click', chooseQuality);
|
||||
};
|
||||
|
||||
setup();
|
||||
},
|
||||
stop() {
|
||||
document.querySelector('.top-row-buttons.ytmusic-player')?.removeChild(this.qualitySettingsButton);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { dialog, BrowserWindow } from 'electron';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createMain(({ handle }) => ({
|
||||
onLoad(win: BrowserWindow) {
|
||||
handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(win, {
|
||||
type: 'question',
|
||||
buttons: qualityLabels,
|
||||
defaultId: currentIndex,
|
||||
title: 'Choose Video Quality',
|
||||
message: 'Choose Video Quality:',
|
||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||
cancelId: -1,
|
||||
}));
|
||||
}
|
||||
}));
|
||||
@ -1,54 +0,0 @@
|
||||
import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import type { YoutubePlayer } from '../../types/youtube-player';
|
||||
|
||||
export default builder.createRenderer(({ invoke }) => {
|
||||
function $(selector: string): HTMLElement | null {
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
const qualitySettingsButton = ElementFromHtml(qualitySettingsTemplate);
|
||||
|
||||
let api: YoutubePlayer;
|
||||
|
||||
const chooseQuality = () => {
|
||||
setTimeout(() => $('#player')?.click());
|
||||
|
||||
const qualityLevels = api.getAvailableQualityLevels();
|
||||
|
||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||
|
||||
invoke<{ response: number }>('qualityChanger', api.getAvailableQualityLabels(), currentIndex)
|
||||
.then((promise) => {
|
||||
if (promise.response === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuality = qualityLevels[promise.response];
|
||||
api.setPlaybackQualityRange(newQuality);
|
||||
api.setPlaybackQuality(newQuality);
|
||||
});
|
||||
};
|
||||
|
||||
function setup() {
|
||||
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
|
||||
|
||||
qualitySettingsButton.addEventListener('click', chooseQuality);
|
||||
}
|
||||
|
||||
return {
|
||||
onPlayerApiReady(playerApi) {
|
||||
api = playerApi;
|
||||
|
||||
setup();
|
||||
},
|
||||
onUnload() {
|
||||
$('.top-row-buttons.ytmusic-player')?.removeChild(qualitySettingsButton);
|
||||
qualitySettingsButton.removeEventListener('click', chooseQuality);
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,4 +1,6 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
|
||||
export type ShortcutMappingType = {
|
||||
previous: string;
|
||||
@ -12,7 +14,7 @@ export type ShortcutsPluginConfig = {
|
||||
local: ShortcutMappingType;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('shortcuts', {
|
||||
export default createPlugin({
|
||||
name: 'Shortcuts (& MPRIS)',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -29,12 +31,7 @@ const builder = createPluginBuilder('shortcuts', {
|
||||
next: '',
|
||||
},
|
||||
} as ShortcutsPluginConfig,
|
||||
menu: onMenu,
|
||||
|
||||
backend: onMainLoad,
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { BrowserWindow, globalShortcut } from 'electron';
|
||||
import is from 'electron-is';
|
||||
import electronLocalshortcut from 'electron-localshortcut';
|
||||
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
||||
|
||||
import registerMPRIS from './mpris';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
|
||||
import builder, { ShortcutMappingType } from './index';
|
||||
import type { ShortcutMappingType, ShortcutsPluginConfig } from './index';
|
||||
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
|
||||
function _registerGlobalShortcut(webContents: Electron.WebContents, shortcut: string, action: (webContents: Electron.WebContents) => void) {
|
||||
@ -16,30 +17,28 @@ function _registerGlobalShortcut(webContents: Electron.WebContents, shortcut: st
|
||||
}
|
||||
|
||||
function _registerLocalShortcut(win: BrowserWindow, shortcut: string, action: (webContents: Electron.WebContents) => void) {
|
||||
electronLocalshortcut.register(win, shortcut, () => {
|
||||
registerElectronLocalShortcut(win, shortcut, () => {
|
||||
action(win.webContents);
|
||||
});
|
||||
}
|
||||
|
||||
export default builder.createMain(({ getConfig }) => {
|
||||
return {
|
||||
async onLoad(win) {
|
||||
export const onMainLoad = async ({ getConfig, window }: BackendContext<ShortcutsPluginConfig>) => {
|
||||
const config = await getConfig();
|
||||
|
||||
const songControls = getSongControls(win);
|
||||
const songControls = getSongControls(window);
|
||||
const { playPause, next, previous, search } = songControls;
|
||||
|
||||
if (config.overrideMediaKeys) {
|
||||
_registerGlobalShortcut(win.webContents, 'MediaPlayPause', playPause);
|
||||
_registerGlobalShortcut(win.webContents, 'MediaNextTrack', next);
|
||||
_registerGlobalShortcut(win.webContents, 'MediaPreviousTrack', previous);
|
||||
_registerGlobalShortcut(window.webContents, 'MediaPlayPause', playPause);
|
||||
_registerGlobalShortcut(window.webContents, 'MediaNextTrack', next);
|
||||
_registerGlobalShortcut(window.webContents, 'MediaPreviousTrack', previous);
|
||||
}
|
||||
|
||||
_registerLocalShortcut(win, 'CommandOrControl+F', search);
|
||||
_registerLocalShortcut(win, 'CommandOrControl+L', search);
|
||||
_registerLocalShortcut(window, 'CommandOrControl+F', search);
|
||||
_registerLocalShortcut(window, 'CommandOrControl+L', search);
|
||||
|
||||
if (is.linux()) {
|
||||
registerMPRIS(win);
|
||||
registerMPRIS(window);
|
||||
}
|
||||
|
||||
const { global, local } = config;
|
||||
@ -66,12 +65,10 @@ export default builder.createMain(({ getConfig }) => {
|
||||
}
|
||||
|
||||
if (type === 'global') {
|
||||
_registerGlobalShortcut(win.webContents, container[action], actionCallback);
|
||||
_registerGlobalShortcut(window.webContents, container[action], actionCallback);
|
||||
} else { // Type === "local"
|
||||
_registerLocalShortcut(win, local[action], actionCallback);
|
||||
}
|
||||
_registerLocalShortcut(window, local[action], actionCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
||||
|
||||
import builder, { ShortcutsPluginConfig } from './index';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
|
||||
import type { ShortcutsPluginConfig } from './index';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
export default builder.createMenu(async ({ window, getConfig, setConfig }) => {
|
||||
export const onMenu = async ({ window, getConfig, setConfig }: MenuContext<ShortcutsPluginConfig>): Promise<MenuTemplate> => {
|
||||
const config = await getConfig();
|
||||
|
||||
/**
|
||||
@ -52,4 +53,4 @@ export default builder.createMenu(async ({ window, getConfig, setConfig }) => {
|
||||
click: (item) => setConfig({ overrideMediaKeys: item.checked }),
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,9 +2,9 @@ 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';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import config from '@/config';
|
||||
|
||||
function setupMPRIS() {
|
||||
const instance = new mpris({
|
||||
|
||||
@ -1,23 +1,20 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { onRendererLoad, onRendererUnload } from './renderer';
|
||||
|
||||
export type SkipSilencesPluginConfig = {
|
||||
enabled: boolean;
|
||||
onlySkipBeginning: boolean;
|
||||
};
|
||||
|
||||
const builder = createPluginBuilder('skip-silences', {
|
||||
export default createPlugin({
|
||||
name: 'Skip Silences',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
onlySkipBeginning: false,
|
||||
} as SkipSilencesPluginConfig,
|
||||
renderer: {
|
||||
start: onRendererLoad,
|
||||
stop: onRendererUnload,
|
||||
}
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import builder, { type SkipSilencesPluginConfig } from './index';
|
||||
import { RendererContext } from '@/types/contexts';
|
||||
|
||||
import type { SkipSilencesPluginConfig } from './index';
|
||||
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
let config: SkipSilencesPluginConfig;
|
||||
|
||||
let isSilent = false;
|
||||
@ -112,8 +113,7 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
video?.addEventListener('seeked', playOrSeekHandler);
|
||||
};
|
||||
|
||||
return {
|
||||
async onLoad() {
|
||||
export const onRendererLoad = async ({ getConfig }: RendererContext<SkipSilencesPluginConfig>) => {
|
||||
config = await getConfig();
|
||||
|
||||
document.addEventListener(
|
||||
@ -123,8 +123,9 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
passive: true,
|
||||
},
|
||||
);
|
||||
},
|
||||
onUnload() {
|
||||
};
|
||||
|
||||
export const onRendererUnload = () => {
|
||||
document.removeEventListener(
|
||||
'audioCanPlay',
|
||||
audioCanPlayListener,
|
||||
@ -135,6 +136,4 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
video?.removeEventListener('play', playOrSeekHandler);
|
||||
video?.removeEventListener('seeked', playOrSeekHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import is from 'electron-is';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
import { sortSegments } from './segments';
|
||||
|
||||
import type { GetPlayerResponse } from '@/types/get-player-response';
|
||||
import type { Segment, SkipSegment } from './types';
|
||||
|
||||
export type SponsorBlockPluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -6,7 +13,9 @@ export type SponsorBlockPluginConfig = {
|
||||
categories: ('sponsor' | 'intro' | 'outro' | 'interaction' | 'selfpromo' | 'music_offtopic')[];
|
||||
};
|
||||
|
||||
const builder = createPluginBuilder('sponsorblock', {
|
||||
let currentSegments: Segment[] = [];
|
||||
|
||||
export default createPlugin({
|
||||
name: 'SponsorBlock',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -21,12 +30,83 @@ const builder = createPluginBuilder('sponsorblock', {
|
||||
'music_offtopic',
|
||||
],
|
||||
} as SponsorBlockPluginConfig,
|
||||
async backend({ getConfig, ipc }) {
|
||||
const fetchSegments = async (apiURL: string, categories: string[], videoId: string) => {
|
||||
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoId}&categories=${JSON.stringify(
|
||||
categories,
|
||||
)}`;
|
||||
try {
|
||||
const resp = await fetch(sponsorBlockURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
export default builder;
|
||||
const segments = await resp.json() as SkipSegment[];
|
||||
return sortSegments(
|
||||
segments.map((submission) => submission.segment),
|
||||
);
|
||||
} catch (error) {
|
||||
if (is.dev()) {
|
||||
console.log('error on sponsorblock request:', error);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
const { apiURL, categories } = config;
|
||||
|
||||
ipc.on('video-src-changed', async (data: GetPlayerResponse) => {
|
||||
const segments = await fetchSegments(apiURL, categories, data?.videoDetails?.videoId);
|
||||
ipc.send('sponsorblock-skip', segments);
|
||||
});
|
||||
},
|
||||
renderer: {
|
||||
timeUpdateListener: (e: Event) => {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
const target = e.target;
|
||||
|
||||
for (const segment of currentSegments) {
|
||||
if (
|
||||
target.currentTime >= segment[0]
|
||||
&& target.currentTime < segment[1]
|
||||
) {
|
||||
target.currentTime = segment[1];
|
||||
if (window.electronIs.dev()) {
|
||||
console.log('SponsorBlock: skipping segment', segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resetSegments: () => currentSegments = [],
|
||||
start({ ipc }) {
|
||||
ipc.on('sponsorblock-skip', (segments: Segment[]) => {
|
||||
currentSegments = segments;
|
||||
});
|
||||
},
|
||||
onPlayerApiReady() {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
|
||||
video.addEventListener('timeupdate', this.timeUpdateListener);
|
||||
// Reset segments on song end
|
||||
video.addEventListener('emptied', this.resetSegments);
|
||||
},
|
||||
stop() {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
|
||||
video.removeEventListener('timeupdate', this.timeUpdateListener);
|
||||
video.removeEventListener('emptied', this.resetSegments);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
import is from 'electron-is';
|
||||
|
||||
import { sortSegments } from './segments';
|
||||
|
||||
import { SkipSegment } from './types';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import type { GetPlayerResponse } from '../../types/get-player-response';
|
||||
|
||||
const fetchSegments = async (apiURL: string, categories: string[], videoId: string) => {
|
||||
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoId}&categories=${JSON.stringify(
|
||||
categories,
|
||||
)}`;
|
||||
try {
|
||||
const resp = await fetch(sponsorBlockURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const segments = await resp.json() as SkipSegment[];
|
||||
return sortSegments(
|
||||
segments.map((submission) => submission.segment),
|
||||
);
|
||||
} catch (error) {
|
||||
if (is.dev()) {
|
||||
console.log('error on sponsorblock request:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default builder.createMain(({ getConfig, on, send }) => ({
|
||||
async onLoad() {
|
||||
const config = await getConfig();
|
||||
|
||||
const { apiURL, categories } = config;
|
||||
|
||||
on('video-src-changed', async (_, data: GetPlayerResponse) => {
|
||||
const segments = await fetchSegments(apiURL, categories, data?.videoDetails?.videoId);
|
||||
send('sponsorblock-skip', segments);
|
||||
});
|
||||
}
|
||||
}));
|
||||
@ -1,49 +0,0 @@
|
||||
import { Segment } from './types';
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createRenderer(({ on }) => {
|
||||
let currentSegments: Segment[] = [];
|
||||
|
||||
const timeUpdateListener = (e: Event) => {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
const target = e.target;
|
||||
|
||||
for (const segment of currentSegments) {
|
||||
if (
|
||||
target.currentTime >= segment[0]
|
||||
&& target.currentTime < segment[1]
|
||||
) {
|
||||
target.currentTime = segment[1];
|
||||
if (window.electronIs.dev()) {
|
||||
console.log('SponsorBlock: skipping segment', segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetSegments = () => currentSegments = [];
|
||||
|
||||
return ({
|
||||
onLoad() {
|
||||
on('sponsorblock-skip', (_, segments: Segment[]) => {
|
||||
currentSegments = segments;
|
||||
});
|
||||
},
|
||||
onPlayerApiReady() {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
|
||||
video.addEventListener('timeupdate', timeUpdateListener);
|
||||
// Reset segments on song end
|
||||
video.addEventListener('emptied', resetSegments);
|
||||
},
|
||||
onUnload() {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
|
||||
video.removeEventListener('timeupdate', timeUpdateListener);
|
||||
video.removeEventListener('emptied', resetSegments);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,17 +1,84 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import playIcon from '@assets/media-icons-black/play.png?asset&asarUnpack';
|
||||
import pauseIcon from '@assets/media-icons-black/pause.png?asset&asarUnpack';
|
||||
import nextIcon from '@assets/media-icons-black/next.png?asset&asarUnpack';
|
||||
import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||
|
||||
const builder = createPluginBuilder('taskbar-mediacontrol', {
|
||||
import { nativeImage } from 'electron';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||
import { mediaIcons } from '@/types/media-icons';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Taskbar Media Control',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
backend({ window }) {
|
||||
let currentSongInfo: SongInfo;
|
||||
|
||||
const { playPause, next, previous } = getSongControls(window);
|
||||
|
||||
const setThumbar = (songInfo: SongInfo) => {
|
||||
// Wait for song to start before setting thumbar
|
||||
if (!songInfo?.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Win32 require full rewrite of components
|
||||
window.setThumbarButtons([
|
||||
{
|
||||
tooltip: 'Previous',
|
||||
icon: nativeImage.createFromPath(get('previous')),
|
||||
click() {
|
||||
previous();
|
||||
},
|
||||
}, {
|
||||
tooltip: 'Play/Pause',
|
||||
// Update icon based on play state
|
||||
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
|
||||
click() {
|
||||
playPause();
|
||||
},
|
||||
}, {
|
||||
tooltip: 'Next',
|
||||
icon: nativeImage.createFromPath(get('next')),
|
||||
click() {
|
||||
next();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Util
|
||||
const get = (kind: keyof typeof mediaIcons): string => {
|
||||
switch (kind) {
|
||||
case 'play':
|
||||
return playIcon;
|
||||
case 'pause':
|
||||
return pauseIcon;
|
||||
case 'next':
|
||||
return nextIcon;
|
||||
case 'previous':
|
||||
return previousIcon;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Update currentsonginfo for win.on('show')
|
||||
currentSongInfo = songInfo;
|
||||
// Update thumbar
|
||||
setThumbar(songInfo);
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
// Need to set thumbar again after win.show
|
||||
window.on('show', () => {
|
||||
setThumbar(currentSongInfo);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { BrowserWindow, nativeImage } from 'electron';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import { mediaIcons } from '../../types/media-icons';
|
||||
|
||||
import playIcon from '../../../assets/media-icons-black/play.png?asset&asarUnpack';
|
||||
import pauseIcon from '../../../assets/media-icons-black/pause.png?asset&asarUnpack';
|
||||
import nextIcon from '../../../assets/media-icons-black/next.png?asset&asarUnpack';
|
||||
import previousIcon from '../../../assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||
|
||||
export default builder.createMain(() => {
|
||||
return {
|
||||
onLoad(win) {
|
||||
let currentSongInfo: SongInfo;
|
||||
|
||||
const { playPause, next, previous } = getSongControls(win);
|
||||
|
||||
const setThumbar = (win: BrowserWindow, songInfo: SongInfo) => {
|
||||
// Wait for song to start before setting thumbar
|
||||
if (!songInfo?.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Win32 require full rewrite of components
|
||||
win.setThumbarButtons([
|
||||
{
|
||||
tooltip: 'Previous',
|
||||
icon: nativeImage.createFromPath(get('previous')),
|
||||
click() {
|
||||
previous();
|
||||
},
|
||||
}, {
|
||||
tooltip: 'Play/Pause',
|
||||
// Update icon based on play state
|
||||
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
|
||||
click() {
|
||||
playPause();
|
||||
},
|
||||
}, {
|
||||
tooltip: 'Next',
|
||||
icon: nativeImage.createFromPath(get('next')),
|
||||
click() {
|
||||
next();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Util
|
||||
const get = (kind: keyof typeof mediaIcons): string => {
|
||||
switch (kind) {
|
||||
case 'play':
|
||||
return playIcon;
|
||||
case 'pause':
|
||||
return pauseIcon;
|
||||
case 'next':
|
||||
return nextIcon;
|
||||
case 'previous':
|
||||
return previousIcon;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Update currentsonginfo for win.on('show')
|
||||
currentSongInfo = songInfo;
|
||||
// Update thumbar
|
||||
setThumbar(win, songInfo);
|
||||
});
|
||||
|
||||
// Need to set thumbar again after win.show
|
||||
win.on('show', () => {
|
||||
setThumbar(win, currentSongInfo);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,17 +1,98 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { type NativeImage, TouchBar } from 'electron';
|
||||
|
||||
const builder = createPluginBuilder('touchbar', {
|
||||
import { createPlugin } from '@/utils';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'TouchBar',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
backend({ window }) {
|
||||
const {
|
||||
TouchBarButton,
|
||||
TouchBarLabel,
|
||||
TouchBarSpacer,
|
||||
TouchBarSegmentedControl,
|
||||
TouchBarScrubber,
|
||||
} = TouchBar;
|
||||
|
||||
// Songtitle label
|
||||
const songTitle = new TouchBarLabel({
|
||||
label: '',
|
||||
});
|
||||
// This will store the song controls once available
|
||||
let controls: (() => void)[] = [];
|
||||
|
||||
// This will store the song image once available
|
||||
const songImage: {
|
||||
icon?: NativeImage;
|
||||
} = {};
|
||||
|
||||
// Pause/play button
|
||||
const pausePlayButton = new TouchBarButton({});
|
||||
|
||||
// The song control buttons (control functions are in the same order)
|
||||
const buttons = new TouchBarSegmentedControl({
|
||||
mode: 'buttons',
|
||||
segments: [
|
||||
new TouchBarButton({
|
||||
label: '⏮',
|
||||
}),
|
||||
pausePlayButton,
|
||||
new TouchBarButton({
|
||||
label: '⏭',
|
||||
}),
|
||||
new TouchBarButton({
|
||||
label: '👎',
|
||||
}),
|
||||
new TouchBarButton({
|
||||
label: '👍',
|
||||
}),
|
||||
],
|
||||
change: (i) => controls[i](),
|
||||
});
|
||||
|
||||
export default builder;
|
||||
// This is the touchbar object, this combines everything with proper layout
|
||||
const touchBar = new TouchBar({
|
||||
items: [
|
||||
new TouchBarScrubber({
|
||||
items: [songImage, songTitle],
|
||||
continuous: false,
|
||||
}),
|
||||
new TouchBarSpacer({
|
||||
size: 'flexible',
|
||||
}),
|
||||
buttons,
|
||||
],
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
|
||||
const { playPause, next, previous, dislike, like } = getSongControls(window);
|
||||
|
||||
// If the page is ready, register the callback
|
||||
window.once('ready-to-show', () => {
|
||||
controls = [previous, playPause, next, dislike, like];
|
||||
|
||||
// Register the callback
|
||||
registerCallback((songInfo) => {
|
||||
// Song information changed, so lets update the touchBar
|
||||
|
||||
// Set the song title
|
||||
songTitle.label = songInfo.title;
|
||||
|
||||
// Changes the pause button if paused
|
||||
pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸';
|
||||
|
||||
// Get image source
|
||||
songImage.icon = songInfo.image
|
||||
? songInfo.image.resize({ height: 23 })
|
||||
: undefined;
|
||||
|
||||
window.setTouchBar(touchBar);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
import { TouchBar, NativeImage } from 'electron';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import registerCallback from '../../providers/song-info';
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
|
||||
export default builder.createMain(() => {
|
||||
return {
|
||||
onLoad(win) {
|
||||
const {
|
||||
TouchBarButton,
|
||||
TouchBarLabel,
|
||||
TouchBarSpacer,
|
||||
TouchBarSegmentedControl,
|
||||
TouchBarScrubber,
|
||||
} = TouchBar;
|
||||
|
||||
// Songtitle label
|
||||
const songTitle = new TouchBarLabel({
|
||||
label: '',
|
||||
});
|
||||
// This will store the song controls once available
|
||||
let controls: (() => void)[] = [];
|
||||
|
||||
// This will store the song image once available
|
||||
const songImage: {
|
||||
icon?: NativeImage;
|
||||
} = {};
|
||||
|
||||
// Pause/play button
|
||||
const pausePlayButton = new TouchBarButton({});
|
||||
|
||||
// The song control buttons (control functions are in the same order)
|
||||
const buttons = new TouchBarSegmentedControl({
|
||||
mode: 'buttons',
|
||||
segments: [
|
||||
new TouchBarButton({
|
||||
label: '⏮',
|
||||
}),
|
||||
pausePlayButton,
|
||||
new TouchBarButton({
|
||||
label: '⏭',
|
||||
}),
|
||||
new TouchBarButton({
|
||||
label: '👎',
|
||||
}),
|
||||
new TouchBarButton({
|
||||
label: '👍',
|
||||
}),
|
||||
],
|
||||
change: (i) => controls[i](),
|
||||
});
|
||||
|
||||
// This is the touchbar object, this combines everything with proper layout
|
||||
const touchBar = new TouchBar({
|
||||
items: [
|
||||
new TouchBarScrubber({
|
||||
items: [songImage, songTitle],
|
||||
continuous: false,
|
||||
}),
|
||||
new TouchBarSpacer({
|
||||
size: 'flexible',
|
||||
}),
|
||||
buttons,
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const { playPause, next, previous, dislike, like } = getSongControls(win);
|
||||
|
||||
// If the page is ready, register the callback
|
||||
win.once('ready-to-show', () => {
|
||||
controls = [previous, playPause, next, dislike, like];
|
||||
|
||||
// Register the callback
|
||||
registerCallback((songInfo) => {
|
||||
// Song information changed, so lets update the touchBar
|
||||
|
||||
// Set the song title
|
||||
songTitle.label = songInfo.title;
|
||||
|
||||
// Changes the pause button if paused
|
||||
pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸';
|
||||
|
||||
// Get image source
|
||||
songImage.icon = songInfo.image
|
||||
? songInfo.image.resize({ height: 23 })
|
||||
: undefined;
|
||||
|
||||
win.setTouchBar(touchBar);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,17 +1,89 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { net } from 'electron';
|
||||
|
||||
const builder = createPluginBuilder('tuna-obs', {
|
||||
import is from 'electron-is';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
|
||||
interface Data {
|
||||
album: string | null | undefined;
|
||||
album_url: string;
|
||||
artists: string[];
|
||||
cover: string;
|
||||
cover_url: string;
|
||||
duration: number;
|
||||
progress: number;
|
||||
status: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Tuna OBS',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
backend: {
|
||||
data: {
|
||||
cover: '',
|
||||
cover_url: '',
|
||||
title: '',
|
||||
artists: [] as string[],
|
||||
status: '',
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
album_url: '',
|
||||
album: undefined,
|
||||
} as Data,
|
||||
start({ ipc }) {
|
||||
const secToMilisec = (t: number) => Math.round(Number(t) * 1e3);
|
||||
|
||||
const post = (data: Data) => {
|
||||
const port = 1608;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
net.fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ data }),
|
||||
}).catch((error: { code: number, errno: number }) => {
|
||||
if (is.dev()) {
|
||||
console.debug(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ipc.on('ytmd:player-api-loaded', () => ipc.send('setupTimeChangedListener'));
|
||||
ipc.on('timeChanged', (t: number) => {
|
||||
if (!this.data.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.progress = secToMilisec(t);
|
||||
post(this.data);
|
||||
});
|
||||
|
||||
export default builder;
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
this.data.duration = secToMilisec(songInfo.songDuration);
|
||||
this.data.progress = secToMilisec(songInfo.elapsedSeconds ?? 0);
|
||||
this.data.cover = songInfo.imageSrc ?? '';
|
||||
this.data.cover_url = songInfo.imageSrc ?? '';
|
||||
this.data.album_url = songInfo.imageSrc ?? '';
|
||||
this.data.title = songInfo.title;
|
||||
this.data.artists = [songInfo.artist];
|
||||
this.data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||
this.data.album = songInfo.album;
|
||||
post(this.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import { net } from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import registerCallback from '../../providers/song-info';
|
||||
|
||||
const secToMilisec = (t: number) => Math.round(Number(t) * 1e3);
|
||||
|
||||
interface Data {
|
||||
album: string | null | undefined;
|
||||
album_url: string;
|
||||
artists: string[];
|
||||
cover: string;
|
||||
cover_url: string;
|
||||
duration: number;
|
||||
progress: number;
|
||||
status: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const data: Data = {
|
||||
cover: '',
|
||||
cover_url: '',
|
||||
title: '',
|
||||
artists: [] as string[],
|
||||
status: '',
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
album_url: '',
|
||||
album: undefined,
|
||||
};
|
||||
|
||||
const post = (data: Data) => {
|
||||
const port = 1608;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
net.fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ data }),
|
||||
}).catch((error: { code: number, errno: number }) => {
|
||||
if (is.dev()) {
|
||||
console.debug(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default builder.createMain(({ send, handle, on }) => {
|
||||
return {
|
||||
onLoad() {
|
||||
on('ytmd:player-api-loaded', () => send('setupTimeChangedListener'));
|
||||
on('timeChanged', (t: number) => {
|
||||
if (!data.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.progress = secToMilisec(t);
|
||||
post(data);
|
||||
});
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.duration = secToMilisec(songInfo.songDuration);
|
||||
data.progress = secToMilisec(songInfo.elapsedSeconds ?? 0);
|
||||
data.cover = songInfo.imageSrc ?? '';
|
||||
data.cover_url = songInfo.imageSrc ?? '';
|
||||
data.album_url = songInfo.imageSrc ?? '';
|
||||
data.title = songInfo.title;
|
||||
data.artists = [songInfo.artist];
|
||||
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||
data.album = songInfo.album;
|
||||
post(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,7 +1,12 @@
|
||||
import buttonTemplate from './templates/button_template.html?raw';
|
||||
import forceHideStyle from './force-hide.css?inline';
|
||||
import buttonSwitcherStyle from './button-switcher.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
||||
import { YoutubePlayer } from '@/types/youtube-player';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
import { ThumbnailElement } from '@/types/get-player-response';
|
||||
|
||||
export type VideoTogglePluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -11,7 +16,7 @@ export type VideoTogglePluginConfig = {
|
||||
align: 'left' | 'middle' | 'right';
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('video-toggle', {
|
||||
export default createPlugin({
|
||||
name: 'Video Toggle',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -21,16 +26,273 @@ const builder = createPluginBuilder('video-toggle', {
|
||||
forceHide: false,
|
||||
align: 'left',
|
||||
} as VideoTogglePluginConfig,
|
||||
styles: [
|
||||
stylesheets: [
|
||||
buttonSwitcherStyle,
|
||||
forceHideStyle,
|
||||
],
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Mode',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom toggle',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'custom',
|
||||
click() {
|
||||
setConfig({ mode: 'custom' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Native toggle',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'native',
|
||||
click() {
|
||||
setConfig({ mode: 'native' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Disabled',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'disabled',
|
||||
click() {
|
||||
setConfig({ mode: 'disabled' });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Alignment',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Left',
|
||||
type: 'radio',
|
||||
checked: config.align === 'left',
|
||||
click() {
|
||||
setConfig({ align: 'left' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Middle',
|
||||
type: 'radio',
|
||||
checked: config.align === 'middle',
|
||||
click() {
|
||||
setConfig({ align: 'middle' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Right',
|
||||
type: 'radio',
|
||||
checked: config.align === 'right',
|
||||
click() {
|
||||
setConfig({ align: 'right' });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Force Remove Video Tab',
|
||||
type: 'checkbox',
|
||||
checked: config.forceHide,
|
||||
click(item) {
|
||||
setConfig({ forceHide: item.checked });
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderer: {
|
||||
config: null as VideoTogglePluginConfig | null,
|
||||
applyStyleClass: (config: VideoTogglePluginConfig) => {
|
||||
if (config.forceHide) {
|
||||
document.body.classList.add('video-toggle-force-hide');
|
||||
document.body.classList.remove('video-toggle-custom-mode');
|
||||
} else if (!config.mode || config.mode === 'custom') {
|
||||
document.body.classList.add('video-toggle-custom-mode');
|
||||
document.body.classList.remove('video-toggle-force-hide');
|
||||
}
|
||||
},
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
this.applyStyleClass(config);
|
||||
|
||||
if (config.forceHide) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.mode) {
|
||||
case 'native': {
|
||||
document.querySelector('ytmusic-player-page')?.setAttribute('has-av-switcher', '');
|
||||
document.querySelector('ytmusic-player')?.setAttribute('has-av-switcher', '');
|
||||
return;
|
||||
}
|
||||
|
||||
case 'disabled': {
|
||||
document.querySelector('ytmusic-player-page')?.removeAttribute('has-av-switcher');
|
||||
document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher');
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
async onPlayerApiReady(api, { getConfig }) {
|
||||
const config = await getConfig();
|
||||
this.config = config;
|
||||
|
||||
const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ?
|
||||
preciseVolumeMoveVolumeHud as (_: boolean) => void
|
||||
: (() => {});
|
||||
|
||||
const player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player');
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
|
||||
const switchButtonDiv = ElementFromHtml(buttonTemplate);
|
||||
|
||||
const forceThumbnail = (img: HTMLImageElement) => {
|
||||
const thumbnails: ThumbnailElement[] = (document.querySelector('#movie_player') as unknown as YoutubePlayer).getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? [];
|
||||
if (thumbnails && thumbnails.length > 0) {
|
||||
const thumbnail = thumbnails.at(-1)?.url.split('?')[0];
|
||||
if (thumbnail) img.src = thumbnail;
|
||||
}
|
||||
};
|
||||
|
||||
const setVideoState = (showVideo: boolean) => {
|
||||
if (this.config) {
|
||||
this.config.hideVideo = !showVideo;
|
||||
}
|
||||
window.mainConfig.plugins.setOptions('video-toggle', config);
|
||||
|
||||
const checkbox = document.querySelector<HTMLInputElement>('.video-switch-button-checkbox'); // custom mode
|
||||
if (checkbox) checkbox.checked = !config.hideVideo;
|
||||
|
||||
if (player) {
|
||||
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
||||
|
||||
document.querySelector<HTMLElement>('#song-video.ytmusic-player')!.style.display = showVideo ? 'block' : 'none';
|
||||
document.querySelector<HTMLElement>('#song-image')!.style.display = showVideo ? 'none' : 'block';
|
||||
|
||||
if (showVideo && video && !video.style.top) {
|
||||
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
||||
}
|
||||
|
||||
moveVolumeHud(showVideo);
|
||||
}
|
||||
};
|
||||
|
||||
const videoStarted = () => {
|
||||
if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_ATV') {
|
||||
// Video doesn't exist -> switch to song mode
|
||||
setVideoState(false);
|
||||
// Hide toggle button
|
||||
switchButtonDiv.style.display = 'none';
|
||||
} else {
|
||||
const songImage = document.querySelector<HTMLImageElement>('#song-image img');
|
||||
if (!songImage) {
|
||||
return;
|
||||
}
|
||||
// Switch to high-res thumbnail
|
||||
forceThumbnail(songImage);
|
||||
// Show toggle button
|
||||
switchButtonDiv.style.display = 'initial';
|
||||
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
||||
if (!this.config?.hideVideo && document.querySelector<HTMLElement>('#song-video.ytmusic-player')?.style.display === 'none') {
|
||||
setVideoState(true);
|
||||
} else {
|
||||
moveVolumeHud(!this.config?.hideVideo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* On load, after a delay, the page overrides the playback-mode to 'OMV_PREFERRED' which causes weird aspect ratio in the image container
|
||||
* this function fix the problem by overriding that override :)
|
||||
*/
|
||||
const forcePlaybackMode = () => {
|
||||
if (player) {
|
||||
const playbackModeObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLElement) {
|
||||
const target = mutation.target;
|
||||
if (target.getAttribute('playback-mode') !== 'ATV_PREFERRED') {
|
||||
playbackModeObserver.disconnect();
|
||||
target.setAttribute('playback-mode', 'ATV_PREFERRED');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
playbackModeObserver.observe(player, { attributeFilter: ['playback-mode'] });
|
||||
}
|
||||
};
|
||||
|
||||
const observeThumbnail = () => {
|
||||
const playbackModeObserver = new MutationObserver((mutations) => {
|
||||
if (!player?.videoMode_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLImageElement) {
|
||||
const target = mutation.target;
|
||||
if (!target.src.startsWith('data:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
forceThumbnail(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
playbackModeObserver.observe(document.querySelector('#song-image img')!, { attributeFilter: ['src'] });
|
||||
};
|
||||
|
||||
if (config.mode !== 'native' && config.mode != 'disabled') {
|
||||
document.querySelector<HTMLVideoElement>('#player')?.prepend(switchButtonDiv);
|
||||
|
||||
setVideoState(!config.hideVideo);
|
||||
forcePlaybackMode();
|
||||
// Fix black video
|
||||
if (video) {
|
||||
video.style.height = 'auto';
|
||||
}
|
||||
|
||||
//Prevents bubbling to the player which causes it to stop or resume
|
||||
switchButtonDiv.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
export default builder;
|
||||
// Button checked = show video
|
||||
switchButtonDiv.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
setVideoState(target.checked);
|
||||
});
|
||||
|
||||
video?.addEventListener('srcChanged', videoStarted);
|
||||
|
||||
observeThumbnail();
|
||||
|
||||
switch (config.align) {
|
||||
case 'right': {
|
||||
switchButtonDiv.style.left = 'calc(100% - 240px)';
|
||||
return;
|
||||
}
|
||||
|
||||
case 'middle': {
|
||||
switchButtonDiv.style.left = 'calc(50% - 120px)';
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
case 'left': {
|
||||
switchButtonDiv.style.left = '0px';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
this.config = newConfig;
|
||||
this.applyStyleClass(newConfig);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Mode',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom toggle',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'custom',
|
||||
click() {
|
||||
setConfig({ mode: 'custom' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Native toggle',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'native',
|
||||
click() {
|
||||
setConfig({ mode: 'native' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Disabled',
|
||||
type: 'radio',
|
||||
checked: config.mode === 'disabled',
|
||||
click() {
|
||||
setConfig({ mode: 'disabled' });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Alignment',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Left',
|
||||
type: 'radio',
|
||||
checked: config.align === 'left',
|
||||
click() {
|
||||
setConfig({ align: 'left' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Middle',
|
||||
type: 'radio',
|
||||
checked: config.align === 'middle',
|
||||
click() {
|
||||
setConfig({ align: 'middle' });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Right',
|
||||
type: 'radio',
|
||||
checked: config.align === 'right',
|
||||
click() {
|
||||
setConfig({ align: 'right' });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Force Remove Video Tab',
|
||||
type: 'checkbox',
|
||||
checked: config.forceHide,
|
||||
click(item) {
|
||||
setConfig({ forceHide: item.checked });
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,207 +0,0 @@
|
||||
import buttonTemplate from './templates/button_template.html?raw';
|
||||
|
||||
import builder, { type VideoTogglePluginConfig } from './index';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/renderer';
|
||||
|
||||
import type { ThumbnailElement } from '../../types/get-player-response';
|
||||
import type { YoutubePlayer } from '../../types/youtube-player';
|
||||
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ?
|
||||
preciseVolumeMoveVolumeHud as (_: boolean) => void
|
||||
: (() => {});
|
||||
|
||||
let config: VideoTogglePluginConfig = builder.config;
|
||||
let player: HTMLElement & { videoMode_: boolean } | null;
|
||||
let video: HTMLVideoElement | null;
|
||||
let api: YoutubePlayer;
|
||||
|
||||
const switchButtonDiv = ElementFromHtml(buttonTemplate);
|
||||
|
||||
function setup(playerApi: YoutubePlayer) {
|
||||
api = playerApi;
|
||||
player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player');
|
||||
video = document.querySelector<HTMLVideoElement>('video');
|
||||
|
||||
document.querySelector<HTMLVideoElement>('#player')?.prepend(switchButtonDiv);
|
||||
|
||||
setVideoState(!config.hideVideo);
|
||||
forcePlaybackMode();
|
||||
// Fix black video
|
||||
if (video) {
|
||||
video.style.height = 'auto';
|
||||
}
|
||||
|
||||
//Prevents bubbling to the player which causes it to stop or resume
|
||||
switchButtonDiv.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Button checked = show video
|
||||
switchButtonDiv.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
setVideoState(target.checked);
|
||||
});
|
||||
|
||||
video?.addEventListener('srcChanged', videoStarted);
|
||||
|
||||
observeThumbnail();
|
||||
|
||||
switch (config.align) {
|
||||
case 'right': {
|
||||
switchButtonDiv.style.left = 'calc(100% - 240px)';
|
||||
return;
|
||||
}
|
||||
|
||||
case 'middle': {
|
||||
switchButtonDiv.style.left = 'calc(50% - 120px)';
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
case 'left': {
|
||||
switchButtonDiv.style.left = '0px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVideoState(showVideo: boolean) {
|
||||
config.hideVideo = !showVideo;
|
||||
window.mainConfig.plugins.setOptions('video-toggle', config);
|
||||
|
||||
const checkbox = document.querySelector<HTMLInputElement>('.video-switch-button-checkbox'); // custom mode
|
||||
if (checkbox) checkbox.checked = !config.hideVideo;
|
||||
|
||||
if (player) {
|
||||
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
||||
|
||||
document.querySelector<HTMLElement>('#song-video.ytmusic-player')!.style.display = showVideo ? 'block' : 'none';
|
||||
document.querySelector<HTMLElement>('#song-image')!.style.display = showVideo ? 'none' : 'block';
|
||||
|
||||
if (showVideo && video && !video.style.top) {
|
||||
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
||||
}
|
||||
|
||||
moveVolumeHud(showVideo);
|
||||
}
|
||||
}
|
||||
|
||||
function videoStarted() {
|
||||
if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_ATV') {
|
||||
// Video doesn't exist -> switch to song mode
|
||||
setVideoState(false);
|
||||
// Hide toggle button
|
||||
switchButtonDiv.style.display = 'none';
|
||||
} else {
|
||||
const songImage = document.querySelector<HTMLImageElement>('#song-image img');
|
||||
if (!songImage) {
|
||||
return;
|
||||
}
|
||||
// Switch to high-res thumbnail
|
||||
forceThumbnail(songImage);
|
||||
// Show toggle button
|
||||
switchButtonDiv.style.display = 'initial';
|
||||
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
||||
if (!config.hideVideo && document.querySelector<HTMLElement>('#song-video.ytmusic-player')?.style.display === 'none') {
|
||||
setVideoState(true);
|
||||
} else {
|
||||
moveVolumeHud(!config.hideVideo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On load, after a delay, the page overrides the playback-mode to 'OMV_PREFERRED' which causes weird aspect ratio in the image container
|
||||
// this function fix the problem by overriding that override :)
|
||||
function forcePlaybackMode() {
|
||||
if (player) {
|
||||
const playbackModeObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLElement) {
|
||||
const target = mutation.target;
|
||||
if (target.getAttribute('playback-mode') !== 'ATV_PREFERRED') {
|
||||
playbackModeObserver.disconnect();
|
||||
target.setAttribute('playback-mode', 'ATV_PREFERRED');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
playbackModeObserver.observe(player, { attributeFilter: ['playback-mode'] });
|
||||
}
|
||||
}
|
||||
|
||||
function observeThumbnail() {
|
||||
const playbackModeObserver = new MutationObserver((mutations) => {
|
||||
if (!player?.videoMode_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLImageElement) {
|
||||
const target = mutation.target;
|
||||
if (!target.src.startsWith('data:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
forceThumbnail(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
playbackModeObserver.observe(document.querySelector('#song-image img')!, { attributeFilter: ['src'] });
|
||||
}
|
||||
|
||||
function forceThumbnail(img: HTMLImageElement) {
|
||||
const thumbnails: ThumbnailElement[] = (document.querySelector('#movie_player') as unknown as YoutubePlayer).getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? [];
|
||||
if (thumbnails && thumbnails.length > 0) {
|
||||
const thumbnail = thumbnails.at(-1)?.url.split('?')[0];
|
||||
if (typeof thumbnail === 'string') img.src = thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
const applyStyleClass = (config: VideoTogglePluginConfig) => {
|
||||
if (config.forceHide) {
|
||||
document.body.classList.add('video-toggle-force-hide');
|
||||
document.body.classList.remove('video-toggle-custom-mode');
|
||||
} else if (!config.mode || config.mode === 'custom') {
|
||||
document.body.classList.add('video-toggle-custom-mode');
|
||||
document.body.classList.remove('video-toggle-force-hide');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
async onLoad() {
|
||||
config = await getConfig();
|
||||
applyStyleClass(config);
|
||||
|
||||
if (config.forceHide) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.mode) {
|
||||
case 'native': {
|
||||
document.querySelector('ytmusic-player-page')?.setAttribute('has-av-switcher', '');
|
||||
document.querySelector('ytmusic-player')?.setAttribute('has-av-switcher', '');
|
||||
return;
|
||||
}
|
||||
|
||||
case 'disabled': {
|
||||
document.querySelector('ytmusic-player-page')?.removeAttribute('has-av-switcher');
|
||||
document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher');
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
if (config.mode !== 'native' && config.mode != 'disabled') setup(playerApi);
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
|
||||
applyStyleClass(newConfig);
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,6 +1,11 @@
|
||||
import emptyStyle from './empty-player.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { Visualizer } from './visualizers/visualizer';
|
||||
import {
|
||||
ButterchurnVisualizer as butterchurn,
|
||||
VudioVisualizer as vudio,
|
||||
WaveVisualizer as wave
|
||||
} from './visualizers';
|
||||
|
||||
type WaveColor = {
|
||||
gradient: string[];
|
||||
@ -51,7 +56,7 @@ export type VisualizerPluginConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
const builder = createPluginBuilder('visualizer', {
|
||||
export default createPlugin({
|
||||
name: 'Visualizer',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
@ -120,13 +125,97 @@ const builder = createPluginBuilder('visualizer', {
|
||||
],
|
||||
},
|
||||
} as VisualizerPluginConfig,
|
||||
styles: [emptyStyle],
|
||||
stylesheets: [emptyStyle],
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
const visualizerTypes = ['butterchurn', 'vudio', 'wave'] as const; // For bundling
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Type',
|
||||
submenu: visualizerTypes.map((visualizerType) => ({
|
||||
label: visualizerType,
|
||||
type: 'radio',
|
||||
checked: config.type === visualizerType,
|
||||
click() {
|
||||
setConfig({ type: visualizerType });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
async renderer({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let visualizerType: { new(...args: any[]): Visualizer<unknown> } = vudio;
|
||||
|
||||
if (config.type === 'wave') {
|
||||
visualizerType = wave;
|
||||
} else if (config.type === 'butterchurn') {
|
||||
visualizerType = butterchurn;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
'audioCanPlay',
|
||||
(e) => {
|
||||
const video = document.querySelector<HTMLVideoElement & { captureStream(): MediaStream; }>('video');
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visualizerContainer = document.querySelector<HTMLElement>('#player');
|
||||
if (!visualizerContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = document.querySelector<HTMLCanvasElement>('#visualizer');
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = 'visualizer';
|
||||
visualizerContainer?.prepend(canvas);
|
||||
}
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvas) {
|
||||
canvas.width = visualizerContainer.clientWidth;
|
||||
canvas.height = visualizerContainer.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
|
||||
const gainNode = e.detail.audioContext.createGain();
|
||||
gainNode.gain.value = 1.25;
|
||||
e.detail.audioSource.connect(gainNode);
|
||||
|
||||
const visualizer = new visualizerType(
|
||||
e.detail.audioContext,
|
||||
e.detail.audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
gainNode,
|
||||
video.captureStream(),
|
||||
config,
|
||||
);
|
||||
|
||||
const resizeVisualizer = (width: number, height: number) => {
|
||||
resizeCanvas();
|
||||
visualizer.resize(width, height);
|
||||
};
|
||||
|
||||
resizeVisualizer(canvas.width, canvas.height);
|
||||
const visualizerContainerObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
resizeVisualizer(entry.contentRect.width, entry.contentRect.height);
|
||||
}
|
||||
});
|
||||
visualizerContainerObserver.observe(visualizerContainer);
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
visualizer.render();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
const visualizerTypes = ['butterchurn', 'vudio', 'wave'] as const; // For bundling
|
||||
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Type',
|
||||
submenu: visualizerTypes.map((visualizerType) => ({
|
||||
label: visualizerType,
|
||||
type: 'radio',
|
||||
checked: config.type === visualizerType,
|
||||
click() {
|
||||
setConfig({ type: visualizerType });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,82 +0,0 @@
|
||||
import { ButterchurnVisualizer as butterchurn, WaveVisualizer as wave, VudioVisualizer as vudio } from './visualizers';
|
||||
import { Visualizer } from './visualizers/visualizer';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
return {
|
||||
async onLoad() {
|
||||
const config = await getConfig();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let visualizerType: { new(...args: any[]): Visualizer<unknown> } = vudio;
|
||||
|
||||
if (config.type === 'wave') {
|
||||
visualizerType = wave;
|
||||
} else if (config.type === 'butterchurn') {
|
||||
visualizerType = butterchurn;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
'audioCanPlay',
|
||||
(e) => {
|
||||
const video = document.querySelector<HTMLVideoElement & { captureStream(): MediaStream; }>('video');
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visualizerContainer = document.querySelector<HTMLElement>('#player');
|
||||
if (!visualizerContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = document.querySelector<HTMLCanvasElement>('#visualizer');
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = 'visualizer';
|
||||
visualizerContainer?.prepend(canvas);
|
||||
}
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvas) {
|
||||
canvas.width = visualizerContainer.clientWidth;
|
||||
canvas.height = visualizerContainer.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
|
||||
const gainNode = e.detail.audioContext.createGain();
|
||||
gainNode.gain.value = 1.25;
|
||||
e.detail.audioSource.connect(gainNode);
|
||||
|
||||
const visualizer = new visualizerType(
|
||||
e.detail.audioContext,
|
||||
e.detail.audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
gainNode,
|
||||
video.captureStream(),
|
||||
config,
|
||||
);
|
||||
|
||||
const resizeVisualizer = (width: number, height: number) => {
|
||||
resizeCanvas();
|
||||
visualizer.resize(width, height);
|
||||
};
|
||||
|
||||
resizeVisualizer(canvas.width, canvas.height);
|
||||
const visualizerContainerObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
resizeVisualizer(entry.contentRect.width, entry.contentRect.height);
|
||||
}
|
||||
});
|
||||
visualizerContainerObserver.observe(visualizerContainer);
|
||||
|
||||
visualizer.render();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import youtubeMusicTrayIcon from '../../assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
import youtubeMusicTrayIcon from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
|
||||
const promptOptions = {
|
||||
customStylesheet: 'dark',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
import setupSongInfo from './providers/song-info-front';
|
||||
import {
|
||||
createContext,
|
||||
forceLoadRendererPlugin,
|
||||
forceUnloadRendererPlugin,
|
||||
getAllLoadedRendererPlugins,
|
||||
@ -74,10 +75,10 @@ function onApiLoaded() {
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
Object.values(getAllLoadedRendererPlugins())
|
||||
.forEach((plugin) => {
|
||||
Object.entries(getAllLoadedRendererPlugins())
|
||||
.forEach(([id, plugin]) => {
|
||||
if (typeof plugin.renderer !== 'function') {
|
||||
plugin.renderer?.onPlayerApiReady?.(api!);
|
||||
plugin.renderer?.onPlayerApiReady?.(api!, createContext(id));
|
||||
}
|
||||
});
|
||||
|
||||
@ -134,7 +135,7 @@ function onApiLoaded() {
|
||||
if (api) {
|
||||
const plugin = getLoadedRendererPlugin(id);
|
||||
if (plugin && typeof plugin.renderer !== 'function') {
|
||||
plugin.renderer?.onPlayerApiReady?.(api);
|
||||
plugin.renderer?.onPlayerApiReady?.(api, createContext(id));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Menu, nativeImage, Tray } from 'electron';
|
||||
|
||||
import youtubeMusicTrayIcon from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import config from './config';
|
||||
import getSongControls from './providers/song-controls';
|
||||
|
||||
import youtubeMusicTrayIcon from '../assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
|
||||
import type { MenuTemplate } from './menu';
|
||||
|
||||
// Prevent tray being garbage collected
|
||||
|
||||
@ -18,10 +18,13 @@ export type PluginLifecycleExtra<Config, Context, This> = This & {
|
||||
start?: PluginLifecycleSimple<Context, This>;
|
||||
stop?: PluginLifecycleSimple<Context, This>;
|
||||
onConfigChange?: (this: This, newConfig: Config) => void | Promise<void>;
|
||||
onPlayerApiReady?: (this: This, playerApi: YoutubePlayer) => void | Promise<void>;
|
||||
};
|
||||
export type RendererPluginLifecycleExtra<Config, Context, This> = This & PluginLifecycleExtra<Config, Context, This> & {
|
||||
onPlayerApiReady?: (this: This, playerApi: YoutubePlayer, context: Context) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginLifecycle<Config, Context, This> = PluginLifecycleSimple<Context, This> | PluginLifecycleExtra<Config, Context, This>;
|
||||
export type RendererPluginLifecycle<Config, Context, This> = PluginLifecycleSimple<Context, This> | RendererPluginLifecycleExtra<Config, Context, This>;
|
||||
|
||||
export interface PluginDef<
|
||||
BackendProperties,
|
||||
@ -46,5 +49,5 @@ export interface PluginDef<
|
||||
} & PluginLifecycle<Config, PreloadContext<Config>, PreloadProperties>;
|
||||
renderer?: {
|
||||
[Key in keyof RendererProperties]: RendererProperties[Key]
|
||||
} & PluginLifecycle<Config, RendererContext<Config>, RendererProperties>;
|
||||
} & RendererPluginLifecycle<Config, RendererContext<Config>, RendererProperties>;
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@assets": ["./assets/*"]
|
||||
"@assets/*": ["./assets/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["./dist"],
|
||||
|
||||
Reference in New Issue
Block a user