Compare commits

...

6 Commits

Author SHA1 Message Date
26fa1f85b2 fix(discord-rpc, scrobbler): Align artist and title with the last.fm's de facto standard
- Display only the main artist.
- Display the title in its original language without romanization.

- fix #3358
- fix #3641
2025-09-06 10:25:54 +09:00
555817e2f5 feat(downloader): Add context menu button for playlists and albums (#3768)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2025-09-06 09:12:24 +09:00
f8654dfdb9 fix(i18n): fix missing i18n 2025-09-06 09:08:17 +09:00
96c0fc412c chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (442 of 442 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2025-09-06 00:00:58 +00:00
a70a4106df chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (442 of 442 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2025-09-06 00:00:57 +00:00
895210cbb6 feat(transparent-player): new plugin for Acrylic, Mica or Tabbed effects (#3529)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2025-09-06 09:00:39 +09:00
22 changed files with 438 additions and 64 deletions

View File

@ -31,6 +31,7 @@ export interface DefaultConfig {
likeButtons: string; likeButtons: string;
proxy: string; proxy: string;
startingPage: string; startingPage: string;
backgroundMaterial?: 'none' | 'mica' | 'acrylic' | 'tabbed';
overrideUserAgent: boolean; overrideUserAgent: boolean;
usePodcastParticipantAsArtist: boolean; usePodcastParticipantAsArtist: boolean;
themes: string[]; themes: string[];

View File

@ -853,6 +853,26 @@
"description": "Fügt ein TouchBar-Widget für macOS-Benutzer hinzu", "description": "Fügt ein TouchBar-Widget für macOS-Benutzer hinzu",
"name": "TouchBar" "name": "TouchBar"
}, },
"transparent-player": {
"description": "Macht das Player-Fenster transparent",
"name": "Transparent Player",
"menu": {
"opacity": {
"label": "Hintergrund-Sichtbarkeit",
"submenu": {
"percent": "{{opacity}}%"
}
},
"type": {
"label": "Typ",
"submenu": {
"acrylic": "Acrylic",
"mica": "Mica",
"tabbed": "Tabbed"
}
}
}
},
"tuna-obs": { "tuna-obs": {
"description": "Integration mit dem OBS-Plugin Tuna", "description": "Integration mit dem OBS-Plugin Tuna",
"name": "Tuna OBS" "name": "Tuna OBS"

View File

@ -758,6 +758,7 @@
"token": "Enter ListenBrainz user token" "token": "Enter ListenBrainz user token"
}, },
"scrobble-alternative-title": "Use alternative titles", "scrobble-alternative-title": "Use alternative titles",
"scrobble-alternative-artist": "Use alternative artists",
"scrobble-other-media": "Scrobble other media" "scrobble-other-media": "Scrobble other media"
}, },
"name": "Scrobbler", "name": "Scrobbler",
@ -874,6 +875,27 @@
"description": "Adds a TouchBar widget for macOS users", "description": "Adds a TouchBar widget for macOS users",
"name": "TouchBar" "name": "TouchBar"
}, },
"transparent-player": {
"description": "Makes the app window transparent",
"name": "Transparent Player",
"menu": {
"opacity": {
"label": "Opacity",
"submenu": {
"percent": "{{opacity}}%"
}
},
"type": {
"label": "Type",
"submenu": {
"acrylic": "Acrylic",
"mica": "Mica",
"tabbed": "Tabbed",
"none": "None"
}
}
}
},
"tuna-obs": { "tuna-obs": {
"description": "Integration with OBS's plugin Tuna", "description": "Integration with OBS's plugin Tuna",
"name": "Tuna OBS" "name": "Tuna OBS"

View File

@ -421,6 +421,19 @@
} }
} }
}, },
"custom-output-device": {
"description": "Configure um dispositivo de saída de mídia personalizado para músicas",
"menu": {
"device-selector": "Selecionar dispositivo"
},
"name": "Dispositivo de saída personalizado",
"prompt": {
"device-selector": {
"label": "Escolha o dispositivo de saída de mídia que será usado",
"title": "Selecionar dispositivo de saída"
}
}
},
"disable-autoplay": { "disable-autoplay": {
"description": "Faz a música começar no modo \"pausado\"", "description": "Faz a música começar no modo \"pausado\"",
"menu": { "menu": {
@ -444,7 +457,15 @@
"hide-duration-left": "Ocultar duração restante", "hide-duration-left": "Ocultar duração restante",
"hide-github-button": "Ocultar botão do GitHub", "hide-github-button": "Ocultar botão do GitHub",
"play-on-youtube-music": "Reproduzir no YouTube Music", "play-on-youtube-music": "Reproduzir no YouTube Music",
"set-inactivity-timeout": "Definir tempo limite de inatividade" "set-inactivity-timeout": "Definir tempo limite de inatividade",
"set-status-display-type": {
"label": "Texto de status",
"submenu": {
"artist": "Ouvindo {artist}",
"title": "Ouvindo {song title}",
"youtube-music": "Ouvindo YouTube Music"
}
}
}, },
"name": "Rich Presence do Discord", "name": "Rich Presence do Discord",
"prompt": { "prompt": {

View File

@ -421,6 +421,19 @@
} }
} }
}, },
"custom-output-device": {
"description": "Настройка устройства вывода медиа для песен",
"menu": {
"device-selector": "Выберите устройство"
},
"name": "Пользовательское устройство вывода",
"prompt": {
"device-selector": {
"label": "Выберите устройство вывода медиа, которое будет использоваться",
"title": "Выберите устройство вывода"
}
}
},
"disable-autoplay": { "disable-autoplay": {
"description": "Запускает песню сразу на паузе", "description": "Запускает песню сразу на паузе",
"menu": { "menu": {
@ -444,7 +457,15 @@
"hide-duration-left": "Скрыть сколько осталось времени", "hide-duration-left": "Скрыть сколько осталось времени",
"hide-github-button": "Скрыть ссылку на GitHub", "hide-github-button": "Скрыть ссылку на GitHub",
"play-on-youtube-music": "Воспроизвести на YouTube Music", "play-on-youtube-music": "Воспроизвести на YouTube Music",
"set-inactivity-timeout": "Поставить таймер неактивности" "set-inactivity-timeout": "Поставить таймер неактивности",
"set-status-display-type": {
"label": "Текст статуса",
"submenu": {
"artist": "Слушает {исполнитель}",
"title": "Слушает {название трека}",
"youtube-music": "Слушает YouTube Music"
}
}
}, },
"name": "Discord Rich Presence", "name": "Discord Rich Presence",
"prompt": { "prompt": {

View File

@ -59,7 +59,7 @@ import ErrorHtmlAsset from '@assets/error.html?asset';
import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config'; import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config';
import { type PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
@ -338,8 +338,8 @@ async function createMainWindow() {
titleBarStyle: useInlineMenu titleBarStyle: useInlineMenu
? 'hidden' ? 'hidden'
: is.macOS() : is.macOS()
? 'hiddenInset' ? 'hiddenInset'
: 'default', : 'default',
autoHideMenuBar: config.get('options.hideMenu'), autoHideMenuBar: config.get('options.hideMenu'),
}; };
@ -349,7 +349,7 @@ async function createMainWindow() {
delete decorations.titleBarStyle; delete decorations.titleBarStyle;
} }
const win = new BrowserWindow({ const electronWindowSettings: Electron.BrowserWindowConstructorOptions = {
icon, icon,
width: windowSize.width, width: windowSize.width,
height: windowSize.height, height: windowSize.height,
@ -369,7 +369,10 @@ async function createMainWindow() {
}), }),
}, },
...decorations, ...decorations,
}); };
const win = new BrowserWindow(electronWindowSettings);
await initHook(win); await initHook(win);
initTheme(win); initTheme(win);
@ -529,8 +532,8 @@ app.once('browser-window-created', (_event, win) => {
const updatedUserAgent = is.macOS() const updatedUserAgent = is.macOS()
? userAgents.mac ? userAgents.mac
: is.windows() : is.windows()
? userAgents.windows ? userAgents.windows
: userAgents.linux; : userAgents.linux;
win.webContents.userAgent = updatedUserAgent; win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent; app.userAgentFallback = updatedUserAgent;
@ -951,15 +954,18 @@ function removeContentSecurityPolicy(
betterSession.webRequest.setResolver( betterSession.webRequest.setResolver(
'onHeadersReceived', 'onHeadersReceived',
async (listeners) => { async (listeners) => {
return listeners.reduce(async (accumulator, listener) => { return listeners.reduce(
const acc = await accumulator; async (accumulator, listener) => {
if (acc.cancel) { const acc = await accumulator;
return acc; if (acc.cancel) {
} return acc;
}
const result = await listener.apply(); const result = await listener.apply();
return { ...accumulator, ...result }; return { ...accumulator, ...result };
}, Promise.resolve({ cancel: false })); },
Promise.resolve({ cancel: false }),
);
}, },
); );
} }

View File

@ -31,7 +31,7 @@ export default createPlugin<
alpha?: number, alpha?: number,
ratioMultiply?: number, ratioMultiply?: number,
): string; ): string;
updateColor(): void; updateColor(alpha: number): void;
}, },
{ {
enabled: boolean; enabled: boolean;
@ -143,7 +143,16 @@ export default createPlugin<
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0'); document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
} }
this.updateColor(); let alpha: number | null = null;
if (await window.mainConfig.plugins.isEnabled('transparent-player')) {
const value: unknown = window.mainConfig.get(
'plugins.transparent-player.opacity',
);
if (typeof value === 'number' && value >= 0 && value <= 1) {
alpha = value;
}
}
this.updateColor(alpha ?? 1);
}); });
}, },
onConfigChange(config) { onConfigChange(config) {
@ -163,7 +172,7 @@ export default createPlugin<
} }
return `color-mix(in srgb, ${color} ${originalRatio}, ${keyColor} ${colorRatio})`; return `color-mix(in srgb, ${color} ${originalRatio}, ${keyColor} ${colorRatio})`;
}, },
updateColor() { updateColor(alpha: number) {
const variableMap = { const variableMap = {
'--ytmusic-color-black1': '#212121', '--ytmusic-color-black1': '#212121',
'--ytmusic-color-black2': '#181818', '--ytmusic-color-black2': '#181818',
@ -202,19 +211,20 @@ export default createPlugin<
Object.entries(variableMap).map(([variable, color]) => { Object.entries(variableMap).map(([variable, color]) => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
variable, variable,
this.getMixedColor(color, COLOR_KEY), this.getMixedColor(color, COLOR_KEY, alpha),
'important', 'important',
); );
}); });
document.body.style.setProperty( document.body.style.setProperty(
'background', 'background',
this.getMixedColor('#030303', COLOR_KEY), this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
'important', 'important',
); );
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--ytmusic-background', '--ytmusic-background',
this.getMixedColor('#030303', DARK_COLOR_KEY), // #030303
this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
'important', 'important',
); );
}, },

View File

@ -23,13 +23,3 @@ export enum TimerKey {
UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates
DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries
} }
/**
* An enum for Discord's activity.status_display_type field, governing which field of the activity should be used after
* "Listening to..." in the user's Discord status.
*/
export const DiscordStatusDisplayType = {
YOUTUBE_MUSIC: 0,
ARTIST: 1,
TITLE: 2,
} as const;

View File

@ -99,9 +99,9 @@ export class DiscordService {
const activityInfo: SetActivity = { const activityInfo: SetActivity = {
type: ActivityType.Listening, type: ActivityType.Listening,
statusDisplayType: config.statusDisplayType, statusDisplayType: config.statusDisplayType,
details: truncateString(songInfo.title, 128), // Song title details: truncateString(songInfo.alternativeTitle ?? songInfo.title, 128), // Song title
detailsUrl: songInfo.url ?? undefined, detailsUrl: songInfo.url ?? undefined,
state: truncateString(songInfo.artist, 128), // Artist name state: truncateString(songInfo.tags?.at(0) ?? songInfo.artist, 128), // Artist name
stateUrl: songInfo.artistUrl, stateUrl: songInfo.artistUrl,
largeImageKey: songInfo.imageSrc ?? undefined, largeImageKey: songInfo.imageSrc ?? undefined,
largeImageText: songInfo.album largeImageText: songInfo.album

View File

@ -1,8 +1,9 @@
import { StatusDisplayType } from 'discord-api-types/v10';
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'; import { t } from '@/i18n';
import { DiscordStatusDisplayType } from './constants';
export type DiscordPluginConfig = { export type DiscordPluginConfig = {
enabled: boolean; enabled: boolean;
@ -37,7 +38,7 @@ export type DiscordPluginConfig = {
/** /**
* Controls which field is displayed in the Discord status text * Controls which field is displayed in the Discord status text
*/ */
statusDisplayType: (typeof DiscordStatusDisplayType)[keyof typeof DiscordStatusDisplayType]; statusDisplayType: (typeof StatusDisplayType)[keyof typeof StatusDisplayType];
}; };
export default createPlugin({ export default createPlugin({
@ -52,7 +53,7 @@ export default createPlugin({
playOnYouTubeMusic: true, playOnYouTubeMusic: true,
hideGitHubButton: false, hideGitHubButton: false,
hideDurationLeft: false, hideDurationLeft: false,
statusDisplayType: DiscordStatusDisplayType.ARTIST, statusDisplayType: StatusDisplayType.Details,
} as DiscordPluginConfig, } as DiscordPluginConfig,
menu: onMenu, menu: onMenu,
backend, backend,

View File

@ -1,30 +1,27 @@
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { discordService } from './main'; import { StatusDisplayType } from 'discord-api-types/v10';
import { discordService } from './main';
import { singleton } from '@/providers/decorators'; 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 { t } from '@/i18n';
import { DiscordStatusDisplayType } from './constants';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { DiscordPluginConfig } from './index'; import type { DiscordPluginConfig } from './index';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
const registerRefreshOnce = singleton((refreshMenu: () => void) => { const registerRefreshOnce = singleton((refreshMenu: () => void) => {
discordService?.registerRefreshCallback(refreshMenu); discordService?.registerRefreshCallback(refreshMenu);
}); });
const DiscordStatusDisplayTypeLabels = { const DiscordStatusDisplayTypeLabels: Record<StatusDisplayType, string> = {
[DiscordStatusDisplayType.YOUTUBE_MUSIC]: [StatusDisplayType.Name]:
'plugins.discord.menu.set-status-display-type.submenu.youtube-music', 'plugins.discord.menu.set-status-display-type.submenu.youtube-music',
[DiscordStatusDisplayType.ARTIST]: [StatusDisplayType.State]:
'plugins.discord.menu.set-status-display-type.submenu.artist', 'plugins.discord.menu.set-status-display-type.submenu.artist',
[DiscordStatusDisplayType.TITLE]: [StatusDisplayType.Details]:
'plugins.discord.menu.set-status-display-type.submenu.title', 'plugins.discord.menu.set-status-display-type.submenu.title',
}; };
@ -105,18 +102,24 @@ export const onMenu = async ({
}, },
{ {
label: t('plugins.discord.menu.set-status-display-type.label'), label: t('plugins.discord.menu.set-status-display-type.label'),
submenu: Object.values(DiscordStatusDisplayType).map( submenu: Object.values(StatusDisplayType)
(statusDisplayType) => ({ .filter(
label: t(DiscordStatusDisplayTypeLabels[statusDisplayType]), (v) => typeof StatusDisplayType[v as StatusDisplayType] !== 'number',
)
.map((statusDisplayType) => ({
label: t(
DiscordStatusDisplayTypeLabels[
statusDisplayType as StatusDisplayType
],
),
type: 'radio', type: 'radio',
checked: config.statusDisplayType == statusDisplayType, checked: config.statusDisplayType === statusDisplayType,
click() { click() {
setConfig({ setConfig({
statusDisplayType, statusDisplayType: statusDisplayType as StatusDisplayType,
}); });
}, },
}), })),
),
}, },
]; ];
}; };

View File

@ -6,7 +6,10 @@ 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 { t } from '@/i18n'; import { t } from '@/i18n';
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check'; import {
isAlbumOrPlaylist,
isMusicOrVideoTrack,
} from '@/plugins/utils/renderer/check';
import { DownloadButton } from './templates/download'; import { DownloadButton } from './templates/download';
@ -25,7 +28,7 @@ const menuObserver = new MutationObserver(() => {
if ( if (
!menu || !menu ||
menu.contains(buttonContainer) || menu.contains(buttonContainer) ||
!isMusicOrVideoTrack() || !(isMusicOrVideoTrack() || isAlbumOrPlaylist()) ||
!buttonContainer !buttonContainer
) { ) {
return; return;

View File

@ -329,6 +329,7 @@ export const TitleBar = (props: TitleBarProps) => {
data-macos={props.isMacOS} data-macos={props.isMacOS}
data-show={mouseY() < 32} data-show={mouseY() < 32}
data-ytmd-main-panel={true} data-ytmd-main-panel={true}
id={'ytmd-title-bar-main-panel'}
> >
<IconButton <IconButton
onClick={() => setCollapsed(!collapsed())} onClick={() => setCollapsed(!collapsed())}

View File

@ -13,11 +13,17 @@ export interface ScrobblerPluginConfig {
*/ */
scrobbleOtherMedia: boolean; scrobbleOtherMedia: boolean;
/** /**
* Use alternative titles for scrobbling (Useful for non-roman song titles) * Use alternative titles for scrobbling (Useful for non-roman song titles, e.g. (Not) A Devil -> デビルじゃないもん)
* *
* @default false * @default true
*/ */
alternativeTitles: boolean; alternativeTitles: boolean;
/**
* Use alternative artist for scrobbling (e.g., DECO27 & (or) PinocchioP -> DECO27 / marasy -> まらしぃ)
*
* @default true
*/
alternativeArtist: boolean;
scrobblers: { scrobblers: {
lastfm: { lastfm: {
/** /**
@ -77,7 +83,8 @@ export interface ScrobblerPluginConfig {
export const defaultConfig: ScrobblerPluginConfig = { export const defaultConfig: ScrobblerPluginConfig = {
enabled: false, enabled: false,
scrobbleOtherMedia: true, scrobbleOtherMedia: true,
alternativeTitles: false, alternativeTitles: true,
alternativeArtist: true,
scrobblers: { scrobblers: {
lastfm: { lastfm: {
enabled: false, enabled: false,

View File

@ -105,6 +105,15 @@ export const onMenu = async ({
setConfig(config); setConfig(config);
}, },
}, },
{
label: t('plugins.scrobbler.menu.scrobble-alternative-artist'),
type: 'checkbox',
checked: Boolean(config.alternativeArtist),
click(item) {
config.alternativeArtist = item.checked;
setConfig(config);
},
},
{ {
label: 'Last.fm', label: 'Last.fm',
submenu: [ submenu: [

View File

@ -132,10 +132,15 @@ export class LastFmScrobbler extends ScrobblerBase {
? songInfo.alternativeTitle ? songInfo.alternativeTitle
: songInfo.title; : songInfo.title;
const artist =
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
? songInfo.tags?.at(0)
: songInfo.artist;
const postData: LastFmSongData = { const postData: LastFmSongData = {
track: title, track: title,
duration: songInfo.songDuration, duration: songInfo.songDuration,
artist: songInfo.artist, artist: artist,
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video ...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
api_key: config.scrobblers.lastfm.apiKey, api_key: config.scrobblers.lastfm.apiKey,
sk: config.scrobblers.lastfm.sessionKey, sk: config.scrobblers.lastfm.sessionKey,

View File

@ -81,8 +81,13 @@ function createRequestBody(
? songInfo.alternativeTitle ? songInfo.alternativeTitle
: songInfo.title; : songInfo.title;
const artist =
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
? songInfo.tags?.at(0)
: songInfo.artist;
const trackMetadata = { const trackMetadata = {
artist_name: songInfo.artist, artist_name: artist,
track_name: title, track_name: title,
release_name: songInfo.album ?? undefined, release_name: songInfo.album ?? undefined,
additional_info: { additional_info: {

View File

@ -0,0 +1,112 @@
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import { Platform } from '@/types/plugins';
import { MaterialType, type TransparentPlayerConfig } from './types';
import style from './style.css?inline';
import type { BrowserWindow } from 'electron';
const defaultConfig: TransparentPlayerConfig = {
enabled: false,
opacity: 0.5,
type: MaterialType.ACRYLIC,
};
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
const typeList = Object.values(MaterialType);
export default createPlugin({
name: () => t('plugins.transparent-player.name'),
description: () => t('plugins.transparent-player.description'),
addedVersion: '3.10.x',
restartNeeded: true,
platform: Platform.Windows,
config: defaultConfig,
stylesheets: [style],
async menu({ getConfig, setConfig }) {
const config = await getConfig();
return [
{
label: t('plugins.transparent-player.menu.opacity.label'),
submenu: opacityList.map((opacity) => ({
label: t('plugins.transparent-player.menu.opacity.submenu.percent', {
opacity: opacity * 100,
}),
type: 'radio',
checked: config.opacity === opacity,
click() {
setConfig({ opacity });
},
})),
},
{
label: t('plugins.transparent-player.menu.type.label'),
submenu: typeList.map((type) => ({
label: t(`plugins.transparent-player.menu.type.submenu.${type}`),
type: 'radio',
checked: config.type === type,
click() {
setConfig({ type });
},
})),
},
];
},
backend: {
mainWindow: null as BrowserWindow | null,
async start({ window, getConfig }) {
this.mainWindow = window;
const config = await getConfig();
window.setBackgroundMaterial?.(config.type);
window.setBackgroundColor?.(`rgba(0, 0, 0, ${config.opacity})`);
},
onConfigChange(newConfig) {
this.mainWindow?.setBackgroundMaterial?.(newConfig.type);
},
stop({ window }) {
window.setBackgroundMaterial?.('none');
},
},
renderer: {
props: {
enabled: defaultConfig.enabled,
opacity: defaultConfig.opacity,
type: defaultConfig.type,
} as TransparentPlayerConfig,
async start({ getConfig }) {
const config = await getConfig();
this.props = config;
if (config.enabled) {
document.body.classList.add('transparent-background-color');
document.body.classList.add('transparent-player-backdrop-filter');
if (!(await window.mainConfig.plugins.isEnabled('album-color-theme'))) {
document.body.classList.add('transparent-player');
}
this.applyVariables();
}
},
onConfigChange(newConfig) {
this.props = newConfig;
this.applyVariables();
},
stop() {
document.body.classList.remove('transparent-background-color');
document.body.classList.remove('transparent-player-backdrop-filter');
document.body.classList.remove('transparent-player');
document.documentElement.style.removeProperty(
'--ytmd-transparent-player-opacity',
);
},
applyVariables(this: { props: TransparentPlayerConfig }) {
const { opacity } = this.props;
document.documentElement.style.setProperty(
'--ytmd-transparent-player-opacity',
opacity.toString(),
);
},
},
});

View File

@ -0,0 +1,106 @@
:root {
--ytmd-transparent-player-transparency-color: #111;
--ytmd-transparent-player-transparent-background: rgb(
from var(--ytmd-transparent-player-transparency-color) r g b /
var(--ytmd-transparent-player-opacity, 0.5)
);
--ytmd-transparent-player-transparent-background-dark: rgb(
from var(--ytmd-transparent-player-transparency-color) r g b / 0.8
);
--ytmd-transparent-player-backdrop-blur: blur(20px);
}
body.transparent-background-color {
background-color: var(--ytmd-transparent-player-transparent-background) !important;
}
body.transparent-player-backdrop-filter {
#layout {
#nav-bar-background,
#player-bar-background {
backdrop-filter: var(--ytmd-transparent-player-backdrop-blur) !important;
}
}
#search-page {
#tabs {
&.stuck {
backdrop-filter: var(--ytmd-transparent-player-backdrop-blur) !important;
}
}
}
ytmusic-menu-popup-renderer {
backdrop-filter: var(--ytmd-transparent-player-backdrop-blur) !important;
}
#ytmd-title-bar-main-panel {
backdrop-filter: var(--ytmd-transparent-player-backdrop-blur) !important;
}
}
body.transparent-player {
ytmusic-app {
ytmusic-app-layout[player-page-open] {
#nav-bar-background.ytmusic-app-layout,
#player-bar-background.ytmusic-app-layout {
opacity: 0 !important;
}
}
#layout {
#nav-bar-background,
#player-bar-background {
background: var(--ytmd-transparent-player-transparent-background-dark) !important;
}
#mini-guide-background {
background: none !important;
border: 0 !important;
}
#guide {
#guide-wrapper {
background: none !important;
border: 0 !important;
}
}
ytmusic-player-bar {
background: none !important;
}
#player-page {
background: none !important;
}
#search-page {
#tabs {
&.stuck {
background: var(--ytmd-transparent-player-transparent-background) !important;
}
}
}
#browse-page {
#background {
display: none !important;
}
.background-gradient {
background: none !important;
}
}
}
}
/* Window Top Panel */
nav[data-ytmd-main-panel] {
background-color: transparent !important;
}
/* Video Toggle Plugin */
.av-toggle.ytmusic-av-toggle {
background-color: var(--ytmd-transparent-player-transparent-background);
}
}

View File

@ -0,0 +1,12 @@
export enum MaterialType {
MICA = 'mica',
ACRYLIC = 'acrylic',
TABBED = 'tabbed',
NONE = 'none',
}
export type TransparentPlayerConfig = {
enabled: boolean;
opacity: number;
type: MaterialType;
};

View File

@ -22,6 +22,24 @@ export const isMusicOrVideoTrack = () => {
return false; return false;
}; };
export const isAlbumOrPlaylist = () => {
for (const menuSelector of document.querySelectorAll<
HTMLAnchorElement & {
data: {
addToPlaylistEndpoint: {
playlistId: string;
};
clickTrackingParams: string;
};
}
>('tp-yt-paper-listbox #navigation-endpoint')) {
if (menuSelector?.data?.addToPlaylistEndpoint?.playlistId) {
return true;
}
}
return false;
};
export const isPlayerMenu = (menu?: HTMLElement | null) => { export const isPlayerMenu = (menu?: HTMLElement | null) => {
return ( return (
menu?.parentElement as menu?.parentElement as

View File

@ -3,7 +3,8 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob'; import { globSync } from 'glob';
import { Project } from 'ts-morph'; import { Project } from 'ts-morph';
import { Platform } from '../src/types/plugins'
import { Platform } from '../src/types/plugins';
const kebabToCamel = (text: string) => const kebabToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
@ -75,7 +76,7 @@ export const pluginVirtualModuleGenerator = (
} }
writer.blankLine(); writer.blankLine();
if (mode === "main" || mode === "preload") { if (mode === 'main' || mode === 'preload') {
writer.writeLine("import * as is from 'electron-is';"); writer.writeLine("import * as is from 'electron-is';");
writer.writeLine('globalThis.electronIs = is;'); writer.writeLine('globalThis.electronIs = is;');
} }
@ -137,7 +138,7 @@ export const pluginVirtualModuleGenerator = (
}; };
function supportsPlatform({ platform }: { platform: string }) { function supportsPlatform({ platform }: { platform: string }) {
if (typeof platform !== "number") return true; if (typeof platform !== 'number') return true;
const is = globalThis.electronIs; const is = globalThis.electronIs;