mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
feat: add support i18n (#1468)
This commit is contained in:
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 screen’s 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) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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() {},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -99,7 +99,7 @@ export const createPanel = (
|
||||
children.push(...children);
|
||||
}
|
||||
|
||||
panel.appendChild(menu);
|
||||
return panel.appendChild(menu);
|
||||
});
|
||||
|
||||
/* methods */
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user