mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
feat: add support i18n (#1468)
This commit is contained in:
@ -155,6 +155,7 @@
|
|||||||
"filenamify": "6.0.0",
|
"filenamify": "6.0.0",
|
||||||
"howler": "2.2.4",
|
"howler": "2.2.4",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
|
"i18next": "23.7.7",
|
||||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"node-html-parser": "6.1.11",
|
"node-html-parser": "6.1.11",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -97,6 +97,9 @@ dependencies:
|
|||||||
html-to-text:
|
html-to-text:
|
||||||
specifier: 9.0.5
|
specifier: 9.0.5
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
|
i18next:
|
||||||
|
specifier: 23.7.7
|
||||||
|
version: 23.7.7
|
||||||
keyboardevent-from-electron-accelerator:
|
keyboardevent-from-electron-accelerator:
|
||||||
specifier: 2.0.0
|
specifier: 2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@ -3856,6 +3859,12 @@ packages:
|
|||||||
engines: {node: '>=14.18.0'}
|
engines: {node: '>=14.18.0'}
|
||||||
dev: true
|
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:
|
/iconv-corefoundation@1.1.7:
|
||||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||||
engines: {node: ^8.11.2 || >=10}
|
engines: {node: ^8.11.2 || >=10}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface DefaultConfig {
|
|||||||
'window-position': WindowPositionConfig;
|
'window-position': WindowPositionConfig;
|
||||||
url: string;
|
url: string;
|
||||||
options: {
|
options: {
|
||||||
|
language?: string;
|
||||||
tray: boolean;
|
tray: boolean;
|
||||||
appVisible: boolean;
|
appVisible: boolean;
|
||||||
autoUpdates: boolean;
|
autoUpdates: boolean;
|
||||||
|
|||||||
18
src/i18n/index.ts
Normal file
18
src/i18n/index.ts
Normal 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
572
src/i18n/resources/en.json
Normal 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 screen’s 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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/i18n/resources/index.ts
Normal file
11
src/i18n/resources/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import enJson from './en.json';
|
||||||
|
import koJson from './ko.json';
|
||||||
|
|
||||||
|
export const languageResources = {
|
||||||
|
en: {
|
||||||
|
translation: enJson
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
translation: koJson
|
||||||
|
}
|
||||||
|
};
|
||||||
7
src/i18n/resources/ko.json
Normal file
7
src/i18n/resources/ko.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"name": "Korean",
|
||||||
|
"local-name": "한국어",
|
||||||
|
"code": "ko"
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/index.ts
92
src/index.ts
@ -1,6 +1,7 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import url from 'node:url';
|
import url from 'node:url';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
@ -49,6 +50,9 @@ import {
|
|||||||
} from '@/loader/main';
|
} from '@/loader/main';
|
||||||
|
|
||||||
import { LoggerPrefix } from '@/utils';
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
import { loadI18n, setLanguage, t } from '@/i18n';
|
||||||
|
|
||||||
|
import { languageResources } from '@/i18n/resources';
|
||||||
|
|
||||||
import type { PluginConfig } from '@/types/plugins';
|
import type { PluginConfig } from '@/types/plugins';
|
||||||
|
|
||||||
@ -183,10 +187,17 @@ const showNeedToRestartDialog = (id: string) => {
|
|||||||
|
|
||||||
const dialogOptions: Electron.MessageBoxOptions = {
|
const dialogOptions: Electron.MessageBoxOptions = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['Restart Now', 'Later'],
|
buttons: [
|
||||||
title: 'Restart Required',
|
t('main.dialog.need-to-restart.buttons.restart-now'),
|
||||||
message: `"${plugin?.name ?? id}" needs to restart`,
|
t('main.dialog.need-to-restart.buttons.later'),
|
||||||
detail: `"${plugin?.name ?? id}" plugin requires a restart to take effect`,
|
],
|
||||||
|
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,
|
defaultId: 0,
|
||||||
cancelId: 1,
|
cancelId: 1,
|
||||||
};
|
};
|
||||||
@ -227,7 +238,7 @@ function initTheme(win: BrowserWindow) {
|
|||||||
() => {
|
() => {
|
||||||
console.warn(
|
console.warn(
|
||||||
LoggerPrefix,
|
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', () => {
|
win.webContents.once('did-finish-load', () => {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(LoggerPrefix, 'did finish load');
|
console.debug(LoggerPrefix, t('main.console.did-finish-load.dev-tools'));
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -305,13 +316,13 @@ async function createMainWindow() {
|
|||||||
) {
|
) {
|
||||||
// Window is offscreen
|
// Window is offscreen
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
console.warn(
|
||||||
`Window tried to render offscreen, windowSize=${String(
|
LoggerPrefix,
|
||||||
winSize,
|
t('main.console.window.tried-to-render-offscreen', {
|
||||||
)}, displaySize=${String(display.bounds)}, position=${String(
|
winSize: String(winSize),
|
||||||
windowPosition,
|
displaySize: String(display.bounds),
|
||||||
)}`,
|
windowPosition: String(windowPosition),
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
win.setSize(scaledWidth, scaledHeight);
|
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 () => {
|
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')) {
|
if (config.get('options.autoResetAppCache')) {
|
||||||
// Clear cache after 20s
|
// Clear cache after 20s
|
||||||
const clearCacheTimeout = setTimeout(() => {
|
const clearCacheTimeout = setTimeout(() => {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log('Clearing app cache.');
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('main.console.when-ready.clearing-cache-after-20s'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.defaultSession.clearCache();
|
session.defaultSession.clearCache();
|
||||||
@ -614,7 +640,7 @@ app.whenReady().then(async () => {
|
|||||||
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
|
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
|
||||||
const command = protocolArgv.slice(uri.length, lastIndex);
|
const command = protocolArgv.slice(uri.length, lastIndex);
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.debug(`Received command over protocol: "${command}"`);
|
console.debug(LoggerPrefix, t('main.console.second-instance.receive-command', { command }));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProtocol(command);
|
handleProtocol(command);
|
||||||
@ -651,10 +677,14 @@ app.whenReady().then(async () => {
|
|||||||
'https://github.com/th-ch/youtube-music/releases/latest';
|
'https://github.com/th-ch/youtube-music/releases/latest';
|
||||||
const dialogOptions: Electron.MessageBoxOptions = {
|
const dialogOptions: Electron.MessageBoxOptions = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK', 'Download', 'Disable updates'],
|
buttons: [
|
||||||
title: 'Application Update',
|
t('main.dialog.update-available.buttons.download'),
|
||||||
message: 'A new version is available',
|
t('main.dialog.update-available.buttons.later'),
|
||||||
detail: `A new version is available and can be downloaded at ${downloadLink}`,
|
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>;
|
let dialogPromise: Promise<Electron.MessageBoxReturnValue>;
|
||||||
@ -689,9 +719,8 @@ app.whenReady().then(async () => {
|
|||||||
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Hide Menu Enabled',
|
title: t('main.dialog.hide-menu-enabled.title'),
|
||||||
message:
|
message: t('main.dialog.hide-menu-enabled.message'),
|
||||||
"Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
|
|
||||||
});
|
});
|
||||||
config.set('options.hideMenuWarned', true);
|
config.set('options.hideMenuWarned', true);
|
||||||
}
|
}
|
||||||
@ -722,16 +751,25 @@ function showUnresponsiveDialog(
|
|||||||
details: Electron.RenderProcessGoneDetails,
|
details: Electron.RenderProcessGoneDetails,
|
||||||
) {
|
) {
|
||||||
if (details) {
|
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
|
dialog
|
||||||
.showMessageBox(win, {
|
.showMessageBox(win, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Window Unresponsive',
|
title: t('main.dialog.unresponsive.title'),
|
||||||
message: 'The Application is Unresponsive',
|
message: t('main.dialog.unresponsive.message'),
|
||||||
detail: 'We are sorry for the inconvenience! please choose what to do:',
|
detail: t('main.dialog.unresponsive.detail'),
|
||||||
buttons: ['Wait', 'Relaunch', 'Quit'],
|
buttons: [
|
||||||
|
t('main.dialog.unresponsive.buttons.wait'),
|
||||||
|
t('main.dialog.unresponsive.buttons.relaunch'),
|
||||||
|
t('main.dialog.unresponsive.buttons.quit'),
|
||||||
|
],
|
||||||
cancelId: 0,
|
cancelId: 0,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { allPlugins, mainPlugins } from 'virtual:plugins';
|
|||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|
||||||
@ -67,14 +69,23 @@ export const forceUnloadMainPlugin = async (
|
|||||||
plugin.backend)
|
plugin.backend)
|
||||||
) {
|
) {
|
||||||
delete loadedPluginMap[id];
|
delete loadedPluginMap[id];
|
||||||
console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
|
console.log(LoggerPrefix, t(
|
||||||
|
'common.console.plugins.unloaded',
|
||||||
|
{ pluginName: id },
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log(LoggerPrefix, `Cannot unload "${id}" plugin`);
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.unload-failed', { pluginName: id }),
|
||||||
|
);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(LoggerPrefix, `Cannot unload "${id}" plugin`);
|
console.error(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.unload-failed', { pluginName: id }),
|
||||||
|
);
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
@ -100,18 +111,24 @@ export const forceLoadMainPlugin = async (
|
|||||||
) {
|
) {
|
||||||
loadedPluginMap[id] = plugin;
|
loadedPluginMap[id] = plugin;
|
||||||
} else {
|
} else {
|
||||||
console.log(LoggerPrefix, `Cannot load "${id}" plugin`);
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.load-failed', { pluginName: id }),
|
||||||
|
);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
|
console.error(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.initialize-failed', { pluginName: id }),
|
||||||
|
);
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAllMainPlugins = async (win: BrowserWindow) => {
|
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 pluginConfigs = config.plugins.getPlugins();
|
||||||
const queue: Promise<void>[] = [];
|
const queue: Promise<void>[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { setApplicationMenu } from '@/menu';
|
|||||||
|
|
||||||
import { LoggerPrefix } from '@/utils';
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
||||||
import type { PluginConfig } from '@/types/plugins';
|
import type { PluginConfig } from '@/types/plugins';
|
||||||
@ -48,9 +50,15 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
} else return;
|
} else return;
|
||||||
|
|
||||||
console.log(LoggerPrefix, `Successfully loaded '${id}::menu'`);
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.loaded', { pluginName: `${id}::menu` })
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(LoggerPrefix, `Cannot initialize '${id}::menu': `);
|
console.error(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('common.console.plugins.initialize-failed', { pluginName: `${id}::menu` }),
|
||||||
|
);
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
|||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { PreloadContext } from '@/types/contexts';
|
import type { PreloadContext } from '@/types/contexts';
|
||||||
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
||||||
|
|
||||||
@ -31,10 +33,13 @@ export const forceUnloadPreloadPlugin = async (id: string) => {
|
|||||||
context: createContext(id),
|
context: createContext(id),
|
||||||
});
|
});
|
||||||
if (hasStopped || (hasStopped === null && loadedPluginMap[id].preload)) {
|
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];
|
delete loadedPluginMap[id];
|
||||||
} else {
|
} 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;
|
loadedPluginMap[id] = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(LoggerPrefix, `"${id}" plugin is loaded`);
|
console.log(LoggerPrefix, t('common.console.plugins.loaded', { pluginName: id }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
|
console.error(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id }));
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
|||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
||||||
const loadedPluginMap: Record<
|
const loadedPluginMap: Record<
|
||||||
@ -54,9 +55,9 @@ export const forceUnloadRendererPlugin = async (id: string) => {
|
|||||||
document.querySelector(`style#plugin-${id}`)?.remove();
|
document.querySelector(`style#plugin-${id}`)?.remove();
|
||||||
}
|
}
|
||||||
if (hasStopped || (hasStopped === null && plugin?.renderer)) {
|
if (hasStopped || (hasStopped === null && plugin?.renderer)) {
|
||||||
console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
|
console.log(LoggerPrefix, t('common.console.plugins.unloaded', { pluginName: id }));
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
console.log(LoggerPrefix, `Cannot initialize "${id}" plugin`);
|
console.log(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
115
src/menu.ts
115
src/menu.ts
@ -18,6 +18,8 @@ import { startingPages } from './providers/extracted-data';
|
|||||||
import promptOptions from './providers/prompt-options';
|
import promptOptions from './providers/prompt-options';
|
||||||
|
|
||||||
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
|
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
|
||||||
|
import { setLanguage, t } from '@/i18n';
|
||||||
|
import { languageResources } from '@/i18n/resources';
|
||||||
|
|
||||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ export const mainMenuTemplate = async (
|
|||||||
{
|
{
|
||||||
label: pluginLabel,
|
label: pluginLabel,
|
||||||
submenu: [
|
submenu: [
|
||||||
pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu),
|
pluginEnabledMenu(id, t('main.menu.plugins.enabled'), true, innerRefreshMenu),
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
...template,
|
...template,
|
||||||
],
|
],
|
||||||
@ -102,16 +104,18 @@ export const mainMenuTemplate = async (
|
|||||||
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const availableLanguages = Object.keys(languageResources) as unknown as (keyof typeof languageResources)[];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Plugins',
|
label: t('main.menu.plugins.label'),
|
||||||
submenu: pluginMenus,
|
submenu: pluginMenus,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Options',
|
label: t('main.menu.options.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Auto-update',
|
label: t('main.menu.options.submenu.auto-update'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.autoUpdates'),
|
checked: config.get('options.autoUpdates'),
|
||||||
click(item: MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.resumeOnStart'),
|
checked: config.get('options.resumeOnStart'),
|
||||||
click(item: MenuItem) {
|
click(item: MenuItem) {
|
||||||
@ -127,7 +131,7 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Starting page',
|
label: t('main.menu.options.submenu.starting-page.label'),
|
||||||
submenu: (() => {
|
submenu: (() => {
|
||||||
const subMenuArray: Electron.MenuItemConstructorOptions[] =
|
const subMenuArray: Electron.MenuItemConstructorOptions[] =
|
||||||
Object.keys(startingPages).map((name) => ({
|
Object.keys(startingPages).map((name) => ({
|
||||||
@ -139,7 +143,7 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
subMenuArray.unshift({
|
subMenuArray.unshift({
|
||||||
label: 'Unset',
|
label: t('main.menu.options.submenu.starting-page.unset'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.get('options.startingPage') === '',
|
checked: config.get('options.startingPage') === '',
|
||||||
click() {
|
click() {
|
||||||
@ -150,10 +154,10 @@ export const mainMenuTemplate = async (
|
|||||||
})(),
|
})(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Visual Tweaks',
|
label: t('main.menu.options.submenu.visual-tweaks.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Remove upgrade button',
|
label: t('main.menu.options.submenu.visual-tweaks.submenu.remove-upgrade-button'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.removeUpgradeButton'),
|
checked: config.get('options.removeUpgradeButton'),
|
||||||
click(item: MenuItem) {
|
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: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Default',
|
label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.default'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: !config.get('options.likeButtons'),
|
checked: !config.get('options.likeButtons'),
|
||||||
click() {
|
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',
|
type: 'radio',
|
||||||
checked: config.get('options.likeButtons') === 'force',
|
checked: config.get('options.likeButtons') === 'force',
|
||||||
click() {
|
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',
|
type: 'radio',
|
||||||
checked: config.get('options.likeButtons') === 'hide',
|
checked: config.get('options.likeButtons') === 'hide',
|
||||||
click() {
|
click() {
|
||||||
@ -193,10 +197,10 @@ export const mainMenuTemplate = async (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Theme',
|
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'No theme',
|
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
|
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
|
||||||
click() {
|
click() {
|
||||||
@ -205,7 +209,7 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Import custom CSS file',
|
label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.import-css-file'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
async click() {
|
async click() {
|
||||||
const { filePaths } = await dialog.showOpenDialog({
|
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',
|
type: 'checkbox',
|
||||||
checked: true,
|
checked: true,
|
||||||
click(item: MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.alwaysOnTop'),
|
checked: config.get('options.alwaysOnTop'),
|
||||||
click(item: MenuItem) {
|
click(item: MenuItem) {
|
||||||
@ -245,7 +249,7 @@ export const mainMenuTemplate = async (
|
|||||||
...((is.windows() || is.linux()
|
...((is.windows() || is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: 'Hide menu',
|
label: t('main.menu.options.submenu.hide-menu.label'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.hideMenu'),
|
checked: config.get('options.hideMenu'),
|
||||||
click(item) {
|
click(item) {
|
||||||
@ -253,9 +257,8 @@ export const mainMenuTemplate = async (
|
|||||||
if (item.checked && !config.get('options.hideMenuWarned')) {
|
if (item.checked && !config.get('options.hideMenuWarned')) {
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Hide Menu Enabled',
|
title: t('main.menu.options.submenu.hide-menu.dialog.title'),
|
||||||
message:
|
message: t('main.menu.options.submenu.hide-menu.dialog.message'),
|
||||||
'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -267,7 +270,7 @@ export const mainMenuTemplate = async (
|
|||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
// 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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.startAtLogin'),
|
checked: config.get('options.startAtLogin'),
|
||||||
click(item) {
|
click(item) {
|
||||||
@ -277,10 +280,10 @@ export const mainMenuTemplate = async (
|
|||||||
]
|
]
|
||||||
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
||||||
{
|
{
|
||||||
label: 'Tray',
|
label: t('main.menu.options.submenu.tray.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Disabled',
|
label: t('main.menu.options.submenu.tray.submenu.disabled'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: !config.get('options.tray'),
|
checked: !config.get('options.tray'),
|
||||||
click() {
|
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',
|
type: 'radio',
|
||||||
checked:
|
checked:
|
||||||
config.get('options.tray') && config.get('options.appVisible'),
|
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',
|
type: 'radio',
|
||||||
checked:
|
checked:
|
||||||
config.get('options.tray') && !config.get('options.appVisible'),
|
config.get('options.tray') && !config.get('options.appVisible'),
|
||||||
@ -310,7 +313,7 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Play/Pause on click',
|
label: t('main.menu.options.submenu.tray.submenu.play-pause-on-click'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.trayClickPlayPause'),
|
checked: config.get('options.trayClickPlayPause'),
|
||||||
click(item: MenuItem) {
|
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' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Advanced options',
|
label: t('main.menu.options.submenu.advanced-options.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Set Proxy',
|
label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.label'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
async click(item: MenuItem) {
|
async click(item: MenuItem) {
|
||||||
await setProxy(item, win);
|
await setProxy(item, win);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Override useragent',
|
label: t('main.menu.options.submenu.advanced-options.submenu.override-user-agent'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.overrideUserAgent'),
|
checked: config.get('options.overrideUserAgent'),
|
||||||
click(item: MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.disableHardwareAcceleration'),
|
checked: config.get('options.disableHardwareAcceleration'),
|
||||||
click(item: MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.restartOnConfigChanges'),
|
checked: config.get('options.restartOnConfigChanges'),
|
||||||
click(item: MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.get('options.autoResetAppCache'),
|
checked: config.get('options.autoResetAppCache'),
|
||||||
click(item: MenuItem) {
|
click(item: MenuItem) {
|
||||||
@ -374,7 +397,7 @@ export const mainMenuTemplate = async (
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
is.macOS()
|
is.macOS()
|
||||||
? {
|
? {
|
||||||
label: 'Toggle DevTools',
|
label: t('main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools'),
|
||||||
// Cannot use "toggleDevTools" role in macOS
|
// Cannot use "toggleDevTools" role in macOS
|
||||||
click() {
|
click() {
|
||||||
const { webContents } = win;
|
const { webContents } = win;
|
||||||
@ -387,7 +410,7 @@ export const mainMenuTemplate = async (
|
|||||||
}
|
}
|
||||||
: { role: 'toggleDevTools' },
|
: { role: 'toggleDevTools' },
|
||||||
{
|
{
|
||||||
label: 'Edit config.json',
|
label: t('main.menu.options.submenu.advanced-options.submenu.edit-config-json'),
|
||||||
click() {
|
click() {
|
||||||
config.edit();
|
config.edit();
|
||||||
},
|
},
|
||||||
@ -397,7 +420,7 @@ export const mainMenuTemplate = async (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: t('main.menu.view.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{ role: 'reload' },
|
{ role: 'reload' },
|
||||||
{ role: 'forceReload' },
|
{ role: 'forceReload' },
|
||||||
@ -416,10 +439,10 @@ export const mainMenuTemplate = async (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Navigation',
|
label: t('main.menu.navigation.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Go back',
|
label: t('main.menu.navigation.submenu.go-back'),
|
||||||
click() {
|
click() {
|
||||||
if (win.webContents.canGoBack()) {
|
if (win.webContents.canGoBack()) {
|
||||||
win.webContents.goBack();
|
win.webContents.goBack();
|
||||||
@ -427,7 +450,7 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Go forward',
|
label: t('main.menu.navigation.submenu.go-forward'),
|
||||||
click() {
|
click() {
|
||||||
if (win.webContents.canGoForward()) {
|
if (win.webContents.canGoForward()) {
|
||||||
win.webContents.goForward();
|
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() {
|
click() {
|
||||||
const currentURL = win.webContents.getURL();
|
const currentURL = win.webContents.getURL();
|
||||||
clipboard.writeText(currentURL);
|
clipboard.writeText(currentURL);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Restart App',
|
label: t('main.menu.navigation.submenu.restart'),
|
||||||
click: restart,
|
click: restart,
|
||||||
},
|
},
|
||||||
{ role: 'quit' },
|
{ role: 'quit' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'About',
|
label: t('main.menu.about'),
|
||||||
submenu: [{ role: 'about' }],
|
submenu: [{ role: 'about' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -486,13 +509,13 @@ export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
|
|||||||
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Set Proxy',
|
title: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.title'),
|
||||||
label: 'Enter Proxy Address: (leave empty to disable)',
|
label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.label'),
|
||||||
value: config.get('options.proxy'),
|
value: config.get('options.proxy'),
|
||||||
type: 'input',
|
type: 'input',
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'url',
|
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,
|
width: 450,
|
||||||
...promptOptions(),
|
...promptOptions(),
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import {
|
|||||||
import injectCliqzPreload from './injectors/inject-cliqz-preload';
|
import injectCliqzPreload from './injectors/inject-cliqz-preload';
|
||||||
import { inject, isInjected } from './injectors/inject';
|
import { inject, isInjected } from './injectors/inject';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
interface AdblockerConfig {
|
interface AdblockerConfig {
|
||||||
@ -41,8 +43,8 @@ interface AdblockerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Adblocker',
|
name: t('plugins.adblocker.name'),
|
||||||
description: 'Block all ads and tracking out of the box',
|
description: t('plugins.adblocker.description'),
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -56,7 +58,7 @@ export default createPlugin({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Blocker',
|
label: t('plugins.adblocker.menu.blocker'),
|
||||||
submenu: Object.values(blockers).map((blocker) => ({
|
submenu: Object.values(blockers).map((blocker) => ({
|
||||||
label: blocker,
|
label: blocker,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import { FastAverageColor } from 'fast-average-color';
|
|||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Album Color Theme',
|
name: t('plugins.album-color-theme.name'),
|
||||||
description:
|
description: t('plugins.album-color-theme.description'),
|
||||||
'Applies a dynamic theme and visual effects based on the album color palette',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type AmbientModePluginConfig = {
|
export type AmbientModePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -24,9 +25,8 @@ const defaultConfig: AmbientModePluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Ambient Mode',
|
name: t('plugins.ambient-mode.name'),
|
||||||
description:
|
description: t('plugins.ambient-mode.description'),
|
||||||
'Applies a lighting effect by casting gentle colors from the video, into your screen’s background.',
|
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
stylesheets: [style],
|
stylesheets: [style],
|
||||||
@ -42,9 +42,11 @@ export default createPlugin({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Smoothness transition',
|
label: t('plugins.ambient-mode.menu.smoothness-transition.label'),
|
||||||
submenu: interpolationTimeList.map((interpolationTime) => ({
|
submenu: interpolationTimeList.map((interpolationTime) => ({
|
||||||
label: `During ${interpolationTime / 1000}s`,
|
label: t('plugins.ambient-mode.menu.smoothness-transition.submenu.during', {
|
||||||
|
interpolationTime: interpolationTime / 1000,
|
||||||
|
}),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.interpolationTime === interpolationTime,
|
checked: config.interpolationTime === interpolationTime,
|
||||||
click() {
|
click() {
|
||||||
@ -53,9 +55,9 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Quality',
|
label: t('plugins.ambient-mode.menu.quality.label'),
|
||||||
submenu: qualityList.map((quality) => ({
|
submenu: qualityList.map((quality) => ({
|
||||||
label: `${quality} pixels`,
|
label: t('plugins.ambient-mode.menu.quality.submenu.pixels', { quality }),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.quality === quality,
|
checked: config.quality === quality,
|
||||||
click() {
|
click() {
|
||||||
@ -64,9 +66,9 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Size',
|
label: t('plugins.ambient-mode.menu.size.label'),
|
||||||
submenu: sizeList.map((size) => ({
|
submenu: sizeList.map((size) => ({
|
||||||
label: `${size}%`,
|
label: t('plugins.ambient-mode.menu.size.submenu.percent', { size }),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.size === size,
|
checked: config.size === size,
|
||||||
click() {
|
click() {
|
||||||
@ -75,9 +77,9 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Buffer',
|
label: t('plugins.ambient-mode.menu.buffer.label'),
|
||||||
submenu: bufferList.map((buffer) => ({
|
submenu: bufferList.map((buffer) => ({
|
||||||
label: `${buffer}`,
|
label: t('plugins.ambient-mode.menu.buffer.submenu.buffer', { buffer }),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.buffer === buffer,
|
checked: config.buffer === buffer,
|
||||||
click() {
|
click() {
|
||||||
@ -86,9 +88,9 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Opacity',
|
label: t('plugins.ambient-mode.menu.opacity.label'),
|
||||||
submenu: opacityList.map((opacity) => ({
|
submenu: opacityList.map((opacity) => ({
|
||||||
label: `${opacity * 100}%`,
|
label: t('plugins.ambient-mode.menu.opacity.submenu.percent', { opacity: opacity * 100 }),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.opacity === opacity,
|
checked: config.opacity === opacity,
|
||||||
click() {
|
click() {
|
||||||
@ -97,9 +99,9 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Blur amount',
|
label: t('plugins.ambient-mode.menu.blur-amount.label'),
|
||||||
submenu: blurAmountList.map((blur) => ({
|
submenu: blurAmountList.map((blur) => ({
|
||||||
label: `${blur} pixels`,
|
label: t('plugins.ambient-mode.menu.blur-amount.submenu.pixels', { blurAmount: blur }),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.blur === blur,
|
checked: config.blur === blur,
|
||||||
click() {
|
click() {
|
||||||
@ -108,7 +110,7 @@ export default createPlugin({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Using fullscreen',
|
label: t('plugins.ambient-mode.menu.use-fullscreen.label'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.fullscreen,
|
checked: config.fullscreen,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Audio Compressor',
|
name: t('plugins.audio-compressor.name'),
|
||||||
description:
|
description: t('plugins.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)',
|
|
||||||
|
|
||||||
renderer() {
|
renderer() {
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Blur Navigation Bar',
|
name: t('plugins.blur-nav-bar.name'),
|
||||||
description: 'makes navigation bar transparent and blurry',
|
description: t('plugins.blur-nav-bar.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
stylesheets: [style],
|
stylesheets: [style],
|
||||||
renderer() {},
|
renderer() {},
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Bypass Age Restrictions',
|
name: t('plugins.bypass-age-restrictions.name'),
|
||||||
description: "bypass YouTube's age verification",
|
description: t('plugins.bypass-age-restrictions.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
|
|
||||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
// 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 promptOptions from '@/providers/prompt-options';
|
||||||
import { createBackend } from '@/utils';
|
import { createBackend } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createBackend({
|
export default createBackend({
|
||||||
start({ ipc: { handle }, window }) {
|
start({ ipc: { handle }, window }) {
|
||||||
@ -10,8 +11,10 @@ export default createBackend({
|
|||||||
async (captionLabels: Record<string, string>, currentIndex: string) =>
|
async (captionLabels: Record<string, string>, currentIndex: string) =>
|
||||||
await prompt(
|
await prompt(
|
||||||
{
|
{
|
||||||
title: 'Choose Caption',
|
title: t('plugins.captions-selector.prompt.selector.title'),
|
||||||
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
label: t('plugins.captions-selector.prompt.selector.label', {
|
||||||
|
language: captionLabels[currentIndex] || t('plugins.captions-selector.prompt.selector.none'),
|
||||||
|
}),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
value: currentIndex,
|
value: currentIndex,
|
||||||
selectOptions: captionLabels,
|
selectOptions: captionLabels,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { YoutubePlayer } from '@/types/youtube-player';
|
|||||||
|
|
||||||
import backend from './back';
|
import backend from './back';
|
||||||
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin<
|
export default createPlugin<
|
||||||
unknown,
|
unknown,
|
||||||
@ -18,8 +19,8 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
CaptionsSelectorConfig
|
CaptionsSelectorConfig
|
||||||
>({
|
>({
|
||||||
name: 'Captions Selector',
|
name: t('plugins.captions-selector.name'),
|
||||||
description: 'Caption selector for YouTube Music audio tracks',
|
description: t('plugins.captions-selector.description'),
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
disableCaptions: false,
|
disableCaptions: false,
|
||||||
@ -31,7 +32,7 @@ export default createPlugin<
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Automatically select last used caption',
|
label: t('plugins.captions-selector.menu.autoload'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.autoload as boolean,
|
checked: config.autoload as boolean,
|
||||||
click(item) {
|
click(item) {
|
||||||
@ -39,7 +40,7 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'No captions by default',
|
label: t('plugins.captions-selector.menu.disable-captions'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.disableCaptions as boolean,
|
checked: config.disableCaptions as boolean,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin<
|
export default createPlugin<
|
||||||
unknown,
|
unknown,
|
||||||
@ -8,8 +9,8 @@ export default createPlugin<
|
|||||||
isCompactSidebarDisabled: () => boolean;
|
isCompactSidebarDisabled: () => boolean;
|
||||||
}
|
}
|
||||||
>({
|
>({
|
||||||
name: 'Compact Sidebar',
|
name: t('plugins.compact-sidebar.name'),
|
||||||
description: 'Always set the sidebar in compact mode',
|
description: t('plugins.compact-sidebar.description'),
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { createPlugin } from '@/utils';
|
|||||||
import { VolumeFader } from './fader';
|
import { VolumeFader } from './fader';
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type CrossfadePluginConfig = {
|
export type CrossfadePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -29,8 +30,8 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
CrossfadePluginConfig
|
CrossfadePluginConfig
|
||||||
>({
|
>({
|
||||||
name: 'Crossfade [beta]',
|
name: t('plugins.crossfade.name'),
|
||||||
description: 'Crossfade between songs',
|
description: t('plugins.crossfade.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -67,11 +68,11 @@ export default createPlugin<
|
|||||||
): Promise<Omit<CrossfadePluginConfig, 'enabled'> | undefined> => {
|
): Promise<Omit<CrossfadePluginConfig, 'enabled'> | undefined> => {
|
||||||
const res = await prompt(
|
const res = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Crossfade Options',
|
title: t('plugins.crossfade.prompt.options'),
|
||||||
type: 'multiInput',
|
type: 'multiInput',
|
||||||
multiInputOptions: [
|
multiInputOptions: [
|
||||||
{
|
{
|
||||||
label: 'Fade in duration (ms)',
|
label: t('plugins.crossfade.prompt.options.multi-input.fade-in-duration'),
|
||||||
value: options.fadeInDuration,
|
value: options.fadeInDuration,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
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,
|
value: options.fadeOutDuration,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
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,
|
value: options.secondsBeforeEnd,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -100,8 +101,11 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fade scaling',
|
label: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.label'),
|
||||||
selectOptions: { linear: 'Linear', logarithmic: 'Logarithmic' },
|
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,
|
value: options.fadeScaling,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -135,7 +139,7 @@ export default createPlugin<
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Advanced',
|
label: t('plugins.crossfade.menu.advanced'),
|
||||||
async click() {
|
async click() {
|
||||||
const newOptions = await promptCrossfadeValues(
|
const newOptions = await promptCrossfadeValues(
|
||||||
window,
|
window,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
@ -19,8 +21,8 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
DisableAutoPlayPluginConfig
|
DisableAutoPlayPluginConfig
|
||||||
>({
|
>({
|
||||||
name: 'Disable Autoplay',
|
name: t('plugins.disable-autoplay.name'),
|
||||||
description: 'Makes every song start in "paused" mode',
|
description: t('plugins.disable-autoplay.description'),
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -31,7 +33,7 @@ export default createPlugin<
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Applies only on startup',
|
label: t('plugins.disable-autoplay.menu.apply-once'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.applyOnce,
|
checked: config.applyOnce,
|
||||||
async click() {
|
async click() {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { backend } from './main';
|
import { backend } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type DiscordPluginConfig = {
|
export type DiscordPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -35,8 +36,8 @@ export type DiscordPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Discord Rich Presence',
|
name: t('plugins.discord.name'),
|
||||||
description: 'Show your friends what you listen to with Rich Presence',
|
description: t('plugins.discord.description'),
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
|||||||
|
|
||||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
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';
|
import type { DiscordPluginConfig } from './index';
|
||||||
|
|
||||||
@ -38,7 +40,10 @@ const resetInfo = () => {
|
|||||||
info.ready = false;
|
info.ready = false;
|
||||||
clearTimeout(clearActivity);
|
clearTimeout(clearActivity);
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.log('discord disconnected');
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('plugins.discord.backend.disconnected')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cb of refreshCallbacks) {
|
for (const cb of refreshCallbacks) {
|
||||||
@ -68,7 +73,10 @@ let window: Electron.BrowserWindow;
|
|||||||
export const connect = (showError = false) => {
|
export const connect = (showError = false) => {
|
||||||
if (info.rpc.isConnected) {
|
if (info.rpc.isConnected) {
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.log('Attempted to connect with active connection');
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('plugins.discord.backend.already-connected')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -206,7 +214,10 @@ export const backend = createBackend<
|
|||||||
|
|
||||||
info.rpc.on('connected', () => {
|
info.rpc.on('connected', () => {
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.log('discord connected');
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('plugins.discord.backend.connected')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cb of refreshCallbacks) {
|
for (const cb of refreshCallbacks) {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { singleton } from '@/providers/decorators';
|
|||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
import { setMenuOptions } from '@/config/plugins';
|
import { setMenuOptions } from '@/config/plugins';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { DiscordPluginConfig } from './index';
|
import type { DiscordPluginConfig } from './index';
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ export const onMenu = async ({
|
|||||||
click: () => connect(),
|
click: () => connect(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Auto reconnect',
|
label: t('plugins.discord.menu.auto-reconnect'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.autoReconnect,
|
checked: config.autoReconnect,
|
||||||
click(item: Electron.MenuItem) {
|
click(item: Electron.MenuItem) {
|
||||||
@ -41,11 +43,11 @@ export const onMenu = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Clear activity',
|
label: t('plugins.discord.menu.clear-activity'),
|
||||||
click: clear,
|
click: clear,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Clear activity after timeout',
|
label: t('plugins.discord.menu.clear-activity-after-timeout'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.activityTimeoutEnabled,
|
checked: config.activityTimeoutEnabled,
|
||||||
click(item: Electron.MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.playOnYouTubeMusic,
|
checked: config.playOnYouTubeMusic,
|
||||||
click(item: Electron.MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.hideGitHubButton,
|
checked: config.hideGitHubButton,
|
||||||
click(item: Electron.MenuItem) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.hideDurationLeft,
|
checked: config.hideDurationLeft,
|
||||||
click(item: Electron.MenuItem) {
|
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),
|
click: () => setInactivityTimeout(window, config),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -97,8 +99,8 @@ async function setInactivityTimeout(
|
|||||||
) {
|
) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Set Inactivity Timeout',
|
title: t('plugins.discord.prompt.set-inactivity-timeout.title'),
|
||||||
label: 'Enter inactivity timeout in seconds:',
|
label: t('plugins.discord.prompt.set-inactivity-timeout.label'),
|
||||||
value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)),
|
value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)),
|
||||||
type: 'counter',
|
type: 'counter',
|
||||||
counterOptions: { minimum: 0, multiFire: true },
|
counterOptions: { minimum: 0, multiFire: true },
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import style from './style.css?inline';
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { onConfigChange, onMainLoad } from './main';
|
import { onConfigChange, onMainLoad } from './main';
|
||||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type DownloaderPluginConfig = {
|
export type DownloaderPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -25,8 +26,8 @@ export const defaultConfig: DownloaderPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Downloader',
|
name: t('plugins.downloader.name'),
|
||||||
description: 'Downloads MP3 / source audio directly from the interface',
|
description: t('plugins.downloader.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
stylesheets: [style],
|
stylesheets: [style],
|
||||||
|
|||||||
@ -34,6 +34,8 @@ import { cleanupName, getImage, SongInfo } from '@/providers/song-info';
|
|||||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||||
import { cache } from '@/providers/decorators';
|
import { cache } from '@/providers/decorators';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
|
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
|
||||||
|
|
||||||
import type { DownloaderPluginConfig } from '../index';
|
import type { DownloaderPluginConfig } from '../index';
|
||||||
@ -74,9 +76,9 @@ const sendError = (error: Error, source?: string) => {
|
|||||||
console.trace(error);
|
console.trace(error);
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK'],
|
buttons: [t('plugins.downloader.backend.dialog.error.buttons.ok')],
|
||||||
title: 'Error in download!',
|
title: t('plugins.downloader.backend.dialog.error.title'),
|
||||||
message: 'Argh! Apologies, download failed…',
|
message: t('plugins.downloader.backend.dialog.error.message'),
|
||||||
detail: 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;
|
let id: string | null;
|
||||||
if (isId) {
|
if (isId) {
|
||||||
id = idOrUrl;
|
id = idOrUrl;
|
||||||
} else {
|
} else {
|
||||||
id = getVideoId(idOrUrl);
|
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);
|
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw new Error('Video not found');
|
throw new Error(
|
||||||
|
t('plugins.downloader.backend.feedback.video-id-not-found'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = getMetadata(info);
|
const metadata = getMetadata(info);
|
||||||
@ -277,7 +286,11 @@ async function downloadSongUnsafe(
|
|||||||
const stream = await info.download(downloadOptions);
|
const stream = await info.download(downloadOptions);
|
||||||
|
|
||||||
console.info(
|
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);
|
const iterableStream = Utils.streamToIterable(stream);
|
||||||
@ -312,7 +325,9 @@ async function downloadSongUnsafe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendFeedback(null, -1);
|
sendFeedback(null, -1);
|
||||||
console.info(`Done: "${filePath}"`);
|
console.info(t('plugins.downloader.backend.feedback.done', {
|
||||||
|
filePath,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function iterableStreamToTargetFile(
|
async function iterableStreamToTargetFile(
|
||||||
@ -331,13 +346,21 @@ async function iterableStreamToTargetFile(
|
|||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
const ratio = downloaded / contentLength;
|
const ratio = downloaded / contentLength;
|
||||||
const progress = Math.floor(ratio * 100);
|
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
|
// 15% for download, 85% for conversion
|
||||||
// This is a very rough estimate, trying to make the progress bar look nice
|
// This is a very rough estimate, trying to make the progress bar look nice
|
||||||
increasePlaylistProgress(ratio * 0.15);
|
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 buffer = Buffer.concat(chunks);
|
||||||
const safeVideoName = randomBytes(32).toString('hex');
|
const safeVideoName = randomBytes(32).toString('hex');
|
||||||
@ -348,13 +371,18 @@ async function iterableStreamToTargetFile(
|
|||||||
await ffmpeg.load();
|
await ffmpeg.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFeedback('Preparing file…');
|
sendFeedback(t('plugins.downloader.backend.feedback.preparing-file'));
|
||||||
ffmpeg.FS('writeFile', safeVideoName, buffer);
|
ffmpeg.FS('writeFile', safeVideoName, buffer);
|
||||||
|
|
||||||
sendFeedback('Converting…');
|
sendFeedback(t('plugins.downloader.backend.feedback.converting'));
|
||||||
|
|
||||||
ffmpeg.setProgress(({ ratio }) => {
|
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));
|
increasePlaylistProgress(0.15 + (ratio * 0.85));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -371,7 +399,9 @@ async function iterableStreamToTargetFile(
|
|||||||
ffmpeg.FS('unlink', safeVideoName);
|
ffmpeg.FS('unlink', safeVideoName);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFeedback('Saving…');
|
sendFeedback(
|
||||||
|
t('plugins.downloader.backend.feedback.saving'),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ffmpeg.FS('readFile', safeVideoNameWithExtension);
|
return ffmpeg.FS('readFile', safeVideoNameWithExtension);
|
||||||
@ -397,7 +427,9 @@ async function writeID3(
|
|||||||
sendFeedback: (str: string, value?: number) => void,
|
sendFeedback: (str: string, value?: number) => void,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
sendFeedback('Writing ID3 tags...');
|
sendFeedback(
|
||||||
|
t('plugins.downloader.backend.feedback.writing-id3'),
|
||||||
|
);
|
||||||
const tags: NodeID3.Tags = {};
|
const tags: NodeID3.Tags = {};
|
||||||
|
|
||||||
// Create the metadata tags
|
// Create the metadata tags
|
||||||
@ -452,14 +484,22 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl));
|
getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl));
|
||||||
|
|
||||||
if (!playlistId) {
|
if (!playlistId) {
|
||||||
sendError(new Error('No playlist ID found'));
|
sendError(new Error(
|
||||||
|
t('plugins.downloader.backend.feedback.playlist-id-not-found'),
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendFeedback = (message?: unknown) => sendFeedback_(win, message);
|
const sendFeedback = (message?: unknown) => sendFeedback_(win, message);
|
||||||
|
|
||||||
console.log(`trying to get playlist ID: '${playlistId}'`);
|
console.log(
|
||||||
sendFeedback('Getting playlist info…');
|
t('plugins.downloader.backend.feedback.trying-to-get-playlist-id', {
|
||||||
|
playlistId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
sendFeedback(
|
||||||
|
t('plugins.downloader.backend.feedback.getting-playlist-info'),
|
||||||
|
);
|
||||||
let playlist: Playlist;
|
let playlist: Playlist;
|
||||||
const items: YTNodes.MusicResponsiveListItem[] = [];
|
const items: YTNodes.MusicResponsiveListItem[] = [];
|
||||||
try {
|
try {
|
||||||
@ -470,16 +510,18 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
sendError(
|
sendError(
|
||||||
Error(
|
Error(
|
||||||
`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(
|
t('plugins.downloader.backend.feedback.playlist-is-mix-or-private', {
|
||||||
error,
|
error: String(error),
|
||||||
)}`,
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playlist || !playlist.items || playlist.items.length === 0) {
|
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;
|
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||||
@ -500,7 +542,9 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 1) {
|
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!);
|
await downloadSongFromId(items.at(0)!.id!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -514,7 +558,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
const playlistFolder = join(folder, safePlaylistTitle);
|
const playlistFolder = join(folder, safePlaylistTitle);
|
||||||
if (existsSync(playlistFolder)) {
|
if (existsSync(playlistFolder)) {
|
||||||
if (!config.skipExisting) {
|
if (!config.skipExisting) {
|
||||||
sendError(new Error(`The folder ${playlistFolder} already exists`));
|
sendError(new Error(
|
||||||
|
t('plugins.downloader.backend.feedback.folder-already-exists', {
|
||||||
|
playlistFolder,
|
||||||
|
})
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -523,15 +571,23 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
|
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK'],
|
buttons: [t('plugins.downloader.backend.dialog.start-download-playlist.buttons.ok')],
|
||||||
title: 'Started Download',
|
title: t('plugins.downloader.backend.dialog.start-download-playlist.title'),
|
||||||
message: `Downloading Playlist "${playlistTitle}"`,
|
message: t('plugins.downloader.backend.dialog.start-download-playlist.message', {
|
||||||
detail: `(${items.length} songs)`,
|
playlistTitle,
|
||||||
|
}),
|
||||||
|
detail: t('plugins.downloader.backend.dialog.start-download-playlist.detail', {
|
||||||
|
playlistSize: items.length,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
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 {
|
try {
|
||||||
for (const song of items) {
|
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;
|
const trackId = isAlbum ? counter : undefined;
|
||||||
await downloadSongFromId(
|
await downloadSongFromId(
|
||||||
song.id!,
|
song.id!,
|
||||||
@ -561,9 +622,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
).catch((error) =>
|
).catch((error) =>
|
||||||
sendError(
|
sendError(
|
||||||
new Error(
|
new Error(
|
||||||
`Error downloading "${
|
t('plugins.downloader.backend.feedback.error-while-downloading', {
|
||||||
song.author!.name
|
author: song.author!.name,
|
||||||
} - ${song.title!}":\n ${error}`,
|
title: song.title!,
|
||||||
|
error: String(error),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { MenuContext } from '@/types/contexts';
|
|||||||
import type { MenuTemplate } from '@/menu';
|
import type { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
import type { DownloaderPluginConfig } from './index';
|
import type { DownloaderPluginConfig } from './index';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export const onMenu = async ({
|
export const onMenu = async ({
|
||||||
getConfig,
|
getConfig,
|
||||||
@ -21,7 +22,7 @@ export const onMenu = async ({
|
|||||||
click: () => downloadPlaylist(),
|
click: () => downloadPlaylist(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Choose download folder',
|
label: t('plugins.downloader.menu.choose-download-folder'),
|
||||||
click() {
|
click() {
|
||||||
const result = dialog.showOpenDialogSync({
|
const result = dialog.showOpenDialogSync({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
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) => ({
|
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||||
label: preset,
|
label: preset,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
@ -44,7 +45,7 @@ export const onMenu = async ({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Skip existing files',
|
label: t('plugins.downloader.menu.skip-existing'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.skipExisting,
|
checked: config.skipExisting,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -4,11 +4,14 @@ import defaultConfig from '@/config/defaults';
|
|||||||
import { getSongMenu } from '@/providers/dom-elements';
|
import { getSongMenu } from '@/providers/dom-elements';
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import { ElementFromHtml } from '../utils/renderer';
|
import { ElementFromHtml } from '../utils/renderer';
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
import type { DownloaderPluginConfig } from './index';
|
import type { DownloaderPluginConfig } from './index';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
let menu: Element | null = null;
|
let menu: Element | null = null;
|
||||||
let progress: Element | null = null;
|
let progress: Element | null = null;
|
||||||
@ -75,7 +78,10 @@ export const onRendererLoad = ({
|
|||||||
if (progress) {
|
if (progress) {
|
||||||
progress.innerHTML = feedback || 'Download';
|
progress.innerHTML = feedback || 'Download';
|
||||||
} else {
|
} 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"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="ytmcustom-download"
|
id="ytmcustom-download"
|
||||||
>
|
>
|
||||||
Download
|
<ytmd-trans key="plugins.downloader.templates.button"></ytmd-trans>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Exponential Volume',
|
name: t('plugins.exponential-volume.name'),
|
||||||
description:
|
description: t('plugins.exponential-volume.description'),
|
||||||
"Makes the volume slider exponential so it's easier to select lower volumes.",
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -3,14 +3,15 @@ import { createPlugin } from '@/utils';
|
|||||||
import { onMainLoad } from './main';
|
import { onMainLoad } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export interface InAppMenuConfig {
|
export interface InAppMenuConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
hideDOMWindowControls: boolean;
|
hideDOMWindowControls: boolean;
|
||||||
}
|
}
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'In-App Menu',
|
name: t('plugins.in-app-menu.name'),
|
||||||
description: 'gives menu-bars a fancy, dark or album-color look',
|
description: t('plugins.in-app-menu.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled:
|
enabled:
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { InAppMenuConfig } from './index';
|
import type { InAppMenuConfig } from './index';
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { MenuTemplate } from '@/menu';
|
import type { MenuTemplate } from '@/menu';
|
||||||
@ -13,7 +15,7 @@ export const onMenu = async ({
|
|||||||
if (is.linux()) {
|
if (is.linux()) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Hide DOM Window Controls',
|
label: t('plugins.in-app-menu.hide-dom-window-controls'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.hideDOMWindowControls,
|
checked: config.hideDOMWindowControls,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export const createPanel = (
|
|||||||
children.push(...children);
|
children.push(...children);
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.appendChild(menu);
|
return panel.appendChild(menu);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* methods */
|
/* methods */
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import registerCallback from '@/providers/song-info';
|
import registerCallback from '@/providers/song-info';
|
||||||
import { addScrobble, getAndSetSessionKey, setNowPlaying } from './main';
|
import { addScrobble, getAndSetSessionKey, setNowPlaying } from './main';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export interface LastFmPluginConfig {
|
export interface LastFmPluginConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -33,8 +34,8 @@ export interface LastFmPluginConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Last.fm',
|
name: t('plugins.last-fm.name'),
|
||||||
description: 'Add scrobbling support for Last.fm',
|
description: t('plugins.last-fm.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { net } from 'electron';
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import registerCallback from '@/providers/song-info';
|
import registerCallback from '@/providers/song-info';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
type LumiaData = {
|
type LumiaData = {
|
||||||
origin: string;
|
origin: string;
|
||||||
@ -23,8 +24,8 @@ type LumiaData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Lumia Stream [beta]',
|
name: t('plugins.lumiastream.name'),
|
||||||
description: 'Adds Lumia Stream support',
|
description: t('plugins.lumiastream.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import style from './style.css?inline';
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { onConfigChange, onMainLoad } from './main';
|
import { onConfigChange, onMainLoad } from './main';
|
||||||
import { onRendererLoad } from './renderer';
|
import { onRendererLoad } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type LyricsGeniusPluginConfig = {
|
export type LyricsGeniusPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -9,8 +10,8 @@ export type LyricsGeniusPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Lyrics Genius',
|
name: t('plugins.lyrics-genius.name'),
|
||||||
description: 'Adds lyrics support for most songs',
|
description: t('plugins.lyrics-genius.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -22,7 +23,7 @@ export default createPlugin({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Romanized Lyrics',
|
label: t('plugins.lyrics-genius.menu.romanized-lyrics'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.romanizedLyrics,
|
checked: config.romanizedLyrics,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import type { SongInfo } from '@/providers/song-info';
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
|
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export const onRendererLoad = ({
|
export const onRendererLoad = ({
|
||||||
ipc: { invoke, on },
|
ipc: { invoke, on },
|
||||||
@ -55,7 +58,10 @@ export const onRendererLoad = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronIs.dev()) {
|
if (window.electronIs.dev()) {
|
||||||
console.log('Fetched lyrics from Genius');
|
console.log(
|
||||||
|
LoggerPrefix,
|
||||||
|
t('plugins.lyric-genius.renderer.fetched-lyrics'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tryToInjectLyric = (callback?: () => void) => {
|
const tryToInjectLyric = (callback?: () => void) => {
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import style from './style.css?inline';
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import forwardHTML from './templates/forward.html?raw';
|
import forwardHTML from './templates/forward.html?raw';
|
||||||
import backHTML from './templates/back.html?raw';
|
import backHTML from './templates/back.html?raw';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Navigation',
|
name: t('plugins.navigation.name'),
|
||||||
description:
|
description: t('plugins.navigation.description'),
|
||||||
'Next/Back navigation arrows directly integrated in the interface, like in your favorite browser',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Remove Google Login',
|
name: t('plugins.no-google-login.name'),
|
||||||
description: 'Remove Google login buttons and links from the interface',
|
description: t('plugins.no-google-login.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createPlugin } from '@/utils';
|
|||||||
|
|
||||||
import { onConfigChange, onMainLoad } from './main';
|
import { onConfigChange, onMainLoad } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export interface NotificationsPluginConfig {
|
export interface NotificationsPluginConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -35,9 +36,8 @@ export const defaultConfig: NotificationsPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Notifications',
|
name: t('plugins.notifications.name'),
|
||||||
description:
|
description: t('plugins.notifications.description'),
|
||||||
'Display a notification when a song starts playing (interactive notifications are available on windows)',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
menu: onMenu,
|
menu: onMenu,
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { MenuItem } from 'electron';
|
|||||||
|
|
||||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { NotificationsPluginConfig } from './index';
|
import type { NotificationsPluginConfig } from './index';
|
||||||
|
|
||||||
import type { MenuTemplate } from '@/menu';
|
import type { MenuTemplate } from '@/menu';
|
||||||
@ -34,7 +36,7 @@ export const onMenu = async ({
|
|||||||
if (is.linux()) {
|
if (is.linux()) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Notification Priority',
|
label: t('plugins.notifications.menu.priority'),
|
||||||
submenu: urgencyLevels.map((level) => ({
|
submenu: urgencyLevels.map((level) => ({
|
||||||
label: level.name,
|
label: level.name,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
@ -46,7 +48,7 @@ export const onMenu = async ({
|
|||||||
} else if (is.windows()) {
|
} else if (is.windows()) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Interactive Notifications',
|
label: t('plugins.notifications.menu.interactive'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.interactive,
|
checked: config.interactive,
|
||||||
// Doesn't update until restart
|
// 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)
|
// Submenu with settings for interactive notifications (name shouldn't be too long)
|
||||||
label: 'Interactive Settings',
|
label: t('plugins.notifications.menu.interactive-settings.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Open/Close on tray click',
|
label: t('plugins.notifications.menu.interactive-settings.submenu.tray-controls'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.trayControls,
|
checked: config.trayControls,
|
||||||
click: (item: MenuItem) =>
|
click: (item: MenuItem) =>
|
||||||
setConfig({ trayControls: item.checked }),
|
setConfig({ trayControls: item.checked }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hide Button Text',
|
label: t('plugins.notifications.menu.interactive-settings.submenu.hide-button-text'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.hideButtonText,
|
checked: config.hideButtonText,
|
||||||
click: (item: MenuItem) =>
|
click: (item: MenuItem) =>
|
||||||
setConfig({ hideButtonText: item.checked }),
|
setConfig({ hideButtonText: item.checked }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Refresh on Play/Pause',
|
label: t('plugins.notifications.menu.interactive-settings.submenu.refresh-on-play-pause'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.refreshOnPlayPause,
|
checked: config.refreshOnPlayPause,
|
||||||
click: (item: MenuItem) =>
|
click: (item: MenuItem) =>
|
||||||
@ -80,7 +82,7 @@ export const onMenu = async ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Style',
|
label: t('plugins.notifications.menu.toast-style'),
|
||||||
submenu: getToastStyleMenuItems(config),
|
submenu: getToastStyleMenuItems(config),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -92,7 +94,7 @@ export const onMenu = async ({
|
|||||||
return [
|
return [
|
||||||
...getMenu(),
|
...getMenu(),
|
||||||
{
|
{
|
||||||
label: 'Show notification on unpause',
|
label: t('plugins.notifications.menu.unpause-notification'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.unpauseNotification,
|
checked: config.unpauseNotification,
|
||||||
click: (item) => setConfig({ unpauseNotification: item.checked }),
|
click: (item) => setConfig({ unpauseNotification: item.checked }),
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { createPlugin } from '@/utils';
|
|||||||
import { onConfigChange, onMainLoad } from './main';
|
import { onConfigChange, onMainLoad } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type PictureInPicturePluginConfig = {
|
export type PictureInPicturePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -18,8 +19,8 @@ export type PictureInPicturePluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Picture In Picture',
|
name: t('plugins.picture-in-picture.name'),
|
||||||
description: 'Allows to switch the app to picture-in-picture mode',
|
description: t('plugins.picture-in-picture.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
'enabled': false,
|
'enabled': false,
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import prompt from 'custom-electron-prompt';
|
|||||||
|
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { PictureInPicturePluginConfig } from './index';
|
import type { PictureInPicturePluginConfig } from './index';
|
||||||
|
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
@ -16,7 +18,7 @@ export const onMenu = async ({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Always on top',
|
label: t('plugins.picture-in-picture.menu.always-on-top'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.alwaysOnTop,
|
checked: config.alwaysOnTop,
|
||||||
click(item) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.savePosition,
|
checked: config.savePosition,
|
||||||
click(item) {
|
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',
|
type: 'checkbox',
|
||||||
checked: config.saveSize,
|
checked: config.saveSize,
|
||||||
click(item) {
|
click(item) {
|
||||||
@ -41,19 +43,19 @@ export const onMenu = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hotkey',
|
label: t('plugins.picture-in-picture.menu.hotkey.label'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: !!config.hotkey,
|
checked: !!config.hotkey,
|
||||||
async click(item) {
|
async click(item) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Picture in Picture Hotkey',
|
title: t('plugins.picture-in-picture.menu.prompt.title'),
|
||||||
label: 'Choose a hotkey for toggling Picture in Picture',
|
label: t('plugins.picture-in-picture.menu.prompt.label'),
|
||||||
type: 'keybind',
|
type: 'keybind',
|
||||||
keybindOptions: [
|
keybindOptions: [
|
||||||
{
|
{
|
||||||
value: 'hotkey',
|
value: 'hotkey',
|
||||||
label: 'Hotkey',
|
label: t('plugins.picture-in-picture.menu.prompt.keybind-options.hotkey'),
|
||||||
default: config.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',
|
type: 'checkbox',
|
||||||
checked: config.useNativePiP,
|
checked: config.useNativePiP,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="ytmcustom-pip"
|
id="ytmcustom-pip"
|
||||||
>
|
>
|
||||||
Picture in picture
|
<ytmd-trans key="plugins.picture-in-picture.templates.button"></ytmd-trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { onPlayerApiReady, onUnload } from './renderer';
|
import { onPlayerApiReady, onUnload } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Playback Speed',
|
name: t('plugins.playback-speed.name'),
|
||||||
description:
|
description: t('plugins.playback-speed.description'),
|
||||||
'Listen fast, listen slow! Adds a slider that controls song speed',
|
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="ytmcustom-playback-speed"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,32 +7,41 @@ import { createPlugin } from '@/utils';
|
|||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
import { overrideListener } from './override';
|
import { overrideListener } from './override';
|
||||||
import { onConfigChange, onPlayerApiReady } from './renderer';
|
import { onConfigChange, onPlayerApiReady } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type PreciseVolumePluginConfig = {
|
export type PreciseVolumePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Percentage of volume to change
|
||||||
|
*/
|
||||||
steps: number;
|
steps: number;
|
||||||
|
/**
|
||||||
|
* Enable ArrowUp + ArrowDown local shortcuts
|
||||||
|
*/
|
||||||
arrowsShortcut: boolean;
|
arrowsShortcut: boolean;
|
||||||
globalShortcuts: {
|
globalShortcuts: {
|
||||||
volumeUp: string;
|
volumeUp: string;
|
||||||
volumeDown: string;
|
volumeDown: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Plugin save volume between session here
|
||||||
|
*/
|
||||||
savedVolume: number | undefined;
|
savedVolume: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Precise Volume',
|
name: t('plugins.precise-volume.name'),
|
||||||
description:
|
description: t('plugins.precise-volume.description'),
|
||||||
'Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
steps: 1, // Percentage of volume to change
|
steps: 1,
|
||||||
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
|
arrowsShortcut: true,
|
||||||
globalShortcuts: {
|
globalShortcuts: {
|
||||||
volumeUp: '',
|
volumeUp: '',
|
||||||
volumeDown: '',
|
volumeDown: '',
|
||||||
},
|
},
|
||||||
savedVolume: undefined, // Plugin save volume between session here
|
savedVolume: undefined,
|
||||||
} as PreciseVolumePluginConfig,
|
} as PreciseVolumePluginConfig,
|
||||||
stylesheets: [hudStyle],
|
stylesheets: [hudStyle],
|
||||||
menu: async ({ setConfig, getConfig, window }) => {
|
menu: async ({ setConfig, getConfig, window }) => {
|
||||||
@ -66,8 +75,8 @@ export default createPlugin({
|
|||||||
async function promptVolumeSteps(options: PreciseVolumePluginConfig) {
|
async function promptVolumeSteps(options: PreciseVolumePluginConfig) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Volume Steps',
|
title: t('plugins.precise-volume.prompt.volume-steps.title'),
|
||||||
label: 'Choose Volume Increase/Decrease Steps',
|
label: t('plugins.precise-volume.prompt.volume-steps.label'),
|
||||||
value: options.steps || 1,
|
value: options.steps || 1,
|
||||||
type: 'counter',
|
type: 'counter',
|
||||||
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||||
@ -89,17 +98,17 @@ export default createPlugin({
|
|||||||
) {
|
) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Global Volume Keybinds',
|
title: t('plugins.precise-volume.prompt.global-shortcuts.title'),
|
||||||
label: 'Choose Global Volume Keybinds:',
|
label: t('plugins.precise-volume.prompt.global-shortcuts.label'),
|
||||||
type: 'keybind',
|
type: 'keybind',
|
||||||
keybindOptions: [
|
keybindOptions: [
|
||||||
kb(
|
kb(
|
||||||
'Increase Volume',
|
t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.increase'),
|
||||||
'volumeUp',
|
'volumeUp',
|
||||||
options.globalShortcuts?.volumeUp,
|
options.globalShortcuts?.volumeUp,
|
||||||
),
|
),
|
||||||
kb(
|
kb(
|
||||||
'Decrease Volume',
|
t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.decrease'),
|
||||||
'volumeDown',
|
'volumeDown',
|
||||||
options.globalShortcuts?.volumeDown,
|
options.globalShortcuts?.volumeDown,
|
||||||
),
|
),
|
||||||
@ -132,7 +141,7 @@ export default createPlugin({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Local Arrowkeys Controls',
|
label: t('plugins.precise-volume.menu.arrows-shortcuts'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: Boolean(config.arrowsShortcut),
|
checked: Boolean(config.arrowsShortcut),
|
||||||
click(item) {
|
click(item) {
|
||||||
@ -140,7 +149,7 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Global Hotkeys',
|
label: t('plugins.precise-volume.menu.global-shortcuts'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: Boolean(
|
checked: Boolean(
|
||||||
config.globalShortcuts?.volumeUp ??
|
config.globalShortcuts?.volumeUp ??
|
||||||
@ -149,7 +158,7 @@ export default createPlugin({
|
|||||||
click: (item) => promptGlobalShortcuts(config, item),
|
click: (item) => promptGlobalShortcuts(config, item),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Set Custom Volume Steps',
|
label: t('plugins.precise-volume.menu.custom-volume-steps'),
|
||||||
click: () => promptVolumeSteps(config),
|
click: () => promptVolumeSteps(config),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?ra
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Video Quality Changer',
|
name: t('plugins.quality-changer.name'),
|
||||||
description:
|
description: t('plugins.quality-changer.description'),
|
||||||
'Allows changing the video quality with a button on the video overlay',
|
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -24,9 +24,11 @@ export default createPlugin({
|
|||||||
type: 'question',
|
type: 'question',
|
||||||
buttons: qualityLabels,
|
buttons: qualityLabels,
|
||||||
defaultId: currentIndex,
|
defaultId: currentIndex,
|
||||||
title: 'Choose Video Quality',
|
title: t('plugins.quality-changer.backend.dialog.title'),
|
||||||
message: 'Choose Video Quality:',
|
message: t('plugins.quality-changer.backend.dialog.message'),
|
||||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
detail: t('plugins.quality-changer.backend.dialog.detail', {
|
||||||
|
quality: qualityLabels[currentIndex],
|
||||||
|
}),
|
||||||
cancelId: -1,
|
cancelId: -1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { onMainLoad } from './main';
|
import { onMainLoad } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type ShortcutMappingType = {
|
export type ShortcutMappingType = {
|
||||||
previous: string;
|
previous: string;
|
||||||
@ -15,9 +16,8 @@ export type ShortcutsPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Shortcuts (& MPRIS)',
|
name: t('plugins.shortcuts.name'),
|
||||||
description:
|
description: t('plugins.shortcuts.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',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
|||||||
|
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { ShortcutsPluginConfig } from './index';
|
import type { ShortcutsPluginConfig } from './index';
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
@ -29,14 +31,14 @@ export const onMenu = async ({
|
|||||||
) {
|
) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: 'Global Keybinds',
|
title: t('plugins.shortcuts.prompt.keybind.title'),
|
||||||
label: 'Choose Global Keybinds for Songs Control:',
|
label: t('plugins.shortcuts.prompt.keybind.label'),
|
||||||
type: 'keybind',
|
type: 'keybind',
|
||||||
keybindOptions: [
|
keybindOptions: [
|
||||||
// If default=undefined then no default is used
|
// If default=undefined then no default is used
|
||||||
kb('Previous', 'previous', config.global?.previous),
|
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.previous'), 'previous', config.global?.previous),
|
||||||
kb('Play / Pause', 'playPause', config.global?.playPause),
|
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.play-pause'), 'playPause', config.global?.playPause),
|
||||||
kb('Next', 'next', config.global?.next),
|
kb(t('plugins.shortcuts.prompt.keybind.keybind-options.next'), 'next', config.global?.next),
|
||||||
],
|
],
|
||||||
height: 270,
|
height: 270,
|
||||||
...promptOptions(),
|
...promptOptions(),
|
||||||
@ -59,11 +61,11 @@ export const onMenu = async ({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Set Global Song Controls',
|
label: t('plugins.shortcuts.menu.set-keybinds'),
|
||||||
click: () => promptKeybind(config, window),
|
click: () => promptKeybind(config, window),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Override MediaKeys',
|
label: t('plugins.shortcuts.menu.override-media-keys'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.overrideMediaKeys,
|
checked: config.overrideMediaKeys,
|
||||||
click: (item) => setConfig({ overrideMediaKeys: item.checked }),
|
click: (item) => setConfig({ overrideMediaKeys: item.checked }),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { onRendererLoad, onRendererUnload } from './renderer';
|
import { onRendererLoad, onRendererUnload } from './renderer';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export type SkipSilencesPluginConfig = {
|
export type SkipSilencesPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -7,8 +8,8 @@ export type SkipSilencesPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Skip Silences',
|
name: t('plugins.skip-silences.name'),
|
||||||
description: 'Automatically skip silenced sections',
|
description: t('plugins.skip-silences.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { createPlugin } from '@/utils';
|
|||||||
|
|
||||||
import { sortSegments } from './segments';
|
import { sortSegments } from './segments';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type { GetPlayerResponse } from '@/types/get-player-response';
|
import type { GetPlayerResponse } from '@/types/get-player-response';
|
||||||
import type { Segment, SkipSegment } from './types';
|
import type { Segment, SkipSegment } from './types';
|
||||||
|
|
||||||
@ -23,9 +25,8 @@ export type SponsorBlockPluginConfig = {
|
|||||||
let currentSegments: Segment[] = [];
|
let currentSegments: Segment[] = [];
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'SponsorBlock',
|
name: t('plugins.sponsorblock.name'),
|
||||||
description:
|
description: t('plugins.sponsorblock.description'),
|
||||||
"Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing",
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -9,10 +9,11 @@ import { createPlugin } from '@/utils';
|
|||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||||
import { mediaIcons } from '@/types/media-icons';
|
import { mediaIcons } from '@/types/media-icons';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Taskbar Media Control',
|
name: t('plugins.taskbar-mediacontrol.name'),
|
||||||
description: 'Control playback from your Windows taskbar',
|
description: t('plugins.taskbar-mediacontrol.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { type NativeImage, TouchBar } from 'electron';
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import registerCallback from '@/providers/song-info';
|
import registerCallback from '@/providers/song-info';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'TouchBar',
|
name: t('plugins.touchbar.name'),
|
||||||
description: 'Custom TouchBar layout for macOS',
|
description: t('plugins.touchbar.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import is from 'electron-is';
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import registerCallback from '@/providers/song-info';
|
import registerCallback from '@/providers/song-info';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
album: string | null | undefined;
|
album: string | null | undefined;
|
||||||
@ -18,8 +19,8 @@ interface Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Tuna OBS',
|
name: t('plugins.tuna-obs.name'),
|
||||||
description: "Integration with OBS's plugin Tuna",
|
description: t('plugins.tuna-obs.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { createPlugin } from '@/utils';
|
|||||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||||
import { ThumbnailElement } from '@/types/get-player-response';
|
import { ThumbnailElement } from '@/types/get-player-response';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
export type VideoTogglePluginConfig = {
|
export type VideoTogglePluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -16,9 +18,8 @@ export type VideoTogglePluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Video Toggle',
|
name: t('plugins.video-toggle.name'),
|
||||||
description:
|
description: t('plugins.video-toggle.description'),
|
||||||
'Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab',
|
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -28,15 +29,15 @@ export default createPlugin({
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
} as VideoTogglePluginConfig,
|
} as VideoTogglePluginConfig,
|
||||||
stylesheets: [buttonSwitcherStyle, forceHideStyle],
|
stylesheets: [buttonSwitcherStyle, forceHideStyle],
|
||||||
menu: async ({ getConfig, setConfig }) => {
|
menu: async ({ getConfig, setConfig }): Promise<MenuTemplate> => {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Mode',
|
label: t('plugins.video-toggle.menu.mode.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Custom toggle',
|
label: t('plugins.video-toggle.menu.mode.submenu.custom'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.mode === 'custom',
|
checked: config.mode === 'custom',
|
||||||
click() {
|
click() {
|
||||||
@ -44,7 +45,7 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Native toggle',
|
label: t('plugins.video-toggle.menu.mode.submenu.native'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.mode === 'native',
|
checked: config.mode === 'native',
|
||||||
click() {
|
click() {
|
||||||
@ -52,7 +53,7 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Disabled',
|
label: t('plugins.video-toggle.menu.mode.submenu.disabled'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.mode === 'disabled',
|
checked: config.mode === 'disabled',
|
||||||
click() {
|
click() {
|
||||||
@ -62,10 +63,10 @@ export default createPlugin({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Alignment',
|
label: t('plugins.video-toggle.menu.align.label'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Left',
|
label: t('plugins.video-toggle.menu.align.submenu.left'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.align === 'left',
|
checked: config.align === 'left',
|
||||||
click() {
|
click() {
|
||||||
@ -73,7 +74,7 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Middle',
|
label: t('plugins.video-toggle.menu.align.submenu.middle'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.align === 'middle',
|
checked: config.align === 'middle',
|
||||||
click() {
|
click() {
|
||||||
@ -81,7 +82,7 @@ export default createPlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Right',
|
label: t('plugins.video-toggle.menu.align.submenu.right'),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.align === 'right',
|
checked: config.align === 'right',
|
||||||
click() {
|
click() {
|
||||||
@ -91,7 +92,7 @@ export default createPlugin({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Force Remove Video Tab',
|
label: t('plugins.video-toggle.menu.force-hide'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.forceHide,
|
checked: config.forceHide,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
<div class="video-switch-button">
|
<div class="video-switch-button">
|
||||||
<input checked="true" class="video-switch-button-checkbox" type="checkbox" />
|
<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>
|
</div>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
VudioVisualizer as vudio,
|
VudioVisualizer as vudio,
|
||||||
WaveVisualizer as wave,
|
WaveVisualizer as wave,
|
||||||
} from './visualizers';
|
} from './visualizers';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
type WaveColor = {
|
type WaveColor = {
|
||||||
gradient: string[];
|
gradient: string[];
|
||||||
@ -57,8 +58,8 @@ export type VisualizerPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: 'Visualizer',
|
name: t('plugins.visualizer.name'),
|
||||||
description: 'Adds a visualizer to the player',
|
description: t('plugins.visualizer.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -133,7 +134,7 @@ export default createPlugin({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Type',
|
label: t('plugins.visualizer.menu.visualizer-type'),
|
||||||
submenu: visualizerTypes.map((visualizerType) => ({
|
submenu: visualizerTypes.map((visualizerType) => ({
|
||||||
label: visualizerType,
|
label: visualizerType,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
@ -8,8 +10,12 @@ import {
|
|||||||
forceUnloadPreloadPlugin,
|
forceUnloadPreloadPlugin,
|
||||||
loadAllPreloadPlugins,
|
loadAllPreloadPlugins,
|
||||||
} from './loader/preload';
|
} from './loader/preload';
|
||||||
|
import { loadI18n, setLanguage } from '@/i18n';
|
||||||
|
|
||||||
loadAllPreloadPlugins();
|
loadI18n().then(async () => {
|
||||||
|
await setLanguage(config.get('options.language') ?? 'en');
|
||||||
|
loadAllPreloadPlugins();
|
||||||
|
});
|
||||||
|
|
||||||
ipcRenderer.on('plugin:unload', async (_, id: string) => {
|
ipcRenderer.on('plugin:unload', async (_, id: string) => {
|
||||||
await forceUnloadPreloadPlugin(id);
|
await forceUnloadPreloadPlugin(id);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import { startingPages } from './providers/extracted-data';
|
import { startingPages } from './providers/extracted-data';
|
||||||
import setupSongInfo from './providers/song-info-front';
|
import setupSongInfo from './providers/song-info-front';
|
||||||
import {
|
import {
|
||||||
@ -9,6 +11,8 @@ import {
|
|||||||
loadAllRendererPlugins,
|
loadAllRendererPlugins,
|
||||||
} from './loader/renderer';
|
} from './loader/renderer';
|
||||||
|
|
||||||
|
import { loadI18n, setLanguage, t as i18t } from '@/i18n';
|
||||||
|
|
||||||
import type { PluginConfig } from '@/types/plugins';
|
import type { PluginConfig } from '@/types/plugins';
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
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 () => {
|
(async () => {
|
||||||
|
await loadI18n();
|
||||||
|
await setLanguage(window.mainConfig.get('options.language') ?? 'en');
|
||||||
|
window.i18n = {
|
||||||
|
t: i18t.bind(i18next),
|
||||||
|
};
|
||||||
|
defineYTMDTransElements();
|
||||||
|
|
||||||
await loadAllRendererPlugins();
|
await loadAllRendererPlugins();
|
||||||
isPluginLoaded = true;
|
isPluginLoaded = true;
|
||||||
|
|
||||||
|
|||||||
4
src/reset.d.ts
vendored
4
src/reset.d.ts
vendored
@ -5,6 +5,7 @@ import type is from 'electron-is';
|
|||||||
|
|
||||||
import type config from './config';
|
import type config from './config';
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
|
import type { t } from '@/i18n';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Compressor {
|
interface Compressor {
|
||||||
@ -30,6 +31,9 @@ declare global {
|
|||||||
download: () => void;
|
download: () => void;
|
||||||
togglePictureInPicture: () => void;
|
togglePictureInPicture: () => void;
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
|
i18n: {
|
||||||
|
t: typeof t,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BackendContext,
|
BackendContext,
|
||||||
PreloadContext,
|
PreloadContext,
|
||||||
@ -106,12 +108,25 @@ export const startPlugin = async <Config extends PluginConfig>(
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
LoggerPrefix,
|
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;
|
return lifecycle ? true : null;
|
||||||
} catch (err) {
|
} 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);
|
console.trace(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -140,12 +155,25 @@ export const stopPlugin = async <Config extends PluginConfig>(
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
LoggerPrefix,
|
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;
|
return true;
|
||||||
} catch (err) {
|
} 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);
|
console.trace(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user