orchard/resources/functions/utils.js
2021-11-19 17:02:28 -06:00

395 lines
No EOL
16 KiB
JavaScript

const {app, nativeImage, nativeTheme, Notification, dialog} = require("electron"),
{existsSync, readFileSync, readdirSync, constants, access, writeFileSync, copyFileSync} = require("fs"),
{readdir, mkdir} = require("fs/promises"),
{join, resolve} = require("path"),
{autoUpdater} = require("electron-updater"),
os = require("os"),
rimraf = require("rimraf"),
chmod = require("chmodr"),
clone = require("git-clone/promise"),
trayIconDir = (nativeTheme.shouldUseDarkColors ? join(__dirname, `../icons/media/light/`) : join(__dirname, `../icons/media/dark/`)),
ElectronSentry = require("@sentry/electron");
const Utils = {
/* hexToRgb - Converts hex codes to rgb */
hexToRgb: (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
},
/* matchRuleShort - Used for wildcards */
matchRuleShort: (str, rule) => {
var escapeRegex = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
return new RegExp("^" + rule.split("*").map(escapeRegex).join(".*") + "$").test(str);
},
/* isVibrancySupported - Checks if the operating system support electron-acrylic-window (Windows 10 or greater) */
isVibrancySupported: () => {
return (process.platform === 'win32' && parseInt(os.release().split('.')[0]) >= 10)
},
/* isAcrylicSupported - Checks if the operating system supports the acrylic transparency affect (Windows RS3 (Redstone 3) 1709 or Greater) */
isAcrylicSupported: () => {
return (process.platform === 'win32' && parseInt(os.release().replace(/\./g, "").replace(',', '.')) >= 10016299)
},
/* fetchThemeMeta - Fetches the meta data associated to a theme */
fetchThemeMeta: (fileName) => {
const filePath = resolve(app.getPath("userData"), "themes", `${fileName}.css`);
let fileMeta = {
name: null,
author: null,
description: null,
transparency: {dark: null, light: null},
options: []
};
if (!existsSync(filePath)) return fileMeta;
const file = readFileSync(filePath, "utf8");
if (!file) return;
file.split(/\r?\n/).forEach((line) => {
if (line.includes("@name")) {
fileMeta.name = line.split("@name ")[1].trim();
}
if (line.includes("@author")) {
fileMeta.author = line.split("@author ")[1].trim();
}
if (line.includes("@description")) {
fileMeta.description = line.split("@description ")[1]
}
if (line.includes("@option")) {
var themeOption = line.split("@option ")[1].trim().split("|")
fileMeta.options.push({
key: themeOption[0],
name: themeOption[1],
defaultValue: themeOption[2]
})
}
if (line.includes("--lightTransparency")) {
fileMeta.transparency.light = line.split("--lightTransparency: ")[1].trim().split(' ')[0];
}
if (line.includes("--darkTransparency")) {
fileMeta.transparency.dark = line.split("--darkTransparency: ")[1].trim().split(' ')[0];
}
if (fileMeta.transparency.dark && fileMeta.transparency.light) {
fileMeta.transparency = nativeTheme.shouldUseDarkColors ? fileMeta.transparency.dark : fileMeta.transparency.light
}
if (!fileMeta.transparency.dark || !fileMeta.transparency.light) {
if (line.includes("--transparency")) {
fileMeta.transparency = line.split("--transparency: ")[1].split(' ')[0];
}
}
});
if (typeof fileMeta.transparency == "object") {
if (!fileMeta.transparency.dark || !fileMeta.transparency.light) {
fileMeta.transparency = false;
}
}
console.verbose(`[fetchThemeMeta] Returning ${JSON.stringify(fileMeta)}`);
return fileMeta
},
/* fetchTransparencyOptions - Fetches the transparency options */
fetchTransparencyOptions: () => {
if (process.platform === "darwin" && (!app.cfg.get('visual.transparencyEffect') || !Utils.isVibrancySupported())) {
app.transparency = true;
return "fullscreen-ui"
} else if (!app.cfg.get('visual.transparencyEffect') || !Utils.isVibrancySupported()) {
console.verbose(`[fetchTransparencyOptions] Vibrancy not created. Required options not met. (transparencyEffect: ${app.cfg.get('visual.transparencyEffect')} | isVibrancySupported: ${Utils.isVibrancySupported()})`);
app.transparency = false;
return false
} else if (process.platform === "win32" && app.cfg.get('visual.transparencyEffect') === "mica") {
return false
}
console.log('[fetchTransparencyOptions] Fetching Transparency Options')
let transparencyOptions = {
theme: null,
effect: app.cfg.get('visual.transparencyEffect'),
debug: app.cfg.get('advanced.verboseLogging'),
}
//------------------------------------------
// Disable on blur for acrylic
//------------------------------------------
if (app.cfg.get('visual.transparencyEffect') === 'acrylic') {
transparencyOptions.disableOnBlur = app.cfg.get('visual.transparencyDisableBlur');
}
//------------------------------------------
// Set the transparency theme
//------------------------------------------
if (app.cfg.get('visual.transparencyTheme') === 'appearance-based') {
if (app.cfg.get('visual.theme') && app.cfg.get('visual.theme') !== "default") {
transparencyOptions.theme = Utils.fetchThemeMeta(app.cfg.get('visual.theme')).transparency; /* Fetch the Transparency from the Themes Folder */
} else if ((!app.cfg.get('visual.theme') || app.cfg.get('visual.theme') === "default") && app.cfg.get('visual.transparencyEffect') === 'acrylic') {
transparencyOptions.theme = (nativeTheme.shouldUseDarkColors ? '#3C3C4307' : '#EBEBF507') /* Default Theme when Using Acrylic */
} else { // Fallback
transparencyOptions.theme = (nativeTheme.shouldUseDarkColors ? 'dark' : 'light')
}
} else {
transparencyOptions.theme = app.cfg.get('visual.transparencyTheme');
}
//------------------------------------------
// Set the refresh rate
//------------------------------------------
if (app.cfg.get('visual.transparencyMaximumRefreshRate')) {
transparencyOptions.useCustomWindowRefreshMethod = true
transparencyOptions.maximumRefreshRate = app.cfg.get('visual.transparencyMaximumRefreshRate')
}
app.transparency = true
console.log(`[fetchTransparencyOptions] Returning: ${JSON.stringify(transparencyOptions)}`)
return transparencyOptions
},
/* fetchThemesListing - Fetches the themes directory listing (Lists .css files) */
fetchThemesListing: () => {
if (!existsSync(resolve(app.getPath("userData"), "themes"))) return;
let themesFileNames = [], themesListing = {};
readdirSync(resolve(app.getPath("userData"), "themes")).forEach((value) => {
if (value.split('.').pop() === 'css') {
themesFileNames.push(value.split('.').shift())
}
});
// Get the Info
themesFileNames.forEach((themeFileName) => {
const themeData = Utils.fetchThemeMeta(themeFileName);
if (themeData && themeData.name && themeData.description && themeData.author) {
themesListing[themeFileName] = themeData;
}
})
return themesListing
},
/* fetchPluginsListing - Fetches the plugins directory listing (Lists .js files) */
fetchPluginsListing: () => {
if (!existsSync(resolve(app.getPath("userData"), "plugins"))) return;
let pluginsFileNames = [], pluginsListing = {};
readdirSync(resolve(app.getPath("userData"), "plugins")).forEach((value) => {
if (value.split('.').pop() === 'js') {
pluginsFileNames.push(value.split('.').shift())
}
});
console.log(pluginsFileNames)
return pluginsFileNames
},
/* fetchOperatingSystem - Fetches the operating system name */
fetchOperatingSystem: () => {
if (process.platform === "win32") {
const release = parseInt(os.release().replaceAll('.', ''))
if (release >= 10022000) {
return 'Windows 11'
} else if (release < 10022000 && release >= 10010240) {
return 'Windows 10'
}
}
},
/* updateThemes - Purges the themes directory and clones a fresh copy of the themes */
updateThemes: async () => {
if (app.watcher) {
app.watcher.close()
}
const tmpDir = join(os.tmpdir(), "ame-themes")
const themesDir = join(app.getPath("userData"), "themes")
if (existsSync(themesDir)) {
if (existsSync(tmpDir)) {
rimraf(tmpDir, [], async (err) => {
if (err) return err
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', tmpDir, [], (err) => console.log(err))
})
} else {
await mkdir(tmpDir, {recursive: true})
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', tmpDir, [], (err) => console.log(err))
}
// Base Line Directory Comparison
const updateList = await readdir(tmpDir);
const foundChanges = {};
for (const file of updateList) {
if (file.split('.').pop() === 'css' && file !== "Template.css") { // Reduces listing compare down to css files
console.verbose(`[compareDirectories] Comparing ${file}`)
if (!existsSync(join(themesDir, file))) {
copyFileSync(join(tmpDir, file), join(themesDir, file))
foundChanges[file] = 'added'
} else {
const updateFile = readFileSync(join(tmpDir, file));
const origFile = readFileSync(join(themesDir, file));
if (origFile.toString() !== updateFile.toString()) {
writeFileSync(join(themesDir, file), updateFile)
foundChanges[file] = 'updated'
}
}
}
}
return foundChanges
} else {
await mkdir(tmpDir, {recursive: true})
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', themesDir, [], (err) => console.log(err))
return {'initial': true}
}
},
/* permissionsCheck - Checks of the file can be read and written to, if it cannot be chmod -r is run on the directory */
permissionsCheck: (folder, file) => {
console.verbose(`[permissionsCheck] Running check on ${join(folder, file)}`)
access(join(folder, file), constants.R_OK | constants.W_OK, (err) => {
if (err) { // File cannot be read after cloning
console.error(`[permissionsCheck][access] ${err}`)
chmod(folder, 0o777, (err) => {
if (err) {
console.error(`[permissionsCheck][chmod] ${err} - Theme set to default to prevent application launch halt.`);
}
});
} else {
console.verbose('[permissionsCheck] Check passed.')
}
})
},
/* initAnalytics - Sentry Analytics */
initAnalytics: () => {
if (app.cfg.get('general.analyticsEnabled') && app.isPackaged) {
ElectronSentry.init({dsn: "https://20e1c34b19d54dfcb8231e3ef7975240@o954055.ingest.sentry.io/5903033"});
}
},
/* checkForUpdates - Checks for update using electron-updater (Part of electron-builder) */
checkForUpdates: (manual) => {
if (!app.isPackaged || process.env.NODE_ENV !== 'production') return;
autoUpdater.logger = require("electron-log");
autoUpdater.logger.transports.file.resolvePath = (vars) => {
return join(app.getPath('userData'), 'logs', vars.fileName);
}
autoUpdater.logger.transports.file.level = "info";
if (app.cfg.get('advanced.autoUpdaterBetaBuilds')) {
autoUpdater.allowPrerelease = true
autoUpdater.allowDowngrade = false
}
autoUpdater.on('update-not-available', () => {
if (manual === true) {
let bodyVer = `You are on the latest version. (v${app.getVersion()})`
new Notification({title: "Apple Music", body: bodyVer}).show()
}
})
autoUpdater.on('download-progress', (progress) => {
let convertedProgress = parseFloat(progress);
app.win.setProgressBar(convertedProgress)
})
autoUpdater.on("error", function (error) {
console.error(`[checkForUpdates] Error ${error}`)
});
autoUpdater.on('update-downloaded', (updateInfo) => {
console.warn('[checkForUpdates] New version downloaded. Starting user prompt.');
dialog.showMessageBox({
type: 'info',
title: 'Updates Available',
message: `Update was found and downloaded, would you like to install the update now?`,
details: updateInfo,
buttons: ['Sure', 'No']
}).then(({response}) => {
if (response === 0) {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
} else {
updater.enabled = true
updater = null
}
})
})
autoUpdater.checkForUpdates()
.then(r => {
console.verbose(`[checkForUpdates] Check for updates completed. Response: ${r}`)
})
.catch(err => {
console.error(`[checkUpdates] An error occurred while checking for updates: ${err}`)
})
},
/* Media Controlling Functions (Pause/Play/Skip/Previous) */
media: {
pausePlay() {
console.verbose('[AppleMusic] pausePlay run.')
app.win.webContents.executeJavaScript("MusicKitInterop.pausePlay()").catch((err) => console.error(err))
},
nextTrack() {
console.verbose('[AppleMusic] nextTrack run.')
app.win.webContents.executeJavaScript("MusicKitInterop.nextTrack()").catch((err) => console.error(err))
},
previousTrack() {
console.verbose('[AppleMusic] previousTrack run.')
app.win.webContents.executeJavaScript("MusicKitInterop.previousTrack()").catch((err) => console.error(err))
}
},
/* Media-associated Icons (Used for Thumbar and TouchBar) */
icons: {
pause: nativeImage.createFromPath(join(trayIconDir, 'pause.png')).resize({width: 32, height: 32}),
play: nativeImage.createFromPath(join(trayIconDir, 'play.png')).resize({width: 32, height: 32}),
nextTrack: nativeImage.createFromPath(join(trayIconDir, 'next.png')).resize({width: 32, height: 32}),
previousTrack: nativeImage.createFromPath(join(trayIconDir, 'previous.png')).resize({width: 32, height: 32}),
inactive: {
play: nativeImage.createFromPath(join(trayIconDir, 'play-inactive.png')).resize({width: 32, height: 32}),
nextTrack: nativeImage.createFromPath(join(trayIconDir, 'next-inactive.png')).resize({
width: 32,
height: 32
}),
previousTrack: nativeImage.createFromPath(join(trayIconDir, 'previous-inactive.png')).resize({
width: 32,
height: 32
}),
}
}
}
Utils.initAnalytics()
module.exports = Utils;