import { Menu, Tray, app, clipboard, ipcMain, nativeImage, shell } from "electron"; import log from "electron-log"; import { readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import os from "os"; import { utils } from "../base/utils.js"; /** * @file Creates App instance * @author CiderCollective */ /** @namespace */ export class AppEvents { private protocols: string[] = ["ame", "cider", "itms", "itmss", "musics", "music"]; private plugin: any = undefined; private tray: any = undefined; private i18n: any = undefined; /** @constructor */ constructor() { this.start(); } /** * Handles all actions that occur for the app on start (Mainly commandline arguments) * @returns {void} */ private start(): void { AppEvents.initLogging(); console.info("[AppEvents] App started"); /********************************************************************************************************************** * Startup arguments handling **********************************************************************************************************************/ if (app.commandLine.hasSwitch("version") || app.commandLine.hasSwitch("v")) { console.log(app.getVersion()); app.exit(); } // Verbose Check if (app.commandLine.hasSwitch("verbose")) { console.log("[Cider] User has launched the application with --verbose"); } // Log File Location if (app.commandLine.hasSwitch("log") || app.commandLine.hasSwitch("l")) { console.log(join(app.getPath("userData"), "logs")); app.exit(); } // Try limiting JS memory to 350MB. app.commandLine.appendSwitch("js-flags", "--max-old-space-size=350"); // Expose GC app.commandLine.appendSwitch("js-flags", "--expose_gc"); if (process.platform === "win32") { app.setAppUserModelId(app.getName()); // For notification name } /*********************************************************************************************************************** * Commandline arguments **********************************************************************************************************************/ switch (utils.getStoreValue("visual.hw_acceleration") as string) { default: case "default": app.commandLine.appendSwitch("enable-accelerated-mjpeg-decode"); app.commandLine.appendSwitch("enable-accelerated-video"); app.commandLine.appendSwitch("disable-gpu-driver-bug-workarounds"); app.commandLine.appendSwitch("ignore-gpu-blacklist"); app.commandLine.appendSwitch("enable-native-gpu-memory-buffers"); app.commandLine.appendSwitch("enable-accelerated-video-decode"); app.commandLine.appendSwitch("enable-gpu-rasterization"); app.commandLine.appendSwitch("enable-native-gpu-memory-buffers"); app.commandLine.appendSwitch("enable-oop-rasterization"); break; case "webgpu": console.info("[AppEvents] WebGPU is enabled."); app.commandLine.appendSwitch("enable-unsafe-webgpu"); if (process.platform === "linux") { app.commandLine.appendSwitch("enable-features", "Vulkan"); } break; case "disabled": console.info("[AppEvents] Hardware acceleration is disabled."); app.commandLine.appendSwitch("disable-gpu"); app.disableHardwareAcceleration(); break; } if (process.platform === "linux") { app.commandLine.appendSwitch("disable-features", "MediaSessionService"); if (os.version().includes("SteamOS")) { app.commandLine.appendSwitch("enable-features", "UseOzonePlatform"); app.commandLine.appendSwitch("ozone-platform", "x11"); } } /*********************************************************************************************************************** * Protocols **********************************************************************************************************************/ /** */ if (process.defaultApp) { if (process.argv.length >= 2) { this.protocols.forEach((protocol: string) => { app.setAsDefaultProtocolClient(protocol, process.execPath, [resolve(process.argv[1])]); }); } } else { this.protocols.forEach((protocol: string) => { app.setAsDefaultProtocolClient(protocol); }); } } public quit() { console.log("[AppEvents] App quit"); } public ready(plug: any) { this.plugin = plug; console.log("[AppEvents] App ready"); AppEvents.setLoginSettings(); } public bwCreated() { app.on("open-url", (event, url) => { event.preventDefault(); if (this.protocols.some((protocol: string) => url.includes(protocol))) { this.LinkHandler(url); console.log(url); } }); if (process.platform === "darwin") { app.setUserActivity( "8R23J2835D.com.ciderapp.webremote.play", { title: "Web Remote", description: "Connect to your Web Remote", }, "https://webremote.cider.sh", ); } this.InstanceHandler(); if (process.platform !== "darwin") { this.InitTray(); } } /*********************************************************************************************************************** * Private methods **********************************************************************************************************************/ /** * Handles links (URI) and protocols for the application * @param arg */ private LinkHandler(arg: string) { if (!arg) return; // LastFM Auth URL if (arg.includes("auth")) { const authURI = arg.split("/auth/")[1]; if (authURI.startsWith("lastfm")) { // If we wanted more auth options console.log("token: ", authURI.split("lastfm?token=")[1]); utils .getWindow() .webContents.executeJavaScript(`ipcRenderer.send('lastfm:auth', ${JSON.stringify(authURI.split("lastfm?token=")[1])})`) .catch(console.error); } } else if (arg.includes("playpause")) { //language=JS utils.getWindow().webContents.executeJavaScript("MusicKitInterop.playPause()"); } else if (arg.includes("nextitem")) { //language=JS utils.getWindow().webContents.executeJavaScript("app.mk.skipToNextItem()"); } // Play else if (arg.includes("/play/")) { //Steer away from protocol:// specific conditionals const playParam = arg.split("/play/")[1]; const mediaType = { "s/": "song", "a/": "album", "p/": "playlist", }; for (const [key, value] of Object.entries(mediaType)) { if (playParam.includes(key)) { const id = playParam.split(key)[1]; utils.getWindow().webContents.send("play", value, id); console.debug(`[LinkHandler] Attempting to load ${value} by id: ${id}`); } } } else if (arg.includes("music.apple.com")) { // URL (used with itms/itmss/music/musics uris) console.log(arg); let url = arg.split("//")[1]; console.warn(`[LinkHandler] Attempting to load url: ${url}`); utils.getWindow().webContents.send("play", "url", url); } else if (arg.includes("/debug/appdata")) { shell.openPath(app.getPath("userData")); } else if (arg.includes("/debug/logs")) { shell.openPath(app.getPath("logs")); } else if (arg.includes("/discord")) { shell.openExternal("https://discord.gg/applemusic"); } else if (arg.includes("/github")) { shell.openExternal("https://github.com/ciderapp/cider"); } else if (arg.includes("/donate")) { shell.openExternal("https://opencollective.com/ciderapp"); } else if (arg.includes("/beep")) { shell.beep(); } else { utils.getWindow().webContents.executeJavaScript(`app.appRoute(${JSON.stringify(arg.split("//")[1])})`); } } /** * Handles the creation of a new instance of the app */ private InstanceHandler() { // Detects of an existing instance is running (So if the lock has been achieved, no existing instance has been found) const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { // Runs on the new instance if another instance has been found console.log("[Cider] Another instance has been found, quitting."); app.quit(); } else { // Runs on the first instance if no other instance has been found app.on("second-instance", (_event, startArgs) => { console.log("[InstanceHandler] (second-instance) Instance started with " + startArgs.toString()); startArgs.forEach((arg) => { console.log(arg); if (arg.includes("cider://") || arg.includes("itms://") || arg.includes("itmss://") || arg.includes("music://") || arg.includes("musics://")) { console.debug("[InstanceHandler] (second-instance) Link detected with " + arg); this.LinkHandler(arg); } else if (arg.includes("--force-quit")) { console.warn("[InstanceHandler] (second-instance) Force Quit found. Quitting App."); app.quit(); } else if (utils.getWindow()) { if (utils.getWindow().isMinimized()) utils.getWindow().restore(); utils.getWindow().show(); utils.getWindow().focus(); } }); }); } } /** * Initializes the applications tray */ private InitTray() { const icons = { win32: nativeImage.createFromPath(join(dirname(fileURLToPath(import.meta.url)), `../../resources/icons/icon.ico`)).resize({ width: 32, height: 32, }), linux: nativeImage.createFromPath(join(dirname(fileURLToPath(import.meta.url)), `../../resources/icons/icon.png`)).resize({ width: 32, height: 32, }), darwin: nativeImage.createFromPath(join(dirname(fileURLToPath(import.meta.url)), `../../resources/icons/icon.png`)).resize({ width: 20, height: 20, }), }; this.tray = new Tray(process.platform === "win32" ? icons.win32 : process.platform === "darwin" ? icons.darwin : icons.linux); this.tray.setToolTip(app.getName()); this.setTray(false); this.tray.on("double-click", () => { if (utils.getWindow()) { if (utils.getWindow().isVisible()) { utils.getWindow().focus(); } else { utils.getWindow().show(); } } }); utils.getWindow().on("show", () => { this.setTray(true); }); utils.getWindow().on("restore", () => { this.setTray(true); }); utils.getWindow().on("hide", () => { this.setTray(false); }); utils.getWindow().on("minimize", () => { this.setTray(false); }); } /** * Sets the tray context menu to a given state * @param visible - BrowserWindow Visibility */ private setTray(visible: boolean = utils.getWindow().isVisible()) { this.i18n = utils.getLocale(utils.getStoreValue("general.language")); const ciderIcon = nativeImage.createFromPath(join(dirname(fileURLToPath(import.meta.url)), `../../resources/icons/icon.png`)).resize({ width: 24, height: 24, }); const menu = Menu.buildFromTemplate([ { label: app.getName(), enabled: false, icon: ciderIcon, }, { type: "separator" }, /* For now only idea i dont know if posible to implement this could be implemented in a plugin if you would like track info, it would be impractical to put listeners in this file. -Core { label: this.i18n['action.tray.listento'], enabled: false, }, { visible: visible, label: 'track info', enabled: false, }, {type: 'separator'}, */ { visible: !visible, label: this.i18n["term.playpause"], click: () => { utils.getWindow().webContents.executeJavaScript("MusicKitInterop.playPause()"); }, }, { visible: !visible, label: this.i18n["term.next"], click: () => { utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.next()`); }, }, { visible: !visible, label: this.i18n["term.previous"], click: () => { utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.previous()`); }, }, { type: "separator", visible: !visible }, { label: visible ? this.i18n["action.tray.minimize"] : `${this.i18n["action.tray.show"]}`, click: () => { if (utils.getWindow()) { if (visible) { utils.getWindow().hide(); } else { utils.getWindow().show(); } } }, }, { label: this.i18n["term.quit"], click: () => { app.quit(); }, }, ]); this.tray.setContextMenu(menu); } /** * Initializes logging in the application * @private */ private static initLogging() { log.transports.console.format = "[{h}:{i}:{s}.{ms}] [{level}] {text}"; Object.assign(console, log.functions); console.debug = function (...args: any[]) { if (!app.isPackaged) { log.debug(...args); } }; ipcMain.on("fetch-log", (_event) => { const data = readFileSync(log.transports.file.getFile().path, { encoding: "utf8", flag: "r", }); clipboard.writeText(data); }); } /** * Set login settings * @private */ private static setLoginSettings() { if (utils.getStoreValue("general.onStartup.enabled")) { app.setLoginItemSettings({ openAtLogin: true, path: app.getPath("exe"), args: [`${utils.getStoreValue("general.onStartup.hidden") ? "--hidden" : ""}`], }); } else { app.setLoginItemSettings({ openAtLogin: false, path: app.getPath("exe"), }); } } }