aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/src')
-rw-r--r--apps/theming/src/AdminTheming.vue201
-rw-r--r--apps/theming/src/UserTheming.vue (renamed from apps/theming/src/UserThemes.vue)163
-rw-r--r--apps/theming/src/admin-settings.js27
-rw-r--r--apps/theming/src/components/AppOrderSelector.vue8
-rw-r--r--apps/theming/src/components/AppOrderSelectorElement.vue6
-rw-r--r--apps/theming/src/components/BackgroundSettings.vue208
-rw-r--r--apps/theming/src/components/ItemPreview.vue31
-rw-r--r--apps/theming/src/components/UserAppMenuSection.vue39
-rw-r--r--apps/theming/src/components/UserPrimaryColor.vue160
-rw-r--r--apps/theming/src/components/admin/AppMenuSection.vue18
-rw-r--r--apps/theming/src/components/admin/CheckboxField.vue35
-rw-r--r--apps/theming/src/components/admin/ColorPickerField.vue118
-rw-r--r--apps/theming/src/components/admin/FileInputField.vue40
-rw-r--r--apps/theming/src/components/admin/TextField.vue23
-rw-r--r--apps/theming/src/components/admin/shared/field.scss23
-rw-r--r--apps/theming/src/helpers/refreshStyles.js41
-rw-r--r--apps/theming/src/mixins/admin/FieldMixin.js21
-rw-r--r--apps/theming/src/mixins/admin/TextValueMixin.js68
-rw-r--r--apps/theming/src/personal-settings.js27
19 files changed, 668 insertions, 589 deletions
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue
index daef18ebdce..e899024ca53 100644
--- a/apps/theming/src/AdminTheming.vue
+++ b/apps/theming/src/AdminTheming.vue
@@ -1,23 +1,6 @@
<!--
- - @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/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -44,32 +27,50 @@
:placeholder="field.placeholder"
:type="field.type"
:value.sync="field.value"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<!-- Primary color picker -->
- <ColorPickerField :name="colorPickerField.name"
- :default-value="colorPickerField.defaultValue"
- :display-name="colorPickerField.displayName"
- :value.sync="colorPickerField.value"
+ <ColorPickerField :name="primaryColorPickerField.name"
+ :description="primaryColorPickerField.description"
+ :default-value="primaryColorPickerField.defaultValue"
+ :display-name="primaryColorPickerField.displayName"
+ :value.sync="primaryColorPickerField.value"
data-admin-theming-setting-primary-color
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
+
+ <!-- Background color picker -->
+ <ColorPickerField name="background_color"
+ :description="t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.')"
+ :default-value.sync="defaultBackgroundColor"
+ :display-name="t('theming', 'Background color')"
+ :value.sync="backgroundColor"
+ data-admin-theming-setting-background-color
+ @update:theming="refreshStyles" />
<!-- Default background picker -->
- <FileInputField v-for="field in fileInputFields"
- :key="field.name"
- :aria-label="field.ariaLabel"
- :data-admin-theming-setting-file="field.name"
- :default-mime-value="field.defaultMimeValue"
- :display-name="field.displayName"
- :mime-name="field.mimeName"
- :mime-value.sync="field.mimeValue"
- :name="field.name"
- @update:theming="$emit('update:theming')" />
+ <FileInputField :aria-label="t('theming', 'Upload new logo')"
+ data-admin-theming-setting-file="logo"
+ :display-name="t('theming', 'Logo')"
+ mime-name="logoMime"
+ :mime-value.sync="logoMime"
+ name="logo"
+ @update:theming="refreshStyles" />
+
+ <FileInputField :aria-label="t('theming', 'Upload new background and login image')"
+ data-admin-theming-setting-file="background"
+ :display-name="t('theming', 'Background and login image')"
+ mime-name="backgroundMime"
+ :mime-value.sync="backgroundMime"
+ name="background"
+ @uploaded="backgroundURL = $event"
+ @update:theming="refreshStyles" />
+
<div class="admin-theming__preview" data-admin-theming-preview>
<div class="admin-theming__preview-logo" data-admin-theming-preview-logo />
</div>
</div>
</NcSettingsSection>
+
<NcSettingsSection :name="t('theming', 'Advanced options')">
<div class="admin-theming-advanced">
<TextField v-for="field in advancedTextFields"
@@ -81,7 +82,7 @@
:display-name="field.displayName"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<FileInputField v-for="field in advancedFileInputFields"
:key="field.name"
:name="field.name"
@@ -90,7 +91,7 @@
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:aria-label="field.ariaLabel"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<CheckboxField :name="userThemingField.name"
:value="userThemingField.value"
:default-value="userThemingField.defaultValue"
@@ -98,7 +99,7 @@
:label="userThemingField.label"
:description="userThemingField.description"
data-admin-theming-setting-disable-user-theming
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<a v-if="!canThemeIcons"
:href="docUrlIcons"
rel="noreferrer noopener">
@@ -112,9 +113,10 @@
<script>
import { loadState } from '@nextcloud/initial-state'
+import { refreshStyles } from './helpers/refreshStyles.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import CheckboxField from './components/admin/CheckboxField.vue'
import ColorPickerField from './components/admin/ColorPickerField.vue'
import FileInputField from './components/admin/FileInputField.vue'
@@ -122,9 +124,12 @@ import TextField from './components/admin/TextField.vue'
import AppMenuSection from './components/admin/AppMenuSection.vue'
const {
+ defaultBackgroundURL,
+
backgroundMime,
+ backgroundURL,
+ backgroundColor,
canThemeIcons,
- color,
docUrl,
docUrlIcons,
faviconMime,
@@ -134,6 +139,7 @@ const {
logoMime,
name,
notThemableErrorMessage,
+ primaryColor,
privacyPolicyUrl,
slogan,
url,
@@ -171,32 +177,14 @@ const textFields = [
},
]
-const colorPickerField = {
- name: 'color',
- value: color,
+const primaryColorPickerField = {
+ name: 'primary_color',
+ value: primaryColor,
defaultValue: '#0082c9',
- displayName: t('theming', 'Color'),
+ displayName: t('theming', 'Primary color'),
+ description: t('theming', 'The primary color is used for highlighting elements like important buttons. It might get slightly adjusted depending on the current color schema.'),
}
-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',
@@ -259,17 +247,17 @@ export default {
TextField,
},
- emits: [
- 'update:theming',
- ],
-
- textFields,
-
data() {
return {
+ backgroundMime,
+ backgroundURL,
+ backgroundColor,
+ defaultBackgroundColor: '#0069c3',
+
+ logoMime,
+
textFields,
- colorPickerField,
- fileInputFields,
+ primaryColorPickerField,
advancedTextFields,
advancedFileInputFields,
userThemingField,
@@ -282,6 +270,64 @@ export default {
notThemableErrorMessage,
}
},
+
+ computed: {
+ cssBackgroundImage() {
+ if (this.backgroundURL) {
+ return `url('${this.backgroundURL}')`
+ }
+ return 'unset'
+ },
+ },
+
+ watch: {
+ backgroundMime() {
+ if (this.backgroundMime === '') {
+ // Reset URL to default value for preview
+ this.backgroundURL = defaultBackgroundURL
+ } else if (this.backgroundMime === 'backgroundColor') {
+ // Reset URL to empty image when only color is configured
+ this.backgroundURL = ''
+ }
+ },
+ async backgroundURL() {
+ // When the background is changed we need to emulate the background color change
+ if (this.backgroundURL !== '') {
+ const color = await this.calculateDefaultBackground()
+ this.defaultBackgroundColor = color
+ this.backgroundColor = color
+ }
+ },
+ },
+
+ async mounted() {
+ if (this.backgroundURL) {
+ this.defaultBackgroundColor = await this.calculateDefaultBackground()
+ }
+ },
+
+ methods: {
+ refreshStyles,
+
+ /**
+ * Same as on server - if a user uploads an image the mean color will be set as the background color
+ */
+ calculateDefaultBackground() {
+ const toHex = (num) => `00${num.toString(16)}`.slice(-2)
+
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.src = this.backgroundURL
+ img.onload = () => {
+ const context = document.createElement('canvas').getContext('2d')
+ context.imageSmoothingEnabled = true
+ context.drawImage(img, 0, 0, 1, 1)
+ resolve('#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join(''))
+ }
+ img.onerror = reject
+ })
+ },
+ },
}
</script>
@@ -301,15 +347,8 @@ export default {
background-position: center;
text-align: center;
margin-top: 10px;
- /* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
- But without the user variables. That way the admin can preview the render as guest*/
- /* As guest, there is no user color color-background-plain */
- background-color: var(--color-primary-element-default);
- /* As guest, there is no user background (--image-background)
- 1. Empty background if defined
- 2. Else default background
- 3. Finally default gradient (should not happened, the background is always defined anyway) */
- background-image: var(--image-background-plain, var(--image-background-default));
+ background-color: v-bind('backgroundColor');
+ background-image: v-bind('cssBackgroundImage');
&-logo {
width: 20%;
diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserTheming.vue
index d941bf9c1db..baebf09bcc5 100644
--- a/apps/theming/src/UserThemes.vue
+++ b/apps/theming/src/UserTheming.vue
@@ -1,30 +1,11 @@
<!--
- - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- - @copyright Copyright (c) 2022 Greta Doci <gretadoci@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/>.
- -
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<section>
<NcSettingsSection :name="t('theming', 'Appearance and accessibility settings')"
- :limit-width="false"
class="theming">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="description" />
@@ -51,22 +32,37 @@
type="font"
@change="changeFont" />
</div>
+
+ <h3>{{ t('theming', 'Misc accessibility options') }}</h3>
+ <NcCheckboxRadioSwitch type="checkbox"
+ :checked="enableBlurFilter === 'yes'"
+ :indeterminate="enableBlurFilter === ''"
+ @update:checked="changeEnableBlurFilter">
+ {{ t('theming', 'Enable blur background filter (may increase GPU load)') }}
+ </NcCheckboxRadioSwitch>
+ </NcSettingsSection>
+
+ <NcSettingsSection :name="t('theming', 'Primary color')"
+ :description="isUserThemingDisabled
+ ? t('theming', 'Customization has been disabled by your administrator')
+ : t('theming', 'Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements.')">
+ <UserPrimaryColor v-if="!isUserThemingDisabled"
+ ref="primaryColor"
+ @refresh-styles="refreshGlobalStyles" />
</NcSettingsSection>
- <NcSettingsSection :name="t('theming', 'Background')"
- class="background"
- data-user-theming-background-disabled>
- <template v-if="isUserThemingDisabled">
- <p>{{ t('theming', 'Customization has been disabled by your administrator') }}</p>
- </template>
- <template v-else>
- <p>{{ t('theming', 'Set a custom background') }}</p>
- <BackgroundSettings class="background__grid" @update:background="refreshGlobalStyles" />
- </template>
+ <NcSettingsSection class="background"
+ :name="t('theming', 'Background and color')"
+ :description="isUserThemingDisabled
+ ? t('theming', 'Customization has been disabled by your administrator')
+ : t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color.')">
+ <BackgroundSettings v-if="!isUserThemingDisabled"
+ class="background__grid"
+ @update:background="refreshGlobalStyles" />
</NcSettingsSection>
- <NcSettingsSection :name="t('theming', 'Keyboard shortcuts')">
- <p>{{ t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.') }}</p>
+ <NcSettingsSection :name="t('theming', 'Keyboard shortcuts')"
+ :description="t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.')">
<NcCheckboxRadioSwitch class="theming__preview-toggle"
:checked.sync="shortcutsDisabled"
type="switch"
@@ -80,24 +76,30 @@
</template>
<script>
-import { generateOcsUrl } from '@nextcloud/router'
+import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
-import axios from '@nextcloud/axios'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import { generateOcsUrl } from '@nextcloud/router'
+import { refreshStyles } from './helpers/refreshStyles'
+
+import axios, { isAxiosError } from '@nextcloud/axios'
+
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import BackgroundSettings from './components/BackgroundSettings.vue'
import ItemPreview from './components/ItemPreview.vue'
import UserAppMenuSection from './components/UserAppMenuSection.vue'
+import UserPrimaryColor from './components/UserPrimaryColor.vue'
const availableThemes = loadState('theming', 'themes', [])
const enforceTheme = loadState('theming', 'enforceTheme', '')
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
+const enableBlurFilter = loadState('theming', 'enableBlurFilter', '')
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
export default {
- name: 'UserThemes',
+ name: 'UserTheming',
components: {
ItemPreview,
@@ -105,6 +107,7 @@ export default {
NcSettingsSection,
BackgroundSettings,
UserAppMenuSection,
+ UserPrimaryColor,
},
data() {
@@ -115,6 +118,8 @@ export default {
enforceTheme,
shortcutsDisabled,
isUserThemingDisabled,
+
+ enableBlurFilter,
}
},
@@ -133,35 +138,32 @@ export default {
},
description() {
- // using the `t` replace method escape html, we have to do it manually :/
return t(
'theming',
- 'Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level.',
+ 'Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level.',
+ {
+ linkstart: '<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">',
+ linkend: '</a>',
+ },
+ {
+ escape: false,
+ },
)
- .replace('{guidelines}', this.guidelinesLink)
- .replace('{linkend}', '</a>')
- },
-
- guidelinesLink() {
- return '<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">'
},
descriptionDetail() {
return t(
'theming',
'If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!',
+ {
+ issuetracker: '<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">',
+ designteam: '<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">',
+ linkend: '</a>',
+ },
+ {
+ escape: false,
+ },
)
- .replace('{issuetracker}', this.issuetrackerLink)
- .replace('{designteam}', this.designteamLink)
- .replace(/\{linkend\}/g, '</a>')
- },
-
- issuetrackerLink() {
- return '<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">'
- },
-
- designteamLink() {
- return '<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">'
},
},
@@ -173,20 +175,9 @@ export default {
methods: {
// Refresh server-side generated theming CSS
- refreshGlobalStyles() {
- [...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)
- })
- },
-
- updateBackground(data) {
- this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value
- this.refreshGlobalStyles()
+ async refreshGlobalStyles() {
+ await refreshStyles()
+ this.$nextTick(() => this.$refs.primaryColor.reload())
},
changeTheme({ enabled, id }) {
@@ -240,6 +231,22 @@ export default {
}
},
+ async changeEnableBlurFilter() {
+ this.enableBlurFilter = this.enableBlurFilter === 'no' ? 'yes' : 'no'
+ await axios({
+ url: generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
+ appId: 'theming',
+ configKey: 'force_enable_blur_filter',
+ }),
+ data: {
+ configValue: this.enableBlurFilter,
+ },
+ method: 'POST',
+ })
+ // Refresh the styles
+ this.$emit('update:background')
+ },
+
updateBodyAttributes() {
const enabledThemesIDs = this.themes.filter(theme => theme.enabled === true).map(theme => theme.id)
const enabledFontsIDs = this.fonts.filter(font => font.enabled === true).map(font => font.id)
@@ -275,9 +282,13 @@ export default {
})
}
- } catch (err) {
- console.error(err, err.response)
- OC.Notification.showTemporary(t('theming', err.response.data.ocs.meta.message + '. Unable to apply the setting.'))
+ } catch (error) {
+ console.error('theming: Unable to apply setting.', error)
+ let message = t('theming', 'Unable to apply the setting.')
+ if (isAxiosError(error) && error.response.data.ocs?.meta?.message) {
+ message = `${error.response.data.ocs.meta.message}. ${message}`
+ }
+ showError(message)
}
},
},
@@ -292,7 +303,7 @@ export default {
}
// Proper highlight for links and focus feedback
- &::v-deep a {
+ :deep(a) {
font-weight: bold;
&:hover,
@@ -303,12 +314,10 @@ export default {
&__preview-list {
--gap: 30px;
-
display: grid;
margin-top: var(--gap);
column-gap: var(--gap);
row-gap: var(--gap);
- grid-template-columns: 1fr 1fr;
}
}
diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js
index 774d58e4a5c..622837658f9 100644
--- a/apps/theming/src/admin-settings.js
+++ b/apps/theming/src/admin-settings.js
@@ -1,32 +1,14 @@
/**
- * @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/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
import Vue from 'vue'
-import { refreshStyles } from './helpers/refreshStyles.js'
import App from './AdminTheming.vue'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
Vue.prototype.OC = OC
Vue.prototype.t = t
@@ -34,4 +16,3 @@ 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/AppOrderSelector.vue b/apps/theming/src/components/AppOrderSelector.vue
index b1fff45de78..9c065211bc1 100644
--- a/apps/theming/src/components/AppOrderSelector.vue
+++ b/apps/theming/src/components/AppOrderSelector.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<Fragment>
<div :id="statusInfoId"
@@ -25,9 +29,11 @@
</template>
<script lang="ts">
+import type { PropType } from 'vue'
+
import { translate as t } from '@nextcloud/l10n'
import { useSortable } from '@vueuse/integrations/useSortable'
-import { PropType, computed, defineComponent, onUpdated, ref } from 'vue'
+import { computed, defineComponent, onUpdated, ref } from 'vue'
import { Fragment } from 'vue-frag'
import AppOrderSelectorElement from './AppOrderSelectorElement.vue'
diff --git a/apps/theming/src/components/AppOrderSelectorElement.vue b/apps/theming/src/components/AppOrderSelectorElement.vue
index 23e7b0b07c9..fc41e8e6165 100644
--- a/apps/theming/src/components/AppOrderSelectorElement.vue
+++ b/apps/theming/src/components/AppOrderSelectorElement.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<li :data-cy-app-order-element="app.id"
:class="{
@@ -61,7 +65,7 @@ import { defineComponent, nextTick, ref } from 'vue'
import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue'
import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
interface IApp {
id: string // app id
diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue
index 0a8c49be45e..58b76dd9602 100644
--- a/apps/theming/src/components/BackgroundSettings.vue
+++ b/apps/theming/src/components/BackgroundSettings.vue
@@ -1,27 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Greta Doci <gretadoci@gmail.com>
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - 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/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div class="background-selector" data-user-theming-background-settings>
@@ -32,15 +12,34 @@
'background background__filepicker': true,
'background--active': backgroundImage === 'custom'
}"
- :data-color-bright="invertTextColor(Theming.color)"
data-user-theming-background-custom
tabindex="0"
@click="pickFile">
{{ t('theming', 'Custom background') }}
- <ImageEdit v-if="backgroundImage !== 'custom'" :size="26" />
+ <ImageEdit v-if="backgroundImage !== 'custom'" :size="20" />
<Check :size="44" />
</button>
+ <!-- Custom color picker -->
+ <NcColorPicker v-model="Theming.backgroundColor" @update:value="debouncePickColor">
+ <button :class="{
+ 'icon-loading': loading === 'color',
+ 'background background__color': true,
+ 'background--active': backgroundImage === 'color'
+ }"
+ :aria-pressed="backgroundImage === 'color'"
+ :data-color="Theming.backgroundColor"
+ :data-color-bright="invertTextColor(Theming.backgroundColor)"
+ :style="{ backgroundColor: Theming.backgroundColor, '--border-color': Theming.backgroundColor}"
+ data-user-theming-background-color
+ tabindex="0"
+ @click="backgroundImage !== 'color' && debouncePickColor(Theming.backgroundColor)">
+ {{ t('theming', 'Plain background') /* TRANSLATORS: Background using a single color */ }}
+ <ColorPalette v-if="backgroundImage !== 'color'" :size="20" />
+ <Check :size="44" />
+ </button>
+ </NcColorPicker>
+
<!-- Default background -->
<button :aria-pressed="backgroundImage === 'default'"
:class="{
@@ -48,8 +47,8 @@
'background background__default': true,
'background--active': backgroundImage === 'default'
}"
- :data-color-bright="invertTextColor(Theming.defaultColor)"
- :style="{ '--border-color': Theming.defaultColor }"
+ :data-color-bright="invertTextColor(Theming.defaultBackgroundColor)"
+ :style="{ '--border-color': Theming.defaultBackgroundColor }"
data-user-theming-background-default
tabindex="0"
@click="setDefault">
@@ -57,32 +56,6 @@
<Check :size="44" />
</button>
- <!-- Custom color picker -->
- <NcColorPicker v-model="Theming.color" @input="debouncePickColor">
- <button class="background background__color"
- :data-color="Theming.color"
- :data-color-bright="invertTextColor(Theming.color)"
- :style="{ backgroundColor: Theming.color, '--border-color': Theming.color}"
- data-user-theming-background-color
- tabindex="0">
- {{ t('theming', 'Change color') }}
- </button>
- </NcColorPicker>
-
- <!-- Remove background -->
- <button :aria-pressed="isBackgroundDisabled"
- :class="{
- 'background background__delete': true,
- 'background--active': isBackgroundDisabled
- }"
- data-user-theming-background-clear
- tabindex="0"
- @click="removeBackground">
- {{ t('theming', 'No background') }}
- <Close v-if="!isBackgroundDisabled" :size="32" />
- <Check :size="44" />
- </button>
-
<!-- Background set selection -->
<button v-for="shippedBackground in shippedBackgrounds"
:key="shippedBackground.name"
@@ -94,7 +67,7 @@
'icon-loading': loading === shippedBackground.name,
'background--active': backgroundImage === shippedBackground.name
}"
- :data-color-bright="shippedBackground.details.theming === 'dark'"
+ :data-color-bright="invertTextColor(shippedBackground.details.background_color)"
:data-user-theming-background-shipped="shippedBackground.name"
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
tabindex="0"
@@ -105,24 +78,25 @@
</template>
<script>
-import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
+import { generateFilePath, generateUrl } from '@nextcloud/router'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
-import { Palette } from 'node-vibrant/lib/color.js'
import axios from '@nextcloud/axios'
import debounce from 'debounce'
-import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
-import Vibrant from 'node-vibrant'
+import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
import Check from 'vue-material-design-icons/Check.vue'
-import Close from 'vue-material-design-icons/Close.vue'
import ImageEdit from 'vue-material-design-icons/ImageEdit.vue'
+import ColorPalette from 'vue-material-design-icons/PaletteOutline.vue'
-const backgroundImage = loadState('theming', 'backgroundImage')
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
-const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
-const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
+const backgroundImage = loadState('theming', 'userBackgroundImage')
+const {
+ backgroundImage: defaultBackgroundImage,
+ // backgroundColor: defaultBackgroundColor,
+ backgroundMime: defaultBackgroundMime,
+ defaultShippedBackground,
+} = loadState('theming', 'themingDefaults')
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
@@ -131,7 +105,7 @@ export default {
components: {
Check,
- Close,
+ ColorPalette,
ImageEdit,
NcColorPicker,
},
@@ -149,7 +123,12 @@ export default {
computed: {
shippedBackgrounds() {
return Object.keys(shippedBackgroundList)
- .map(fileName => {
+ .filter((background) => {
+ // If the admin did not changed the global background
+ // let's hide the default background to not show it twice
+ return background !== defaultShippedBackground || !this.isGlobalBackgroundDefault
+ })
+ .map((fileName) => {
return {
name: fileName,
url: prefixWithBaseUrl(fileName),
@@ -157,27 +136,18 @@ export default {
details: shippedBackgroundList[fileName],
}
})
- .filter(background => {
- // If the admin did not changed the global background
- // let's hide the default background to not show it twice
- if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) {
- return background.name !== defaultShippedBackground
- }
- return true
- })
},
isGlobalBackgroundDefault() {
- return !!themingDefaultBackground
+ return defaultBackgroundMime === ''
},
isGlobalBackgroundDeleted() {
- return themingDefaultBackground === 'backgroundColor'
+ return defaultBackgroundMime === 'backgroundColor'
},
- isBackgroundDisabled() {
- return this.backgroundImage === 'disabled'
- || !this.backgroundImage
+ cssDefaultBackgroundImage() {
+ return `url('${defaultBackgroundImage}')`
},
},
@@ -225,7 +195,7 @@ export default {
async update(data) {
// Update state
this.backgroundImage = data.backgroundImage
- this.Theming.color = data.backgroundColor
+ this.Theming.backgroundColor = data.backgroundColor
// Notify parent and reload style
this.$emit('update:background')
@@ -244,9 +214,9 @@ export default {
this.update(result.data)
},
- async setFile(path, color = null) {
+ async setFile(path) {
this.loading = 'custom'
- const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path, color })
+ const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
this.update(result.data)
},
@@ -256,15 +226,15 @@ export default {
this.update(result.data)
},
- async pickColor(event) {
+ async pickColor(color) {
this.loading = 'color'
- const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
- const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
- this.update(result.data)
+ const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' })
+ this.update(data)
},
+
debouncePickColor: debounce(function(...args) {
this.pickColor(...args)
- }, 200),
+ }, 1000),
pickFile() {
const picker = getFilePickerBuilder(t('theming', 'Select a background from your files'))
@@ -291,45 +261,7 @@ export default {
}
this.loading = 'custom'
-
- // Extract primary color from image
- let response = null
- let color = null
- try {
- const fileUrl = generateRemoteUrl('dav/files/' + getCurrentUser().uid + path)
- response = await axios.get(fileUrl, { responseType: 'blob' })
- const blobUrl = URL.createObjectURL(response.data)
- const palette = await this.getColorPaletteFromBlob(blobUrl)
-
- // DarkVibrant is accessible AND visually pleasing
- // Vibrant is not accessible enough and others are boring
- color = palette?.DarkVibrant?.hex
- this.setFile(path, color)
-
- // Log data
- console.debug('Extracted colour', color, 'from custom image', path, palette)
- } catch (error) {
- this.setFile(path)
- console.error('Unable to extract colour from custom image', { error, path, response, color })
- }
- },
-
- /**
- * Extract a Vibrant color palette from a blob URL
- *
- * @param {string} blobUrl the blob URL
- * @return {Promise<Palette>}
- */
- getColorPaletteFromBlob(blobUrl) {
- return new Promise((resolve, reject) => {
- const vibrant = new Vibrant(blobUrl)
- vibrant.getPalette((error, palette) => {
- if (error) {
- reject(error)
- }
- resolve(palette)
- })
- })
+ this.setFile(path)
},
},
}
@@ -341,38 +273,48 @@ export default {
flex-wrap: wrap;
justify-content: center;
+ .background-color {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 176px;
+ height: 96px;
+ margin: 8px;
+ border-radius: var(--border-radius-large);
+ background-color: var(--color-primary);
+ }
+
.background {
overflow: hidden;
width: 176px;
height: 96px;
margin: 8px;
text-align: center;
+ word-wrap: break-word;
+ hyphens: auto;
border: 2px solid var(--color-main-background);
border-radius: var(--border-radius-large);
background-position: center center;
background-size: cover;
&__filepicker {
+ background-color: var(--color-background-dark);
+
&.background--active {
- color: white;
+ color: var(--color-background-plain-text);
background-image: var(--image-background);
}
}
&__default {
- background-color: var(--color-primary-default);
- background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), var(--image-background-plain, var(--image-background-default));
+ background-color: var(--color-background-plain);
+ background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), v-bind(cssDefaultBackgroundImage);
}
&__filepicker, &__default, &__color {
border-color: var(--color-border);
}
- &__color {
- color: var(--color-primary-text);
- background-color: var(--color-primary-default);
- }
-
// Over a background image
&__default,
&__shipped {
diff --git a/apps/theming/src/components/ItemPreview.vue b/apps/theming/src/components/ItemPreview.vue
index 5f817d04f49..e4a1acd3e2a 100644
--- a/apps/theming/src/components/ItemPreview.vue
+++ b/apps/theming/src/components/ItemPreview.vue
@@ -1,13 +1,22 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div :class="'theming__preview--' + theme.id" class="theming__preview">
<div class="theming__preview-image" :style="{ backgroundImage: 'url(' + img + ')' }" @click="onToggle" />
<div class="theming__preview-description">
<h3>{{ theme.title }}</h3>
- <p class="theming__preview-explanation">{{ theme.description }}</p>
+ <p class="theming__preview-explanation">
+ {{ theme.description }}
+ </p>
<span v-if="enforced" class="theming__preview-warning" role="note">
{{ t('theming', 'Theme selection is enforced') }}
</span>
- <NcCheckboxRadioSwitch class="theming__preview-toggle"
+
+ <!-- Only show checkbox if we can change themes -->
+ <NcCheckboxRadioSwitch v-show="!enforced"
+ class="theming__preview-toggle"
:checked.sync="checked"
:disabled="enforced"
:name="name"
@@ -20,7 +29,7 @@
<script>
import { generateFilePath } from '@nextcloud/router'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
export default {
name: 'ItemPreview',
@@ -67,6 +76,10 @@ export default {
return this.selected
},
set(checked) {
+ if (this.enforced) {
+ return
+ }
+
console.debug('Changed theme', this.theme.id, checked)
// If this is a radio, we can only enable
@@ -83,6 +96,10 @@ export default {
methods: {
onToggle() {
+ if (this.enforced) {
+ return
+ }
+
if (this.switchType === 'radio') {
this.checked = true
return
@@ -100,11 +117,9 @@ export default {
.theming__preview {
// We make previews on 16/10 screens
--ratio: 16;
-
position: relative;
display: flex;
justify-content: flex-start;
- max-width: 800px;
&,
* {
@@ -115,7 +130,7 @@ export default {
flex-basis: calc(16px * var(--ratio));
flex-shrink: 0;
height: calc(10px * var(--ratio));
- margin-right: var(--gap);
+ margin-inline-end: var(--gap);
cursor: pointer;
border-radius: var(--border-radius);
background-repeat: no-repeat;
@@ -141,10 +156,6 @@ export default {
}
}
- &--default {
- grid-column: span 2;
- }
-
&-warning {
color: var(--color-warning);
}
diff --git a/apps/theming/src/components/UserAppMenuSection.vue b/apps/theming/src/components/UserAppMenuSection.vue
index 44ebd864fc6..d4221190f6b 100644
--- a/apps/theming/src/components/UserAppMenuSection.vue
+++ b/apps/theming/src/components/UserAppMenuSection.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSettingsSection :name="t('theming', 'Navigation bar settings')">
<p>
@@ -28,6 +32,9 @@
</template>
<script lang="ts">
+import type { IApp } from './AppOrderSelector.vue'
+import type { INavigationEntry } from '../../../../core/src/types/navigation.d.ts'
+
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
@@ -35,31 +42,11 @@ import { generateOcsUrl } from '@nextcloud/router'
import { computed, defineComponent, ref } from 'vue'
import axios from '@nextcloud/axios'
-import AppOrderSelector, { IApp } from './AppOrderSelector.vue'
+import AppOrderSelector from './AppOrderSelector.vue'
import IconUndo from 'vue-material-design-icons/Undo.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-
-/** See NavigationManager */
-interface INavigationEntry {
- /** Navigation id */
- id: string
- /** Order where this entry should be shown */
- order: number
- /** Target of the navigation entry */
- href: string
- /** The icon used for the naviation entry */
- icon: string
- /** Type of the navigation entry ('link' vs 'settings') */
- type: 'link' | 'settings'
- /** Localized name of the navigation entry */
- name: string
- /** Whether this is the default app */
- default?: boolean
- /** App that registered this navigation entry (not necessarly the same as the id) */
- app?: string
-}
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
/** The app order user setting */
type IAppOrder = Record<string, { order: number, app?: string }>
@@ -92,9 +79,9 @@ export default defineComponent({
/**
* Array of all available apps, it is set by a core controller for the app menu, so it is always available
*/
- const initialAppOrder = Object.values(loadState<Record<string, INavigationEntry>>('core', 'apps'))
+ const initialAppOrder = loadState<INavigationEntry[]>('core', 'apps')
.filter(({ type }) => type === 'link')
- .map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp }))
+ .map((app) => ({ ...app, label: app.name, default: app.default && app.id === enforcedDefaultApp }))
/**
* Check if a custom app order is used or the default is shown
diff --git a/apps/theming/src/components/UserPrimaryColor.vue b/apps/theming/src/components/UserPrimaryColor.vue
new file mode 100644
index 00000000000..f10b8a01825
--- /dev/null
+++ b/apps/theming/src/components/UserPrimaryColor.vue
@@ -0,0 +1,160 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div class="primary-color__wrapper">
+ <NcColorPicker v-model="primaryColor"
+ data-user-theming-primary-color
+ @update:value="debouncedOnUpdate">
+ <button ref="trigger"
+ class="color-container primary-color__trigger"
+ :style="{ 'background-color': primaryColor }"
+ data-user-theming-primary-color-trigger>
+ {{ t('theming', 'Primary color') }}
+ <NcLoadingIcon v-if="loading" />
+ <IconColorPalette v-else :size="20" />
+ </button>
+ </NcColorPicker>
+ <NcButton type="tertiary" :disabled="isdefaultPrimaryColor" @click="onReset">
+ <template #icon>
+ <IconUndo :size="20" />
+ </template>
+ {{ t('theming', 'Reset primary color') }}
+ </NcButton>
+ </div>
+</template>
+
+<script lang="ts">
+import { showError } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { generateOcsUrl } from '@nextcloud/router'
+import { colord } from 'colord'
+import { defineComponent } from 'vue'
+import axios from '@nextcloud/axios'
+import debounce from 'debounce'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import IconColorPalette from 'vue-material-design-icons/PaletteOutline.vue'
+import IconUndo from 'vue-material-design-icons/UndoVariant.vue'
+
+const { primaryColor, defaultPrimaryColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultPrimaryColor: '#0082c9' })
+
+export default defineComponent({
+ name: 'UserPrimaryColor',
+
+ components: {
+ IconColorPalette,
+ IconUndo,
+ NcButton,
+ NcColorPicker,
+ NcLoadingIcon,
+ },
+
+ emits: ['refresh-styles'],
+
+ data() {
+ return {
+ primaryColor,
+ loading: false,
+ }
+ },
+
+ computed: {
+ isdefaultPrimaryColor() {
+ return colord(this.primaryColor).isEqual(colord(defaultPrimaryColor))
+ },
+
+ debouncedOnUpdate() {
+ return debounce(this.onUpdate, 1000)
+ },
+ },
+
+ methods: {
+ t,
+
+ /**
+ * Global styles are reloaded so we might need to update the current value
+ */
+ reload() {
+ const trigger = this.$refs.trigger as HTMLButtonElement
+ const newColor = window.getComputedStyle(trigger).backgroundColor
+ if (newColor.toLowerCase() !== this.primaryColor) {
+ this.primaryColor = newColor
+ }
+ },
+
+ onReset() {
+ this.primaryColor = defaultPrimaryColor
+ this.onUpdate(null)
+ },
+
+ async onUpdate(value: string | null) {
+ this.loading = true
+ const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
+ appId: 'theming',
+ configKey: 'primary_color',
+ })
+ try {
+ if (value) {
+ await axios.post(url, {
+ configValue: value,
+ })
+ } else {
+ await axios.delete(url)
+ }
+ this.$emit('refresh-styles')
+ } catch (e) {
+ console.error('Could not update primary color', e)
+ showError(t('theming', 'Could not set primary color'))
+ }
+ this.loading = false
+ },
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.primary-color {
+ &__wrapper {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ &__trigger {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ background-color: var(--color-primary);
+ color: var(--color-primary-text);
+ width: 350px;
+ max-width: 100vw;
+ height: 96px;
+
+ word-wrap: break-word;
+ hyphens: auto;
+
+ border: 2px solid var(--color-main-background);
+ border-radius: var(--border-radius-large);
+
+ &:active {
+ background-color: var(--color-primary-hover) !important;
+ }
+
+ &:hover,
+ &:focus,
+ &:focus-visible {
+ border-color: var(--color-main-background) !important;
+ outline: 2px solid var(--color-main-text) !important;
+ }
+ }
+}
+</style>
diff --git a/apps/theming/src/components/admin/AppMenuSection.vue b/apps/theming/src/components/admin/AppMenuSection.vue
index bed170504c9..bf229f15df4 100644
--- a/apps/theming/src/components/admin/AppMenuSection.vue
+++ b/apps/theming/src/components/admin/AppMenuSection.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSettingsSection :name="t('theming', 'Navigation bar settings')">
<h3>{{ t('theming', 'Default app') }}</h3>
@@ -26,6 +30,8 @@
</template>
<script lang="ts">
+import type { INavigationEntry } from '../../../../../core/src/types/navigation'
+
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
@@ -34,9 +40,9 @@ import { computed, defineComponent } from 'vue'
import axios from '@nextcloud/axios'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import AppOrderSelector from '../AppOrderSelector.vue'
export default defineComponent({
@@ -71,9 +77,8 @@ export default defineComponent({
/**
* All enabled apps which can be navigated
*/
- const allApps = Object.values(
- loadState<Record<string, { id: string, name?: string, icon: string }>>('core', 'apps'),
- ).map(({ id, name, icon }) => ({ label: name, id, icon }))
+ const allApps = loadState<INavigationEntry[]>('core', 'apps')
+ .map(({ id, name, icon }) => ({ label: name, id, icon }))
/**
* Currently selected app, wrapps the setter
@@ -110,6 +115,7 @@ export default defineComponent({
h3, h4 {
font-weight: bold;
}
+
h4, h5 {
margin-block-start: 12px;
}
diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue
index fa8477a7283..42d86ded4e7 100644
--- a/apps/theming/src/components/admin/CheckboxField.vue
+++ b/apps/theming/src/components/admin/CheckboxField.vue
@@ -1,38 +1,23 @@
<!--
- - @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/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="field">
<label :for="id">{{ displayName }}</label>
<div class="field__row">
- <NcCheckboxRadioSwitch type="switch"
- :id="id"
+ <NcCheckboxRadioSwitch :id="id"
+ type="switch"
:checked.sync="localValue"
@update:checked="save">
{{ label }}
</NcCheckboxRadioSwitch>
</div>
- <p class="field__description">{{ description }}</p>
+ <p class="field__description">
+ {{ description }}
+ </p>
<NcNoteCard v-if="errorMessage"
type="error"
@@ -43,8 +28,8 @@
</template>
<script>
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
@@ -90,7 +75,7 @@ export default {
</script>
<style lang="scss" scoped>
-@import './shared/field.scss';
+@use './shared/field' as *;
.field {
&__description {
diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue
index 7885bfeb233..4ec6d47fef6 100644
--- a/apps/theming/src/components/admin/ColorPickerField.vue
+++ b/apps/theming/src/components/admin/ColorPickerField.vue
@@ -1,23 +1,6 @@
<!--
- - @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/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -27,24 +10,34 @@
<NcColorPicker :value.sync="localValue"
:advanced-fields="true"
@update:value="debounceSave">
- <NcButton class="field__button"
+ <NcButton :id="id"
+ class="field__button"
type="primary"
- :id="id"
:aria-label="t('theming', 'Select a custom color')"
- data-admin-theming-setting-primary-color-picker>
+ data-admin-theming-setting-color-picker>
+ <template #icon>
+ <NcLoadingIcon v-if="loading"
+ :appearance="calculatedTextColor === '#ffffff' ? 'light' : 'dark'"
+ :size="20" />
+ <Palette v-else :size="20" />
+ </template>
{{ value }}
</NcButton>
</NcColorPicker>
+ <div class="field__color-preview" data-admin-theming-setting-color />
<NcButton v-if="value !== defaultValue"
type="tertiary"
:aria-label="t('theming', 'Reset to default')"
- data-admin-theming-setting-primary-color-reset
+ data-admin-theming-setting-color-reset
@click="undo">
<template #icon>
<Undo :size="20" />
</template>
</NcButton>
</div>
+ <div v-if="description" class="description">
+ {{ description }}
+ </div>
<NcNoteCard v-if="errorMessage"
type="error"
@@ -55,11 +48,15 @@
</template>
<script>
-import { debounce } from 'debounce'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import { colord } from 'colord'
+import debounce from 'debounce'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
+import Palette from 'vue-material-design-icons/Palette.vue'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
@@ -69,8 +66,10 @@ export default {
components: {
NcButton,
NcColorPicker,
+ NcLoadingIcon,
NcNoteCard,
Undo,
+ Palette,
},
mixins: [
@@ -82,10 +81,18 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ default: '',
+ },
value: {
type: String,
required: true,
},
+ textColor: {
+ type: String,
+ default: null,
+ },
defaultValue: {
type: String,
required: true,
@@ -96,40 +103,55 @@ export default {
},
},
+ emits: ['update:theming'],
+
+ data() {
+ return {
+ loading: false,
+ }
+ },
+
+ computed: {
+ calculatedTextColor() {
+ const color = colord(this.value)
+ return color.isLight() ? '#000000' : '#ffffff'
+ },
+ usedTextColor() {
+ if (this.textColor) {
+ return this.textColor
+ }
+ return this.calculatedTextColor
+ },
+ },
+
methods: {
debounceSave: debounce(async function() {
+ this.loading = true
await this.save()
+ this.$emit('update:theming')
+ this.loading = false
}, 200),
},
}
</script>
<style lang="scss" scoped>
-@import './shared/field.scss';
+@use './shared/field' as *;
+
+.description {
+ color: var(--color-text-maxcontrast);
+}
.field {
- // Override default NcButton styles
&__button {
- width: 230px !important;
- border-radius: var(--border-radius-large) !important;
- background-color: var(--color-primary-default) !important;
-
- // emulated hover state because it would not make sense
- // to create a dedicated global variable for the color-primary-default
- &:hover::after {
- background-color: white;
- content: "";
- position: absolute;
- width: 100%;
- height: 100%;
- opacity: .2;
- filter: var(--primary-invert-if-bright);
- }
+ background-color: v-bind('value') !important;
+ color: v-bind('usedTextColor') !important;
+ }
- // Above the ::after
- &::v-deep * {
- z-index: 1;
- }
+ &__color-preview {
+ width: var(--default-clickable-area);
+ border-radius: var(--border-radius-large);
+ background-color: v-bind('value');
}
}
</style>
diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue
index 3d6fda9ec70..d5e0052f5bd 100644
--- a/apps/theming/src/components/admin/FileInputField.vue
+++ b/apps/theming/src/components/admin/FileInputField.vue
@@ -1,31 +1,14 @@
<!--
- - @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/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="field">
<label :for="id">{{ displayName }}</label>
<div class="field__row">
- <NcButton type="secondary"
- :id="id"
+ <NcButton :id="id"
+ type="secondary"
:aria-label="ariaLabel"
data-admin-theming-setting-file-picker
@click="activateLocalFilePicker">
@@ -82,10 +65,10 @@ import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import Delete from 'vue-material-design-icons/Delete.vue'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import Delete from 'vue-material-design-icons/DeleteOutline.vue'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
import Upload from 'vue-material-design-icons/Upload.vue'
@@ -126,7 +109,7 @@ export default {
},
defaultMimeValue: {
type: String,
- required: true,
+ default: '',
},
displayName: {
type: String,
@@ -182,9 +165,10 @@ export default {
const url = generateUrl('/apps/theming/ajax/uploadImage')
try {
this.showLoading = true
- await axios.post(url, formData)
+ const { data } = await axios.post(url, formData)
this.showLoading = false
this.$emit('update:mime-value', file.type)
+ this.$emit('uploaded', data.data.url)
this.handleSuccess()
} catch (e) {
this.showLoading = false
@@ -225,7 +209,7 @@ export default {
</script>
<style lang="scss" scoped>
-@import './shared/field.scss';
+@use './shared/field' as *;
.field {
&__loading-icon {
diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue
index 5e96f45a95b..6ec52733aed 100644
--- a/apps/theming/src/components/admin/TextField.vue
+++ b/apps/theming/src/components/admin/TextField.vue
@@ -1,23 +1,6 @@
<!--
- - @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/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -40,7 +23,7 @@
</template>
<script>
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
diff --git a/apps/theming/src/components/admin/shared/field.scss b/apps/theming/src/components/admin/shared/field.scss
index 54fc57b3ee5..2347f31f7c5 100644
--- a/apps/theming/src/components/admin/shared/field.scss
+++ b/apps/theming/src/components/admin/shared/field.scss
@@ -1,23 +1,6 @@
-/**
- * @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/>.
- *
+/*!
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
.field {
diff --git a/apps/theming/src/helpers/refreshStyles.js b/apps/theming/src/helpers/refreshStyles.js
index 0c4a7cea22b..ba198be0a00 100644
--- a/apps/theming/src/helpers/refreshStyles.js
+++ b/apps/theming/src/helpers/refreshStyles.js
@@ -1,33 +1,26 @@
/**
- * @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/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-export const refreshStyles = () => {
- // Refresh server-side generated theming CSS
- [...document.head.querySelectorAll('link.theme')].forEach(theme => {
+/**
+ * Refresh server-side generated theming CSS
+ * This resolves when all themes are reloaded
+ */
+export async function refreshStyles() {
+ const themes = [...document.head.querySelectorAll('link.theme')]
+ const promises = themes.map((theme) => new Promise((resolve) => {
const url = new URL(theme.href)
url.searchParams.set('v', Date.now())
const newTheme = theme.cloneNode()
newTheme.href = url.toString()
- newTheme.onload = () => theme.remove()
+ newTheme.onload = () => {
+ theme.remove()
+ resolve()
+ }
document.head.append(newTheme)
- })
+ }))
+
+ // Wait until all themes are loaded
+ await Promise.allSettled(promises)
}
diff --git a/apps/theming/src/mixins/admin/FieldMixin.js b/apps/theming/src/mixins/admin/FieldMixin.js
index 811fa0c0bba..743e711777a 100644
--- a/apps/theming/src/mixins/admin/FieldMixin.js
+++ b/apps/theming/src/mixins/admin/FieldMixin.js
@@ -1,23 +1,6 @@
/**
- * @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/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const styleRefreshFields = [
diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js
index 4cce8bb301a..94d63ce1c8c 100644
--- a/apps/theming/src/mixins/admin/TextValueMixin.js
+++ b/apps/theming/src/mixins/admin/TextValueMixin.js
@@ -1,23 +1,6 @@
/**
- * @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/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import axios from '@nextcloud/axios'
@@ -38,25 +21,56 @@ export default {
data() {
return {
+ /** @type {string|boolean} */
localValue: this.value,
}
},
+ computed: {
+ valueToPost() {
+ if (this.type === 'url') {
+ // if this is already encoded just make sure there is no doublequote (HTML XSS)
+ // otherwise simply URL encode
+ return this.isUrlEncoded(this.localValue)
+ ? this.localValue.replaceAll('"', '%22')
+ : encodeURI(this.localValue)
+ }
+ // Convert boolean to string as server expects string value
+ if (typeof this.localValue === 'boolean') {
+ return this.localValue ? 'yes' : 'no'
+ }
+ return this.localValue
+ },
+ },
+
methods: {
+ /**
+ * Check if URL is percent-encoded
+ * @param {string} url The URL to check
+ * @return {boolean}
+ */
+ isUrlEncoded(url) {
+ try {
+ return decodeURI(url) !== url
+ } catch {
+ return false
+ }
+ },
+
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,
+ value: this.valueToPost,
})
this.$emit('update:value', this.localValue)
this.handleSuccess()
} catch (e) {
- this.errorMessage = e.response.data.data?.message
+ console.error('Failed to save changes', e)
+ this.errorMessage = e.response?.data.data?.message
}
},
@@ -64,10 +78,14 @@ export default {
this.reset()
const url = generateUrl('/apps/theming/ajax/undoChanges')
try {
- await axios.post(url, {
+ const { data } = await axios.post(url, {
setting: this.name,
})
- this.$emit('update:value', this.defaultValue)
+
+ if (data.data.value) {
+ this.$emit('update:defaultValue', data.data.value)
+ }
+ this.$emit('update:value', data.data.value || this.defaultValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
diff --git a/apps/theming/src/personal-settings.js b/apps/theming/src/personal-settings.js
index ba7bf2e0ee2..bbee88e3804 100644
--- a/apps/theming/src/personal-settings.js
+++ b/apps/theming/src/personal-settings.js
@@ -1,32 +1,15 @@
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.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/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
import Vue from 'vue'
import { refreshStyles } from './helpers/refreshStyles.js'
-import App from './UserThemes.vue'
+import App from './UserTheming.vue'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
Vue.prototype.OC = OC
Vue.prototype.t = t