Skip to content

Commit

Permalink
Merge pull request #835 from nextcloud/feat/settings
Browse files Browse the repository at this point in the history
feat: add application config aka settings
  • Loading branch information
ShGKme authored Oct 22, 2024
2 parents 8ec5fb6 + a908929 commit e3a1523
Show file tree
Hide file tree
Showing 21 changed files with 631 additions and 13 deletions.
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,13 @@ module.exports = {
'@nextcloud/no-deprecations': 'off',
'@nextcloud/no-removed-apis': 'off',
},

overrides: [
{
files: '*.ts',
rules: {
'jsdoc/require-returns-type': 'off', // TODO upstream
},
},
],
}
144 changes: 144 additions & 0 deletions src/app/AppConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { join } from 'node:path'
import { readFile, writeFile } from 'node:fs/promises'
import { app } from 'electron'
import { isLinux } from '../shared/os.utils.js'

const APP_CONFIG_FILE_NAME = 'config.json'

// Windows: C:\Users\<username>\AppData\Roaming\Nextcloud Talk\config.json
// Linux: ~/.config/Nextcloud Talk/config.json (or $XDG_CONFIG_HOME)
// macOS: ~/Library/Application Support/Nextcloud Talk/config.json
const APP_CONFIG_FILE_PATH = join(app.getPath('userData'), APP_CONFIG_FILE_NAME)

/**
* Application level config. Applied to all accounts and persist on re-login.
* Stored in the application data directory.
*/
export type AppConfig = {
// ----------------
// General settings
// ----------------

// Nothing yet...

// -------------------
// Appearance settings
// -------------------

/**
* Application theme.
* Default: 'default' to follow the system theme.
*/
theme: 'default' | 'dark' | 'light'
/**
* Whether to use a custom title bar or the system default.
* Default: true on Linux, false otherwise.
*/
systemTitleBar: boolean

// ----------------
// Privacy settings
// ----------------

// Nothing yet...

// ----------------------
// Notifications settings
// ----------------------

// Nothing yet...
}

/**
* Get the default config
*/
const defaultAppConfig: AppConfig = {
theme: 'default',
systemTitleBar: isLinux(),
}

/** Local cache of the config file mixed with the default values */
const appConfig: Partial<AppConfig> = {}
/** Whether the application config has been read from the config file and ready to use */
let initialized = false

/**
* Read the application config from the file
*/
async function readAppConfigFile(): Promise<Partial<AppConfig>> {
try {
const content = await readFile(APP_CONFIG_FILE_PATH, 'utf-8')
return JSON.parse(content)
} catch (error) {
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
console.error('Failed to read the application config file', error)
}
// No file or invalid file - no custom config
return {}
}
}

/**
* Write the application config to the config file
* @param config - The config to write
*/
async function writeAppConfigFile(config: Partial<AppConfig>) {
try {
// Format for readability
const content = JSON.stringify(config, null, 2)
await writeFile(APP_CONFIG_FILE_PATH, content)
} catch (error) {
console.error('Failed to write the application config file', error)
throw error
}
}

/**
* Load the application config into the application memory
*/
export async function loadAppConfig() {
const config = await readAppConfigFile()
Object.assign(appConfig, config)
initialized = true
}

export function getAppConfig(): AppConfig
export function getAppConfig<T extends keyof AppConfig>(key?: T): AppConfig[T]
/**
* Get an application config value
* @param key - The config key to get
* @return - If key is provided, the value of the key. Otherwise, the full config
*/
export function getAppConfig<T extends keyof AppConfig>(key?: T): AppConfig | AppConfig[T] {
if (!initialized) {
throw new Error('The application config is not initialized yet')
}

const config = Object.assign({}, defaultAppConfig, appConfig)

if (key) {
return config[key]
}

return config
}

/**
* Set an application config value
* @param key - Settings key to set
* @param value - Value to set or undefined to reset to the default value
* @return Promise<AppConfig> - The full settings after the change
*/
export async function setAppConfig<K extends keyof AppConfig>(key: K, value?: AppConfig[K]) {
if (value !== undefined) {
appConfig[key] = value
} else {
delete appConfig[key]
}
await writeAppConfigFile(appConfig)
}
4 changes: 2 additions & 2 deletions src/authentication/authentication.window.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { BrowserWindow } = require('electron')
const { BASE_TITLE, TITLE_BAR_HEIGHT } = require('../constants.js')
const { applyContextMenu } = require('../app/applyContextMenu.js')
const { getBrowserWindowIcon } = require('../shared/icons.utils.js')
const { isLinux } = require('../shared/os.utils.js')
const { getAppConfig } = require('../app/AppConfig.ts')

/**
* @return {import('electron').BrowserWindow}
Expand All @@ -29,7 +29,7 @@ function createAuthenticationWindow() {
preload: AUTHENTICATION_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
icon: getBrowserWindowIcon(),
titleBarStyle: isLinux() ? 'default' : 'hidden',
titleBarStyle: getAppConfig('systemTitleBar') ? 'default' : 'hidden',
titleBarOverlay: {
color: '#00679E00', // Transparent
symbolColor: '#FFFFFF', // White
Expand Down
23 changes: 21 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { getOs, isLinux, isMac, isWayland } = require('./shared/os.utils.js')
const { createTalkWindow } = require('./talk/talk.window.js')
const { createWelcomeWindow } = require('./welcome/welcome.window.js')
const { installVueDevtools } = require('./install-vue-devtools.js')
const { loadAppConfig, getAppConfig, setAppConfig } = require('./app/AppConfig.ts')

/**
* Parse command line arguments
Expand Down Expand Up @@ -67,6 +68,8 @@ ipcMain.on('app:relaunch', () => {
app.relaunch()
app.exit(0)
})
ipcMain.handle('app:config:get', (event, key) => getAppConfig(key))
ipcMain.handle('app:config:set', (event, key, value) => setAppConfig(key, value))
ipcMain.handle('app:getDesktopCapturerSources', async () => {
// macOS 10.15 Catalina or higher requires consent for screen access
if (isMac() && systemPreferences.getMediaAccessStatus('screen') !== 'granted') {
Expand Down Expand Up @@ -97,7 +100,15 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => {
}))
})

/**
* Whether the window is being relaunched.
* At this moment there are no active windows, but the application should not quit yet.
*/
let isInWindowRelaunch = false

app.whenReady().then(async () => {
await loadAppConfig()

try {
await installVueDevtools()
} catch (error) {
Expand Down Expand Up @@ -256,9 +267,17 @@ app.whenReady().then(async () => {
mainWindow = upgradeWindow
})

ipcMain.on('app:relaunchWindow', () => {
isInWindowRelaunch = true
mainWindow.destroy()
mainWindow = createMainWindow()
mainWindow.once('ready-to-show', () => mainWindow.show())
isInWindowRelaunch = false
})

// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', function() {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createMainWindow()
}
Expand All @@ -269,7 +288,7 @@ app.whenReady().then(async () => {
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
if (process.platform !== 'darwin' && !isInWindowRelaunch) {
app.quit()
}
})
19 changes: 18 additions & 1 deletion src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,26 @@ const TALK_DESKTOP = {
*/
getDesktopCapturerSources: () => ipcRenderer.invoke('app:getDesktopCapturerSources'),
/**
* Relaunch the application
* Relaunch an entire application
*/
relaunch: () => ipcRenderer.send('app:relaunch'),
/**
* Relaunch the main window without relaunching an entire application
*/
relaunchWindow: () => ipcRenderer.send('app:relaunchWindow'),
/**
* Get an application config value by key
* @param {string} [key] - Config key
* @return {Promise<Record<string, unknown> | unknown>}
*/
getAppConfig: (key) => ipcRenderer.invoke('app:config:get', key),
/**
* Set an application config value by key
* @param {string} key - Config key
* @param {any} [value] - Config value
* @return {Promise<void>}
*/
setAppConfig: (key, value) => ipcRenderer.invoke('app:config:set', key, value),
/**
* Send appData to main process on restore
*
Expand Down
44 changes: 44 additions & 0 deletions src/shared/appConfig.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { AppConfig } from '../app/AppConfig.ts'

let appConfig: AppConfig | null = null

/**
* Initialize the AppConfig
*/
export async function initAppConfig() {
if (appConfig) {
return
}

appConfig = await window.TALK_DESKTOP.getAppConfig()
}

/**
* Get AppConfig
* @return - AppConfig
*/
export function getAppConfig() {
if (!appConfig) {
throw new Error('AppConfig is not initialized')
}

return appConfig
}

/**
* Get an application config value
* @param key - The key of the config value
* @return - The config value
*/
export function getAppConfigValue<K extends keyof AppConfig>(key: K) {
if (!appConfig) {
throw new Error('AppConfig is not initialized')
}

return appConfig[key]
}
4 changes: 3 additions & 1 deletion src/shared/setupWebPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { applyBodyThemeAttrs } from './theme.utils.js'
import { appData } from '../app/AppData.js'
import { initGlobals } from './globals/globals.js'
import { setupInitialState } from './initialState.service.js'
import { getAppConfigValue, initAppConfig } from './appConfig.service.ts' // eslint-disable-line import/namespace
import { TITLE_BAR_HEIGHT } from '../constants.js'

/**
Expand Down Expand Up @@ -204,11 +205,12 @@ function applyHeaderHeight() {
export async function setupWebPage() {
document.title = await window.TALK_DESKTOP.getAppName()
appData.restore()
await initAppConfig()
applyInitialState()
initGlobals()
window.OS = await window.TALK_DESKTOP.getOs()
applyUserData()
applyBodyThemeAttrs()
applyBodyThemeAttrs(getAppConfigValue('theme'))
applyHeaderHeight()
applyAxiosInterceptors()
await applyL10n()
Expand Down
Loading

0 comments on commit e3a1523

Please sign in to comment.