mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
feat: migration to TypeScript part 2
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -122,6 +122,9 @@ const defaultConfig = {
|
|||||||
'captions-selector': {
|
'captions-selector': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
disableCaptions: false,
|
disableCaptions: false,
|
||||||
|
autoload: false,
|
||||||
|
lastCaptionsCode: '',
|
||||||
|
disabledCaptions: false,
|
||||||
},
|
},
|
||||||
'skip-silences': {
|
'skip-silences': {
|
||||||
onlySkipBeginning: false,
|
onlySkipBeginning: false,
|
||||||
|
|||||||
@ -70,13 +70,14 @@ interface PluginConfigOptions {
|
|||||||
* setupMyPlugin(win, config);
|
* setupMyPlugin(win, config);
|
||||||
* };
|
* };
|
||||||
*/
|
*/
|
||||||
type ConfigType<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
|
export type ConfigType<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
|
type Mode<T, Mode extends 'r' | 'm'> = Mode extends 'r' ? Promise<T> : T;
|
||||||
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||||
private name: string;
|
private readonly name: string;
|
||||||
private config: ConfigType<T>;
|
private readonly config: ConfigType<T>;
|
||||||
private defaultConfig: ConfigType<T>;
|
private readonly defaultConfig: ConfigType<T>;
|
||||||
private enableFront: boolean;
|
private readonly enableFront: boolean;
|
||||||
|
|
||||||
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
|
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
|
||||||
private allSubscribers: ((config: ConfigType<T>) => void)[] = [];
|
private allSubscribers: ((config: ConfigType<T>) => void)[] = [];
|
||||||
@ -102,7 +103,7 @@ export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
|||||||
activePlugins[name] = this;
|
activePlugins[name] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: keyof ConfigType<T>): Promise<ValueOf<ConfigType<T>>> {
|
get<Key extends keyof ConfigType<T> = keyof ConfigType<T>>(key: Key): ConfigType<T>[Key] {
|
||||||
return this.config[key];
|
return this.config[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,11 +113,11 @@ export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
|||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll() {
|
getAll(): ConfigType<T> {
|
||||||
return { ...this.config };
|
return { ...this.config };
|
||||||
}
|
}
|
||||||
|
|
||||||
setAll(options: ConfigType<T>) {
|
setAll(options: Partial<ConfigType<T>>) {
|
||||||
if (!options || typeof options !== 'object') {
|
if (!options || typeof options !== 'object') {
|
||||||
throw new Error('Options must be an object.');
|
throw new Error('Options must be an object.');
|
||||||
}
|
}
|
||||||
@ -124,7 +125,7 @@ export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
|||||||
let changed = false;
|
let changed = false;
|
||||||
for (const [key, value] of Object.entries(options) as Entries<typeof options>) {
|
for (const [key, value] of Object.entries(options) as Entries<typeof options>) {
|
||||||
if (this.config[key] !== value) {
|
if (this.config[key] !== value) {
|
||||||
this.config[key] = value;
|
if (value !== undefined) this.config[key] = value;
|
||||||
this.#onChange(key, false);
|
this.#onChange(key, false);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
12
custom-electron-prompt.d.ts
vendored
12
custom-electron-prompt.d.ts
vendored
@ -24,8 +24,8 @@ declare module 'custom-electron-prompt' {
|
|||||||
cancel?: string;
|
cancel?: string;
|
||||||
};
|
};
|
||||||
alwaysOnTop?: boolean;
|
alwaysOnTop?: boolean;
|
||||||
value?: string;
|
value?: unknown;
|
||||||
type?: 'input' | 'select' | 'counter';
|
type?: 'input' | 'select' | 'counter' | 'multiInput';
|
||||||
selectOptions?: Record<string, string>;
|
selectOptions?: Record<string, string>;
|
||||||
keybindOptions?: PromptKeybindOptions[];
|
keybindOptions?: PromptKeybindOptions[];
|
||||||
counterOptions?: PromptCounterOptions;
|
counterOptions?: PromptCounterOptions;
|
||||||
@ -37,7 +37,13 @@ declare module 'custom-electron-prompt' {
|
|||||||
frame?: boolean;
|
frame?: boolean;
|
||||||
customScript?: string;
|
customScript?: string;
|
||||||
enableRemoteModule?: boolean;
|
enableRemoteModule?: boolean;
|
||||||
inputAttrs: Partial<HTMLInputElement>;
|
inputAttrs?: Partial<HTMLInputElement>;
|
||||||
|
multiInputOptions?: {
|
||||||
|
label: string;
|
||||||
|
value: unknown;
|
||||||
|
inputAttrs?: Partial<HTMLInputElement>;
|
||||||
|
selectOptions?: Record<string, string>;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise<string | null>;
|
const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise<string | null>;
|
||||||
|
|||||||
88
navigation.d.ts
vendored
Normal file
88
navigation.d.ts
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
interface NavigationOptions {
|
||||||
|
info: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationHistoryEntry extends EventTarget {
|
||||||
|
readonly url?: string;
|
||||||
|
readonly key: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly index: number;
|
||||||
|
readonly sameDocument: boolean;
|
||||||
|
getState(): any;
|
||||||
|
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationTransition {
|
||||||
|
readonly navigationType: NavigationType;
|
||||||
|
readonly from: NavigationHistoryEntry;
|
||||||
|
readonly finished: Promise<undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationResult {
|
||||||
|
committed: Promise<NavigationHistoryEntry>;
|
||||||
|
finished: Promise<NavigationHistoryEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationNavigateOptions extends NavigationOptions {
|
||||||
|
state: any;
|
||||||
|
history?: NavigationHistoryBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationReloadOptions extends NavigationOptions {
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationUpdateCurrentEntryOptions {
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationEventsMap {
|
||||||
|
currententrychange: NavigateEvent;
|
||||||
|
navigate: NavigateEvent;
|
||||||
|
navigateerror: NavigateEvent;
|
||||||
|
navigatesuccess: NavigateEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Navigation extends EventTarget {
|
||||||
|
entries(): Array<NavigationHistoryEntry>;
|
||||||
|
readonly currentEntry?: NavigationHistoryEntry;
|
||||||
|
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): undefined;
|
||||||
|
readonly transition?: NavigationTransition;
|
||||||
|
readonly canGoBack: boolean;
|
||||||
|
readonly canGoForward: boolean;
|
||||||
|
navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
|
||||||
|
reload(options?: NavigationReloadOptions): NavigationResult;
|
||||||
|
traverseTo(key: string, options?: NavigationOptions): NavigationResult;
|
||||||
|
back(options?: NavigationOptions): NavigationResult;
|
||||||
|
forward(options?: NavigationOptions): NavigationResult;
|
||||||
|
onnavigate: ((this: Navigation, ev: Event) => any) | null;
|
||||||
|
onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null;
|
||||||
|
onnavigateerror: ((this: Navigation, ev: Event) => any) | null;
|
||||||
|
oncurrententrychange: ((this: Navigation, ev: Event) => any) | null;
|
||||||
|
|
||||||
|
addEventListener<K extends keyof NavigationEventsMap>(name: K, listener: (event: NavigationEventsMap[K]) => void);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class NavigateEvent extends Event {
|
||||||
|
canIntercept: boolean;
|
||||||
|
destination: NavigationHistoryEntry;
|
||||||
|
downloadRequest: string | null;
|
||||||
|
formData: FormData;
|
||||||
|
hashChange: boolean;
|
||||||
|
info: Record<string, string>;
|
||||||
|
navigationType: 'push' | 'reload' | 'replace' | 'traverse';
|
||||||
|
signal: AbortSignal;
|
||||||
|
userInitiated: boolean;
|
||||||
|
|
||||||
|
intercept(options?: Record<string, unknown>): void;
|
||||||
|
scroll(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationHistoryBehavior = 'auto' | 'push' | 'replace';
|
||||||
|
|
||||||
|
declare const Navigation: {
|
||||||
|
prototype: Navigation;
|
||||||
|
new(): Navigation;
|
||||||
|
};
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
@ -44,6 +44,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/howler": "^2.2.8",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
@ -1200,6 +1201,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.12.tgz",
|
||||||
"integrity": "sha512-P20p/YBrqUBmzD6KhIQ8EiY4/RRzlekL4eCvfQnulFPfjmiGxKIoyCeI7qam5I7oKH3P8EU4ptEi0EfyGoLysw=="
|
"integrity": "sha512-P20p/YBrqUBmzD6KhIQ8EiY4/RRzlekL4eCvfQnulFPfjmiGxKIoyCeI7qam5I7oKH3P8EU4ptEi0EfyGoLysw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/howler": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-7OK+cGHTWIDCOvBlEc61Lzj2tJhCpmeqiqdeNbZvTxLHluBMF6xz/2wjoQkK1M8mJIStp40OdPnkp8xIvwwsuw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||||
|
|||||||
@ -149,6 +149,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/howler": "^2.2.8",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/await-thenable */
|
||||||
|
/* renderer */
|
||||||
|
|
||||||
import { PluginConfig } from '../../config/dynamic';
|
import { PluginConfig } from '../../config/dynamic';
|
||||||
|
|
||||||
const config = new PluginConfig('adblocker', { enableFront: true });
|
const config = new PluginConfig('adblocker', { enableFront: true });
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
import config from './config';
|
import config, { blockers } from './config';
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
const blockerConfig = await config.get('blocker');
|
|
||||||
|
|
||||||
|
export default () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Blocker',
|
label: 'Blocker',
|
||||||
submenu: Object.values(config.blockers).map((blocker) => ({
|
submenu: Object.values(blockers).map((blocker: string) => ({
|
||||||
label: blocker,
|
label: blocker,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: (blockerConfig || config.blockers.WithBlocklists) === blocker,
|
checked: (config.get('blocker') || blockers.WithBlocklists) === blocker,
|
||||||
click() {
|
click() {
|
||||||
config.set('blocker', blocker);
|
config.set('blocker', blocker);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
const applyCompressor = (e) => {
|
|
||||||
const { audioContext } = e.detail;
|
|
||||||
|
|
||||||
const compressor = audioContext.createDynamicsCompressor();
|
|
||||||
compressor.threshold.value = -50;
|
|
||||||
compressor.ratio.value = 12;
|
|
||||||
compressor.knee.value = 40;
|
|
||||||
compressor.attack.value = 0;
|
|
||||||
compressor.release.value = 0.25;
|
|
||||||
|
|
||||||
e.detail.audioSource.connect(compressor);
|
|
||||||
compressor.connect(audioContext.destination);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = () =>
|
|
||||||
document.addEventListener('audioCanPlay', applyCompressor, {
|
|
||||||
once: true, // Only create the audio compressor once, not on each video
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
17
plugins/audio-compressor/front.ts
Normal file
17
plugins/audio-compressor/front.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export default () =>
|
||||||
|
document.addEventListener('audioCanPlay', (e) => {
|
||||||
|
const { audioContext } = e.detail;
|
||||||
|
|
||||||
|
const compressor = audioContext.createDynamicsCompressor();
|
||||||
|
compressor.threshold.value = -50;
|
||||||
|
compressor.ratio.value = 12;
|
||||||
|
compressor.knee.value = 40;
|
||||||
|
compressor.attack.value = 0;
|
||||||
|
compressor.release.value = 0.25;
|
||||||
|
|
||||||
|
e.detail.audioSource.connect(compressor);
|
||||||
|
compressor.connect(audioContext.destination);
|
||||||
|
}, {
|
||||||
|
once: true, // Only create the audio compressor once, not on each video
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
@ -1,7 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const { injectCSS } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = (win) => {
|
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
|
||||||
};
|
|
||||||
9
plugins/blur-nav-bar/back.ts
Normal file
9
plugins/blur-nav-bar/back.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
import { injectCSS } from '../utils';
|
||||||
|
|
||||||
|
export default (win: BrowserWindow) => {
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
module.exports = () => {
|
export default () => {
|
||||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||||
require('simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js');
|
require('simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js');
|
||||||
};
|
};
|
||||||
@ -1,19 +0,0 @@
|
|||||||
const { ipcMain } = require('electron');
|
|
||||||
const prompt = require('custom-electron-prompt');
|
|
||||||
|
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
|
||||||
|
|
||||||
module.exports = (win) => {
|
|
||||||
ipcMain.handle('captionsSelector', async (_, captionLabels, currentIndex) => await prompt(
|
|
||||||
{
|
|
||||||
title: 'Choose Caption',
|
|
||||||
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
|
||||||
type: 'select',
|
|
||||||
value: currentIndex,
|
|
||||||
selectOptions: captionLabels,
|
|
||||||
resizable: true,
|
|
||||||
...promptOptions(),
|
|
||||||
},
|
|
||||||
win,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
19
plugins/captions-selector/back.ts
Normal file
19
plugins/captions-selector/back.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
|
import promptOptions from '../../providers/prompt-options';
|
||||||
|
|
||||||
|
export default (win: BrowserWindow) => {
|
||||||
|
ipcMain.handle('captionsSelector', async (_, captionLabels: Record<string, string>, currentIndex: string) => await prompt(
|
||||||
|
{
|
||||||
|
title: 'Choose Caption',
|
||||||
|
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
||||||
|
type: 'select',
|
||||||
|
value: currentIndex,
|
||||||
|
selectOptions: captionLabels,
|
||||||
|
resizable: true,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
win,
|
||||||
|
));
|
||||||
|
};
|
||||||
@ -1,4 +0,0 @@
|
|||||||
const { PluginConfig } = require('../../config/dynamic');
|
|
||||||
|
|
||||||
const config = new PluginConfig('captions-selector', { enableFront: true });
|
|
||||||
module.exports = { ...config };
|
|
||||||
4
plugins/captions-selector/config.ts
Normal file
4
plugins/captions-selector/config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PluginConfig } from '../../config/dynamic';
|
||||||
|
|
||||||
|
const config = new PluginConfig('captions-selector', { enableFront: true });
|
||||||
|
export default { ...config } as PluginConfig<'captions-selector'>;
|
||||||
@ -1,20 +1,37 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
/* eslint-disable @typescript-eslint/await-thenable */
|
||||||
|
/* renderer */
|
||||||
|
|
||||||
const configProvider = require('./config');
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
const { ElementFromFile, templatePath } = require('../utils');
|
import configProvider from './config';
|
||||||
|
|
||||||
let config;
|
import { ElementFromFile, templatePath } from '../utils';
|
||||||
|
import { YoutubePlayer } from '../../types/youtube-player';
|
||||||
|
import { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
function $(selector) {
|
interface LanguageOptions {
|
||||||
return document.querySelector(selector);
|
displayName: string;
|
||||||
|
id: string | null;
|
||||||
|
is_default: boolean;
|
||||||
|
is_servable: boolean;
|
||||||
|
is_translateable: boolean;
|
||||||
|
kind: string;
|
||||||
|
languageCode: string; // 2 length
|
||||||
|
languageName: string;
|
||||||
|
name: string | null;
|
||||||
|
vss_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config: ConfigType<'captions-selector'>;
|
||||||
|
|
||||||
|
const $ = <Element extends HTMLElement>(selector: string): Element => document.querySelector(selector)!;
|
||||||
|
|
||||||
const captionsSettingsButton = ElementFromFile(
|
const captionsSettingsButton = ElementFromFile(
|
||||||
templatePath(__dirname, 'captions-settings-template.html'),
|
templatePath(__dirname, 'captions-settings-template.html'),
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = async () => {
|
export default async () => {
|
||||||
|
// RENDERER
|
||||||
config = await configProvider.getAll();
|
config = await configProvider.getAll();
|
||||||
|
|
||||||
configProvider.subscribeAll((newConfig) => {
|
configProvider.subscribeAll((newConfig) => {
|
||||||
@ -23,12 +40,12 @@ module.exports = async () => {
|
|||||||
document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true });
|
document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
function setup(api) {
|
function setup(api: YoutubePlayer) {
|
||||||
$('.right-controls-buttons').append(captionsSettingsButton);
|
$('.right-controls-buttons').append(captionsSettingsButton);
|
||||||
|
|
||||||
let captionTrackList = api.getOption('captions', 'tracklist');
|
let captionTrackList = api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
||||||
|
|
||||||
$('video').addEventListener('srcChanged', async () => {
|
$('video').addEventListener('srcChanged', () => {
|
||||||
if (config.disableCaptions) {
|
if (config.disableCaptions) {
|
||||||
setTimeout(() => api.unloadModule('captions'), 100);
|
setTimeout(() => api.unloadModule('captions'), 100);
|
||||||
captionsSettingsButton.style.display = 'none';
|
captionsSettingsButton.style.display = 'none';
|
||||||
@ -37,8 +54,8 @@ function setup(api) {
|
|||||||
|
|
||||||
api.loadModule('captions');
|
api.loadModule('captions');
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(() => {
|
||||||
captionTrackList = api.getOption('captions', 'tracklist');
|
captionTrackList = api.getOption('captions', 'tracklist') ?? [];
|
||||||
|
|
||||||
if (config.autoload && config.lastCaptionsCode) {
|
if (config.autoload && config.lastCaptionsCode) {
|
||||||
api.setOption('captions', 'track', {
|
api.setOption('captions', 'track', {
|
||||||
@ -54,9 +71,9 @@ function setup(api) {
|
|||||||
|
|
||||||
captionsSettingsButton.addEventListener('click', async () => {
|
captionsSettingsButton.addEventListener('click', async () => {
|
||||||
if (captionTrackList?.length) {
|
if (captionTrackList?.length) {
|
||||||
const currentCaptionTrack = api.getOption('captions', 'track');
|
const currentCaptionTrack = api.getOption<LanguageOptions>('captions', 'track')!;
|
||||||
let currentIndex = currentCaptionTrack
|
let currentIndex = currentCaptionTrack
|
||||||
? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode))
|
? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode)!)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const captionLabels = [
|
const captionLabels = [
|
||||||
@ -64,7 +81,7 @@ function setup(api) {
|
|||||||
'None',
|
'None',
|
||||||
];
|
];
|
||||||
|
|
||||||
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex);
|
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number;
|
||||||
if (currentIndex === null) {
|
if (currentIndex === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
const config = require('./config');
|
import config from './config';
|
||||||
|
|
||||||
module.exports = () => [
|
import { MenuTemplate } from '../../menu';
|
||||||
|
|
||||||
|
export default (): MenuTemplate => [
|
||||||
{
|
{
|
||||||
label: 'Automatically select last used caption',
|
label: 'Automatically select last used caption',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
@ -1,10 +1,10 @@
|
|||||||
module.exports = () => {
|
export default () => {
|
||||||
const compactSidebar = document.querySelector('#mini-guide');
|
const compactSidebar = document.querySelector('#mini-guide');
|
||||||
const isCompactSidebarDisabled
|
const isCompactSidebarDisabled
|
||||||
= compactSidebar === null
|
= compactSidebar === null
|
||||||
|| window.getComputedStyle(compactSidebar).display === 'none';
|
|| window.getComputedStyle(compactSidebar).display === 'none';
|
||||||
|
|
||||||
if (isCompactSidebarDisabled) {
|
if (isCompactSidebarDisabled) {
|
||||||
document.querySelector('#button').click();
|
(document.querySelector('#button') as HTMLButtonElement)?.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1,15 +0,0 @@
|
|||||||
const { ipcMain } = require('electron');
|
|
||||||
const { Innertube } = require('youtubei.js');
|
|
||||||
|
|
||||||
require('./config');
|
|
||||||
|
|
||||||
module.exports = async () => {
|
|
||||||
const yt = await Innertube.create();
|
|
||||||
|
|
||||||
ipcMain.handle('audio-url', async (_, videoID) => {
|
|
||||||
const info = await yt.getBasicInfo(videoID);
|
|
||||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
15
plugins/crossfade/back.ts
Normal file
15
plugins/crossfade/back.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import { Innertube } from 'youtubei.js';
|
||||||
|
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const yt = await Innertube.create();
|
||||||
|
|
||||||
|
ipcMain.handle('audio-url', async (_, videoID: string) => {
|
||||||
|
const info = await yt.getBasicInfo(videoID);
|
||||||
|
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,4 +0,0 @@
|
|||||||
const { PluginConfig } = require('../../config/dynamic');
|
|
||||||
|
|
||||||
const config = new PluginConfig('crossfade', { enableFront: true });
|
|
||||||
module.exports = { ...config };
|
|
||||||
4
plugins/crossfade/config.ts
Normal file
4
plugins/crossfade/config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PluginConfig } from '../../config/dynamic';
|
||||||
|
|
||||||
|
const config = new PluginConfig('crossfade', { enableFront: true });
|
||||||
|
export default { ...config } as PluginConfig<'crossfade'>;
|
||||||
@ -18,7 +18,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Internal utility: check if value is a valid volume level and throw if not
|
// Internal utility: check if value is a valid volume level and throw if not
|
||||||
const validateVolumeLevel = (value) => {
|
const validateVolumeLevel = (value: number) => {
|
||||||
// Number between 0 and 1?
|
// Number between 0 and 1?
|
||||||
if (!Number.isNaN(value) && value >= 0 && value <= 1) {
|
if (!Number.isNaN(value) && value >= 0 && value <= 1) {
|
||||||
// Yup, that's fine
|
// Yup, that's fine
|
||||||
@ -29,8 +29,51 @@ const validateVolumeLevel = (value) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type VolumeLogger = <Params extends unknown[]>(message: string, ...args: Params) => void;
|
||||||
|
interface VolumeFaderOptions {
|
||||||
|
/**
|
||||||
|
* logging `function(stuff, …)` for execution information (default: no logging)
|
||||||
|
*/
|
||||||
|
logger?: VolumeLogger;
|
||||||
|
/**
|
||||||
|
* either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
|
||||||
|
*/
|
||||||
|
fadeScaling?: string | number;
|
||||||
|
/**
|
||||||
|
* media volume 0…1 to apply during setup (volume not touched by default)
|
||||||
|
*/
|
||||||
|
initialVolume?: number;
|
||||||
|
/**
|
||||||
|
* time in milliseconds to complete a fade (default: 1000 ms)
|
||||||
|
*/
|
||||||
|
fadeDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeFade {
|
||||||
|
volume: {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
time: {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
callback?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
// Main class
|
// Main class
|
||||||
class VolumeFader {
|
export class VolumeFader {
|
||||||
|
private media: HTMLMediaElement;
|
||||||
|
private logger: VolumeLogger | false;
|
||||||
|
private scale: {
|
||||||
|
internalToVolume: (level: number) => number;
|
||||||
|
volumeToInternal: (level: number) => number;
|
||||||
|
};
|
||||||
|
private fadeDuration: number = 1000;
|
||||||
|
private active: boolean = false;
|
||||||
|
private fade: VolumeFade | undefined;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VolumeFader Constructor
|
* VolumeFader Constructor
|
||||||
*
|
*
|
||||||
@ -38,13 +81,8 @@ class VolumeFader {
|
|||||||
* @param options {Object} - an object with optional settings
|
* @param options {Object} - an object with optional settings
|
||||||
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
|
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
|
||||||
*
|
*
|
||||||
* options:
|
|
||||||
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
|
|
||||||
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
|
|
||||||
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
|
|
||||||
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
|
|
||||||
*/
|
*/
|
||||||
constructor(media, options) {
|
constructor(media: HTMLMediaElement, options: VolumeFaderOptions) {
|
||||||
// Passed media element of correct type?
|
// Passed media element of correct type?
|
||||||
if (media instanceof HTMLMediaElement) {
|
if (media instanceof HTMLMediaElement) {
|
||||||
// Save reference to media element
|
// Save reference to media element
|
||||||
@ -70,8 +108,8 @@ class VolumeFader {
|
|||||||
if (options.fadeScaling === 'linear') {
|
if (options.fadeScaling === 'linear') {
|
||||||
// Pass levels unchanged
|
// Pass levels unchanged
|
||||||
this.scale = {
|
this.scale = {
|
||||||
internalToVolume: (level) => level,
|
internalToVolume: (level: number) => level,
|
||||||
volumeToInternal: (level) => level,
|
volumeToInternal: (level: number) => level,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log setting
|
// Log setting
|
||||||
@ -79,7 +117,7 @@ class VolumeFader {
|
|||||||
}
|
}
|
||||||
// No linear, but logarithmic fading…
|
// No linear, but logarithmic fading…
|
||||||
else {
|
else {
|
||||||
let dynamicRange;
|
let dynamicRange: number;
|
||||||
|
|
||||||
// Default dynamic range?
|
// Default dynamic range?
|
||||||
if (
|
if (
|
||||||
@ -91,7 +129,8 @@ class VolumeFader {
|
|||||||
}
|
}
|
||||||
// Custom dynamic range?
|
// Custom dynamic range?
|
||||||
else if (
|
else if (
|
||||||
!Number.isNaN(options.fadeScaling)
|
typeof options.fadeScaling === 'number'
|
||||||
|
&& !Number.isNaN(options.fadeScaling)
|
||||||
&& options.fadeScaling > 0
|
&& options.fadeScaling > 0
|
||||||
) {
|
) {
|
||||||
// Turn amplitude dB into a multiple of 10 power dB
|
// Turn amplitude dB into a multiple of 10 power dB
|
||||||
@ -107,9 +146,9 @@ class VolumeFader {
|
|||||||
|
|
||||||
// Use exponential/logarithmic scaler for expansion/compression
|
// Use exponential/logarithmic scaler for expansion/compression
|
||||||
this.scale = {
|
this.scale = {
|
||||||
internalToVolume: (level) =>
|
internalToVolume: (level: number) =>
|
||||||
this.exponentialScaler(level, dynamicRange),
|
this.exponentialScaler(level, dynamicRange),
|
||||||
volumeToInternal: (level) =>
|
volumeToInternal: (level: number) =>
|
||||||
this.logarithmicScaler(level, dynamicRange),
|
this.logarithmicScaler(level, dynamicRange),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,7 +232,7 @@ class VolumeFader {
|
|||||||
* @throws {TypeError} if fadeDuration is not a number greater than zero
|
* @throws {TypeError} if fadeDuration is not a number greater than zero
|
||||||
* @return {Object} VolumeFader instance for chaining
|
* @return {Object} VolumeFader instance for chaining
|
||||||
*/
|
*/
|
||||||
setFadeDuration(fadeDuration) {
|
setFadeDuration(fadeDuration: number) {
|
||||||
// If duration is a valid number > 0…
|
// If duration is a valid number > 0…
|
||||||
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
|
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
|
||||||
// Set fade duration
|
// Set fade duration
|
||||||
@ -219,7 +258,7 @@ class VolumeFader {
|
|||||||
* @throws {TypeError} if targetVolume is not in the range 0…1
|
* @throws {TypeError} if targetVolume is not in the range 0…1
|
||||||
* @return {Object} VolumeFader instance for chaining
|
* @return {Object} VolumeFader instance for chaining
|
||||||
*/
|
*/
|
||||||
fadeTo(targetVolume, callback) {
|
fadeTo(targetVolume: number, callback?: () => void) {
|
||||||
// Validate volume and throw if invalid
|
// Validate volume and throw if invalid
|
||||||
validateVolumeLevel(targetVolume);
|
validateVolumeLevel(targetVolume);
|
||||||
|
|
||||||
@ -250,11 +289,11 @@ class VolumeFader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience shorthand methods for common fades
|
// Convenience shorthand methods for common fades
|
||||||
fadeIn(callback) {
|
fadeIn(callback: () => void) {
|
||||||
this.fadeTo(1, callback);
|
this.fadeTo(1, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
fadeOut(callback) {
|
fadeOut(callback: () => void) {
|
||||||
this.fadeTo(0, callback);
|
this.fadeTo(0, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,7 +352,7 @@ class VolumeFader {
|
|||||||
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
|
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
|
||||||
* @return {Number} - expanded level (float, 0…1)
|
* @return {Number} - expanded level (float, 0…1)
|
||||||
*/
|
*/
|
||||||
exponentialScaler(input, dynamicRange) {
|
exponentialScaler(input: number, dynamicRange: number) {
|
||||||
// Special case: make zero (or any falsy input) return zero
|
// Special case: make zero (or any falsy input) return zero
|
||||||
if (input === 0) {
|
if (input === 0) {
|
||||||
// Since the dynamic range is limited,
|
// Since the dynamic range is limited,
|
||||||
@ -336,7 +375,7 @@ class VolumeFader {
|
|||||||
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
|
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
|
||||||
* @return {Number} - compressed level (float, 0…1)
|
* @return {Number} - compressed level (float, 0…1)
|
||||||
*/
|
*/
|
||||||
logarithmicScaler(input, dynamicRange) {
|
logarithmicScaler(input: number, dynamicRange: number) {
|
||||||
// Special case: make zero (or any falsy input) return zero
|
// Special case: make zero (or any falsy input) return zero
|
||||||
if (input === 0) {
|
if (input === 0) {
|
||||||
// Logarithm of zero would be -∞, which would map to zero anyway
|
// Logarithm of zero would be -∞, which would map to zero anyway
|
||||||
@ -351,6 +390,6 @@ class VolumeFader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
VolumeFader
|
VolumeFader
|
||||||
};
|
};
|
||||||
@ -1,38 +1,39 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
/* eslint-disable @typescript-eslint/await-thenable */
|
||||||
const { Howl } = require('howler');
|
/* renderer */
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { Howl } from 'howler';
|
||||||
|
|
||||||
// Extracted from https://github.com/bitfasching/VolumeFader
|
// Extracted from https://github.com/bitfasching/VolumeFader
|
||||||
const { VolumeFader } = require('./fader');
|
import { VolumeFader } from './fader';
|
||||||
|
|
||||||
let transitionAudio; // Howler audio used to fade out the current music
|
import configProvider from './config';
|
||||||
|
|
||||||
|
import defaultConfigs from '../../config/defaults';
|
||||||
|
import { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
|
let transitionAudio: Howl; // Howler audio used to fade out the current music
|
||||||
let firstVideo = true;
|
let firstVideo = true;
|
||||||
let waitForTransition;
|
let waitForTransition: Promise<unknown>;
|
||||||
|
|
||||||
/**
|
const defaultConfig = defaultConfigs.plugins.crossfade;
|
||||||
* @type {PluginConfig}
|
|
||||||
*/
|
|
||||||
const configProvider = require('./config');
|
|
||||||
|
|
||||||
const defaultConfig = require('../../config/defaults').plugins.crossfade;
|
let config: ConfigType<'crossfade'>;
|
||||||
|
|
||||||
let config;
|
const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number);
|
||||||
|
|
||||||
const configGetNumber = (key) => Number(config[key]) || defaultConfig[key];
|
const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise<string>;
|
||||||
|
|
||||||
const getStreamURL = async (videoID) => {
|
const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
||||||
return await ipcRenderer.invoke('audio-url', videoID);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
|
||||||
|
|
||||||
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
|
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
|
||||||
|
|
||||||
const watchVideoIDChanges = (cb) => {
|
const watchVideoIDChanges = (cb: (id: string) => void) => {
|
||||||
window.navigation.addEventListener('navigate', (event) => {
|
window.navigation.addEventListener('navigate', (event) => {
|
||||||
const currentVideoID = getVideoIDFromURL(
|
const currentVideoID = getVideoIDFromURL(
|
||||||
event.currentTarget.currentEntry.url,
|
(event.currentTarget as Navigation).currentEntry?.url ?? '',
|
||||||
);
|
);
|
||||||
const nextVideoID = getVideoIDFromURL(event.destination.url);
|
const nextVideoID = getVideoIDFromURL(event.destination.url ?? '');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nextVideoID
|
nextVideoID
|
||||||
@ -51,7 +52,7 @@ const watchVideoIDChanges = (cb) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAudioForCrossfade = async (url) => {
|
const createAudioForCrossfade = (url: string) => {
|
||||||
if (transitionAudio) {
|
if (transitionAudio) {
|
||||||
transitionAudio.unload();
|
transitionAudio.unload();
|
||||||
}
|
}
|
||||||
@ -61,19 +62,19 @@ const createAudioForCrossfade = async (url) => {
|
|||||||
html5: true,
|
html5: true,
|
||||||
volume: 0,
|
volume: 0,
|
||||||
});
|
});
|
||||||
await syncVideoWithTransitionAudio();
|
syncVideoWithTransitionAudio();
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncVideoWithTransitionAudio = async () => {
|
const syncVideoWithTransitionAudio = () => {
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video')!;
|
||||||
|
|
||||||
const videoFader = new VolumeFader(video, {
|
const videoFader = new VolumeFader(video, {
|
||||||
fadeScaling: configGetNumber('fadeScaling'),
|
fadeScaling: configGetNumber('fadeScaling'),
|
||||||
fadeDuration: configGetNumber('fadeInDuration'),
|
fadeDuration: configGetNumber('fadeInDuration'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await transitionAudio.play();
|
transitionAudio.play();
|
||||||
await transitionAudio.seek(video.currentTime);
|
transitionAudio.seek(video.currentTime);
|
||||||
|
|
||||||
video.addEventListener('seeking', () => {
|
video.addEventListener('seeking', () => {
|
||||||
transitionAudio.seek(video.currentTime);
|
transitionAudio.seek(video.currentTime);
|
||||||
@ -83,9 +84,9 @@ const syncVideoWithTransitionAudio = async () => {
|
|||||||
transitionAudio.pause();
|
transitionAudio.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
video.addEventListener('play', async () => {
|
video.addEventListener('play', () => {
|
||||||
await transitionAudio.play();
|
transitionAudio.play();
|
||||||
await transitionAudio.seek(video.currentTime);
|
transitionAudio.seek(video.currentTime);
|
||||||
|
|
||||||
// Fade in
|
// Fade in
|
||||||
const videoVolume = video.volume;
|
const videoVolume = video.volume;
|
||||||
@ -102,7 +103,7 @@ const syncVideoWithTransitionAudio = async () => {
|
|||||||
video.removeEventListener('timeupdate', transitionBeforeEnd);
|
video.removeEventListener('timeupdate', transitionBeforeEnd);
|
||||||
|
|
||||||
// Go to next video - XXX: does not support "repeat 1" mode
|
// Go to next video - XXX: does not support "repeat 1" mode
|
||||||
document.querySelector('.next-button').click();
|
(document.querySelector('.next-button') as HTMLButtonElement).click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,18 +122,18 @@ const onApiLoaded = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const crossfade = async (cb) => {
|
const crossfade = (cb: () => void) => {
|
||||||
if (!isReadyToCrossfade()) {
|
if (!isReadyToCrossfade()) {
|
||||||
cb();
|
cb();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolveTransition;
|
let resolveTransition: () => void;
|
||||||
waitForTransition = new Promise((resolve) => {
|
waitForTransition = new Promise<void>((resolve) => {
|
||||||
resolveTransition = resolve;
|
resolveTransition = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video')!;
|
||||||
|
|
||||||
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
|
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
|
||||||
initialVolume: video.volume,
|
initialVolume: video.volume,
|
||||||
@ -148,7 +149,7 @@ const crossfade = async (cb) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = async () => {
|
export default async () => {
|
||||||
config = await configProvider.getAll();
|
config = await configProvider.getAll();
|
||||||
|
|
||||||
configProvider.subscribeAll((newConfig) => {
|
configProvider.subscribeAll((newConfig) => {
|
||||||
@ -1,11 +1,16 @@
|
|||||||
const prompt = require('custom-electron-prompt');
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
const config = require('./config');
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
import config from './config';
|
||||||
const defaultOptions = require('../../config/defaults').plugins.crossfade;
|
|
||||||
|
|
||||||
module.exports = (win) => [
|
import promptOptions from '../../providers/prompt-options';
|
||||||
|
import configOptions from '../../config/defaults';
|
||||||
|
import { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
|
const defaultOptions = configOptions.plugins.crossfade;
|
||||||
|
|
||||||
|
export default (win: BrowserWindow) => [
|
||||||
{
|
{
|
||||||
label: 'Advanced',
|
label: 'Advanced',
|
||||||
async click() {
|
async click() {
|
||||||
@ -17,7 +22,7 @@ module.exports = (win) => [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function promptCrossfadeValues(win, options) {
|
async function promptCrossfadeValues(win: BrowserWindow, options: ConfigType<'crossfade'>): Promise<Partial<ConfigType<'crossfade'>> | undefined> {
|
||||||
const res = await prompt(
|
const res = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Crossfade Options',
|
title: 'Crossfade Options',
|
||||||
@ -29,8 +34,8 @@ async function promptCrossfadeValues(win, options) {
|
|||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
min: 0,
|
min: '0',
|
||||||
step: 100,
|
step: '100',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -39,8 +44,8 @@ async function promptCrossfadeValues(win, options) {
|
|||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
min: 0,
|
min: '0',
|
||||||
step: 100,
|
step: '100',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50,7 +55,7 @@ async function promptCrossfadeValues(win, options) {
|
|||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
min: 0,
|
min: '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1,14 +0,0 @@
|
|||||||
module.exports = () => {
|
|
||||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
|
||||||
apiEvent.detail.addEventListener('videodatachange', (name) => {
|
|
||||||
if (name === 'dataloaded') {
|
|
||||||
apiEvent.detail.pauseVideo();
|
|
||||||
document.querySelector('video').addEventListener('timeupdate', (e) => {
|
|
||||||
e.target.pause();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
document.querySelector('video').ontimeupdate = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { once: true, passive: true });
|
|
||||||
};
|
|
||||||
14
plugins/disable-autoplay/front.ts
Normal file
14
plugins/disable-autoplay/front.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default () => {
|
||||||
|
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||||
|
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
||||||
|
if (name === 'dataloaded') {
|
||||||
|
apiEvent.detail.pauseVideo();
|
||||||
|
(document.querySelector('video') as HTMLVideoElement)?.addEventListener('timeupdate', (e) => {
|
||||||
|
(e.target as HTMLVideoElement)?.pause();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(document.querySelector('video') as HTMLVideoElement).ontimeupdate = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { once: true, passive: true });
|
||||||
|
};
|
||||||
@ -1,36 +1,35 @@
|
|||||||
'use strict';
|
import { app, dialog } from 'electron';
|
||||||
const { dialog, app } = require('electron');
|
import Discord from '@xhayper/discord-rpc';
|
||||||
const Discord = require('@xhayper/discord-rpc');
|
import { dev } from 'electron-is';
|
||||||
const { dev } = require('electron-is');
|
|
||||||
|
|
||||||
const registerCallback = require('../../providers/song-info');
|
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||||
|
|
||||||
|
import registerCallback from '../../providers/song-info';
|
||||||
|
import pluginConfig from '../../config';
|
||||||
|
|
||||||
// Application ID registered by @Zo-Bro-23
|
// Application ID registered by @Zo-Bro-23
|
||||||
const clientId = '1043858434585526382';
|
const clientId = '1043858434585526382';
|
||||||
|
|
||||||
/**
|
export interface Info {
|
||||||
* @typedef {Object} Info
|
rpc: import('@xhayper/discord-rpc').Client;
|
||||||
* @property {import('@xhayper/discord-rpc').Client} rpc
|
ready: boolean;
|
||||||
* @property {boolean} ready
|
autoReconnect: boolean;
|
||||||
* @property {boolean} autoReconnect
|
lastSongInfo?: import('../../providers/song-info').SongInfo;
|
||||||
* @property {import('../../providers/song-info').SongInfo} lastSongInfo
|
}
|
||||||
*/
|
|
||||||
/**
|
const info: Info = {
|
||||||
* @type {Info}
|
|
||||||
*/
|
|
||||||
const info = {
|
|
||||||
rpc: new Discord.Client({
|
rpc: new Discord.Client({
|
||||||
clientId,
|
clientId,
|
||||||
}),
|
}),
|
||||||
ready: false,
|
ready: false,
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
lastSongInfo: null,
|
lastSongInfo: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(() => void)[]}
|
* @type {(() => void)[]}
|
||||||
*/
|
*/
|
||||||
const refreshCallbacks = [];
|
const refreshCallbacks: (() => void)[] = [];
|
||||||
|
|
||||||
const resetInfo = () => {
|
const resetInfo = () => {
|
||||||
info.ready = false;
|
info.ready = false;
|
||||||
@ -85,8 +84,8 @@ const connectRecursive = () => {
|
|||||||
connectTimeout().catch(connectRecursive);
|
connectTimeout().catch(connectRecursive);
|
||||||
};
|
};
|
||||||
|
|
||||||
let window;
|
let window: Electron.BrowserWindow;
|
||||||
const connect = (showError = false) => {
|
export const connect = (showError = false) => {
|
||||||
if (info.rpc.isConnected) {
|
if (info.rpc.isConnected) {
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.log('Attempted to connect with active connection');
|
console.log('Attempted to connect with active connection');
|
||||||
@ -98,7 +97,7 @@ const connect = (showError = false) => {
|
|||||||
info.ready = false;
|
info.ready = false;
|
||||||
|
|
||||||
// Startup the rpc client
|
// Startup the rpc client
|
||||||
info.rpc.login({ clientId }).catch((error) => {
|
info.rpc.login().catch((error: Error) => {
|
||||||
resetInfo();
|
resetInfo();
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -116,14 +115,17 @@ const connect = (showError = false) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let clearActivity;
|
let clearActivity: NodeJS.Timeout | undefined;
|
||||||
/**
|
let updateActivity: import('../../providers/song-info').SongInfoCallback;
|
||||||
* @type {import('../../providers/song-info').songInfoCallback}
|
|
||||||
*/
|
|
||||||
let updateActivity;
|
|
||||||
|
|
||||||
module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTime, listenAlong, hideDurationLeft }) => {
|
const DiscordOptionsObj = pluginConfig.get('plugins.discord');
|
||||||
info.autoReconnect = autoReconnect;
|
type DiscordOptions = typeof DiscordOptionsObj;
|
||||||
|
|
||||||
|
export default (
|
||||||
|
win: Electron.BrowserWindow,
|
||||||
|
options: DiscordOptions,
|
||||||
|
) => {
|
||||||
|
info.autoReconnect = options.autoReconnect;
|
||||||
|
|
||||||
window = win;
|
window = win;
|
||||||
// We get multiple events
|
// We get multiple events
|
||||||
@ -146,7 +148,7 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear directly if timeout is 0
|
// Clear directly if timeout is 0
|
||||||
if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) {
|
if (songInfo.isPaused && options.activityTimoutEnabled && options.activityTimoutTime === 0) {
|
||||||
info.rpc.user?.clearActivity().catch(console.error);
|
info.rpc.user?.clearActivity().catch(console.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -154,12 +156,12 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
|
|||||||
// Song information changed, so lets update the rich presence
|
// Song information changed, so lets update the rich presence
|
||||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||||
const activityInfo = {
|
const activityInfo: SetActivity = {
|
||||||
details: songInfo.title,
|
details: songInfo.title,
|
||||||
state: songInfo.artist,
|
state: songInfo.artist,
|
||||||
largeImageKey: songInfo.imageSrc,
|
largeImageKey: songInfo.imageSrc ?? '',
|
||||||
largeImageText: songInfo.album,
|
largeImageText: songInfo.album ?? '',
|
||||||
buttons: listenAlong ? [
|
buttons: options.listenAlong ? [
|
||||||
{ label: 'Listen Along', url: songInfo.url },
|
{ label: 'Listen Along', url: songInfo.url },
|
||||||
] : undefined,
|
] : undefined,
|
||||||
};
|
};
|
||||||
@ -169,10 +171,10 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
|
|||||||
activityInfo.smallImageKey = 'paused';
|
activityInfo.smallImageKey = 'paused';
|
||||||
activityInfo.smallImageText = 'Paused';
|
activityInfo.smallImageText = 'Paused';
|
||||||
// Set start the timer so the activity gets cleared after a while if enabled
|
// Set start the timer so the activity gets cleared after a while if enabled
|
||||||
if (activityTimoutEnabled) {
|
if (options.activityTimoutEnabled) {
|
||||||
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10_000);
|
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), options.activityTimoutTime ?? 10_000);
|
||||||
}
|
}
|
||||||
} else if (!hideDurationLeft) {
|
} else if (!options.hideDurationLeft) {
|
||||||
// Add the start and end time of the song
|
// Add the start and end time of the song
|
||||||
const songStartTime = Date.now() - (songInfo.elapsedSeconds * 1000);
|
const songStartTime = Date.now() - (songInfo.elapsedSeconds * 1000);
|
||||||
activityInfo.startTimestamp = songStartTime;
|
activityInfo.startTimestamp = songStartTime;
|
||||||
@ -188,10 +190,10 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
|
|||||||
registerCallback(updateActivity);
|
registerCallback(updateActivity);
|
||||||
connect();
|
connect();
|
||||||
});
|
});
|
||||||
app.on('window-all-closed', module.exports.clear);
|
app.on('window-all-closed', clear);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.clear = () => {
|
export const clear = () => {
|
||||||
if (info.rpc) {
|
if (info.rpc) {
|
||||||
info.rpc.user?.clearActivity();
|
info.rpc.user?.clearActivity();
|
||||||
}
|
}
|
||||||
@ -199,6 +201,5 @@ module.exports.clear = () => {
|
|||||||
clearTimeout(clearActivity);
|
clearTimeout(clearActivity);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.connect = connect;
|
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
|
||||||
module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb);
|
export const isConnected = () => info.rpc !== null;
|
||||||
module.exports.isConnected = () => info.rpc !== null;
|
|
||||||
@ -1,16 +1,22 @@
|
|||||||
const prompt = require('custom-electron-prompt');
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
const { clear, connect, registerRefresh, isConnected } = require('./back');
|
import { Electron } from 'playwright';
|
||||||
|
|
||||||
const { setMenuOptions } = require('../../config/plugins');
|
import { clear, connect, isConnected, registerRefresh } from './back';
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
|
||||||
const { singleton } = require('../../providers/decorators');
|
|
||||||
|
|
||||||
const registerRefreshOnce = singleton((refreshMenu) => {
|
import { setMenuOptions } from '../../config/plugins';
|
||||||
|
import promptOptions from '../../providers/prompt-options';
|
||||||
|
import { singleton } from '../../providers/decorators';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
|
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||||
registerRefresh(refreshMenu);
|
registerRefresh(refreshMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = (win, options, refreshMenu) => {
|
const DiscordOptionsObj = config.get('plugins.discord');
|
||||||
|
type DiscordOptions = typeof DiscordOptionsObj;
|
||||||
|
|
||||||
|
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => {
|
||||||
registerRefreshOnce(refreshMenu);
|
registerRefreshOnce(refreshMenu);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -23,7 +29,7 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
label: 'Auto reconnect',
|
label: 'Auto reconnect',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.autoReconnect,
|
checked: options.autoReconnect,
|
||||||
click(item) {
|
click(item: Electron.MenuItem) {
|
||||||
options.autoReconnect = item.checked;
|
options.autoReconnect = item.checked;
|
||||||
setMenuOptions('discord', options);
|
setMenuOptions('discord', options);
|
||||||
},
|
},
|
||||||
@ -36,7 +42,7 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
label: 'Clear activity after timeout',
|
label: 'Clear activity after timeout',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.activityTimoutEnabled,
|
checked: options.activityTimoutEnabled,
|
||||||
click(item) {
|
click(item: Electron.MenuItem) {
|
||||||
options.activityTimoutEnabled = item.checked;
|
options.activityTimoutEnabled = item.checked;
|
||||||
setMenuOptions('discord', options);
|
setMenuOptions('discord', options);
|
||||||
},
|
},
|
||||||
@ -45,7 +51,7 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
label: 'Listen Along',
|
label: 'Listen Along',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.listenAlong,
|
checked: options.listenAlong,
|
||||||
click(item) {
|
click(item: Electron.MenuItem) {
|
||||||
options.listenAlong = item.checked;
|
options.listenAlong = item.checked;
|
||||||
setMenuOptions('discord', options);
|
setMenuOptions('discord', options);
|
||||||
},
|
},
|
||||||
@ -54,7 +60,7 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
label: 'Hide duration left',
|
label: 'Hide duration left',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.hideDurationLeft,
|
checked: options.hideDurationLeft,
|
||||||
click(item) {
|
click(item: Electron.MenuItem) {
|
||||||
options.hideDurationLeft = item.checked;
|
options.hideDurationLeft = item.checked;
|
||||||
setMenuOptions('discord', options);
|
setMenuOptions('discord', options);
|
||||||
},
|
},
|
||||||
@ -66,11 +72,11 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
async function setInactivityTimeout(win, options) {
|
async function setInactivityTimeout(win: Electron.BrowserWindow, options: DiscordOptions) {
|
||||||
const output = await prompt({
|
const output = await prompt({
|
||||||
title: 'Set Inactivity Timeout',
|
title: 'Set Inactivity Timeout',
|
||||||
label: 'Enter inactivity timeout in seconds:',
|
label: 'Enter inactivity timeout in seconds:',
|
||||||
value: Math.round((options.activityTimoutTime ?? 0) / 1e3),
|
value: String(Math.round((options.activityTimoutTime ?? 0) / 1e3)),
|
||||||
type: 'counter',
|
type: 'counter',
|
||||||
counterOptions: { minimum: 0, multiFire: true },
|
counterOptions: { minimum: 0, multiFire: true },
|
||||||
width: 450,
|
width: 450,
|
||||||
@ -78,7 +84,7 @@ async function setInactivityTimeout(win, options) {
|
|||||||
}, win);
|
}, win);
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
options.activityTimoutTime = Math.round(output * 1e3);
|
options.activityTimoutTime = Math.round(~~output * 1e3);
|
||||||
setMenuOptions('discord', options);
|
setMenuOptions('discord', options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,9 @@
|
|||||||
module.exports = (options) => {
|
import config from '../../config';
|
||||||
|
|
||||||
|
const SkipSilencesOptionsObj = config.get('plugins.skip-silences');
|
||||||
|
type SkipSilencesOptions = typeof SkipSilencesOptionsObj;
|
||||||
|
|
||||||
|
export default (options: SkipSilencesOptions) => {
|
||||||
let isSilent = false;
|
let isSilent = false;
|
||||||
let hasAudioStarted = false;
|
let hasAudioStarted = false;
|
||||||
|
|
||||||
@ -6,7 +11,7 @@ module.exports = (options) => {
|
|||||||
const threshold = -100; // DB (-100 = absolute silence, 0 = loudest)
|
const threshold = -100; // DB (-100 = absolute silence, 0 = loudest)
|
||||||
const interval = 2; // Ms
|
const interval = 2; // Ms
|
||||||
const history = 10;
|
const history = 10;
|
||||||
const speakingHistory = Array.from({ length: history }).fill(0);
|
const speakingHistory = Array.from({ length: history }).fill(0) as number[];
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
'audioCanPlay',
|
'audioCanPlay',
|
||||||
@ -53,11 +58,13 @@ module.exports = (options) => {
|
|||||||
if (history == 0 // Silent
|
if (history == 0 // Silent
|
||||||
|
|
||||||
&& !(
|
&& !(
|
||||||
video.paused
|
video && (
|
||||||
|| video.seeking
|
video.paused
|
||||||
|| video.ended
|
|| video.seeking
|
||||||
|| video.muted
|
|| video.ended
|
||||||
|| video.volume === 0
|
|| video.muted
|
||||||
|
|| video.volume === 0
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
isSilent = true;
|
isSilent = true;
|
||||||
@ -66,7 +73,7 @@ module.exports = (options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
speakingHistory.shift();
|
speakingHistory.shift();
|
||||||
speakingHistory.push(0 + (currentVolume > threshold));
|
speakingHistory.push(Number(currentVolume > threshold));
|
||||||
|
|
||||||
looper();
|
looper();
|
||||||
}, interval);
|
}, interval);
|
||||||
@ -79,17 +86,17 @@ module.exports = (options) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSilent && !video.paused) {
|
if (isSilent && video && !video.paused) {
|
||||||
video.currentTime += 0.2; // In s
|
video.currentTime += 0.2; // In s
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
video.addEventListener('play', () => {
|
video?.addEventListener('play', () => {
|
||||||
hasAudioStarted = false;
|
hasAudioStarted = false;
|
||||||
skipSilence();
|
skipSilence();
|
||||||
});
|
});
|
||||||
|
|
||||||
video.addEventListener('seeked', () => {
|
video?.addEventListener('seeked', () => {
|
||||||
hasAudioStarted = false;
|
hasAudioStarted = false;
|
||||||
skipSilence();
|
skipSilence();
|
||||||
});
|
});
|
||||||
@ -100,7 +107,7 @@ module.exports = (options) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMaxVolume(analyser, fftBins) {
|
function getMaxVolume(analyser: AnalyserNode, fftBins: Float32Array) {
|
||||||
let maxVolume = Number.NEGATIVE_INFINITY;
|
let maxVolume = Number.NEGATIVE_INFINITY;
|
||||||
analyser.getFloatFrequencyData(fftBins);
|
analyser.getFloatFrequencyData(fftBins);
|
||||||
|
|
||||||
@ -1,12 +1,19 @@
|
|||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const getSongControls = require('../../providers/song-controls');
|
import { BrowserWindow, nativeImage } from 'electron';
|
||||||
const registerCallback = require('../../providers/song-info');
|
|
||||||
|
|
||||||
let controls;
|
import getSongControls from '../../providers/song-controls';
|
||||||
let currentSongInfo;
|
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||||
|
|
||||||
module.exports = (win) => {
|
|
||||||
|
let controls: {
|
||||||
|
playPause: () => void;
|
||||||
|
next: () => void;
|
||||||
|
previous: () => void;
|
||||||
|
};
|
||||||
|
let currentSongInfo: SongInfo;
|
||||||
|
|
||||||
|
export default (win: BrowserWindow) => {
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
controls = { playPause, next, previous };
|
controls = { playPause, next, previous };
|
||||||
|
|
||||||
@ -23,7 +30,7 @@ module.exports = (win) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function setThumbar(win, songInfo) {
|
function setThumbar(win: BrowserWindow, songInfo: SongInfo) {
|
||||||
// Wait for song to start before setting thumbar
|
// Wait for song to start before setting thumbar
|
||||||
if (!songInfo?.title) {
|
if (!songInfo?.title) {
|
||||||
return;
|
return;
|
||||||
@ -33,28 +40,28 @@ function setThumbar(win, songInfo) {
|
|||||||
win.setThumbarButtons([
|
win.setThumbarButtons([
|
||||||
{
|
{
|
||||||
tooltip: 'Previous',
|
tooltip: 'Previous',
|
||||||
icon: get('previous'),
|
icon: nativeImage.createFromPath(get('previous')),
|
||||||
click() {
|
click() {
|
||||||
controls.previous(win.webContents);
|
controls.previous();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
tooltip: 'Play/Pause',
|
tooltip: 'Play/Pause',
|
||||||
// Update icon based on play state
|
// Update icon based on play state
|
||||||
icon: songInfo.isPaused ? get('play') : get('pause'),
|
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
|
||||||
click() {
|
click() {
|
||||||
controls.playPause(win.webContents);
|
controls.playPause();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
tooltip: 'Next',
|
tooltip: 'Next',
|
||||||
icon: get('next'),
|
icon: nativeImage.createFromPath(get('next')),
|
||||||
click() {
|
click() {
|
||||||
controls.next(win.webContents);
|
controls.next();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util
|
// Util
|
||||||
function get(kind) {
|
function get(kind: string) {
|
||||||
return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
|
return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
|
||||||
}
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
const { TouchBar } = require('electron');
|
import { TouchBar, NativeImage, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
import registerCallback from '../../providers/song-info';
|
||||||
|
import getSongControls from '../../providers/song-controls';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TouchBarButton,
|
TouchBarButton,
|
||||||
@ -8,21 +11,20 @@ const {
|
|||||||
TouchBarScrubber,
|
TouchBarScrubber,
|
||||||
} = TouchBar;
|
} = TouchBar;
|
||||||
|
|
||||||
const registerCallback = require('../../providers/song-info');
|
|
||||||
const getSongControls = require('../../providers/song-controls');
|
|
||||||
|
|
||||||
// Songtitle label
|
// Songtitle label
|
||||||
const songTitle = new TouchBarLabel({
|
const songTitle = new TouchBarLabel({
|
||||||
label: '',
|
label: '',
|
||||||
});
|
});
|
||||||
// This will store the song controls once available
|
// This will store the song controls once available
|
||||||
let controls = [];
|
let controls: (() => void)[] = [];
|
||||||
|
|
||||||
// This will store the song image once available
|
// This will store the song image once available
|
||||||
const songImage = {};
|
const songImage: {
|
||||||
|
icon?: NativeImage;
|
||||||
|
} = {};
|
||||||
|
|
||||||
// Pause/play button
|
// Pause/play button
|
||||||
const pausePlayButton = new TouchBarButton();
|
const pausePlayButton = new TouchBarButton({});
|
||||||
|
|
||||||
// The song control buttons (control functions are in the same order)
|
// The song control buttons (control functions are in the same order)
|
||||||
const buttons = new TouchBarSegmentedControl({
|
const buttons = new TouchBarSegmentedControl({
|
||||||
@ -59,7 +61,7 @@ const touchBar = new TouchBar({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win: BrowserWindow) => {
|
||||||
const { playPause, next, previous, dislike, like } = getSongControls(win);
|
const { playPause, next, previous, dislike, like } = getSongControls(win);
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
@ -79,7 +81,7 @@ module.exports = (win) => {
|
|||||||
// Get image source
|
// Get image source
|
||||||
songImage.icon = songInfo.image
|
songImage.icon = songInfo.image
|
||||||
? songInfo.image.resize({ height: 23 })
|
? songInfo.image.resize({ height: 23 })
|
||||||
: null;
|
: undefined;
|
||||||
|
|
||||||
win.setTouchBar(touchBar);
|
win.setTouchBar(touchBar);
|
||||||
});
|
});
|
||||||
@ -1,13 +1,26 @@
|
|||||||
const { ipcMain, net } = require('electron');
|
import { ipcMain, net, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
const registerCallback = require('../../providers/song-info');
|
import registerCallback from '../../providers/song-info';
|
||||||
|
|
||||||
const secToMilisec = (t) => Math.round(Number(t) * 1e3);
|
const secToMilisec = (t: number) => Math.round(Number(t) * 1e3);
|
||||||
const data = {
|
|
||||||
|
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: '',
|
||||||
cover_url: '',
|
cover_url: '',
|
||||||
title: '',
|
title: '',
|
||||||
artists: [],
|
artists: [] as string[],
|
||||||
status: '',
|
status: '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@ -15,7 +28,7 @@ const data = {
|
|||||||
album: undefined,
|
album: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const post = async (data) => {
|
const post = (data: Data) => {
|
||||||
const port = 1608;
|
const port = 1608;
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -28,13 +41,12 @@ const post = async (data) => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ data }),
|
body: JSON.stringify({ data }),
|
||||||
}).catch((error) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`));
|
}).catch((error: { code: number, errno: number }) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @param {Electron.BrowserWindow} win */
|
module.exports = (win: BrowserWindow) => {
|
||||||
module.exports = async (win) => {
|
|
||||||
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||||
ipcMain.on('timeChanged', async (_, t) => {
|
ipcMain.on('timeChanged', (_, t: number) => {
|
||||||
if (!data.title) {
|
if (!data.title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -50,9 +62,9 @@ module.exports = async (win) => {
|
|||||||
|
|
||||||
data.duration = secToMilisec(songInfo.songDuration);
|
data.duration = secToMilisec(songInfo.songDuration);
|
||||||
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
||||||
data.cover = songInfo.imageSrc;
|
data.cover = songInfo.imageSrc ?? '';
|
||||||
data.cover_url = songInfo.imageSrc;
|
data.cover_url = songInfo.imageSrc ?? '';
|
||||||
data.album_url = songInfo.imageSrc;
|
data.album_url = songInfo.imageSrc ?? '';
|
||||||
data.title = songInfo.title;
|
data.title = songInfo.title;
|
||||||
data.artists = [songInfo.artist];
|
data.artists = [songInfo.artist];
|
||||||
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||||
@ -7,11 +7,12 @@ import { ValueOf } from '../utils/type-utils';
|
|||||||
|
|
||||||
|
|
||||||
// Creates a DOM element from an HTML string
|
// Creates a DOM element from an HTML string
|
||||||
export const ElementFromHtml = (html: string) => {
|
export const ElementFromHtml = (html: string): HTMLElement => {
|
||||||
const template = document.createElement('template');
|
const template = document.createElement('template');
|
||||||
html = html.trim(); // Never return a text node of whitespace as the result
|
html = html.trim(); // Never return a text node of whitespace as the result
|
||||||
template.innerHTML = html;
|
template.innerHTML = html;
|
||||||
return template.content.firstChild;
|
|
||||||
|
return template.content.firstElementChild as HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Creates a DOM element from a HTML file
|
// Creates a DOM element from a HTML file
|
||||||
|
|||||||
@ -88,7 +88,7 @@ const handleData = async (responseText: string, win: Electron.BrowserWindow) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// This variable will be filled with the callbacks once they register
|
// This variable will be filled with the callbacks once they register
|
||||||
type SongInfoCallback = (songInfo: SongInfo, event: string) => void;
|
export type SongInfoCallback = (songInfo: SongInfo, event?: string) => void;
|
||||||
const callbacks: SongInfoCallback[] = [];
|
const callbacks: SongInfoCallback[] = [];
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
|
|||||||
19
reset.d.ts
vendored
19
reset.d.ts
vendored
@ -2,8 +2,14 @@ import '@total-typescript/ts-reset';
|
|||||||
import { YoutubePlayer } from './types/youtube-player';
|
import { YoutubePlayer } from './types/youtube-player';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
interface Compressor {
|
||||||
|
audioSource: MediaElementAudioSourceNode;
|
||||||
|
audioContext: AudioContext;
|
||||||
|
}
|
||||||
|
|
||||||
interface DocumentEventMap {
|
interface DocumentEventMap {
|
||||||
'apiLoaded': CustomEvent<YoutubePlayer>;
|
'apiLoaded': CustomEvent<YoutubePlayer>;
|
||||||
|
'audioCanPlay': CustomEvent<Compressor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -11,5 +17,18 @@ declare global {
|
|||||||
* YouTube Music internal variable (Last interaction time)
|
* YouTube Music internal variable (Last interaction time)
|
||||||
*/
|
*/
|
||||||
_lact: number;
|
_lact: number;
|
||||||
|
navigation: Navigation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// import { Howl as _Howl } from 'howler';
|
||||||
|
declare module 'howler' {
|
||||||
|
interface Howl {
|
||||||
|
_sounds: {
|
||||||
|
_paused: boolean;
|
||||||
|
_ended: boolean;
|
||||||
|
_id: string;
|
||||||
|
_node: HTMLMediaElement;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
|
"lib": ["dom"],
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
@ -140,9 +140,9 @@ export interface YoutubePlayer {
|
|||||||
getPlaylistId: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getPlaylistId: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
loadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
loadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
unloadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
unloadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
setOption: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
setOption: <T>(optionName: string, key: string, value: T) => void;
|
||||||
getOption: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getOption: <T>(optionName: string, key: string) => T | null | undefined;
|
||||||
getOptions: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getOptions: () => string[];
|
||||||
mute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
mute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
unMute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
unMute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
|||||||
Reference in New Issue
Block a user