Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/ban-types */
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, protocol, Tray } from 'electron';
import { app, BrowserWindow, ipcMain, Menu, nativeImage, nativeTheme, protocol, Tray } from 'electron';
import log from 'electron-log';
import * as path from 'path';
import * as url from 'url';
Expand Down Expand Up @@ -602,6 +602,18 @@ try {
}
});

ipcMain.on('update-dock-icon', (event: any, arg: any) => {
if (!isMacOS() || !app.dock) {
return;
}

if (arg) {
app.dock.setIcon(nativeImage.createFromBuffer(arg));
} else {
app.dock.setIcon(nativeImage.createFromPath(path.join(globalAny.__static, 'icons/icon.icns')));
}
});

ipcMain.handle('settings:getAll', () => settings.getAll());
ipcMain.handle('settings:set', (_, key: string, value: any) => settings.set(key, value));
}
Expand Down
1 change: 1 addition & 0 deletions main/common/settings/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@ export const DEFAULT_SETTINGS = {
jumpToPlayingSong: true,
showSquareImages: false,
useCompactYearView: false,
showAlbumArtOnDockIcon: true,
} as const;
3 changes: 3 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DesktopBase } from './common/io/desktop.base';
import { LifetimeService } from './services/lifetime/lifetime.service';
import { AudioVisualizer } from './services/playback/audio-visualizer';
import { DiscordService } from './services/discord/discord.service';
import { DockService } from './services/dock/dock.service';
import { DatabaseMigratorBase } from './data/database-migrator.base';

@Component({
Expand All @@ -36,6 +37,7 @@ export class AppComponent implements OnInit {
private appearanceService: AppearanceServiceBase,
private translatorService: TranslatorServiceBase,
private discordService: DiscordService,
private dockService: DockService,
private scrobblingService: ScrobblingService,
private trayService: TrayServiceBase,
private mediaSessionService: MediaSessionService,
Expand Down Expand Up @@ -89,6 +91,7 @@ export class AppComponent implements OnInit {
this.translatorService.applyLanguage();
this.trayService.updateTrayContextMenu();
this.mediaSessionService.initialize();
this.dockService.initialize();
this.scrobblingService.initialize();
this.eventListenerService.listenToEvents();
this.lifetimeService.initialize();
Expand Down
2 changes: 2 additions & 0 deletions src/app/common/application/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('validate i18n', () => {
'disc-count',
'disc-number',
'discord',
'dock',
'donate',
'donate-instructions',
'donate-now',
Expand Down Expand Up @@ -263,6 +264,7 @@ describe('validate i18n', () => {
'select-folder',
'select-playlist-folder',
'settings',
'show-album-art-on-dock-icon',
'show-all-folders-in-the-collection',
'show-audio-visualizer',
'show-dopamine',
Expand Down
1 change: 1 addition & 0 deletions src/app/common/settings/settings.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ export abstract class SettingsBase {
public abstract jumpToPlayingSong: boolean;
public abstract showSquareImages: boolean;
public abstract useCompactYearView: boolean;
public abstract showAlbumArtOnDockIcon: boolean;
}
9 changes: 9 additions & 0 deletions src/app/common/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,4 +843,13 @@ export class Settings implements SettingsBase {
public set useCompactYearView(v: boolean) {
this.set('useCompactYearView', v);
}

// showAlbumArtOnDockIcon
public get showAlbumArtOnDockIcon(): boolean {
return this.get<boolean>('showAlbumArtOnDockIcon');
}

public set showAlbumArtOnDockIcon(v: boolean) {
this.set('showAlbumArtOnDockIcon', v);
}
}
139 changes: 139 additions & 0 deletions src/app/services/dock/dock.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Injectable } from '@angular/core';
import { Subscription } from 'rxjs';
import { Constants } from '../../common/application/constants';
import { Logger } from '../../common/logger';
import { FileAccessBase } from '../../common/io/file-access.base';
import { IpcProxyBase } from '../../common/io/ipc-proxy.base';
import { SettingsBase } from '../../common/settings/settings.base';
import { PlaybackService } from '../playback/playback.service';
import { MetadataService } from '../metadata/metadata.service';
import { TrackModel } from '../track/track-model';
import { PlaybackStarted } from '../playback/playback-started';

@Injectable({ providedIn: 'root' })
export class DockService {
private _subscription: Subscription | undefined;

public constructor(
private playbackService: PlaybackService,
private metadataService: MetadataService,
private fileAccess: FileAccessBase,
private ipcProxy: IpcProxyBase,
private settings: SettingsBase,
private logger: Logger,
) {}

public get isMacOS(): boolean {
return process.platform === 'darwin';
}

public get showAlbumArtOnDockIcon(): boolean {
return this.settings.showAlbumArtOnDockIcon;
}

public set showAlbumArtOnDockIcon(v: boolean) {
this.settings.showAlbumArtOnDockIcon = v;
this.initialize();
}

public initialize(): void {
if (!this.isMacOS) {
return;
}

this.removeSubscriptions();

if (this.settings.showAlbumArtOnDockIcon) {
this.addSubscriptions();

if (this.playbackService.currentTrack != undefined) {
this.updateDockIconAsync(this.playbackService.currentTrack);
}
} else {
this.resetDockIcon();
}
}

private addSubscriptions(): void {
this._subscription = new Subscription();

this._subscription.add(
this.playbackService.playbackStarted$.subscribe((playbackStarted: PlaybackStarted) => {
this.updateDockIconAsync(playbackStarted.currentTrack);
}),
);

this._subscription.add(
this.playbackService.playbackStopped$.subscribe(() => {
this.resetDockIcon();
}),
);
}

private removeSubscriptions(): void {
if (this._subscription) {
this._subscription.unsubscribe();
}
}

private async updateDockIconAsync(track: TrackModel): Promise<void> {
try {
const imageUrl = await this.metadataService.createAlbumImageUrlAsync(track, 0);

if (imageUrl === Constants.emptyImage) {
this.resetDockIcon();
return;
}

let artworkBuffer: Buffer;

if (imageUrl.startsWith('data:')) {
const base64 = imageUrl.split(',')[1];
artworkBuffer = Buffer.from(base64, 'base64');
} else if (imageUrl.startsWith('file:///')) {
artworkBuffer = await this.fileAccess.getFileContentAsBufferAsync(imageUrl.replace('file:///', ''));
} else {
this.resetDockIcon();
return;
}

const pngBuffer = await this.applySquircleMaskAsync(artworkBuffer);
this.ipcProxy.sendToMainProcess('update-dock-icon', pngBuffer);
} catch (e: unknown) {
this.logger.error(e, 'Could not update dock icon', 'DockService', 'updateDockIconAsync');
this.resetDockIcon();
}
}

private async applySquircleMaskAsync(artworkBuffer: Buffer): Promise<Buffer> {
const { Jimp } = await import('jimp');

const iconSize = 1024;
const inset = 100;
const contentSize = iconSize - 2 * inset;
const half = contentSize / 2;
const n = 5;

const artwork = await Jimp.read(artworkBuffer);
artwork.resize({ w: contentSize, h: contentSize });

const result = new Jimp({ width: iconSize, height: iconSize, color: 0x00000000 });

for (let y = 0; y < contentSize; y++) {
for (let x = 0; x < contentSize; x++) {
const nx = Math.abs((x - half + 0.5) / half);
const ny = Math.abs((y - half + 0.5) / half);

if (Math.pow(nx, n) + Math.pow(ny, n) <= 1) {
result.setPixelColor(artwork.getPixelColor(x, y), x + inset, y + inset);
}
}
}

return await result.getBuffer('image/png');
}

private resetDockIcon(): void {
this.ipcProxy.sendToMainProcess('update-dock-icon', undefined);
}
}
1 change: 1 addition & 0 deletions src/app/testing/settings-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class SettingsMock implements SettingsBase {
public jumpToPlayingSong: boolean;
public showSquareImages: boolean;
public useCompactYearView: boolean;
public showAlbumArtOnDockIcon: boolean;

public get albumKeyIndex(): string {
return this.albumKeyIndexMock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
{{ 'invert-notification-area-icon-color' | translate }}</app-toggle-switch
>
</div>
<div *ngIf="this.dockService.isMacOS">
<mat-divider class="my-4"></mat-divider>
<div class="title">{{ 'dock' | translate }}</div>
<div>
<app-toggle-switch [(isChecked)]="this.dockService.showAlbumArtOnDockIcon">
{{ 'show-album-art-on-dock-icon' | translate }}</app-toggle-switch
>
</div>
</div>
<mat-divider class="my-4"></mat-divider>
<div class="title">{{ 'follow-song' | translate }}</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IMock, Mock } from 'typemoq';
import { BehaviorSettingsComponent } from './behavior-settings.component';
import { MediaSessionService } from '../../../../services/media-session/media-session.service';
import { DockService } from '../../../../services/dock/dock.service';
import { SettingsBase } from '../../../../common/settings/settings.base';
import { TrayServiceBase } from '../../../../services/tray/tray.service.base';
import { DialogServiceBase } from '../../../../services/dialog/dialog.service.base';
Expand All @@ -11,6 +12,7 @@ describe('BehaviorSettingsComponent', () => {
let component: BehaviorSettingsComponent;
let trayServiceMock: IMock<TrayServiceBase>;
let mediaSessionServiceMock: IMock<MediaSessionService>;
let dockServiceMock: IMock<DockService>;
let dialogServiceMock: IMock<DialogServiceBase>;
let translatorServiceMock: IMock<TranslatorServiceBase>;
let settingsMock: IMock<SettingsBase>;
Expand All @@ -20,13 +22,15 @@ describe('BehaviorSettingsComponent', () => {
settingsMock = Mock.ofType<SettingsBase>();
trayServiceMock = Mock.ofType<TrayServiceBase>();
mediaSessionServiceMock = Mock.ofType<MediaSessionService>();
dockServiceMock = Mock.ofType<DockService>();
dialogServiceMock = Mock.ofType<DialogServiceBase>();
translatorServiceMock = Mock.ofType<TranslatorServiceBase>();
loggerMock = Mock.ofType<Logger>();

component = new BehaviorSettingsComponent(
trayServiceMock.object,
mediaSessionServiceMock.object,
dockServiceMock.object,
dialogServiceMock.object,
translatorServiceMock.object,
settingsMock.object,
Expand Down Expand Up @@ -62,6 +66,15 @@ describe('BehaviorSettingsComponent', () => {
expect(component.mediaSessionService).toBeDefined();
});

it('should define dockService', () => {
// Arrange

// Act

// Assert
expect(component.dockService).toBeDefined();
});

it('should define settings', () => {
// Arrange

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { TrayServiceBase } from '../../../../services/tray/tray.service.base';
import { MediaSessionService } from '../../../../services/media-session/media-session.service';
import { DockService } from '../../../../services/dock/dock.service';
import { SettingsBase } from '../../../../common/settings/settings.base';
import { CollectionUtils } from '../../../../common/utils/collections-utils';
import { DialogServiceBase } from '../../../../services/dialog/dialog.service.base';
Expand All @@ -19,6 +20,7 @@ export class BehaviorSettingsComponent implements OnInit {
public constructor(
public trayService: TrayServiceBase,
public mediaSessionService: MediaSessionService,
public dockService: DockService,
private dialogService: DialogServiceBase,
private translatorService: TranslatorServiceBase,
public settings: SettingsBase,
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "إظهار شاشة الترحيب في إعادة التشغيل التالية",
"online": "متصل",
"discord": "ديسكورد",
"dock": "Dock",
"enable-discord-rich-presence": "تفعيل Discord Rich Presence",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "حول",
"components": "المكونات",
"version": "الإصدار",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Покажете началния екран при следващото рестартиране",
"online": "Online",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Активирайте Discord Rich Presence",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "Относно",
"components": "Компоненти",
"version": "Версия",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Zobrazit při příštím spuštění uvítací obrazovku",
"online": "Online",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Povolit status na Discordu",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "O aplikaci",
"components": "Komponenty",
"version": "Verze",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Zeige den Willkommensbildschirm beim nächsten Start",
"online": "Online",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Discord Rich Presence einschalten",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "Über",
"components": "Komponenten",
"version": "Version",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Δείξε μου την οθόνη καλωσορίσματος στην επόμενη επανεκκίνηση",
"online": "Σε σύνδεση",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Ενεργοποίηση Discord Rich Presence",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "Σχετικά με",
"components": "Components",
"version": "Έκδοση",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Show the welcome screen on the next restart",
"online": "Online",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Enable Discord Rich Presence",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "About",
"components": "Components",
"version": "Version",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"show-welcome-screen-on-next-restart": "Mostrar la pantalla de bienvenida en el próximo reinicio",
"online": "En línea",
"discord": "Discord",
"dock": "Dock",
"enable-discord-rich-presence": "Habilitar Discord Rich Presence",
"show-album-art-on-dock-icon": "Show album art on dock icon",
"about": "Acerca de",
"components": "Componentes",
"version": "Versión",
Expand Down
Loading