diff options
author | Christopher Ng <chrng8@gmail.com> | 2022-10-01 03:04:39 +0000 |
---|---|---|
committer | Christopher Ng <chrng8@gmail.com> | 2022-10-28 00:18:47 +0000 |
commit | 4a2bbc7af9249364ba2455f627522450262cad75 (patch) | |
tree | b0fd373e0aad0f18c35d2272c565b20bdab630a9 /apps/theming/src | |
parent | d007088cf5d89e29065991e0cbe2c890dfa13d96 (diff) | |
download | nextcloud-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.vue | 303 | ||||
-rw-r--r-- | apps/theming/src/admin-settings.js | 33 | ||||
-rw-r--r-- | apps/theming/src/components/admin/CheckboxField.vue | 102 | ||||
-rw-r--r-- | apps/theming/src/components/admin/ColorPickerField.vue | 121 | ||||
-rw-r--r-- | apps/theming/src/components/admin/FileInputField.vue | 248 | ||||
-rw-r--r-- | apps/theming/src/components/admin/TextField.vue | 98 | ||||
-rw-r--r-- | apps/theming/src/components/admin/shared/field.scss | 32 | ||||
-rw-r--r-- | apps/theming/src/helpers/refreshStyles.js | 33 | ||||
-rw-r--r-- | apps/theming/src/mixins/admin/FieldMixin.js | 64 | ||||
-rw-r--r-- | apps/theming/src/mixins/admin/TextValueMixin.js | 77 | ||||
-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) |