diff --git a/src/app/AppConfig.ts b/src/app/AppConfig.ts index dd51aa212..2e0354111 100644 --- a/src/app/AppConfig.ts +++ b/src/app/AppConfig.ts @@ -6,7 +6,7 @@ import { app, webContents } from 'electron' import { readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { isWayland, isMac } from './system.utils.ts' +import { isMac, isWayland } from './system.utils.ts' const APP_CONFIG_FILE_NAME = 'config.json' @@ -156,7 +156,7 @@ const defaultAppConfig: AppConfig = { */ const forcedAppConfig: Partial = Object.fromEntries(Object.entries({ // See: https://github.com/electron/electron/issues/49244 - systemTitleBar: isWayland ? false : undefined + systemTitleBar: isWayland ? false : undefined, }).filter(([, value]) => value !== undefined)) /** Local cache of the config file mixed with the default values */ diff --git a/src/main.js b/src/main.js index 10db1d469..95fd31e3f 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ const { app, ipcMain, desktopCapturer, systemPreferences, shell, session } = require('electron') const { default: mri } = require('mri') const { spawn } = require('node:child_process') +const { readFile } = require('node:fs/promises') const path = require('node:path') const { setupMenu } = require('./app/app.menu.js') const { loadAppConfig, getAppConfig, setAppConfig } = require('./app/AppConfig.ts') @@ -88,6 +89,14 @@ ipcMain.handle('app:setBadgeCount', async (event, count) => app.setBadgeCount(co ipcMain.on('app:relaunch', () => relaunchApp()) ipcMain.handle('app:config:get', (event, key) => getAppConfig(key)) ipcMain.handle('app:config:set', (event, key, value) => setAppConfig(key, value)) +ipcMain.handle('app:custom-css:get', async () => { + const files = [ + ...(isWindows && process.env.ProgramData ? [path.join(process.env.ProgramData, app.getName(), 'custom.css')] : []), + path.join(app.getPath('userData'), 'custom.css'), + ] + const styles = await Promise.all(files.map((file) => readFile(file, 'utf-8').catch(() => ''))) + return styles.filter(Boolean).join('\n\n') +}) ipcMain.on('app:grantUserGesturedPermission', (event, id) => { return event.sender.executeJavaScript(`document.getElementById('${id}')?.click()`, true) }) diff --git a/src/preload.js b/src/preload.js index 80bd416c8..b8ca7d56a 100644 --- a/src/preload.js +++ b/src/preload.js @@ -108,6 +108,12 @@ const TALK_DESKTOP = { * @param {(event: import('electron').IpcRedererEvent, payload: { key: string, value: unknown, appConfig: import('./app/AppConfig.ts').AppConfig}) => void} callback - Callback */ onAppConfigChange: (callback) => ipcRenderer.on('app:config:change', callback), + /** + * Get custom CSS loaded from fixed app data locations + * + * @return {Promise} + */ + getCustomCss: () => ipcRenderer.invoke('app:custom-css:get'), /** * Grant a permission requiring a user gesture * diff --git a/src/shared/setupWebPage.js b/src/shared/setupWebPage.js index 6c5339685..a44033839 100644 --- a/src/shared/setupWebPage.js +++ b/src/shared/setupWebPage.js @@ -243,6 +243,21 @@ function applyHeaderHeight() { document.documentElement.style.setProperty('--header-height', `${TITLE_BAR_HEIGHT}px`) } +/** + * Apply custom CSS from fixed app data locations + */ +async function applyCustomCss() { + const css = await window.TALK_DESKTOP.getCustomCss() + if (!css) { + return + } + + const style = document.createElement('style') + style.id = 'talk-desktop-custom-css' + style.textContent = css + document.head.appendChild(style) +} + /** * Handle download links */ @@ -267,6 +282,7 @@ export async function setupWebPage() { initGlobals() applyUserData() applyHeaderHeight() + await applyCustomCss() applyAxiosInterceptors() applyDownloadLinkHandler() diff --git a/src/talk/renderer/Settings/DesktopSettingsSection.vue b/src/talk/renderer/Settings/DesktopSettingsSection.vue index c4a75b38a..ca0bcda5c 100644 --- a/src/talk/renderer/Settings/DesktopSettingsSection.vue +++ b/src/talk/renderer/Settings/DesktopSettingsSection.vue @@ -6,15 +6,18 @@