feat: add support i18n (#1468)

This commit is contained in:
JellyBrick
2023-12-01 01:30:46 +09:00
committed by GitHub
parent 7f71c36dc0
commit 7401cf69ad
65 changed files with 1226 additions and 303 deletions

View File

@ -155,6 +155,7 @@
"filenamify": "6.0.0",
"howler": "2.2.4",
"html-to-text": "9.0.5",
"i18next": "23.7.7",
"keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.11",

9
pnpm-lock.yaml generated
View File

@ -97,6 +97,9 @@ dependencies:
html-to-text:
specifier: 9.0.5
version: 9.0.5
i18next:
specifier: 23.7.7
version: 23.7.7
keyboardevent-from-electron-accelerator:
specifier: 2.0.0
version: 2.0.0
@ -3856,6 +3859,12 @@ packages:
engines: {node: '>=14.18.0'}
dev: true
/i18next@23.7.7:
resolution: {integrity: sha512-peTvdT+Lma+o0LfLFD7IC2M37N9DJ04dH0IJYOyOHRhDfLo6nK36v7LkrQH35C2l8NHiiXZqGirhKESlEb/5PA==}
dependencies:
'@babel/runtime': 7.23.2
dev: false
/iconv-corefoundation@1.1.7:
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
engines: {node: ^8.11.2 || >=10}

View File

@ -14,6 +14,7 @@ export interface DefaultConfig {
'window-position': WindowPositionConfig;
url: string;
options: {
language?: string;
tray: boolean;
appVisible: boolean;
autoUpdates: boolean;

18
src/i18n/index.ts Normal file
View File

@ -0,0 +1,18 @@
import i18next, { init, t as i18t, changeLanguage } from 'i18next';
import { languageResources } from '@/i18n/resources';
export const loadI18n = async () =>
await init({
resources: languageResources,
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false
}
});
export const setLanguage = async (language: string) => await changeLanguage(language);
export const t = i18t.bind(i18next);

572
src/i18n/resources/en.json Normal file
View File

@ -0,0 +1,572 @@
{
"language": {
"name": "English",
"local-name": "English",
"code": "en"
},
"plugins": {
"adblocker": {
"name": "Adblocker",
"description": "Block all ads and tracking out of the box",
"menu": {
"blocker": "Blocker"
}
},
"album-color-theme": {
"name": "Album Color Theme",
"description": "Applies a dynamic theme and visual effects based on the album color palette"
},
"ambient-mode": {
"name": "Ambient Mode",
"description": "Applies a lighting effect by casting gentle colors from the video, into your screens background.",
"menu": {
"smoothness-transition": {
"label": "Smoothness transition",
"submenu": {
"during": "During {{interpolationTime}}s"
}
},
"quality": {
"label": "Quality",
"submenu": {
"pixels": "{{quality}} pixels"
}
},
"size": {
"label": "Size",
"submenu": {
"percent": "{{size}}%"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacity",
"submenu": {
"percent": "{{opacity}}%"
}
},
"blur-amount": {
"label": "Blur amount",
"submenu": {
"pixels": "{{blurAmount}} pixels"
}
},
"use-fullscreen": {
"label": "Using fullscreen"
}
}
},
"audio-compressor": {
"name": "Audio Compressor",
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)"
},
"blur-nav-bar": {
"name": "Blur Navigation Bar",
"description": "Makes navigation bar transparent and blurry"
},
"bypass-age-restrictions": {
"name": "Bypass Age Restrictions",
"description": "Bypass YouTube's age verification"
},
"captions-selector": {
"name": "Captions Selector",
"description": "Caption selector for YouTube Music audio tracks",
"menu": {
"autoload": "Automatically select last used caption",
"disable-captions": "No captions by default"
},
"templates": {
"title": "Open captions selector"
},
"prompt": {
"selector": {
"title": "Select caption language",
"label": "Current caption language: {{language}}",
"none": "None"
}
}
},
"compact-sidebar": {
"name": "Compact Sidebar",
"description": "Always set the sidebar in compact mode"
},
"crossfade": {
"name": "Crossfade [beta]",
"description": "Crossfade between songs",
"menu": {
"advanced": "Advanced"
},
"prompt": {
"options": {
"title": "Crossfade options",
"multi-input": {
"fade-in-duration": "Fade in duration (milliseconds)",
"fade-out-duration": "Fade out duration (milliseconds)",
"seconds-before-end": "Crossfade N seconds before end",
"fade-scaling": {
"label": "Fade scaling",
"linear": "Linear",
"logarithmic": "Logarithmic"
}
}
}
}
},
"disable-autoplay": {
"name": "Disable Autoplay",
"description": "Makes song start in \"paused\" mode",
"menu": {
"apply-once": "Applies only on startup"
}
},
"discord": {
"name": "Discord Rich Presence",
"description": "Show your friends what you listen to with Rich Presence",
"menu": {
"auto-reconnect": "Auto reconnect",
"clear-activity": "Clear activity",
"clear-activity-after-timeout": "Clear activity after timeout",
"play-on-youtube-music": "Play on YouTube Music",
"hide-github-button": "Hide GitHub link Button",
"hide-duration-left": "Hide duration left",
"set-inactivity-timeout": "Set inactivity timeout"
},
"prompt": {
"set-inactivity-timeout": {
"title": "Set inactivity timeout",
"label": "Enter inactivity timeout in seconds:"
}
},
"backend": {
"connected": "Connected to Discord",
"disconnected": "Disconnected from Discord",
"already-connected": "Attempted to connect with active connection"
}
},
"downloader": {
"name": "Downloader",
"description": "Downloads MP3 / source audio directly from the interface",
"menu": {
"choose-download-folder": "Choose download folder",
"presets": "Presets",
"skip-existing": "Skip existing files"
},
"templates": {
"button": "Download"
},
"backend": {
"dialog": {
"error": {
"title": "Error in download!",
"message": "Argh! Apologies, download failed…",
"buttons": {
"ok": "OK"
}
},
"start-download-playlist": {
"title": "Download started",
"message": "Downloading Playlist {{playlistTitle}}",
"detail": "({{playlistSize}} songs)",
"buttons": {
"ok": "OK"
}
}
},
"feedback": {
"downloading": "Downloading…",
"download-progress": "Download: {{percent}}%",
"loading": "Loading…",
"preparing-file": "Preparing file…",
"converting": "Converting…",
"conversion-progress": "Conversion: {{percent}}%",
"saving": "Saving…",
"writing-id3": "Writing ID3 tags…",
"playlist-id-not-found": "No playlist ID found",
"video-id-not-found": "Video not found",
"download-info": "Downloading {{artist}} - {{title}} [{{videoId}}",
"done": "Done: {{filePath}}",
"trying-to-get-playlist-id": "Trying to get playlist ID: {{playlistId}}",
"getting-playlist-info": "Getting playlist info…",
"playlist-is-mix-or-private": "Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n{{error}}",
"playlist-is-empty": "Playlist is empty",
"playlist-has-only-one-song": "Playlist has only one item, downloading it directly",
"folder-already-exists": "The folder {{playlistFolder}} already exists",
"downloading-playlist": "Downloading playlist \"{{playlistTitle}}\" - {{playlistSize}} songs ({{playlistId}})",
"downloading-counter": "Downloading {{current}}/{{total}}…",
"error-while-downloading": "Error downloading \"{{author}} - {{title}}\": {{error}}"
}
},
"renderer": {
"can-not-update-progress": "Cannot update progress"
}
},
"exponential-volume": {
"name": "Exponential Volume",
"description": "Makes the volume slider exponential so it's easier to select lower volumes."
},
"in-app-menu": {
"name": "In-App Menu",
"description": "Gives menu-bars a fancy, dark or album-color look",
"menu": {
"hide-dom-window-controls": "Hide DOM window controls"
}
},
"last-fm": {
"name": "Last.fm",
"description": "Add scrobbling support for Last.fm"
},
"lumiastream": {
"name": "Lumia Stream [beta]",
"description": "Adds Lumia Stream support"
},
"lyrics-genius": {
"name": "Lyrics Genius",
"description": "Adds lyrics support for most songs",
"menu": {
"romanized-lyrics": "Romanized Lyrics"
},
"renderer": {
"fetched-lyrics": "Fetched lyrics for Genius"
}
},
"navigation": {
"name": "Navigation",
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser"
},
"no-google-login": {
"name": "No Google Login",
"description": "Remove Google login buttons and links from the interface"
},
"notifications": {
"name": "Notifications",
"description": "Display a notification when a song starts playing (interactive notifications are available on Windows)",
"menu": {
"priority": "Notification Priority",
"interactive": "Interactive Notifications",
"interactive-settings": {
"label": "Interactive Settings",
"submenu": {
"tray-controls": "Open/Close on tray click",
"hide-button-text": "Hide button text",
"refresh-on-play-pause": "Refresh on Play/Pause"
}
},
"toast-style": "Toast style",
"unpause-notification": "Show notification on unpause"
}
},
"picture-in-picture": {
"name": "Picture in Picture",
"description": "Allows to switch the app to picture-in-picture mode",
"menu": {
"always-on-top": "Always on top",
"save-window-position": "Save window position",
"save-window-size": "Save window size",
"hotkey": {
"label": "Hotkey",
"prompt": {
"title": "Picture in Picture Hotkey",
"label": "Choose a hotkey for toggle Picture in Picture",
"keybind-options": {
"hotkey": "Hotkey"
}
}
},
"use-native-pip": "Use browser native PiP"
},
"templates": {
"button": "Picture in Picture"
}
},
"playback-speed": {
"name": "Playback Speed",
"description": "Listen fast, listen slow! Adds a slider that controls song speed",
"templates": {
"button": "Speed"
}
},
"precise-volume": {
"name": "Precise Volume",
"description": "Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps",
"menu": {
"arrows-shortcuts": "Local Arrow-keys Controls",
"global-shortcuts": "Global Hotkeys",
"custom-volume-steps": "Set Custom Volume Steps"
},
"prompt": {
"volume-steps": {
"title": "Volume Steps",
"label": "Choose Volume Increase/Decrease Steps"
},
"global-shortcuts": {
"title": "Global Volume Keybinds",
"label": "Choose Global Volume Keybinds:",
"keybind-options": {
"increase": "Increase Volume",
"decrease": "Decrease Volume"
}
}
}
},
"quality-changer": {
"name": "Video Quality Changer",
"description": "Allows changing the video quality with a button on the video overlay",
"backend": {
"dialog": {
"quality-changer": {
"title": "Choose Video Quality",
"message": "Choose Video Quality:",
"detail": "Current Quality: {{quality}}"
}
}
}
},
"shortcuts": {
"name": "Shortcuts (& MPRIS)",
"description": "Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users",
"prompt": {
"keybind": {
"title": "Global Keybinds",
"label": "Choose Global Keybinds for Songs Control:",
"keybind-options": {
"play-pause": "Play / Pause",
"next": "Next",
"previous": "Previous"
}
}
},
"menu": {
"set-keybinds": "Set Global Song Controls",
"override-media-keys": "Override Media Keys"
}
},
"skip-silences": {
"name": "Skip Silences",
"description": "Automatically skip silences sections in songs"
},
"sponsorblock": {
"name": "SponsorBlock",
"description": "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing"
},
"taskbar-mediacontrol": {
"name": "Taskbar Media Control",
"description": "Control playback from your Windows taskbar"
},
"touchbar": {
"name": "TouchBar",
"description": "Adds a TouchBar widget for macOS users"
},
"tuna-obs": {
"name": "Tuna OBS",
"description": "Integration with OBS's plugin Tuna"
},
"video-toggle": {
"name": "Video Toggle",
"description": "Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab",
"menu": {
"mode": {
"label": "Mode",
"submenu": {
"custom": "Custom toggle",
"native": "Native toggle",
"disabled": "Disabled"
}
},
"align": {
"label": "Alignment",
"submenu": {
"left": "Left",
"middle": "Middle",
"right": "Right"
}
},
"force-hide": "Force remove video tab"
},
"templates": {
"button": "Song"
}
},
"visualizer": {
"name": "Visualizer",
"description": "Adds a visualizer to the player",
"menu": {
"visualizer-type": "Visualizer Type"
}
}
},
"main": {
"dialog": {
"need-to-restart": {
"title": "Restart Required",
"message": "\"{{pluginName}}\" needs to restart",
"detail": "\"{{pluginName}}\" plugin requires a restart to take effect",
"buttons": {
"restart-now": "Restart Now",
"later": "Later"
}
},
"update-available": {
"title": "Update Available",
"message": "A new version is available",
"detail": "A new version is available and can be downloaded at {{downloadLink}}",
"buttons": {
"ok": "OK",
"download": "Download",
"disable": "Disable Updates"
}
},
"hide-menu-enabled": {
"title": "Hide Menu Enabled",
"message": "Hide Menu is enabled",
"detail": "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
},
"unresponsive": {
"title": "Window Unresponsive",
"message": "The Application is Unresponsive",
"detail": "We are sorry for the inconvenience! please choose what to do:",
"buttons": {
"wait": "Wait",
"relaunch": "Relaunch",
"quit": "Quit"
}
}
},
"console": {
"i18n": {
"loaded": "i18n loaded"
},
"theme": {
"css-file-not-found": "CSS file \"{{cssFile}}\" does not exist, ignoring"
},
"window": {
"tried-to-render-offscreen": "Window tried to render offscreen, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
},
"when-ready": {
"clearing-cache-after-20s": "Clearing app cache"
},
"second-instance": {
"receive-command": "Received command over protocol: \"{{command}}\""
},
"unresponsive": {
"details": "Unresponsive Error!\n{{error}}"
},
"did-finish-load": {
"dev-tools": "did finish load. dev tools opened"
}
},
"menu": {
"plugins": {
"label": "Plugins",
"enabled": "Enabled"
},
"options": {
"label": "Options",
"submenu": {
"auto-update": "Auto Update",
"resume-on-start": "Resume last song when app starts",
"starting-page": {
"label": "Starting page",
"unset": "Unset"
},
"visual-tweaks": {
"label": "Visual Tweaks",
"submenu": {
"remove-upgrade-button": "Remove upgrade button",
"like-buttons": {
"label": "Like buttons",
"default": "Default",
"force-show": "Force show",
"hide": "Hide"
},
"theme": {
"label": "Theme",
"submenu": {
"no-theme": "No theme",
"import-css-file": "Import custom CSS file"
}
}
}
},
"single-instance-lock": "Single Instance Lock",
"always-on-top": "Always on top",
"hide-menu": {
"label": "Hide Menu",
"dialog": {
"title": "Hide Menu Enabled",
"message": "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)"
}
},
"start-at-login": "Start at login",
"tray": {
"label": "Tray",
"submenu": {
"disabled": "Disabled",
"enabled-and-show-app": "Enabled and show app",
"enabled-and-hide-app": "Enabled and hide app",
"play-pause-on-click": "Play/Pause on click"
}
},
"language": {
"label": "Language",
"dialog": {
"title": "Language Changed",
"message": "Language will be changed after restart"
}
},
"advanced-options": {
"label": "Advanced options",
"submenu": {
"set-proxy": {
"label": "Set proxy",
"prompt": {
"title": "Set proxy",
"label": "Enter Proxy Address: (leave empty to disable)",
"placeholder": "Example: socks5://127.0.0.1:9999"
}
},
"override-user-agent": "Override User-Agent",
"disable-hardware-acceleration": "Disable hardware acceleration",
"restart-on-config-changes": "Restart on config changes",
"auto-reset-app-cache": "Reset App cache when app starts",
"toggle-dev-tools": "Toggle DevTools",
"edit-config-json": "Edit config.json"
}
}
}
},
"view": {
"label": "View"
},
"navigation": {
"label": "Navigation",
"submenu": {
"go-back": "Go back",
"go-forward": "Go forward",
"copy-current-url": "Copy current URL",
"restart": "Restart App"
}
},
"about": "About"
}
},
"common": {
"console": {
"plugins": {
"load-all": "Loading all plugins",
"unloaded": "Plugin \"{{pluginName}}\" unloaded",
"unload-failed": "Failed to unload plugin \"{{pluginName}}\"",
"load-failed": "Failed to load plugin \"{{pluginName}}\"",
"initialize-failed": "Failed to initialize plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" loaded",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executed at {{ms}}ms",
"execute-failed": "Failed to execute plugin {{pluginName}}::{{contextName}}"
}
}
}
}

View File

@ -0,0 +1,11 @@
import enJson from './en.json';
import koJson from './ko.json';
export const languageResources = {
en: {
translation: enJson
},
ko: {
translation: koJson
}
};

View File

@ -0,0 +1,7 @@
{
"language": {
"name": "Korean",
"local-name": "한국어",
"code": "ko"
}
}

View File

@ -1,6 +1,7 @@
import path from 'node:path';
import url from 'node:url';
import fs from 'node:fs';
import process from 'node:process';
import {
BrowserWindow,
@ -49,6 +50,9 @@ import {
} from '@/loader/main';
import { LoggerPrefix } from '@/utils';
import { loadI18n, setLanguage, t } from '@/i18n';
import { languageResources } from '@/i18n/resources';
import type { PluginConfig } from '@/types/plugins';
@ -183,10 +187,17 @@ const showNeedToRestartDialog = (id: string) => {
const dialogOptions: Electron.MessageBoxOptions = {
type: 'info',
buttons: ['Restart Now', 'Later'],
title: 'Restart Required',
message: `"${plugin?.name ?? id}" needs to restart`,
detail: `"${plugin?.name ?? id}" plugin requires a restart to take effect`,
buttons: [
t('main.dialog.need-to-restart.buttons.restart-now'),
t('main.dialog.need-to-restart.buttons.later'),
],
title: t('main.dialog.need-to-restart.title'),
message: t('main.dialog.need-to-restart.message', {
pluginName: plugin?.name ?? id,
}),
detail: t('main.dialog.need-to-restart.detail', {
pluginName: plugin?.name ?? id,
}),
defaultId: 0,
cancelId: 1,
};
@ -227,7 +238,7 @@ function initTheme(win: BrowserWindow) {
() => {
console.warn(
LoggerPrefix,
`CSS file "${cssFile}" does not exist, ignoring`,
t('main.console.theme.css-file-not-found', { cssFile }),
);
},
);
@ -236,7 +247,7 @@ function initTheme(win: BrowserWindow) {
win.webContents.once('did-finish-load', () => {
if (is.dev()) {
console.log(LoggerPrefix, 'did finish load');
console.debug(LoggerPrefix, t('main.console.did-finish-load.dev-tools'));
win.webContents.openDevTools();
}
});
@ -305,13 +316,13 @@ async function createMainWindow() {
) {
// Window is offscreen
if (is.dev()) {
console.log(
`Window tried to render offscreen, windowSize=${String(
winSize,
)}, displaySize=${String(display.bounds)}, position=${String(
windowPosition,
)}`,
);
console.warn(
LoggerPrefix,
t('main.console.window.tried-to-render-offscreen', {
winSize: String(winSize),
displaySize: String(display.bounds),
windowPosition: String(windowPosition),
}));
}
} else {
win.setSize(scaledWidth, scaledHeight);
@ -543,12 +554,27 @@ app.on('activate', async () => {
}
});
const getDefaultLocale = (locale: string) =>
Object.keys(languageResources).includes(locale) ? locale : 'en';
app.whenReady().then(async () => {
if (!config.get('options.language')) {
config.set('options.language', getDefaultLocale(app.getLocale()));
}
await loadI18n().then(async () => {
await setLanguage(config.get('options.language') ?? 'en');
console.log(LoggerPrefix, t('main.console.i18n.loaded'));
});
if (config.get('options.autoResetAppCache')) {
// Clear cache after 20s
const clearCacheTimeout = setTimeout(() => {
if (is.dev()) {
console.log('Clearing app cache.');
console.log(
LoggerPrefix,
t('main.console.when-ready.clearing-cache-after-20s'),
);
}
session.defaultSession.clearCache();
@ -614,7 +640,7 @@ app.whenReady().then(async () => {
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
const command = protocolArgv.slice(uri.length, lastIndex);
if (is.dev()) {
console.debug(`Received command over protocol: "${command}"`);
console.debug(LoggerPrefix, t('main.console.second-instance.receive-command', { command }));
}
handleProtocol(command);
@ -651,10 +677,14 @@ app.whenReady().then(async () => {
'https://github.com/th-ch/youtube-music/releases/latest';
const dialogOptions: Electron.MessageBoxOptions = {
type: 'info',
buttons: ['OK', 'Download', 'Disable updates'],
title: 'Application Update',
message: 'A new version is available',
detail: `A new version is available and can be downloaded at ${downloadLink}`,
buttons: [
t('main.dialog.update-available.buttons.download'),
t('main.dialog.update-available.buttons.later'),
t('main.dialog.update-available.buttons.disable'),
],
title: t('main.dialog.update-available.title'),
message: t('main.dialog.update-available.message'),
detail: t('main.dialog.update-available.detail', { downloadLink }),
};
let dialogPromise: Promise<Electron.MessageBoxReturnValue>;
@ -689,9 +719,8 @@ app.whenReady().then(async () => {
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Hide Menu Enabled',
message:
"Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
title: t('main.dialog.hide-menu-enabled.title'),
message: t('main.dialog.hide-menu-enabled.message'),
});
config.set('options.hideMenuWarned', true);
}
@ -722,16 +751,25 @@ function showUnresponsiveDialog(
details: Electron.RenderProcessGoneDetails,
) {
if (details) {
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
console.error(
LoggerPrefix,
t('main.console.unresponsive.details', {
error: JSON.stringify(details, null, '\t'),
}),
);
}
dialog
.showMessageBox(win, {
type: 'error',
title: 'Window Unresponsive',
message: 'The Application is Unresponsive',
detail: 'We are sorry for the inconvenience! please choose what to do:',
buttons: ['Wait', 'Relaunch', 'Quit'],
title: t('main.dialog.unresponsive.title'),
message: t('main.dialog.unresponsive.message'),
detail: t('main.dialog.unresponsive.detail'),
buttons: [
t('main.dialog.unresponsive.buttons.wait'),
t('main.dialog.unresponsive.buttons.relaunch'),
t('main.dialog.unresponsive.buttons.quit'),
],
cancelId: 0,
})
.then((result) => {

View File

@ -6,6 +6,8 @@ import { allPlugins, mainPlugins } from 'virtual:plugins';
import config from '@/config';
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import { t } from '@/i18n';
import type { PluginConfig, PluginDef } from '@/types/plugins';
import type { BackendContext } from '@/types/contexts';
@ -67,14 +69,23 @@ export const forceUnloadMainPlugin = async (
plugin.backend)
) {
delete loadedPluginMap[id];
console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
console.log(LoggerPrefix, t(
'common.console.plugins.unloaded',
{ pluginName: id },
));
return;
} else {
console.log(LoggerPrefix, `Cannot unload "${id}" plugin`);
console.log(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
return Promise.reject();
}
} catch (err) {
console.error(LoggerPrefix, `Cannot unload "${id}" plugin`);
console.error(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
console.trace(err);
return Promise.reject(err);
}
@ -100,18 +111,24 @@ export const forceLoadMainPlugin = async (
) {
loadedPluginMap[id] = plugin;
} else {
console.log(LoggerPrefix, `Cannot load "${id}" plugin`);
console.log(
LoggerPrefix,
t('common.console.plugins.load-failed', { pluginName: id }),
);
return Promise.reject();
}
} catch (err) {
console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
console.error(
LoggerPrefix,
t('common.console.plugins.initialize-failed', { pluginName: id }),
);
console.trace(err);
return Promise.reject(err);
}
};
export const loadAllMainPlugins = async (win: BrowserWindow) => {
console.log(LoggerPrefix, 'Loading all plugins');
console.log(LoggerPrefix, t('common.console.plugins.load-all'));
const pluginConfigs = config.plugins.getPlugins();
const queue: Promise<void>[] = [];

View File

@ -6,6 +6,8 @@ import { setApplicationMenu } from '@/menu';
import { LoggerPrefix } from '@/utils';
import { t } from '@/i18n';
import type { MenuContext } from '@/types/contexts';
import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
import type { PluginConfig } from '@/types/plugins';
@ -48,9 +50,15 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
}
} else return;
console.log(LoggerPrefix, `Successfully loaded '${id}::menu'`);
console.log(
LoggerPrefix,
t('common.console.plugins.loaded', { pluginName: `${id}::menu` })
);
} catch (err) {
console.error(LoggerPrefix, `Cannot initialize '${id}::menu': `);
console.error(
LoggerPrefix,
t('common.console.plugins.initialize-failed', { pluginName: `${id}::menu` }),
);
console.trace(err);
}
};

View File

@ -5,6 +5,8 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import config from '@/config';
import { t } from '@/i18n';
import type { PreloadContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins';
@ -31,10 +33,13 @@ export const forceUnloadPreloadPlugin = async (id: string) => {
context: createContext(id),
});
if (hasStopped || (hasStopped === null && loadedPluginMap[id].preload)) {
console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
console.log(
LoggerPrefix,
t('common.console.plugins.unloaded', { pluginName: id }),
);
delete loadedPluginMap[id];
} else {
console.error(LoggerPrefix, `Cannot stop "${id}" plugin`);
console.error(LoggerPrefix, t('common.console.plugins.unload-failed', { pluginName: id }));
}
};
@ -57,9 +62,9 @@ export const forceLoadPreloadPlugin = async (id: string) => {
loadedPluginMap[id] = plugin;
}
console.log(LoggerPrefix, `"${id}" plugin is loaded`);
console.log(LoggerPrefix, t('common.console.plugins.loaded', { pluginName: id }));
} catch (err) {
console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
console.error(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id }));
console.trace(err);
}
};

View File

@ -6,6 +6,7 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import type { RendererContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins';
import { t } from '@/i18n';
const unregisterStyleMap: Record<string, (() => void)[]> = {};
const loadedPluginMap: Record<
@ -54,9 +55,9 @@ export const forceUnloadRendererPlugin = async (id: string) => {
document.querySelector(`style#plugin-${id}`)?.remove();
}
if (hasStopped || (hasStopped === null && plugin?.renderer)) {
console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
console.log(LoggerPrefix, t('common.console.plugins.unloaded', { pluginName: id }));
} else {
console.error(LoggerPrefix, `Cannot stop "${id}" plugin`);
console.error(LoggerPrefix, t('common.console.plugins.unload-failed', { pluginName: id }));
}
};
@ -92,9 +93,9 @@ export const forceLoadRendererPlugin = async (id: string) => {
];
}
console.log(LoggerPrefix, `"${id}" plugin is loaded`);
console.log(LoggerPrefix, t('common.console.plugins.loaded', { pluginName: id }));
} else {
console.log(LoggerPrefix, `Cannot initialize "${id}" plugin`);
console.log(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id }));
}
};

View File

@ -18,6 +18,8 @@ import { startingPages } from './providers/extracted-data';
import promptOptions from './providers/prompt-options';
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
import { setLanguage, t } from '@/i18n';
import { languageResources } from '@/i18n/resources';
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
@ -76,7 +78,7 @@ export const mainMenuTemplate = async (
{
label: pluginLabel,
submenu: [
pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu),
pluginEnabledMenu(id, t('main.menu.plugins.enabled'), true, innerRefreshMenu),
{ type: 'separator' },
...template,
],
@ -102,16 +104,18 @@ export const mainMenuTemplate = async (
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
});
const availableLanguages = Object.keys(languageResources) as unknown as (keyof typeof languageResources)[];
return [
{
label: 'Plugins',
label: t('main.menu.plugins.label'),
submenu: pluginMenus,
},
{
label: 'Options',
label: t('main.menu.options.label'),
submenu: [
{
label: 'Auto-update',
label: t('main.menu.options.submenu.auto-update'),
type: 'checkbox',
checked: config.get('options.autoUpdates'),
click(item: MenuItem) {
@ -119,7 +123,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Resume last song when app starts',
label: t('main.menu.options.submenu.resume-on-start'),
type: 'checkbox',
checked: config.get('options.resumeOnStart'),
click(item: MenuItem) {
@ -127,7 +131,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Starting page',
label: t('main.menu.options.submenu.starting-page.label'),
submenu: (() => {
const subMenuArray: Electron.MenuItemConstructorOptions[] =
Object.keys(startingPages).map((name) => ({
@ -139,7 +143,7 @@ export const mainMenuTemplate = async (
},
}));
subMenuArray.unshift({
label: 'Unset',
label: t('main.menu.options.submenu.starting-page.unset'),
type: 'radio',
checked: config.get('options.startingPage') === '',
click() {
@ -150,10 +154,10 @@ export const mainMenuTemplate = async (
})(),
},
{
label: 'Visual Tweaks',
label: t('main.menu.options.submenu.visual-tweaks.label'),
submenu: [
{
label: 'Remove upgrade button',
label: t('main.menu.options.submenu.visual-tweaks.submenu.remove-upgrade-button'),
type: 'checkbox',
checked: config.get('options.removeUpgradeButton'),
click(item: MenuItem) {
@ -164,10 +168,10 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Like buttons',
label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.label'),
submenu: [
{
label: 'Default',
label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.default'),
type: 'radio',
checked: !config.get('options.likeButtons'),
click() {
@ -175,7 +179,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Force show',
label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.force-show'),
type: 'radio',
checked: config.get('options.likeButtons') === 'force',
click() {
@ -183,7 +187,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Hide',
label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.hide'),
type: 'radio',
checked: config.get('options.likeButtons') === 'hide',
click() {
@ -193,10 +197,10 @@ export const mainMenuTemplate = async (
],
},
{
label: 'Theme',
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.label'),
submenu: [
{
label: 'No theme',
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme'),
type: 'radio',
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
click() {
@ -205,7 +209,7 @@ export const mainMenuTemplate = async (
},
{ type: 'separator' },
{
label: 'Import custom CSS file',
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.import-css-file'),
type: 'normal',
async click() {
const { filePaths } = await dialog.showOpenDialog({
@ -222,7 +226,7 @@ export const mainMenuTemplate = async (
],
},
{
label: 'Single instance lock',
label: t('main.menu.options.submenu.single-instance-lock'),
type: 'checkbox',
checked: true,
click(item: MenuItem) {
@ -234,7 +238,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Always on top',
label: t('main.menu.options.submenu.always-on-top'),
type: 'checkbox',
checked: config.get('options.alwaysOnTop'),
click(item: MenuItem) {
@ -245,7 +249,7 @@ export const mainMenuTemplate = async (
...((is.windows() || is.linux()
? [
{
label: 'Hide menu',
label: t('main.menu.options.submenu.hide-menu.label'),
type: 'checkbox',
checked: config.get('options.hideMenu'),
click(item) {
@ -253,9 +257,8 @@ export const mainMenuTemplate = async (
if (item.checked && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(win, {
type: 'info',
title: 'Hide Menu Enabled',
message:
'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
title: t('main.menu.options.submenu.hide-menu.dialog.title'),
message: t('main.menu.options.submenu.hide-menu.dialog.message'),
});
}
},
@ -267,7 +270,7 @@ export const mainMenuTemplate = async (
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[
{
label: 'Start at login',
label: t('main.menu.options.submenu.start-at-login'),
type: 'checkbox',
checked: config.get('options.startAtLogin'),
click(item) {
@ -277,10 +280,10 @@ export const mainMenuTemplate = async (
]
: []) satisfies Electron.MenuItemConstructorOptions[]),
{
label: 'Tray',
label: t('main.menu.options.submenu.tray.label'),
submenu: [
{
label: 'Disabled',
label: t('main.menu.options.submenu.tray.submenu.disabled'),
type: 'radio',
checked: !config.get('options.tray'),
click() {
@ -289,7 +292,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Enabled + app visible',
label: t('main.menu.options.submenu.tray.submenu.enabled-and-show-app'),
type: 'radio',
checked:
config.get('options.tray') && config.get('options.appVisible'),
@ -299,7 +302,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Enabled + app hidden',
label: t('main.menu.options.submenu.tray.submenu.enabled-and-hide-app'),
type: 'radio',
checked:
config.get('options.tray') && !config.get('options.appVisible'),
@ -310,7 +313,7 @@ export const mainMenuTemplate = async (
},
{ type: 'separator' },
{
label: 'Play/Pause on click',
label: t('main.menu.options.submenu.tray.submenu.play-pause-on-click'),
type: 'checkbox',
checked: config.get('options.trayClickPlayPause'),
click(item: MenuItem) {
@ -322,19 +325,39 @@ export const mainMenuTemplate = async (
},
],
},
{
label: t('main.menu.options.submenu.language.label'),
submenu: availableLanguages.map((lang): Electron.MenuItemConstructorOptions => ({
label: `${languageResources[lang].translation.language.name} (${languageResources[lang].translation.language['local-name']})`,
type: 'checkbox',
checked: config.get('options.language') === lang,
click() {
config.setMenuOption('options.language', lang);
refreshMenu(win);
setLanguage(lang);
dialog.showMessageBox(
win,
{
title: t('main.menu.options.submenu.language.dialog.title'),
message: t('main.menu.options.submenu.language.dialog.message'),
}
);
},
})),
},
{ type: 'separator' },
{
label: 'Advanced options',
label: t('main.menu.options.submenu.advanced-options.label'),
submenu: [
{
label: 'Set Proxy',
label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.label'),
type: 'normal',
async click(item: MenuItem) {
await setProxy(item, win);
},
},
{
label: 'Override useragent',
label: t('main.menu.options.submenu.advanced-options.submenu.override-user-agent'),
type: 'checkbox',
checked: config.get('options.overrideUserAgent'),
click(item: MenuItem) {
@ -342,7 +365,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Disable hardware acceleration',
label: t('main.menu.options.submenu.advanced-options.submenu.disable-hardware-acceleration'),
type: 'checkbox',
checked: config.get('options.disableHardwareAcceleration'),
click(item: MenuItem) {
@ -353,7 +376,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Restart on config changes',
label: t('main.menu.options.submenu.advanced-options.submenu.restart-on-config-changes'),
type: 'checkbox',
checked: config.get('options.restartOnConfigChanges'),
click(item: MenuItem) {
@ -364,7 +387,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Reset App cache when app starts',
label: t('main.menu.options.submenu.advanced-options.submenu.auto-reset-app-cache'),
type: 'checkbox',
checked: config.get('options.autoResetAppCache'),
click(item: MenuItem) {
@ -374,7 +397,7 @@ export const mainMenuTemplate = async (
{ type: 'separator' },
is.macOS()
? {
label: 'Toggle DevTools',
label: t('main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools'),
// Cannot use "toggleDevTools" role in macOS
click() {
const { webContents } = win;
@ -387,7 +410,7 @@ export const mainMenuTemplate = async (
}
: { role: 'toggleDevTools' },
{
label: 'Edit config.json',
label: t('main.menu.options.submenu.advanced-options.submenu.edit-config-json'),
click() {
config.edit();
},
@ -397,7 +420,7 @@ export const mainMenuTemplate = async (
],
},
{
label: 'View',
label: t('main.menu.view.label'),
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
@ -416,10 +439,10 @@ export const mainMenuTemplate = async (
],
},
{
label: 'Navigation',
label: t('main.menu.navigation.label'),
submenu: [
{
label: 'Go back',
label: t('main.menu.navigation.submenu.go-back'),
click() {
if (win.webContents.canGoBack()) {
win.webContents.goBack();
@ -427,7 +450,7 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Go forward',
label: t('main.menu.navigation.submenu.go-forward'),
click() {
if (win.webContents.canGoForward()) {
win.webContents.goForward();
@ -435,21 +458,21 @@ export const mainMenuTemplate = async (
},
},
{
label: 'Copy current URL',
label: t('main.menu.navigation.submenu.copy-current-url'),
click() {
const currentURL = win.webContents.getURL();
clipboard.writeText(currentURL);
},
},
{
label: 'Restart App',
label: t('main.menu.navigation.submenu.restart'),
click: restart,
},
{ role: 'quit' },
],
},
{
label: 'About',
label: t('main.menu.about'),
submenu: [{ role: 'about' }],
},
];
@ -486,13 +509,13 @@ export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
const output = await prompt(
{
title: 'Set Proxy',
label: 'Enter Proxy Address: (leave empty to disable)',
title: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.title'),
label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.label'),
value: config.get('options.proxy'),
type: 'input',
inputAttrs: {
type: 'url',
placeholder: "Example: 'socks5://127.0.0.1:9999",
placeholder: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.placeholder'),
},
width: 450,
...promptOptions(),

View File

@ -9,6 +9,8 @@ import {
import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject';
import { t } from '@/i18n';
import type { BrowserWindow } from 'electron';
interface AdblockerConfig {
@ -41,8 +43,8 @@ interface AdblockerConfig {
}
export default createPlugin({
name: 'Adblocker',
description: 'Block all ads and tracking out of the box',
name: t('plugins.adblocker.name'),
description: t('plugins.adblocker.description'),
restartNeeded: false,
config: {
enabled: true,
@ -56,7 +58,7 @@ export default createPlugin({
return [
{
label: 'Blocker',
label: t('plugins.adblocker.menu.blocker'),
submenu: Object.values(blockers).map((blocker) => ({
label: blocker,
type: 'radio',

View File

@ -3,13 +3,13 @@ import { FastAverageColor } from 'fast-average-color';
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
import type { VideoDataChanged } from '@/types/video-data-changed';
export default createPlugin({
name: 'Album Color Theme',
description:
'Applies a dynamic theme and visual effects based on the album color palette',
name: t('plugins.album-color-theme.name'),
description: t('plugins.album-color-theme.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -1,6 +1,7 @@
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export type AmbientModePluginConfig = {
enabled: boolean;
@ -24,9 +25,8 @@ const defaultConfig: AmbientModePluginConfig = {
};
export default createPlugin({
name: 'Ambient Mode',
description:
'Applies a lighting effect by casting gentle colors from the video, into your screens background.',
name: t('plugins.ambient-mode.name'),
description: t('plugins.ambient-mode.description'),
restartNeeded: false,
config: defaultConfig,
stylesheets: [style],
@ -42,9 +42,11 @@ export default createPlugin({
return [
{
label: 'Smoothness transition',
label: t('plugins.ambient-mode.menu.smoothness-transition.label'),
submenu: interpolationTimeList.map((interpolationTime) => ({
label: `During ${interpolationTime / 1000}s`,
label: t('plugins.ambient-mode.menu.smoothness-transition.submenu.during', {
interpolationTime: interpolationTime / 1000,
}),
type: 'radio',
checked: config.interpolationTime === interpolationTime,
click() {
@ -53,9 +55,9 @@ export default createPlugin({
})),
},
{
label: 'Quality',
label: t('plugins.ambient-mode.menu.quality.label'),
submenu: qualityList.map((quality) => ({
label: `${quality} pixels`,
label: t('plugins.ambient-mode.menu.quality.submenu.pixels', { quality }),
type: 'radio',
checked: config.quality === quality,
click() {
@ -64,9 +66,9 @@ export default createPlugin({
})),
},
{
label: 'Size',
label: t('plugins.ambient-mode.menu.size.label'),
submenu: sizeList.map((size) => ({
label: `${size}%`,
label: t('plugins.ambient-mode.menu.size.submenu.percent', { size }),
type: 'radio',
checked: config.size === size,
click() {
@ -75,9 +77,9 @@ export default createPlugin({
})),
},
{
label: 'Buffer',
label: t('plugins.ambient-mode.menu.buffer.label'),
submenu: bufferList.map((buffer) => ({
label: `${buffer}`,
label: t('plugins.ambient-mode.menu.buffer.submenu.buffer', { buffer }),
type: 'radio',
checked: config.buffer === buffer,
click() {
@ -86,9 +88,9 @@ export default createPlugin({
})),
},
{
label: 'Opacity',
label: t('plugins.ambient-mode.menu.opacity.label'),
submenu: opacityList.map((opacity) => ({
label: `${opacity * 100}%`,
label: t('plugins.ambient-mode.menu.opacity.submenu.percent', { opacity: opacity * 100 }),
type: 'radio',
checked: config.opacity === opacity,
click() {
@ -97,9 +99,9 @@ export default createPlugin({
})),
},
{
label: 'Blur amount',
label: t('plugins.ambient-mode.menu.blur-amount.label'),
submenu: blurAmountList.map((blur) => ({
label: `${blur} pixels`,
label: t('plugins.ambient-mode.menu.blur-amount.submenu.pixels', { blurAmount: blur }),
type: 'radio',
checked: config.blur === blur,
click() {
@ -108,7 +110,7 @@ export default createPlugin({
})),
},
{
label: 'Using fullscreen',
label: t('plugins.ambient-mode.menu.use-fullscreen.label'),
type: 'checkbox',
checked: config.fullscreen,
click(item) {

View File

@ -1,9 +1,9 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: 'Audio Compressor',
description:
'Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)',
name: t('plugins.audio-compressor.name'),
description: t('plugins.audio-compressor.description'),
renderer() {
document.addEventListener(

View File

@ -1,9 +1,10 @@
import { createPlugin } from '@/utils';
import style from './style.css?inline';
import { t } from '@/i18n';
export default createPlugin({
name: 'Blur Navigation Bar',
description: 'makes navigation bar transparent and blurry',
name: t('plugins.blur-nav-bar.name'),
description: t('plugins.blur-nav-bar.description'),
restartNeeded: true,
stylesheets: [style],
renderer() {},

View File

@ -1,8 +1,9 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: 'Bypass Age Restrictions',
description: "bypass YouTube's age verification",
name: t('plugins.bypass-age-restrictions.name'),
description: t('plugins.bypass-age-restrictions.description'),
restartNeeded: true,
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript

View File

@ -2,6 +2,7 @@ import prompt from 'custom-electron-prompt';
import promptOptions from '@/providers/prompt-options';
import { createBackend } from '@/utils';
import { t } from '@/i18n';
export default createBackend({
start({ ipc: { handle }, window }) {
@ -10,8 +11,10 @@ export default createBackend({
async (captionLabels: Record<string, string>, currentIndex: string) =>
await prompt(
{
title: 'Choose Caption',
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
title: t('plugins.captions-selector.prompt.selector.title'),
label: t('plugins.captions-selector.prompt.selector.label', {
language: captionLabels[currentIndex] || t('plugins.captions-selector.prompt.selector.none'),
}),
type: 'select',
value: currentIndex,
selectOptions: captionLabels,

View File

@ -3,6 +3,7 @@ import { YoutubePlayer } from '@/types/youtube-player';
import backend from './back';
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
import { t } from '@/i18n';
export default createPlugin<
unknown,
@ -18,8 +19,8 @@ export default createPlugin<
},
CaptionsSelectorConfig
>({
name: 'Captions Selector',
description: 'Caption selector for YouTube Music audio tracks',
name: t('plugins.captions-selector.name'),
description: t('plugins.captions-selector.description'),
config: {
enabled: false,
disableCaptions: false,
@ -31,7 +32,7 @@ export default createPlugin<
const config = await getConfig();
return [
{
label: 'Automatically select last used caption',
label: t('plugins.captions-selector.menu.autoload'),
type: 'checkbox',
checked: config.autoload as boolean,
click(item) {
@ -39,7 +40,7 @@ export default createPlugin<
},
},
{
label: 'No captions by default',
label: t('plugins.captions-selector.menu.disable-captions'),
type: 'checkbox',
checked: config.disableCaptions as boolean,
click(item) {

View File

@ -1,4 +1,5 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin<
unknown,
@ -8,8 +9,8 @@ export default createPlugin<
isCompactSidebarDisabled: () => boolean;
}
>({
name: 'Compact Sidebar',
description: 'Always set the sidebar in compact mode',
name: t('plugins.compact-sidebar.name'),
description: t('plugins.compact-sidebar.description'),
restartNeeded: false,
config: {
enabled: false,

View File

@ -11,6 +11,7 @@ import { createPlugin } from '@/utils';
import { VolumeFader } from './fader';
import type { RendererContext } from '@/types/contexts';
import { t } from '@/i18n';
export type CrossfadePluginConfig = {
enabled: boolean;
@ -29,8 +30,8 @@ export default createPlugin<
},
CrossfadePluginConfig
>({
name: 'Crossfade [beta]',
description: 'Crossfade between songs',
name: t('plugins.crossfade.name'),
description: t('plugins.crossfade.description'),
restartNeeded: true,
config: {
enabled: false,
@ -67,11 +68,11 @@ export default createPlugin<
): Promise<Omit<CrossfadePluginConfig, 'enabled'> | undefined> => {
const res = await prompt(
{
title: 'Crossfade Options',
title: t('plugins.crossfade.prompt.options'),
type: 'multiInput',
multiInputOptions: [
{
label: 'Fade in duration (ms)',
label: t('plugins.crossfade.prompt.options.multi-input.fade-in-duration'),
value: options.fadeInDuration,
inputAttrs: {
type: 'number',
@ -81,7 +82,7 @@ export default createPlugin<
},
},
{
label: 'Fade out duration (ms)',
label: t('plugins.crossfade.prompt.options.multi-input.fade-out-duration'),
value: options.fadeOutDuration,
inputAttrs: {
type: 'number',
@ -91,7 +92,7 @@ export default createPlugin<
},
},
{
label: 'Crossfade x seconds before end',
label: t('plugins.crossfade.prompt.options.multi-input.seconds-before-end'),
value: options.secondsBeforeEnd,
inputAttrs: {
type: 'number',
@ -100,8 +101,11 @@ export default createPlugin<
},
},
{
label: 'Fade scaling',
selectOptions: { linear: 'Linear', logarithmic: 'Logarithmic' },
label: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.label'),
selectOptions: {
linear: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.linear'),
logarithmic: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.logarithmic'),
},
value: options.fadeScaling,
},
],
@ -135,7 +139,7 @@ export default createPlugin<
return [
{
label: 'Advanced',
label: t('plugins.crossfade.menu.advanced'),
async click() {
const newOptions = await promptCrossfadeValues(
window,

View File

@ -1,5 +1,7 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
import type { VideoDataChanged } from '@/types/video-data-changed';
import type { YoutubePlayer } from '@/types/youtube-player';
@ -19,8 +21,8 @@ export default createPlugin<
},
DisableAutoPlayPluginConfig
>({
name: 'Disable Autoplay',
description: 'Makes every song start in "paused" mode',
name: t('plugins.disable-autoplay.name'),
description: t('plugins.disable-autoplay.description'),
restartNeeded: false,
config: {
enabled: false,
@ -31,7 +33,7 @@ export default createPlugin<
return [
{
label: 'Applies only on startup',
label: t('plugins.disable-autoplay.menu.apply-once'),
type: 'checkbox',
checked: config.applyOnce,
async click() {

View File

@ -1,6 +1,7 @@
import { createPlugin } from '@/utils';
import { backend } from './main';
import { onMenu } from './menu';
import { t } from '@/i18n';
export type DiscordPluginConfig = {
enabled: boolean;
@ -35,8 +36,8 @@ export type DiscordPluginConfig = {
};
export default createPlugin({
name: 'Discord Rich Presence',
description: 'Show your friends what you listen to with Rich Presence',
name: t('plugins.discord.name'),
description: t('plugins.discord.description'),
restartNeeded: false,
config: {
enabled: false,

View File

@ -6,7 +6,9 @@ import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
import registerCallback, { type SongInfo } from '@/providers/song-info';
import { createBackend } from '@/utils';
import { createBackend, LoggerPrefix } from '@/utils';
import { t } from '@/i18n';
import type { DiscordPluginConfig } from './index';
@ -38,7 +40,10 @@ const resetInfo = () => {
info.ready = false;
clearTimeout(clearActivity);
if (dev()) {
console.log('discord disconnected');
console.log(
LoggerPrefix,
t('plugins.discord.backend.disconnected')
);
}
for (const cb of refreshCallbacks) {
@ -68,7 +73,10 @@ let window: Electron.BrowserWindow;
export const connect = (showError = false) => {
if (info.rpc.isConnected) {
if (dev()) {
console.log('Attempted to connect with active connection');
console.log(
LoggerPrefix,
t('plugins.discord.backend.already-connected')
);
}
return;
@ -206,7 +214,10 @@ export const backend = createBackend<
info.rpc.on('connected', () => {
if (dev()) {
console.log('discord connected');
console.log(
LoggerPrefix,
t('plugins.discord.backend.connected')
);
}
for (const cb of refreshCallbacks) {

View File

@ -6,6 +6,8 @@ import { singleton } from '@/providers/decorators';
import promptOptions from '@/providers/prompt-options';
import { setMenuOptions } from '@/config/plugins';
import { t } from '@/i18n';
import type { MenuContext } from '@/types/contexts';
import type { DiscordPluginConfig } from './index';
@ -31,7 +33,7 @@ export const onMenu = async ({
click: () => connect(),
},
{
label: 'Auto reconnect',
label: t('plugins.discord.menu.auto-reconnect'),
type: 'checkbox',
checked: config.autoReconnect,
click(item: Electron.MenuItem) {
@ -41,11 +43,11 @@ export const onMenu = async ({
},
},
{
label: 'Clear activity',
label: t('plugins.discord.menu.clear-activity'),
click: clear,
},
{
label: 'Clear activity after timeout',
label: t('plugins.discord.menu.clear-activity-after-timeout'),
type: 'checkbox',
checked: config.activityTimeoutEnabled,
click(item: Electron.MenuItem) {
@ -55,7 +57,7 @@ export const onMenu = async ({
},
},
{
label: 'Play on YouTube Music',
label: t('plugins.discord.menu.play-on-youtube-music'),
type: 'checkbox',
checked: config.playOnYouTubeMusic,
click(item: Electron.MenuItem) {
@ -65,7 +67,7 @@ export const onMenu = async ({
},
},
{
label: 'Hide GitHub link Button',
label: t('plugins.discord.menu.hide-github-button'),
type: 'checkbox',
checked: config.hideGitHubButton,
click(item: Electron.MenuItem) {
@ -75,7 +77,7 @@ export const onMenu = async ({
},
},
{
label: 'Hide duration left',
label: t('plugins.discord.menu.hide-duration-left'),
type: 'checkbox',
checked: config.hideDurationLeft,
click(item: Electron.MenuItem) {
@ -85,7 +87,7 @@ export const onMenu = async ({
},
},
{
label: 'Set inactivity timeout',
label: t('plugins.discord.menu.set-inactivity-timeout'),
click: () => setInactivityTimeout(window, config),
},
];
@ -97,8 +99,8 @@ async function setInactivityTimeout(
) {
const output = await prompt(
{
title: 'Set Inactivity Timeout',
label: 'Enter inactivity timeout in seconds:',
title: t('plugins.discord.prompt.set-inactivity-timeout.title'),
label: t('plugins.discord.prompt.set-inactivity-timeout.label'),
value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)),
type: 'counter',
counterOptions: { minimum: 0, multiFire: true },

View File

@ -5,6 +5,7 @@ import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { onConfigChange, onMainLoad } from './main';
import { onPlayerApiReady, onRendererLoad } from './renderer';
import { t } from '@/i18n';
export type DownloaderPluginConfig = {
enabled: boolean;
@ -25,8 +26,8 @@ export const defaultConfig: DownloaderPluginConfig = {
};
export default createPlugin({
name: 'Downloader',
description: 'Downloads MP3 / source audio directly from the interface',
name: t('plugins.downloader.name'),
description: t('plugins.downloader.description'),
restartNeeded: true,
config: defaultConfig,
stylesheets: [style],

View File

@ -34,6 +34,8 @@ import { cleanupName, getImage, SongInfo } from '@/providers/song-info';
import { getNetFetchAsFetch } from '@/plugins/utils/main';
import { cache } from '@/providers/decorators';
import { t } from '@/i18n';
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
import type { DownloaderPluginConfig } from '../index';
@ -74,9 +76,9 @@ const sendError = (error: Error, source?: string) => {
console.trace(error);
dialog.showMessageBox(win, {
type: 'info',
buttons: ['OK'],
title: 'Error in download!',
message: 'Argh! Apologies, download failed…',
buttons: [t('plugins.downloader.backend.dialog.error.buttons.ok')],
title: t('plugins.downloader.backend.dialog.error.title'),
message: t('plugins.downloader.backend.dialog.error.message'),
detail: message,
});
};
@ -179,20 +181,27 @@ async function downloadSongUnsafe(
}
};
sendFeedback('Downloading...', 2);
sendFeedback(
t('plugins.downloader.backend.feedback.downloading'),
2,
);
let id: string | null;
if (isId) {
id = idOrUrl;
} else {
id = getVideoId(idOrUrl);
if (typeof id !== 'string') throw new Error('Video not found');
if (typeof id !== 'string') throw new Error(
t('plugins.downloader.backend.feedback.video-id-not-found'),
);
}
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
if (!info) {
throw new Error('Video not found');
throw new Error(
t('plugins.downloader.backend.feedback.video-id-not-found'),
);
}
const metadata = getMetadata(info);
@ -277,7 +286,11 @@ async function downloadSongUnsafe(
const stream = await info.download(downloadOptions);
console.info(
`Downloading ${metadata.artist} - ${metadata.title} [${metadata.videoId}]`,
t('plugins.downloader.backend.feedback.download-info', {
artist: metadata.artist,
title: metadata.title,
videoId: metadata.videoId,
}),
);
const iterableStream = Utils.streamToIterable(stream);
@ -312,7 +325,9 @@ async function downloadSongUnsafe(
}
sendFeedback(null, -1);
console.info(`Done: "${filePath}"`);
console.info(t('plugins.downloader.backend.feedback.done', {
filePath,
}));
}
async function iterableStreamToTargetFile(
@ -331,13 +346,21 @@ async function iterableStreamToTargetFile(
chunks.push(chunk);
const ratio = downloaded / contentLength;
const progress = Math.floor(ratio * 100);
sendFeedback(`Download: ${progress}%`, ratio);
sendFeedback(
t('plugins.downloader.backend.feedback.downloading-progress', {
percent: progress,
}),
ratio,
);
// 15% for download, 85% for conversion
// This is a very rough estimate, trying to make the progress bar look nice
increasePlaylistProgress(ratio * 0.15);
}
sendFeedback('Loading…', 2); // Indefinite progress bar after download
sendFeedback(
t('plugins.downloader.backend.feedback.loading'),
2,
); // Indefinite progress bar after download
const buffer = Buffer.concat(chunks);
const safeVideoName = randomBytes(32).toString('hex');
@ -348,13 +371,18 @@ async function iterableStreamToTargetFile(
await ffmpeg.load();
}
sendFeedback('Preparing file');
sendFeedback(t('plugins.downloader.backend.feedback.preparing-file'));
ffmpeg.FS('writeFile', safeVideoName, buffer);
sendFeedback('Converting');
sendFeedback(t('plugins.downloader.backend.feedback.converting'));
ffmpeg.setProgress(({ ratio }) => {
sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio);
sendFeedback(
t('plugins.downloader.backend.feedback.conversion-progress', {
percent: Math.floor(ratio * 100),
}),
ratio,
);
increasePlaylistProgress(0.15 + (ratio * 0.85));
});
@ -371,7 +399,9 @@ async function iterableStreamToTargetFile(
ffmpeg.FS('unlink', safeVideoName);
}
sendFeedback('Saving…');
sendFeedback(
t('plugins.downloader.backend.feedback.saving'),
);
try {
return ffmpeg.FS('readFile', safeVideoNameWithExtension);
@ -397,7 +427,9 @@ async function writeID3(
sendFeedback: (str: string, value?: number) => void,
) {
try {
sendFeedback('Writing ID3 tags...');
sendFeedback(
t('plugins.downloader.backend.feedback.writing-id3'),
);
const tags: NodeID3.Tags = {};
// Create the metadata tags
@ -452,14 +484,22 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl));
if (!playlistId) {
sendError(new Error('No playlist ID found'));
sendError(new Error(
t('plugins.downloader.backend.feedback.playlist-id-not-found'),
));
return;
}
const sendFeedback = (message?: unknown) => sendFeedback_(win, message);
console.log(`trying to get playlist ID: '${playlistId}'`);
sendFeedback('Getting playlist info…');
console.log(
t('plugins.downloader.backend.feedback.trying-to-get-playlist-id', {
playlistId,
}),
);
sendFeedback(
t('plugins.downloader.backend.feedback.getting-playlist-info'),
);
let playlist: Playlist;
const items: YTNodes.MusicResponsiveListItem[] = [];
try {
@ -470,16 +510,18 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
} catch (error: unknown) {
sendError(
Error(
`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(
error,
)}`,
t('plugins.downloader.backend.feedback.playlist-is-mix-or-private', {
error: String(error),
}),
),
);
return;
}
if (!playlist || !playlist.items || playlist.items.length === 0) {
sendError(new Error('Playlist is empty'));
sendError(new Error(
t('plugins.downloader.backend.feedback.playlist-is-empty'),
));
}
const normalPlaylistTitle = playlist.header?.title?.text;
@ -500,7 +542,9 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
}
if (items.length === 1) {
sendFeedback('Playlist has only one item, downloading it directly');
sendFeedback(
t('plugins.downloader.backend.feedback.playlist-has-only-one-song'),
);
await downloadSongFromId(items.at(0)!.id!);
return;
}
@ -514,7 +558,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
const playlistFolder = join(folder, safePlaylistTitle);
if (existsSync(playlistFolder)) {
if (!config.skipExisting) {
sendError(new Error(`The folder ${playlistFolder} already exists`));
sendError(new Error(
t('plugins.downloader.backend.feedback.folder-already-exists', {
playlistFolder,
})
));
return;
}
} else {
@ -523,15 +571,23 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
dialog.showMessageBox(win, {
type: 'info',
buttons: ['OK'],
title: 'Started Download',
message: `Downloading Playlist "${playlistTitle}"`,
detail: `(${items.length} songs)`,
buttons: [t('plugins.downloader.backend.dialog.start-download-playlist.buttons.ok')],
title: t('plugins.downloader.backend.dialog.start-download-playlist.title'),
message: t('plugins.downloader.backend.dialog.start-download-playlist.message', {
playlistTitle,
}),
detail: t('plugins.downloader.backend.dialog.start-download-playlist.detail', {
playlistSize: items.length,
}),
});
if (is.dev()) {
console.log(
`Downloading playlist "${playlistTitle}" - ${items.length} songs (${playlistId})`,
t('plugins.downloader.backend.feedback.downloading-playlist', {
playlistTitle,
playlistSize: items.length,
playlistId,
}),
);
}
@ -551,7 +607,12 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
try {
for (const song of items) {
sendFeedback(`Downloading ${counter}/${items.length}...`);
sendFeedback(
t('plugins.downloader.backend.feedback.downloading-counter', {
current: counter,
total: items.length,
})
);
const trackId = isAlbum ? counter : undefined;
await downloadSongFromId(
song.id!,
@ -561,9 +622,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
).catch((error) =>
sendError(
new Error(
`Error downloading "${
song.author!.name
} - ${song.title!}":\n ${error}`,
t('plugins.downloader.backend.feedback.error-while-downloading', {
author: song.author!.name,
title: song.title!,
error: String(error),
}),
),
),
);

View File

@ -8,6 +8,7 @@ import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu';
import type { DownloaderPluginConfig } from './index';
import { t } from '@/i18n';
export const onMenu = async ({
getConfig,
@ -21,7 +22,7 @@ export const onMenu = async ({
click: () => downloadPlaylist(),
},
{
label: 'Choose download folder',
label: t('plugins.downloader.menu.choose-download-folder'),
click() {
const result = dialog.showOpenDialogSync({
properties: ['openDirectory', 'createDirectory'],
@ -33,7 +34,7 @@ export const onMenu = async ({
},
},
{
label: 'Presets',
label: t('plugins.downloader.menu.presets'),
submenu: Object.keys(DefaultPresetList).map((preset) => ({
label: preset,
type: 'radio',
@ -44,7 +45,7 @@ export const onMenu = async ({
})),
},
{
label: 'Skip existing files',
label: t('plugins.downloader.menu.skip-existing'),
type: 'checkbox',
checked: config.skipExisting,
click(item) {

View File

@ -4,11 +4,14 @@ import defaultConfig from '@/config/defaults';
import { getSongMenu } from '@/providers/dom-elements';
import { getSongInfo } from '@/providers/song-info-front';
import { LoggerPrefix } from '@/utils';
import { ElementFromHtml } from '../utils/renderer';
import type { RendererContext } from '@/types/contexts';
import type { DownloaderPluginConfig } from './index';
import { t } from '@/i18n';
let menu: Element | null = null;
let progress: Element | null = null;
@ -75,7 +78,10 @@ export const onRendererLoad = ({
if (progress) {
progress.innerHTML = feedback || 'Download';
} else {
console.warn('Cannot update progress');
console.warn(
LoggerPrefix,
t('plugins.downloader.renderer.can-not-update-progress'),
);
}
});
};

View File

@ -39,7 +39,7 @@
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-download"
>
Download
<ytmd-trans key="plugins.downloader.templates.button"></ytmd-trans>
</div>
</a>
</div>

View File

@ -1,9 +1,9 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: 'Exponential Volume',
description:
"Makes the volume slider exponential so it's easier to select lower volumes.",
name: t('plugins.exponential-volume.name'),
description: t('plugins.exponential-volume.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -3,14 +3,15 @@ import { createPlugin } from '@/utils';
import { onMainLoad } from './main';
import { onMenu } from './menu';
import { onPlayerApiReady, onRendererLoad } from './renderer';
import { t } from '@/i18n';
export interface InAppMenuConfig {
enabled: boolean;
hideDOMWindowControls: boolean;
}
export default createPlugin({
name: 'In-App Menu',
description: 'gives menu-bars a fancy, dark or album-color look',
name: t('plugins.in-app-menu.name'),
description: t('plugins.in-app-menu.description'),
restartNeeded: true,
config: {
enabled:

View File

@ -1,5 +1,7 @@
import is from 'electron-is';
import { t } from '@/i18n';
import type { InAppMenuConfig } from './index';
import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu';
@ -13,7 +15,7 @@ export const onMenu = async ({
if (is.linux()) {
return [
{
label: 'Hide DOM Window Controls',
label: t('plugins.in-app-menu.hide-dom-window-controls'),
type: 'checkbox',
checked: config.hideDOMWindowControls,
click(item) {

View File

@ -99,7 +99,7 @@ export const createPanel = (
children.push(...children);
}
panel.appendChild(menu);
return panel.appendChild(menu);
});
/* methods */

View File

@ -1,6 +1,7 @@
import { createPlugin } from '@/utils';
import registerCallback from '@/providers/song-info';
import { addScrobble, getAndSetSessionKey, setNowPlaying } from './main';
import { t } from '@/i18n';
export interface LastFmPluginConfig {
enabled: boolean;
@ -33,8 +34,8 @@ export interface LastFmPluginConfig {
}
export default createPlugin({
name: 'Last.fm',
description: 'Add scrobbling support for Last.fm',
name: t('plugins.last-fm.name'),
description: t('plugins.last-fm.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -2,6 +2,7 @@ import { net } from 'electron';
import { createPlugin } from '@/utils';
import registerCallback from '@/providers/song-info';
import { t } from '@/i18n';
type LumiaData = {
origin: string;
@ -23,8 +24,8 @@ type LumiaData = {
};
export default createPlugin({
name: 'Lumia Stream [beta]',
description: 'Adds Lumia Stream support',
name: t('plugins.lumiastream.name'),
description: t('plugins.lumiastream.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -2,6 +2,7 @@ import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { onConfigChange, onMainLoad } from './main';
import { onRendererLoad } from './renderer';
import { t } from '@/i18n';
export type LyricsGeniusPluginConfig = {
enabled: boolean;
@ -9,8 +10,8 @@ export type LyricsGeniusPluginConfig = {
};
export default createPlugin({
name: 'Lyrics Genius',
description: 'Adds lyrics support for most songs',
name: t('plugins.lyrics-genius.name'),
description: t('plugins.lyrics-genius.description'),
restartNeeded: true,
config: {
enabled: false,
@ -22,7 +23,7 @@ export default createPlugin({
return [
{
label: 'Romanized Lyrics',
label: t('plugins.lyrics-genius.menu.romanized-lyrics'),
type: 'checkbox',
checked: config.romanizedLyrics,
click(item) {

View File

@ -1,6 +1,9 @@
import { LoggerPrefix } from '@/utils';
import type { SongInfo } from '@/providers/song-info';
import type { RendererContext } from '@/types/contexts';
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
import { t } from '@/i18n';
export const onRendererLoad = ({
ipc: { invoke, on },
@ -55,7 +58,10 @@ export const onRendererLoad = ({
}
if (window.electronIs.dev()) {
console.log('Fetched lyrics from Genius');
console.log(
LoggerPrefix,
t('plugins.lyric-genius.renderer.fetched-lyrics'),
);
}
const tryToInjectLyric = (callback?: () => void) => {

View File

@ -2,13 +2,14 @@ import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { t } from '@/i18n';
import forwardHTML from './templates/forward.html?raw';
import backHTML from './templates/back.html?raw';
export default createPlugin({
name: 'Navigation',
description:
'Next/Back navigation arrows directly integrated in the interface, like in your favorite browser',
name: t('plugins.navigation.name'),
description: t('plugins.navigation.description'),
restartNeeded: true,
config: {
enabled: true,

View File

@ -1,9 +1,10 @@
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: 'Remove Google Login',
description: 'Remove Google login buttons and links from the interface',
name: t('plugins.no-google-login.name'),
description: t('plugins.no-google-login.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -2,6 +2,7 @@ import { createPlugin } from '@/utils';
import { onConfigChange, onMainLoad } from './main';
import { onMenu } from './menu';
import { t } from '@/i18n';
export interface NotificationsPluginConfig {
enabled: boolean;
@ -35,9 +36,8 @@ export const defaultConfig: NotificationsPluginConfig = {
};
export default createPlugin({
name: 'Notifications',
description:
'Display a notification when a song starts playing (interactive notifications are available on windows)',
name: t('plugins.notifications.name'),
description: t('plugins.notifications.description'),
restartNeeded: true,
config: defaultConfig,
menu: onMenu,

View File

@ -3,6 +3,8 @@ import { MenuItem } from 'electron';
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
import { t } from '@/i18n';
import type { NotificationsPluginConfig } from './index';
import type { MenuTemplate } from '@/menu';
@ -34,7 +36,7 @@ export const onMenu = async ({
if (is.linux()) {
return [
{
label: 'Notification Priority',
label: t('plugins.notifications.menu.priority'),
submenu: urgencyLevels.map((level) => ({
label: level.name,
type: 'radio',
@ -46,7 +48,7 @@ export const onMenu = async ({
} else if (is.windows()) {
return [
{
label: 'Interactive Notifications',
label: t('plugins.notifications.menu.interactive'),
type: 'checkbox',
checked: config.interactive,
// Doesn't update until restart
@ -54,24 +56,24 @@ export const onMenu = async ({
},
{
// Submenu with settings for interactive notifications (name shouldn't be too long)
label: 'Interactive Settings',
label: t('plugins.notifications.menu.interactive-settings.label'),
submenu: [
{
label: 'Open/Close on tray click',
label: t('plugins.notifications.menu.interactive-settings.submenu.tray-controls'),
type: 'checkbox',
checked: config.trayControls,
click: (item: MenuItem) =>
setConfig({ trayControls: item.checked }),
},
{
label: 'Hide Button Text',
label: t('plugins.notifications.menu.interactive-settings.submenu.hide-button-text'),
type: 'checkbox',
checked: config.hideButtonText,
click: (item: MenuItem) =>
setConfig({ hideButtonText: item.checked }),
},
{
label: 'Refresh on Play/Pause',
label: t('plugins.notifications.menu.interactive-settings.submenu.refresh-on-play-pause'),
type: 'checkbox',
checked: config.refreshOnPlayPause,
click: (item: MenuItem) =>
@ -80,7 +82,7 @@ export const onMenu = async ({
],
},
{
label: 'Style',
label: t('plugins.notifications.menu.toast-style'),
submenu: getToastStyleMenuItems(config),
},
];
@ -92,7 +94,7 @@ export const onMenu = async ({
return [
...getMenu(),
{
label: 'Show notification on unpause',
label: t('plugins.notifications.menu.unpause-notification'),
type: 'checkbox',
checked: config.unpauseNotification,
click: (item) => setConfig({ unpauseNotification: item.checked }),

View File

@ -4,6 +4,7 @@ import { createPlugin } from '@/utils';
import { onConfigChange, onMainLoad } from './main';
import { onMenu } from './menu';
import { onPlayerApiReady, onRendererLoad } from './renderer';
import { t } from '@/i18n';
export type PictureInPicturePluginConfig = {
enabled: boolean;
@ -18,8 +19,8 @@ export type PictureInPicturePluginConfig = {
};
export default createPlugin({
name: 'Picture In Picture',
description: 'Allows to switch the app to picture-in-picture mode',
name: t('plugins.picture-in-picture.name'),
description: t('plugins.picture-in-picture.description'),
restartNeeded: true,
config: {
'enabled': false,

View File

@ -2,6 +2,8 @@ import prompt from 'custom-electron-prompt';
import promptOptions from '@/providers/prompt-options';
import { t } from '@/i18n';
import type { PictureInPicturePluginConfig } from './index';
import type { MenuContext } from '@/types/contexts';
@ -16,7 +18,7 @@ export const onMenu = async ({
return [
{
label: 'Always on top',
label: t('plugins.picture-in-picture.menu.always-on-top'),
type: 'checkbox',
checked: config.alwaysOnTop,
click(item) {
@ -25,7 +27,7 @@ export const onMenu = async ({
},
},
{
label: 'Save window position',
label: t('plugins.picture-in-picture.menu.save-window-position'),
type: 'checkbox',
checked: config.savePosition,
click(item) {
@ -33,7 +35,7 @@ export const onMenu = async ({
},
},
{
label: 'Save window size',
label: t('plugins.picture-in-picture.menu.save-window-size'),
type: 'checkbox',
checked: config.saveSize,
click(item) {
@ -41,19 +43,19 @@ export const onMenu = async ({
},
},
{
label: 'Hotkey',
label: t('plugins.picture-in-picture.menu.hotkey.label'),
type: 'checkbox',
checked: !!config.hotkey,
async click(item) {
const output = await prompt(
{
title: 'Picture in Picture Hotkey',
label: 'Choose a hotkey for toggling Picture in Picture',
title: t('plugins.picture-in-picture.menu.prompt.title'),
label: t('plugins.picture-in-picture.menu.prompt.label'),
type: 'keybind',
keybindOptions: [
{
value: 'hotkey',
label: 'Hotkey',
label: t('plugins.picture-in-picture.menu.prompt.keybind-options.hotkey'),
default: config.hotkey,
},
],
@ -74,7 +76,7 @@ export const onMenu = async ({
},
},
{
label: 'Use native PiP',
label: t('plugins.picture-in-picture.menu.use-native-pip'),
type: 'checkbox',
checked: config.useNativePiP,
click(item) {

View File

@ -44,7 +44,7 @@
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-pip"
>
Picture in picture
<ytmd-trans key="plugins.picture-in-picture.templates.button"></ytmd-trans>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
import { createPlugin } from '@/utils';
import { onPlayerApiReady, onUnload } from './renderer';
import { t } from '@/i18n';
export default createPlugin({
name: 'Playback Speed',
description:
'Listen fast, listen slow! Adds a slider that controls song speed',
name: t('plugins.playback-speed.name'),
description: t('plugins.playback-speed.description'),
restartNeeded: false,
config: {
enabled: false,

View File

@ -83,7 +83,7 @@
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-playback-speed"
>
Speed (<span id="playback-speed-value">1</span>)
<ytmd-trans key="plugins.playback-speed.templates.button"></ytmd-trans> (<span id="playback-speed-value">1</span>)
</div>
</div>
</div>

View File

@ -7,32 +7,41 @@ import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options';
import { overrideListener } from './override';
import { onConfigChange, onPlayerApiReady } from './renderer';
import { t } from '@/i18n';
export type PreciseVolumePluginConfig = {
enabled: boolean;
/**
* Percentage of volume to change
*/
steps: number;
/**
* Enable ArrowUp + ArrowDown local shortcuts
*/
arrowsShortcut: boolean;
globalShortcuts: {
volumeUp: string;
volumeDown: string;
};
/**
* Plugin save volume between session here
*/
savedVolume: number | undefined;
};
export default createPlugin({
name: 'Precise Volume',
description:
'Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps',
name: t('plugins.precise-volume.name'),
description: t('plugins.precise-volume.description'),
restartNeeded: true,
config: {
enabled: false,
steps: 1, // Percentage of volume to change
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
steps: 1,
arrowsShortcut: true,
globalShortcuts: {
volumeUp: '',
volumeDown: '',
},
savedVolume: undefined, // Plugin save volume between session here
savedVolume: undefined,
} as PreciseVolumePluginConfig,
stylesheets: [hudStyle],
menu: async ({ setConfig, getConfig, window }) => {
@ -66,8 +75,8 @@ export default createPlugin({
async function promptVolumeSteps(options: PreciseVolumePluginConfig) {
const output = await prompt(
{
title: 'Volume Steps',
label: 'Choose Volume Increase/Decrease Steps',
title: t('plugins.precise-volume.prompt.volume-steps.title'),
label: t('plugins.precise-volume.prompt.volume-steps.label'),
value: options.steps || 1,
type: 'counter',
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
@ -89,17 +98,17 @@ export default createPlugin({
) {
const output = await prompt(
{
title: 'Global Volume Keybinds',
label: 'Choose Global Volume Keybinds:',
title: t('plugins.precise-volume.prompt.global-shortcuts.title'),
label: t('plugins.precise-volume.prompt.global-shortcuts.label'),
type: 'keybind',
keybindOptions: [
kb(
'Increase Volume',
t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.increase'),
'volumeUp',
options.globalShortcuts?.volumeUp,
),
kb(
'Decrease Volume',
t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.decrease'),
'volumeDown',
options.globalShortcuts?.volumeDown,
),
@ -132,7 +141,7 @@ export default createPlugin({
return [
{
label: 'Local Arrowkeys Controls',
label: t('plugins.precise-volume.menu.arrows-shortcuts'),
type: 'checkbox',
checked: Boolean(config.arrowsShortcut),
click(item) {
@ -140,7 +149,7 @@ export default createPlugin({
},
},
{
label: 'Global Hotkeys',
label: t('plugins.precise-volume.menu.global-shortcuts'),
type: 'checkbox',
checked: Boolean(
config.globalShortcuts?.volumeUp ??
@ -149,7 +158,7 @@ export default createPlugin({
click: (item) => promptGlobalShortcuts(config, item),
},
{
label: 'Set Custom Volume Steps',
label: t('plugins.precise-volume.menu.custom-volume-steps'),
click: () => promptVolumeSteps(config),
},
];

View File

@ -4,13 +4,13 @@ import QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?ra
import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { t } from '@/i18n';
import type { YoutubePlayer } from '@/types/youtube-player';
export default createPlugin({
name: 'Video Quality Changer',
description:
'Allows changing the video quality with a button on the video overlay',
name: t('plugins.quality-changer.name'),
description: t('plugins.quality-changer.description'),
restartNeeded: false,
config: {
enabled: false,
@ -24,9 +24,11 @@ export default createPlugin({
type: 'question',
buttons: qualityLabels,
defaultId: currentIndex,
title: 'Choose Video Quality',
message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
title: t('plugins.quality-changer.backend.dialog.title'),
message: t('plugins.quality-changer.backend.dialog.message'),
detail: t('plugins.quality-changer.backend.dialog.detail', {
quality: qualityLabels[currentIndex],
}),
cancelId: -1,
}),
);

View File

@ -1,6 +1,7 @@
import { createPlugin } from '@/utils';
import { onMainLoad } from './main';
import { onMenu } from './menu';
import { t } from '@/i18n';
export type ShortcutMappingType = {
previous: string;
@ -15,9 +16,8 @@ export type ShortcutsPluginConfig = {
};
export default createPlugin({
name: 'Shortcuts (& MPRIS)',
description:
'Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users',
name: t('plugins.shortcuts.name'),
description: t('plugins.shortcuts.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -2,6 +2,8 @@ import prompt, { KeybindOptions } from 'custom-electron-prompt';
import promptOptions from '@/providers/prompt-options';
import { t } from '@/i18n';
import type { ShortcutsPluginConfig } from './index';
import type { BrowserWindow } from 'electron';
import type { MenuContext } from '@/types/contexts';
@ -29,14 +31,14 @@ export const onMenu = async ({
) {
const output = await prompt(
{
title: 'Global Keybinds',
label: 'Choose Global Keybinds for Songs Control:',
title: t('plugins.shortcuts.prompt.keybind.title'),
label: t('plugins.shortcuts.prompt.keybind.label'),
type: 'keybind',
keybindOptions: [
// If default=undefined then no default is used
kb('Previous', 'previous', config.global?.previous),
kb('Play / Pause', 'playPause', config.global?.playPause),
kb('Next', 'next', config.global?.next),
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.previous'), 'previous', config.global?.previous),
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.play-pause'), 'playPause', config.global?.playPause),
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.next'), 'next', config.global?.next),
],
height: 270,
...promptOptions(),
@ -59,11 +61,11 @@ export const onMenu = async ({
return [
{
label: 'Set Global Song Controls',
label: t('plugins.shortcuts.menu.set-keybinds'),
click: () => promptKeybind(config, window),
},
{
label: 'Override MediaKeys',
label: t('plugins.shortcuts.menu.override-media-keys'),
type: 'checkbox',
checked: config.overrideMediaKeys,
click: (item) => setConfig({ overrideMediaKeys: item.checked }),

View File

@ -1,5 +1,6 @@
import { createPlugin } from '@/utils';
import { onRendererLoad, onRendererUnload } from './renderer';
import { t } from '@/i18n';
export type SkipSilencesPluginConfig = {
enabled: boolean;
@ -7,8 +8,8 @@ export type SkipSilencesPluginConfig = {
};
export default createPlugin({
name: 'Skip Silences',
description: 'Automatically skip silenced sections',
name: t('plugins.skip-silences.name'),
description: t('plugins.skip-silences.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -4,6 +4,8 @@ import { createPlugin } from '@/utils';
import { sortSegments } from './segments';
import { t } from '@/i18n';
import type { GetPlayerResponse } from '@/types/get-player-response';
import type { Segment, SkipSegment } from './types';
@ -23,9 +25,8 @@ export type SponsorBlockPluginConfig = {
let currentSegments: Segment[] = [];
export default createPlugin({
name: 'SponsorBlock',
description:
"Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing",
name: t('plugins.sponsorblock.name'),
description: t('plugins.sponsorblock.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -9,10 +9,11 @@ import { createPlugin } from '@/utils';
import getSongControls from '@/providers/song-controls';
import registerCallback, { type SongInfo } from '@/providers/song-info';
import { mediaIcons } from '@/types/media-icons';
import { t } from '@/i18n';
export default createPlugin({
name: 'Taskbar Media Control',
description: 'Control playback from your Windows taskbar',
name: t('plugins.taskbar-mediacontrol.name'),
description: t('plugins.taskbar-mediacontrol.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -3,10 +3,11 @@ import { type NativeImage, TouchBar } from 'electron';
import { createPlugin } from '@/utils';
import getSongControls from '@/providers/song-controls';
import registerCallback from '@/providers/song-info';
import { t } from '@/i18n';
export default createPlugin({
name: 'TouchBar',
description: 'Custom TouchBar layout for macOS',
name: t('plugins.touchbar.name'),
description: t('plugins.touchbar.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -4,6 +4,7 @@ import is from 'electron-is';
import { createPlugin } from '@/utils';
import registerCallback from '@/providers/song-info';
import { t } from '@/i18n';
interface Data {
album: string | null | undefined;
@ -18,8 +19,8 @@ interface Data {
}
export default createPlugin({
name: 'Tuna OBS',
description: "Integration with OBS's plugin Tuna",
name: t('plugins.tuna-obs.name'),
description: t('plugins.tuna-obs.description'),
restartNeeded: true,
config: {
enabled: false,

View File

@ -6,6 +6,8 @@ import { createPlugin } from '@/utils';
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { ThumbnailElement } from '@/types/get-player-response';
import { t } from '@/i18n';
import { MenuTemplate } from '@/menu';
export type VideoTogglePluginConfig = {
enabled: boolean;
@ -16,9 +18,8 @@ export type VideoTogglePluginConfig = {
};
export default createPlugin({
name: 'Video Toggle',
description:
'Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab',
name: t('plugins.video-toggle.name'),
description: t('plugins.video-toggle.description'),
restartNeeded: true,
config: {
enabled: false,
@ -28,15 +29,15 @@ export default createPlugin({
align: 'left',
} as VideoTogglePluginConfig,
stylesheets: [buttonSwitcherStyle, forceHideStyle],
menu: async ({ getConfig, setConfig }) => {
menu: async ({ getConfig, setConfig }): Promise<MenuTemplate> => {
const config = await getConfig();
return [
{
label: 'Mode',
label: t('plugins.video-toggle.menu.mode.label'),
submenu: [
{
label: 'Custom toggle',
label: t('plugins.video-toggle.menu.mode.submenu.custom'),
type: 'radio',
checked: config.mode === 'custom',
click() {
@ -44,7 +45,7 @@ export default createPlugin({
},
},
{
label: 'Native toggle',
label: t('plugins.video-toggle.menu.mode.submenu.native'),
type: 'radio',
checked: config.mode === 'native',
click() {
@ -52,7 +53,7 @@ export default createPlugin({
},
},
{
label: 'Disabled',
label: t('plugins.video-toggle.menu.mode.submenu.disabled'),
type: 'radio',
checked: config.mode === 'disabled',
click() {
@ -62,10 +63,10 @@ export default createPlugin({
],
},
{
label: 'Alignment',
label: t('plugins.video-toggle.menu.align.label'),
submenu: [
{
label: 'Left',
label: t('plugins.video-toggle.menu.align.submenu.left'),
type: 'radio',
checked: config.align === 'left',
click() {
@ -73,7 +74,7 @@ export default createPlugin({
},
},
{
label: 'Middle',
label: t('plugins.video-toggle.menu.align.submenu.middle'),
type: 'radio',
checked: config.align === 'middle',
click() {
@ -81,7 +82,7 @@ export default createPlugin({
},
},
{
label: 'Right',
label: t('plugins.video-toggle.menu.align.submenu.right'),
type: 'radio',
checked: config.align === 'right',
click() {
@ -91,7 +92,7 @@ export default createPlugin({
],
},
{
label: 'Force Remove Video Tab',
label: t('plugins.video-toggle.menu.force-hide'),
type: 'checkbox',
checked: config.forceHide,
click(item) {

View File

@ -1,4 +1,8 @@
<div class="video-switch-button">
<input checked="true" class="video-switch-button-checkbox" type="checkbox" />
<label class="video-switch-button-label" for=""><span class="video-switch-button-label-span">Song</span></label>
<label class="video-switch-button-label" for="">
<span class="video-switch-button-label-span">
<ytmd-trans key="plugins.video-toggle.templates.button"></ytmd-trans>
</span>
</label>
</div>

View File

@ -6,6 +6,7 @@ import {
VudioVisualizer as vudio,
WaveVisualizer as wave,
} from './visualizers';
import { t } from '@/i18n';
type WaveColor = {
gradient: string[];
@ -57,8 +58,8 @@ export type VisualizerPluginConfig = {
};
export default createPlugin({
name: 'Visualizer',
description: 'Adds a visualizer to the player',
name: t('plugins.visualizer.name'),
description: t('plugins.visualizer.description'),
restartNeeded: true,
config: {
enabled: false,
@ -133,7 +134,7 @@ export default createPlugin({
return [
{
label: 'Type',
label: t('plugins.visualizer.menu.visualizer-type'),
submenu: visualizerTypes.map((visualizerType) => ({
label: visualizerType,
type: 'radio',

View File

@ -1,3 +1,5 @@
import process from 'node:process';
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import is from 'electron-is';
@ -8,8 +10,12 @@ import {
forceUnloadPreloadPlugin,
loadAllPreloadPlugins,
} from './loader/preload';
import { loadI18n, setLanguage } from '@/i18n';
loadI18n().then(async () => {
await setLanguage(config.get('options.language') ?? 'en');
loadAllPreloadPlugins();
});
ipcRenderer.on('plugin:unload', async (_, id: string) => {
await forceUnloadPreloadPlugin(id);

View File

@ -1,3 +1,5 @@
import i18next from 'i18next';
import { startingPages } from './providers/extracted-data';
import setupSongInfo from './providers/song-info-front';
import {
@ -9,6 +11,8 @@ import {
loadAllRendererPlugins,
} from './loader/renderer';
import { loadI18n, setLanguage, t as i18t } from '@/i18n';
import type { PluginConfig } from '@/types/plugins';
import type { YoutubePlayer } from '@/types/youtube-player';
@ -151,7 +155,32 @@ async function onApiLoaded() {
}
}
/**
* YouTube Music still using ES5, so we need to define custom elements using ES5 style
*/
const defineYTMDTransElements = () => {
const YTMDTrans = function() {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
YTMDTrans.prototype = Object.create(HTMLElement.prototype);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
YTMDTrans.prototype.connectedCallback = function() {
const that = (this as HTMLElement);
const key = that.getAttribute('key');
if (key) {
that.innerHTML = i18t(key);
}
};
customElements.define('ytmd-trans', YTMDTrans as unknown as CustomElementConstructor);
};
(async () => {
await loadI18n();
await setLanguage(window.mainConfig.get('options.language') ?? 'en');
window.i18n = {
t: i18t.bind(i18next),
};
defineYTMDTransElements();
await loadAllRendererPlugins();
isPluginLoaded = true;

4
src/reset.d.ts vendored
View File

@ -5,6 +5,7 @@ import type is from 'electron-is';
import type config from './config';
import type { VideoDataChanged } from '@/types/video-data-changed';
import type { t } from '@/i18n';
declare global {
interface Compressor {
@ -30,6 +31,9 @@ declare global {
download: () => void;
togglePictureInPicture: () => void;
reload: () => void;
i18n: {
t: typeof t,
}
}
}

View File

@ -1,3 +1,5 @@
import { t } from '@/i18n';
import type {
BackendContext,
PreloadContext,
@ -106,12 +108,25 @@ export const startPlugin = async <Config extends PluginConfig>(
console.log(
LoggerPrefix,
`Executed ${id}::${options.ctx} in ${performance.now() - start} ms`,
t('common.console.plugins.executed-at-ms', {
pluginName: id,
contextName: options.ctx,
ms: performance.now() - start,
}),
);
return lifecycle ? true : null;
} catch (err) {
console.error(LoggerPrefix, `Failed to start ${id}::${options.ctx}`);
console.error(
LoggerPrefix,
t(
'common.console.plugins.execute-failed',
{
pluginName: id,
contextName: options.ctx,
},
),
);
console.trace(err);
return false;
}
@ -140,12 +155,25 @@ export const stopPlugin = async <Config extends PluginConfig>(
console.log(
LoggerPrefix,
`Executed ${id}::${options.ctx} in ${performance.now() - start} ms`,
t('common.console.plugins.executed-at-ms', {
pluginName: id,
contextName: options.ctx,
ms: performance.now() - start,
}),
);
return true;
} catch (err) {
console.error(LoggerPrefix, `Failed to execute ${id}::${options.ctx}`);
console.error(
LoggerPrefix,
t(
'common.console.plugins.execute-failed',
{
pluginName: id,
contextName: options.ctx,
},
),
);
console.trace(err);
return false;
}