aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/src
diff options
context:
space:
mode:
authorChristopher Ng <chrng8@gmail.com>2022-10-01 03:04:39 +0000
committerChristopher Ng <chrng8@gmail.com>2022-10-28 00:18:47 +0000
commit4a2bbc7af9249364ba2455f627522450262cad75 (patch)
treeb0fd373e0aad0f18c35d2272c565b20bdab630a9 /apps/theming/src
parentd007088cf5d89e29065991e0cbe2c890dfa13d96 (diff)
downloadnextcloud-server-4a2bbc7af9249364ba2455f627522450262cad75.tar.gz
nextcloud-server-4a2bbc7af9249364ba2455f627522450262cad75.zip
Rewrite admin theming in Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>
Diffstat (limited to 'apps/theming/src')
-rw-r--r--apps/theming/src/AdminTheming.vue303
-rw-r--r--apps/theming/src/admin-settings.js33
-rw-r--r--apps/theming/src/components/admin/CheckboxField.vue102
-rw-r--r--apps/theming/src/components/admin/ColorPickerField.vue121
-rw-r--r--apps/theming/src/components/admin/FileInputField.vue248
-rw-r--r--apps/theming/src/components/admin/TextField.vue98
-rw-r--r--apps/theming/src/components/admin/shared/field.scss32
-rw-r--r--apps/theming/src/helpers/refreshStyles.js33
-rw-r--r--apps/theming/src/mixins/admin/FieldMixin.js64
-rw-r--r--apps/theming/src/mixins/admin/TextValueMixin.js77
-rw-r--r--apps/theming/src/personal-settings.js (renamed from apps/theming/src/settings.js)15
11 files changed, 1113 insertions, 13 deletions
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue
new file mode 100644
index 00000000000..1d9f5b69512
--- /dev/null
+++ b/apps/theming/src/AdminTheming.vue
@@ -0,0 +1,303 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <section>
+ <NcSettingsSection :title="t('theming', 'Theming')"
+ :description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')"
+ :doc-url="docUrl">
+ <div class="admin-theming">
+ <NcNoteCard v-if="!isThemable"
+ type="error"
+ :show-alert="true">
+ <p>{{ notThemableErrorMessage }}</p>
+ </NcNoteCard>
+ <TextField v-for="field in textFields"
+ :key="field.name"
+ :name="field.name"
+ :value.sync="field.value"
+ :default-value="field.defaultValue"
+ :type="field.type"
+ :display-name="field.displayName"
+ :placeholder="field.placeholder"
+ :maxlength="field.maxlength"
+ @update:theming="$emit('update:theming')" />
+ <ColorPickerField :name="colorPickerField.name"
+ :value.sync="colorPickerField.value"
+ :default-value="colorPickerField.defaultValue"
+ :display-name="colorPickerField.displayName"
+ @update:theming="$emit('update:theming')" />
+ <FileInputField v-for="field in fileInputFields"
+ :key="field.name"
+ :name="field.name"
+ :mime-name="field.mimeName"
+ :mime-value.sync="field.mimeValue"
+ :default-mime-value="field.defaultMimeValue"
+ :display-name="field.displayName"
+ :aria-label="field.ariaLabel"
+ @update:theming="$emit('update:theming')" />
+ <div class="admin-theming__preview">
+ <div class="admin-theming__preview-logo" />
+ </div>
+ </div>
+ </NcSettingsSection>
+ <NcSettingsSection :title="t('theming', 'Advanced options')">
+ <div class="admin-theming-advanced">
+ <TextField v-for="field in advancedTextFields"
+ :key="field.name"
+ :name="field.name"
+ :value.sync="field.value"
+ :default-value="field.defaultValue"
+ :type="field.type"
+ :display-name="field.displayName"
+ :placeholder="field.placeholder"
+ :maxlength="field.maxlength"
+ @update:theming="$emit('update:theming')" />
+ <FileInputField v-for="field in advancedFileInputFields"
+ :key="field.name"
+ :name="field.name"
+ :mime-name="field.mimeName"
+ :mime-value.sync="field.mimeValue"
+ :default-mime-value="field.defaultMimeValue"
+ :display-name="field.displayName"
+ :aria-label="field.ariaLabel"
+ @update:theming="$emit('update:theming')" />
+ <CheckboxField :name="userThemingField.name"
+ :value="userThemingField.value"
+ :default-value="userThemingField.defaultValue"
+ :display-name="userThemingField.displayName"
+ :label="userThemingField.label"
+ :description="userThemingField.description"
+ @update:theming="$emit('update:theming')" />
+ <a v-if="!canThemeIcons"
+ :href="docUrlIcons"
+ rel="noreferrer noopener">
+ <em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em>
+ </a>
+ </div>
+ </NcSettingsSection>
+ </section>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+
+import {
+ NcNoteCard,
+ NcSettingsSection,
+} from '@nextcloud/vue'
+import CheckboxField from './components/admin/CheckboxField.vue'
+import ColorPickerField from './components/admin/ColorPickerField.vue'
+import FileInputField from './components/admin/FileInputField.vue'
+import TextField from './components/admin/TextField.vue'
+
+const {
+ backgroundMime,
+ canThemeIcons,
+ color,
+ docUrl,
+ docUrlIcons,
+ faviconMime,
+ isThemable,
+ legalNoticeUrl,
+ logoheaderMime,
+ logoMime,
+ name,
+ notThemableErrorMessage,
+ privacyPolicyUrl,
+ slogan,
+ url,
+ userThemingDisabled,
+} = loadState('theming', 'adminThemingParameters')
+
+const textFields = [
+ {
+ name: 'name',
+ value: name,
+ defaultValue: 'Nextcloud',
+ type: 'text',
+ displayName: t('theming', 'Name'),
+ placeholder: t('theming', 'Name'),
+ maxlength: 250,
+ },
+ {
+ name: 'url',
+ value: url,
+ defaultValue: 'https://nextcloud.com',
+ type: 'url',
+ displayName: t('theming', 'Web link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+ {
+ name: 'slogan',
+ value: slogan,
+ defaultValue: t('theming', 'a safe home for all your data'),
+ type: 'text',
+ displayName: t('theming', 'Slogan'),
+ placeholder: t('theming', 'Slogan'),
+ maxlength: 500,
+ },
+]
+
+const colorPickerField = {
+ name: 'color',
+ value: color,
+ defaultValue: '#0082c9',
+ displayName: t('theming', 'Color'),
+}
+
+const fileInputFields = [
+ {
+ name: 'logo',
+ mimeName: 'logoMime',
+ mimeValue: logoMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Logo'),
+ ariaLabel: t('theming', 'Upload new logo'),
+ },
+ {
+ name: 'background',
+ mimeName: 'backgroundMime',
+ mimeValue: backgroundMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Background and login image'),
+ ariaLabel: t('theming', 'Upload new background and login image'),
+ },
+]
+
+const advancedTextFields = [
+ {
+ name: 'imprintUrl',
+ value: legalNoticeUrl,
+ defaultValue: '',
+ type: 'url',
+ displayName: t('theming', 'Legal notice link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+ {
+ name: 'privacyUrl',
+ value: privacyPolicyUrl,
+ defaultValue: '',
+ type: 'url',
+ displayName: t('theming', 'Privacy policy link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+]
+
+const advancedFileInputFields = [
+ {
+ name: 'logoheader',
+ mimeName: 'logoheaderMime',
+ mimeValue: logoheaderMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Header logo'),
+ ariaLabel: t('theming', 'Upload new header logo'),
+ },
+ {
+ name: 'favicon',
+ mimeName: 'faviconMime',
+ mimeValue: faviconMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Favicon'),
+ ariaLabel: t('theming', 'Upload new favicon'),
+ },
+]
+
+const userThemingField = {
+ name: 'disable-user-theming',
+ value: userThemingDisabled,
+ defaultValue: false,
+ displayName: t('theming', 'User settings'),
+ label: t('theming', 'Disable user theming'),
+ description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'),
+}
+
+export default {
+ name: 'AdminTheming',
+
+ components: {
+ CheckboxField,
+ ColorPickerField,
+ FileInputField,
+ NcNoteCard,
+ NcSettingsSection,
+ TextField,
+ },
+
+ emits: [
+ 'update:theming',
+ ],
+
+ data() {
+ return {
+ textFields,
+ colorPickerField,
+ fileInputFields,
+ advancedTextFields,
+ advancedFileInputFields,
+ userThemingField,
+
+ canThemeIcons,
+ docUrl,
+ docUrlIcons,
+ isThemable,
+ notThemableErrorMessage,
+ }
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.admin-theming,
+.admin-theming-advanced {
+ display: flex;
+ flex-direction: column;
+ gap: 8px 0;
+}
+
+.admin-theming {
+ &__preview {
+ width: 230px;
+ height: 140px;
+ background-size: cover;
+ background-position: center;
+ text-align: center;
+ margin-top: 10px;
+ background-color: var(--color-primary-default);
+ background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
+
+ &-logo {
+ width: 20%;
+ height: 20%;
+ margin-top: 20px;
+ display: inline-block;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
+ }
+ }
+}
+</style>
diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js
new file mode 100644
index 00000000000..9fce526c463
--- /dev/null
+++ b/apps/theming/src/admin-settings.js
@@ -0,0 +1,33 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import App from './AdminTheming.vue'
+import { refreshStyles } from './helpers/refreshStyles.js'
+
+Vue.prototype.OC = OC
+Vue.prototype.t = t
+
+const View = Vue.extend(App)
+const theming = new View()
+theming.$mount('#admin-theming')
+theming.$on('update:theming', refreshStyles)
diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue
new file mode 100644
index 00000000000..5877614717e
--- /dev/null
+++ b/apps/theming/src/components/admin/CheckboxField.vue
@@ -0,0 +1,102 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcCheckboxRadioSwitch type="switch"
+ :id="id"
+ :checked.sync="localValue"
+ @update:checked="save">
+ {{ label }}
+ </NcCheckboxRadioSwitch>
+ </div>
+
+ <p class="field__description">{{ description }}</p>
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+ </div>
+</template>
+
+<script>
+import {
+ NcCheckboxRadioSwitch,
+ NcNoteCard,
+} from '@nextcloud/vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'CheckboxField',
+
+ components: {
+ NcCheckboxRadioSwitch,
+ NcNoteCard,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ defaultValue: {
+ type: Boolean,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ &__description {
+ color: var(--color-text-maxcontrast);
+ }
+}
+</style>
diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue
new file mode 100644
index 00000000000..2e6ee99a75d
--- /dev/null
+++ b/apps/theming/src/components/admin/ColorPickerField.vue
@@ -0,0 +1,121 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcColorPicker :value.sync="localValue"
+ :advanced-fields="true"
+ @update:value="debounceSave">
+ <NcButton class="field__button"
+ type="primary"
+ :id="id"
+ :aria-label="t('theming', 'Select a custom color')">
+ {{ value }}
+ </NcButton>
+ </NcColorPicker>
+ <NcButton v-if="value !== defaultValue"
+ type="tertiary"
+ :aria-label="t('theming', 'Reset to default')"
+ @click="undo">
+ <template #icon>
+ <Undo :size="20" />
+ </template>
+ </NcButton>
+ </div>
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+ </div>
+</template>
+
+<script>
+import { debounce } from 'debounce'
+import {
+ NcButton,
+ NcColorPicker,
+ NcNoteCard,
+} from '@nextcloud/vue'
+import Undo from 'vue-material-design-icons/UndoVariant.vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'ColorPickerField',
+
+ components: {
+ NcButton,
+ NcColorPicker,
+ NcNoteCard,
+ Undo,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ },
+
+ methods: {
+ debounceSave: debounce(async function() {
+ await this.save()
+ }, 200),
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ // Override default NcButton styles
+ &__button {
+ width: 230px !important;
+ border-radius: var(--border-radius-large) !important;
+ background-color: var(--color-primary-default) !important;
+ &:hover {
+ background-color: var(--color-primary-element-default-hover) !important;
+ }
+ }
+}
+</style>
diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue
new file mode 100644
index 00000000000..537970cc0cc
--- /dev/null
+++ b/apps/theming/src/components/admin/FileInputField.vue
@@ -0,0 +1,248 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcButton type="secondary"
+ :id="id"
+ :aria-label="ariaLabel"
+ @click="activateLocalFilePicker">
+ <template #icon>
+ <Upload :size="20" />
+ </template>
+ {{ t('theming', 'Upload') }}
+ </NcButton>
+ <NcButton v-if="showReset"
+ type="tertiary"
+ :aria-label="t('theming', 'Reset to default')"
+ @click="undo">
+ <template #icon>
+ <Undo :size="20" />
+ </template>
+ </NcButton>
+ <NcButton v-if="showRemove"
+ type="tertiary"
+ :aria-label="t('theming', 'Remove background image')"
+ @click="removeBackground">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </NcButton>
+ <NcLoadingIcon v-if="showLoading"
+ class="field__loading-icon"
+ :size="20" />
+ </div>
+
+ <div v-if="(name === 'logoheader' || name === 'favicon') && mimeValue !== defaultMimeValue"
+ class="field__preview"
+ :class="{
+ 'field__preview--logoheader': name === 'logoheader',
+ 'field__preview--favicon': name === 'favicon',
+ }" />
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+
+ <input ref="input"
+ type="file"
+ @change="onChange">
+ </div>
+</template>
+
+<script>
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+import {
+ NcButton,
+ NcLoadingIcon,
+ NcNoteCard,
+} from '@nextcloud/vue'
+import Delete from 'vue-material-design-icons/Delete.vue'
+import Undo from 'vue-material-design-icons/UndoVariant.vue'
+import Upload from 'vue-material-design-icons/Upload.vue'
+
+import FieldMixin from '../../mixins/admin/FieldMixin.js'
+
+export default {
+ name: 'FileInputField',
+
+ components: {
+ Delete,
+ NcButton,
+ NcLoadingIcon,
+ NcNoteCard,
+ Undo,
+ Upload,
+ },
+
+ mixins: [
+ FieldMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ mimeName: {
+ type: String,
+ required: true,
+ },
+ mimeValue: {
+ type: String,
+ required: true,
+ },
+ defaultMimeValue: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ ariaLabel: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ showLoading: false,
+ }
+ },
+
+ computed: {
+ showReset() {
+ return this.mimeValue !== this.defaultMimeValue
+ },
+
+ showRemove() {
+ if (this.name === 'background') {
+ if (this.mimeValue.startsWith('image/')) {
+ return true
+ }
+ if (this.mimeValue === this.defaultMimeValue) {
+ return true
+ }
+ }
+ return false
+ },
+ },
+
+ methods: {
+ activateLocalFilePicker() {
+ this.reset()
+ // Set to null so that selecting the same file will trigger the change event
+ this.$refs.input.value = null
+ this.$refs.input.click()
+ },
+
+ async onChange(e) {
+ const file = e.target.files[0]
+
+ const formData = new FormData()
+ formData.append('key', this.name)
+ formData.append('image', file)
+
+ const url = generateUrl('/apps/theming/ajax/uploadImage')
+ try {
+ this.showLoading = true
+ await axios.post(url, formData)
+ this.showLoading = false
+ this.$emit('update:mime-value', file.type)
+ this.handleSuccess()
+ } catch (e) {
+ this.showLoading = false
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async undo() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/undoChanges')
+ try {
+ await axios.post(url, {
+ setting: this.mimeName,
+ })
+ this.$emit('update:mime-value', this.defaultMimeValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async removeBackground() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/updateStylesheet')
+ try {
+ await axios.post(url, {
+ setting: this.mimeName,
+ value: 'backgroundColor',
+ })
+ this.$emit('update:mime-value', 'backgroundColor')
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ &__loading-icon {
+ width: 44px;
+ height: 44px;
+ }
+
+ &__preview {
+ width: 70px;
+ height: 70px;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin: 10px 0;
+
+ &--logoheader {
+ background-image: var(--image-logoheader);
+ }
+
+ &--favicon {
+ background-image: var(--image-favicon);
+ }
+ }
+}
+
+input[type="file"] {
+ display: none;
+}
+</style>
diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue
new file mode 100644
index 00000000000..df82415e48a
--- /dev/null
+++ b/apps/theming/src/components/admin/TextField.vue
@@ -0,0 +1,98 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <!-- PENDING undo trailing button icon requires @nextcloud/vue release and bump -->
+ <!-- PENDING custom maxlength requires @nextcloud/vue release and bump -->
+ <NcTextField :value.sync="localValue"
+ :label="displayName"
+ :label-visible="true"
+ :placeholder="placeholder"
+ :type="type"
+ :maxlength="maxlength"
+ :spellcheck="false"
+ :success="showSuccess"
+ :error="Boolean(errorMessage)"
+ :helper-text="errorMessage"
+ :show-trailing-button="value !== defaultValue"
+ trailing-button-icon="undo"
+ @trailing-button-click="undo"
+ @keydown.enter="save"
+ @blur="save" />
+ </div>
+</template>
+
+<script>
+import { NcTextField } from '@nextcloud/vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'TextField',
+
+ components: {
+ NcTextField,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ maxlength: {
+ type: Number,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.field {
+ max-width: 400px;
+}
+</style>
diff --git a/apps/theming/src/components/admin/shared/field.scss b/apps/theming/src/components/admin/shared/field.scss
new file mode 100644
index 00000000000..54fc57b3ee5
--- /dev/null
+++ b/apps/theming/src/components/admin/shared/field.scss
@@ -0,0 +1,32 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px 0;
+
+ &__row {
+ display: flex;
+ gap: 0 4px;
+ }
+}
diff --git a/apps/theming/src/helpers/refreshStyles.js b/apps/theming/src/helpers/refreshStyles.js
new file mode 100644
index 00000000000..0c4a7cea22b
--- /dev/null
+++ b/apps/theming/src/helpers/refreshStyles.js
@@ -0,0 +1,33 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export const refreshStyles = () => {
+ // Refresh server-side generated theming CSS
+ [...document.head.querySelectorAll('link.theme')].forEach(theme => {
+ const url = new URL(theme.href)
+ url.searchParams.set('v', Date.now())
+ const newTheme = theme.cloneNode()
+ newTheme.href = url.toString()
+ newTheme.onload = () => theme.remove()
+ document.head.append(newTheme)
+ })
+}
diff --git a/apps/theming/src/mixins/admin/FieldMixin.js b/apps/theming/src/mixins/admin/FieldMixin.js
new file mode 100644
index 00000000000..811fa0c0bba
--- /dev/null
+++ b/apps/theming/src/mixins/admin/FieldMixin.js
@@ -0,0 +1,64 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+const styleRefreshFields = [
+ 'color',
+ 'logo',
+ 'background',
+ 'logoheader',
+ 'favicon',
+ 'disable-user-theming',
+]
+
+export default {
+ emits: [
+ 'update:theming',
+ ],
+
+ data() {
+ return {
+ showSuccess: false,
+ errorMessage: '',
+ }
+ },
+
+ computed: {
+ id() {
+ return `admin-theming-${this.name}`
+ },
+ },
+
+ methods: {
+ reset() {
+ this.showSuccess = false
+ this.errorMessage = ''
+ },
+
+ handleSuccess() {
+ this.showSuccess = true
+ setTimeout(() => { this.showSuccess = false }, 2000)
+ if (styleRefreshFields.includes(this.name)) {
+ this.$emit('update:theming')
+ }
+ },
+ },
+}
diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js
new file mode 100644
index 00000000000..4cce8bb301a
--- /dev/null
+++ b/apps/theming/src/mixins/admin/TextValueMixin.js
@@ -0,0 +1,77 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+import FieldMixin from './FieldMixin.js'
+
+export default {
+ mixins: [
+ FieldMixin,
+ ],
+
+ watch: {
+ value(value) {
+ this.localValue = value
+ },
+ },
+
+ data() {
+ return {
+ localValue: this.value,
+ }
+ },
+
+ methods: {
+ async save() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/updateStylesheet')
+ // Convert boolean to string as server expects string value
+ const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue
+ try {
+ await axios.post(url, {
+ setting: this.name,
+ value: valueToPost,
+ })
+ this.$emit('update:value', this.localValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async undo() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/undoChanges')
+ try {
+ await axios.post(url, {
+ setting: this.name,
+ })
+ this.$emit('update:value', this.defaultValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+ },
+}
diff --git a/apps/theming/src/settings.js b/apps/theming/src/personal-settings.js
index 9b846117947..97f5e75e27a 100644
--- a/apps/theming/src/settings.js
+++ b/apps/theming/src/personal-settings.js
@@ -22,23 +22,12 @@
import Vue from 'vue'
import App from './UserThemes.vue'
+import { refreshStyles } from './helpers/refreshStyles.js'
-// bind to window
Vue.prototype.OC = OC
Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#theming')
-
-theming.$on('update:background', () => {
- // Refresh server-side generated theming CSS
- [...document.head.querySelectorAll('link.theme')].forEach(theme => {
- const url = new URL(theme.href)
- url.searchParams.set('v', Date.now())
- const newTheme = theme.cloneNode()
- newTheme.href = url.toString()
- newTheme.onload = () => theme.remove()
- document.head.append(newTheme)
- })
-})
+theming.$on('update:background', refreshStyles)