Skip to content

Commit 7b5ac71

Browse files
committed
feat(config): add settings UI
Signed-off-by: Grigorii K. Shartsev <[email protected]>
1 parent c7b5ef2 commit 7b5ac71

10 files changed

+371
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { t } from '@nextcloud/l10n'
8+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
9+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
10+
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
11+
import IconThemeLightDark from 'vue-material-design-icons/ThemeLightDark.vue'
12+
import SettingsSubsection from './components/SettingsSubsection.vue'
13+
import SettingsSelect from './components/SettingsSelect.vue'
14+
import { useAppConfigValue } from './useAppConfigValue.ts'
15+
import { useNcSelectModel } from '../composables/useNcSelectModel.ts'
16+
import { useAppConfig } from './appConfig.store.ts'
17+
import { storeToRefs } from 'pinia'
18+
19+
const { isRelaunchRequired } = storeToRefs(useAppConfig())
20+
21+
const theme = useAppConfigValue('theme')
22+
const themeOptions = [
23+
{ label: t('talk_desktop', 'System default'), value: 'default' } as const,
24+
{ label: t('talk_desktop', 'Light'), value: 'light' } as const,
25+
{ label: t('talk_desktop', 'Dark'), value: 'dark' } as const,
26+
]
27+
const themeOption = useNcSelectModel(theme, themeOptions)
28+
29+
const systemTitleBar = useAppConfigValue('systemTitleBar')
30+
31+
/**
32+
* Restart the app
33+
*/
34+
function relaunch() {
35+
window.TALK_DESKTOP.relaunchWindow()
36+
}
37+
</script>
38+
39+
<template>
40+
<div>
41+
<NcNoteCard v-if="isRelaunchRequired" type="info" class="relaunch-require-note-card">
42+
<div class="relaunch-require-note-card__content">
43+
<span>{{ t('talk_desktop', 'Some changes require a relaunch to take effect') }}</span>
44+
<NcButton type="primary"
45+
size="small"
46+
class="relaunch-require-note-card__button"
47+
@click="relaunch">
48+
{{ t('talk_desktop', 'Restart') }}
49+
</NcButton>
50+
</div>
51+
</NcNoteCard>
52+
53+
<SettingsSubsection :name="t('talk_desktop', 'Appearance')">
54+
<SettingsSelect v-model="themeOption" :options="themeOptions">
55+
<template #icon>
56+
<IconThemeLightDark :size="20" />
57+
</template>
58+
{{ t('talk_desktop', 'Theme') }}
59+
</SettingsSelect>
60+
61+
<NcCheckboxRadioSwitch :checked.sync="systemTitleBar" type="switch">
62+
{{ t('talk_desktop', 'Use system title bar') }}
63+
</NcCheckboxRadioSwitch>
64+
</SettingsSubsection>
65+
</div>
66+
</template>
67+
68+
<style scoped>
69+
.relaunch-require-note-card {
70+
margin-block-start: 0 !important;
71+
}
72+
73+
.relaunch-require-note-card > :deep(div) {
74+
flex: 1; /* TODO: fix in upstream */
75+
}
76+
77+
.relaunch-require-note-card__content {
78+
display: flex;
79+
gap: var(--default-grid-baseline);
80+
align-items: flex-start;
81+
}
82+
83+
.relaunch-require-note-card__button {
84+
margin-inline-start: auto;
85+
flex: 0 0 auto;
86+
}
87+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Ref } from 'vue'
7+
import type { AppConfig } from '../../../app/AppConfig.ts'
8+
import { readonly, ref, watch } from 'vue'
9+
import { defineStore } from 'pinia'
10+
import { getAppConfig } from '../../../shared/appConfig.service.ts'
11+
import { applyBodyThemeAttrs } from '../../../shared/theme.utils.js'
12+
13+
export const useAppConfig = defineStore('appConfig', () => {
14+
const appConfig: Ref<AppConfig> = ref(getAppConfig())
15+
const isRelaunchRequired = ref(false)
16+
const relaunchRequiredConfigs = ['systemTitleBar'] as const
17+
18+
const unwatchRelaunch = watch(
19+
() => relaunchRequiredConfigs.map((key) => appConfig.value[key]),
20+
() => {
21+
isRelaunchRequired.value = true
22+
unwatchRelaunch()
23+
},
24+
)
25+
26+
watch(() => appConfig.value.theme, (newTheme) => applyBodyThemeAttrs(newTheme))
27+
28+
/**
29+
* Get an application config value
30+
* @param key - The key of the config value
31+
* @return - The config
32+
*/
33+
function getAppConfigValue<K extends keyof AppConfig>(key: K) {
34+
return appConfig.value[key]
35+
}
36+
37+
/**
38+
* Set an application config value
39+
* @param key - The key of the config value
40+
* @param value - The value to set
41+
*/
42+
function setAppConfigValue<K extends keyof AppConfig>(key: K, value: AppConfig[K]) {
43+
appConfig.value[key] = value
44+
window.TALK_DESKTOP.setAppConfig(key, value)
45+
}
46+
47+
return {
48+
isRelaunchRequired: readonly(isRelaunchRequired),
49+
appConfig: readonly(appConfig),
50+
getAppConfigValue,
51+
setAppConfigValue,
52+
}
53+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { computed } from 'vue'
8+
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
9+
import type { NcSelectOption } from '../../composables/useNcSelectModel.ts'
10+
11+
const props = defineProps<{
12+
options: NcSelectOption<unknown>[]
13+
modelValue: NcSelectOption<unknown>
14+
}>()
15+
16+
const emit = defineEmits<{
17+
(event: 'update:modelValue', value: NcSelectOption<unknown>): void
18+
}>()
19+
20+
const model = computed({
21+
get: () => props.modelValue,
22+
set: (value: NcSelectOption<unknown>) => emit('update:modelValue', value),
23+
})
24+
</script>
25+
26+
<script lang="ts">
27+
export default {
28+
model: {
29+
prop: 'modelValue',
30+
event: 'update:modelValue',
31+
},
32+
}
33+
</script>
34+
35+
<template>
36+
<label class="settings-select">
37+
<span class="settings-select__label">
38+
<span v-if="$slots.icon" class="settings-select__label-icon">
39+
<slot name="icon" />
40+
</span>
41+
<span>
42+
<slot />
43+
</span>
44+
</span>
45+
<NcSelect v-model="model"
46+
class="settings-select__select"
47+
:options="options"
48+
:clearable="false"
49+
:searchable="false"
50+
label-outside />
51+
</label>
52+
</template>
53+
54+
<style scoped>
55+
.settings-select {
56+
--icon-height: 16px;
57+
--icon-width: 36px;
58+
display: flex;
59+
gap: calc(var(--default-grid-baseline) * 2);
60+
padding: 0 var(--default-grid-baseline) 0 calc((var(--default-clickable-area) - var(--icon-height)) / 2);
61+
}
62+
63+
.settings-select__label {
64+
display: flex;
65+
align-items: center;
66+
gap: var(--default-grid-baseline);
67+
}
68+
69+
.settings-select__label-icon {
70+
width: var(--icon-width);
71+
}
72+
73+
.settings-select__select {
74+
/* TODO: fix in upstream? */
75+
margin: 0 !important;
76+
}
77+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
defineProps<{
8+
name?: string
9+
}>()
10+
</script>
11+
12+
<template>
13+
<div class="settings-subsection">
14+
<h4 v-if="name" class="settings-subsection__title">
15+
{{ name }}
16+
</h4>
17+
<div class="setting-subsection__content">
18+
<slot />
19+
</div>
20+
</div>
21+
</template>
22+
23+
<style scoped>
24+
.setting-subsection__content {
25+
display: flex;
26+
flex-direction: column;
27+
gap: var(--default-grid-baseline);
28+
align-items: stretch;
29+
}
30+
</style>

src/talk/renderer/Settings/index.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { t } from '@nextcloud/l10n'
7+
import DesktopSettingsSection from './DesktopSettingsSection.vue'
8+
import { createCustomElement } from '../utils/createCustomElement.ts'
9+
10+
/**
11+
* Register Talk Desktop settings sections
12+
*/
13+
export function registerTalkDesktopSettingsSection() {
14+
window.OCA.Talk.Settings.registerSection({
15+
id: 'talk-desktop-settings-application',
16+
name: t('talk_desktop', 'Application'),
17+
element: createCustomElement('talk-desktop-settings-application', DesktopSettingsSection).tagName,
18+
})
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { AppConfig } from '../../../app/AppConfig.ts'
7+
import { computed } from 'vue'
8+
import { useAppConfig } from './appConfig.store.ts'
9+
10+
/**
11+
* Get an application config value
12+
* @param key - The key of the config value
13+
* @return - A settable config value
14+
*/
15+
export function useAppConfigValue<K extends keyof AppConfig>(key: K) {
16+
const { getAppConfigValue, setAppConfigValue } = useAppConfig()
17+
18+
return computed({
19+
get() {
20+
return getAppConfigValue(key)
21+
},
22+
set(value: AppConfig[K]) {
23+
setAppConfigValue(key, value)
24+
},
25+
})
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Ref, WritableComputedRef } from 'vue'
7+
import { computed } from 'vue'
8+
9+
export type NcSelectOption<T> = { label: string, value: T }
10+
11+
/**
12+
* Create a model proxy for NcSelect
13+
* @param modelValue - the model value to bind to
14+
* @param options - the list of the select options
15+
* @return - a model proxy for NcSelect
16+
*/
17+
export function useNcSelectModel<T>(modelValue: Ref<T>, options: NcSelectOption<T>[]): WritableComputedRef<NcSelectOption<T>> {
18+
return computed({
19+
get() {
20+
return options.find(item => item.value === modelValue.value)!
21+
},
22+
23+
set(option: NcSelectOption<T>) {
24+
modelValue.value = option.value
25+
},
26+
})
27+
}

src/talk/renderer/talk.main.js

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { setupWebPage } from '../../shared/setupWebPage.js'
1717
import { createViewer } from './Viewer/Viewer.js'
1818
import { createDesktopApp } from './desktop.app.js'
19+
import { registerTalkDesktopSettingsSection } from './Settings/index.ts'
1920

2021
// Initially open the welcome page, if not specified
2122
if (!window.location.hash) {
@@ -36,4 +37,6 @@ initTalkHashIntegration(window.OCA.Talk.instance)
3637

3738
window.OCA.Talk.Desktop.talkRouter.value = window.OCA.Talk.instance.$router
3839

40+
registerTalkDesktopSettingsSection()
41+
3942
await import('./notifications/notifications.store.js')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Component, ComponentInstance, DefineComponent } from 'vue'
7+
import Vue from 'vue'
8+
9+
/**
10+
* Create a custom element from a Vue component
11+
* @param name - Name of the element
12+
* @param component - Vue component to use as a settings section
13+
*/
14+
export function createCustomElement(name: string, component: Component): CustomElementConstructor & { tagName: string } {
15+
class CustomElement extends HTMLElement {
16+
17+
static tagName = name + '-' + Math.random().toString(36).substring(6)
18+
19+
vm: ComponentInstance
20+
rootElement: HTMLElement
21+
isMounted: boolean = false
22+
23+
constructor() {
24+
super()
25+
this.rootElement = document.createElement('div')
26+
const ComponentConstructor = Vue.extend(component as DefineComponent)
27+
this.vm = new ComponentConstructor()
28+
}
29+
30+
connectedCallback() {
31+
if (this.isMounted) {
32+
return
33+
}
34+
this.isMounted = true
35+
this.appendChild(this.rootElement)
36+
this.vm.$mount(this.rootElement)
37+
}
38+
39+
}
40+
41+
window.customElements.define(CustomElement.tagName, CustomElement)
42+
43+
return CustomElement
44+
}

tsconfig.json

+5
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@
1414
},
1515
"vueCompilerOptions": {
1616
"target": 2.7,
17+
"experimentalModelPropName": {
18+
"modelValue": {
19+
"SettingsSelect": true,
20+
},
21+
},
1722
},
1823
}

0 commit comments

Comments
 (0)