diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index 8bb9841ae55..1079079c575 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -81,10 +81,13 @@ class ThemingController extends Controller { if (strlen($value) > 500) { $error = $this->l10n->t('The given web address is too long'); } - if (!$this->isValidUrl($value)) { + if ($value !== '' && !$this->isValidUrl($value)) { $error = $this->l10n->t('The given web address is not a valid URL'); } break; + case 'legalNoticeUrl': + $setting = 'imprintUrl'; + // no break case 'imprintUrl': if (strlen($value) > 500) { $error = $this->l10n->t('The given legal notice address is too long'); @@ -93,6 +96,9 @@ class ThemingController extends Controller { $error = $this->l10n->t('The given legal notice address is not a valid URL'); } break; + case 'privacyPolicyUrl': + $setting = 'privacyUrl'; + // no break case 'privacyUrl': if (strlen($value) > 500) { $error = $this->l10n->t('The given privacy policy address is too long'); @@ -106,30 +112,38 @@ class ThemingController extends Controller { $error = $this->l10n->t('The given slogan is too long'); } break; + case 'primaryColor': + $setting = 'primary_color'; + // no break case 'primary_color': if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { $error = $this->l10n->t('The given color is invalid'); - } else { - $this->appConfig->setAppValueString('primary_color', $value); - $saved = true; } break; + case 'backgroundColor': + $setting = 'background_color'; + // no break case 'background_color': if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { $error = $this->l10n->t('The given color is invalid'); - } else { - $this->appConfig->setAppValueString('background_color', $value); - $saved = true; } break; + case 'disableUserTheming': case 'disable-user-theming': if (!in_array($value, ['yes', 'true', 'no', 'false'])) { - $error = $this->l10n->t('Disable-user-theming should be true or false'); + $error = $this->l10n->t('%1$s should be true or false', ['disable-user-theming']); } else { $this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true'); $saved = true; } break; + case 'backgroundMime': + if ($value !== 'backgroundColor') { + $error = $this->l10n->t('%1$s can only be set to %2$s through the API', ['backgroundMime', 'backgroundColor']); + } + break; + default: + $error = $this->l10n->t('Invalid setting key'); } if ($error !== null) { return new DataResponse([ @@ -291,6 +305,11 @@ class ThemingController extends Controller { */ #[AuthorizedAdminSetting(settings: Admin::class)] public function undo(string $setting): DataResponse { + $setting = match ($setting) { + 'primaryColor' => 'primary_color', + 'backgroundColor' => 'background_color', + default => $setting, + }; $value = $this->themingDefaults->undo($setting); return new DataResponse( diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php index 9fa0f2bb0e7..518afe23189 100644 --- a/apps/theming/lib/Settings/Admin.php +++ b/apps/theming/lib/Settings/Admin.php @@ -23,7 +23,6 @@ use OCP\Util; class Admin implements IDelegatedSettings { public function __construct( - private string $appName, private IConfig $config, private IL10N $l, private ThemingDefaults $themingDefaults, @@ -38,11 +37,11 @@ class Admin implements IDelegatedSettings { * @return TemplateResponse */ public function getForm(): TemplateResponse { - $themable = true; + $themeable = true; $errorMessage = ''; $theme = $this->config->getSystemValue('theme', ''); if ($theme !== '') { - $themable = false; + $themeable = false; $errorMessage = $this->l->t('You are already using a custom theme. Theming app settings might be overwritten by that.'); } @@ -51,9 +50,17 @@ class Admin implements IDelegatedSettings { return $carry; }, []); + $this->initialState->provideInitialState('adminThemingInfo', [ + 'isThemeable' => $themeable, + 'notThemeableErrorMessage' => $errorMessage, + 'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE), + 'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR, + 'docUrl' => $this->urlGenerator->linkToDocs('admin-theming'), + 'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'), + 'canThemeIcons' => $this->imageManager->shouldReplaceIcons(), + ]); + $this->initialState->provideInitialState('adminThemingParameters', [ - 'isThemable' => $themable, - 'notThemableErrorMessage' => $errorMessage, 'name' => $this->themingDefaults->getEntity(), 'url' => $this->themingDefaults->getBaseUrl(), 'slogan' => $this->themingDefaults->getSlogan(), @@ -62,30 +69,25 @@ class Admin implements IDelegatedSettings { 'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''), 'allowedMimeTypes' => $allowedMimeTypes, 'backgroundURL' => $this->imageManager->getImageUrl('background'), - 'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE), - 'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR, 'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''), 'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''), 'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''), 'legalNoticeUrl' => $this->themingDefaults->getImprintUrl(), 'privacyPolicyUrl' => $this->themingDefaults->getPrivacyUrl(), - 'docUrl' => $this->urlGenerator->linkToDocs('admin-theming'), - 'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'), - 'canThemeIcons' => $this->imageManager->shouldReplaceIcons(), - 'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(), + 'disableUserTheming' => $this->themingDefaults->isUserThemingDisabled(), 'defaultApps' => $this->navigationManager->getDefaultEntryIds(), ]); - Util::addScript($this->appName, 'admin-theming'); - - return new TemplateResponse($this->appName, 'settings-admin'); + Util::addStyle(Application::APP_ID, 'settings-admin'); + Util::addScript(Application::APP_ID, 'settings-admin'); + return new TemplateResponse(Application::APP_ID, 'settings-admin'); } /** * @return string the section ID, e.g. 'sharing' */ public function getSection(): string { - return $this->appName; + return Application::APP_ID; } /** @@ -105,7 +107,7 @@ class Admin implements IDelegatedSettings { public function getAuthorizedAppConfig(): array { return [ - $this->appName => '/.*/', + Application::APP_ID => '/.*/', ]; } } diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php index f14deeb35f0..fbe67531b36 100644 --- a/apps/theming/lib/Settings/Personal.php +++ b/apps/theming/lib/Settings/Personal.php @@ -6,6 +6,7 @@ */ namespace OCA\Theming\Settings; +use OCA\Theming\AppInfo\Application; use OCA\Theming\ITheme; use OCA\Theming\Service\BackgroundService; use OCA\Theming\Service\ThemesService; @@ -20,7 +21,6 @@ use OCP\Util; class Personal implements ISettings { public function __construct( - protected string $appName, private string $userId, private IConfig $config, private ThemesService $themesService, @@ -82,9 +82,9 @@ class Personal implements ISettings { 'enforcedDefaultApp' => $forcedDefaultEntry ]); - Util::addScript($this->appName, 'personal-theming'); - - return new TemplateResponse($this->appName, 'settings-personal'); + Util::addStyle(Application::APP_ID, 'settings-personal'); + Util::addScript(Application::APP_ID, 'settings-personal'); + return new TemplateResponse(Application::APP_ID, 'settings-personal'); } /** @@ -92,7 +92,7 @@ class Personal implements ISettings { * @since 9.1 */ public function getSection(): string { - return $this->appName; + return Application::APP_ID; } /** diff --git a/apps/theming/lib/Settings/PersonalSection.php b/apps/theming/lib/Settings/PersonalSection.php index 0a9361d5533..c8078bd1577 100644 --- a/apps/theming/lib/Settings/PersonalSection.php +++ b/apps/theming/lib/Settings/PersonalSection.php @@ -6,6 +6,7 @@ */ namespace OCA\Theming\Settings; +use OCA\Theming\AppInfo\Application; use OCP\IL10N; use OCP\IURLGenerator; use OCP\Settings\IIconSection; @@ -20,7 +21,6 @@ class PersonalSection implements IIconSection { * @param IL10N $l */ public function __construct( - protected string $appName, private IURLGenerator $urlGenerator, private IL10N $l, ) { @@ -34,7 +34,7 @@ class PersonalSection implements IIconSection { * @since 13.0.0 */ public function getIcon() { - return $this->urlGenerator->imagePath($this->appName, 'accessibility-dark.svg'); + return $this->urlGenerator->imagePath(Application::APP_ID, 'accessibility-dark.svg'); } /** @@ -45,7 +45,7 @@ class PersonalSection implements IIconSection { * @since 9.1 */ public function getID() { - return $this->appName; + return Application::APP_ID; } /** diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue deleted file mode 100644 index e813567ab41..00000000000 --- a/apps/theming/src/AdminTheming.vue +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - - diff --git a/apps/theming/src/UserTheming.vue b/apps/theming/src/UserTheming.vue deleted file mode 100644 index 3b8b2f0e62c..00000000000 --- a/apps/theming/src/UserTheming.vue +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js deleted file mode 100644 index c3fd62f214d..00000000000 --- a/apps/theming/src/admin-settings.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { getCSPNonce } from '@nextcloud/auth' -import Vue from 'vue' -import App from './AdminTheming.vue' - -__webpack_nonce__ = getCSPNonce() - -Vue.prototype.OC = OC -Vue.prototype.t = t - -const View = Vue.extend(App) -const theming = new View() -theming.$mount('#admin-theming') diff --git a/apps/theming/src/components/AdminSectionAppMenu.vue b/apps/theming/src/components/AdminSectionAppMenu.vue new file mode 100644 index 00000000000..07d5c1daa49 --- /dev/null +++ b/apps/theming/src/components/AdminSectionAppMenu.vue @@ -0,0 +1,107 @@ + + + + + + + diff --git a/apps/theming/src/components/AdminSectionTheming.vue b/apps/theming/src/components/AdminSectionTheming.vue new file mode 100644 index 00000000000..5bee9102c17 --- /dev/null +++ b/apps/theming/src/components/AdminSectionTheming.vue @@ -0,0 +1,69 @@ + + + + + + + diff --git a/apps/theming/src/components/AdminSectionThemingAdvanced.vue b/apps/theming/src/components/AdminSectionThemingAdvanced.vue new file mode 100644 index 00000000000..991ce65c99c --- /dev/null +++ b/apps/theming/src/components/AdminSectionThemingAdvanced.vue @@ -0,0 +1,136 @@ + + + + + + + diff --git a/apps/theming/src/components/AppOrderSelector.vue b/apps/theming/src/components/AppOrderSelector.vue index beff28172b4..606f6db6789 100644 --- a/apps/theming/src/components/AppOrderSelector.vue +++ b/apps/theming/src/components/AppOrderSelector.vue @@ -2,209 +2,170 @@ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - SPDX-License-Identifier: AGPL-3.0-or-later --> - - - diff --git a/apps/theming/src/components/AppOrderSelectorElement.vue b/apps/theming/src/components/AppOrderSelectorElement.vue index 70857b2b5c7..5dd43cf574d 100644 --- a/apps/theming/src/components/AppOrderSelectorElement.vue +++ b/apps/theming/src/components/AppOrderSelectorElement.vue @@ -2,9 +2,103 @@ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - SPDX-License-Identifier: AGPL-3.0-or-later --> + + + - - diff --git a/apps/theming/src/components/ItemPreview.vue b/apps/theming/src/components/ThemePreviewItem.vue similarity index 61% rename from apps/theming/src/components/ItemPreview.vue rename to apps/theming/src/components/ThemePreviewItem.vue index 63389c82a32..e27846d8d7e 100644 --- a/apps/theming/src/components/ItemPreview.vue +++ b/apps/theming/src/components/ThemePreviewItem.vue @@ -2,6 +2,66 @@ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - SPDX-License-Identifier: AGPL-3.0-or-later --> + + + - - diff --git a/apps/theming/src/components/UserPrimaryColor.vue b/apps/theming/src/components/UserPrimaryColor.vue deleted file mode 100644 index d4d1d6230ad..00000000000 --- a/apps/theming/src/components/UserPrimaryColor.vue +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - diff --git a/apps/theming/src/components/UserSectionAppMenu.vue b/apps/theming/src/components/UserSectionAppMenu.vue new file mode 100644 index 00000000000..47c3a12a14b --- /dev/null +++ b/apps/theming/src/components/UserSectionAppMenu.vue @@ -0,0 +1,167 @@ + + + + + + + diff --git a/apps/theming/src/components/UserSectionBackground.vue b/apps/theming/src/components/UserSectionBackground.vue new file mode 100644 index 00000000000..68e82f246b7 --- /dev/null +++ b/apps/theming/src/components/UserSectionBackground.vue @@ -0,0 +1,306 @@ + + + + + + + diff --git a/apps/theming/src/components/UserSectionHotkeys.vue b/apps/theming/src/components/UserSectionHotkeys.vue new file mode 100644 index 00000000000..6b0cbdcbaaf --- /dev/null +++ b/apps/theming/src/components/UserSectionHotkeys.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/apps/theming/src/components/UserSectionPrimaryColor.vue b/apps/theming/src/components/UserSectionPrimaryColor.vue new file mode 100644 index 00000000000..88985d38295 --- /dev/null +++ b/apps/theming/src/components/UserSectionPrimaryColor.vue @@ -0,0 +1,166 @@ + + + + + + + diff --git a/apps/theming/src/components/admin/AppMenuSection.vue b/apps/theming/src/components/admin/AppMenuSection.vue deleted file mode 100644 index 46316e12a8e..00000000000 --- a/apps/theming/src/components/admin/AppMenuSection.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue deleted file mode 100644 index d75fddfca3e..00000000000 --- a/apps/theming/src/components/admin/CheckboxField.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue index f4e0a9639ff..89729e23486 100644 --- a/apps/theming/src/components/admin/ColorPickerField.vue +++ b/apps/theming/src/components/admin/ColorPickerField.vue @@ -3,164 +3,111 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> + + - - diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue index 14dd8821902..581b69570a4 100644 --- a/apps/theming/src/components/admin/FileInputField.vue +++ b/apps/theming/src/components/admin/FileInputField.vue @@ -3,249 +3,170 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> + + - - - diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue index 55467700430..4892916b2a7 100644 --- a/apps/theming/src/components/admin/TextField.vue +++ b/apps/theming/src/components/admin/TextField.vue @@ -3,82 +3,59 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> - + - + diff --git a/apps/theming/src/components/admin/shared/field.scss b/apps/theming/src/components/admin/shared/field.scss deleted file mode 100644 index 2347f31f7c5..00000000000 --- a/apps/theming/src/components/admin/shared/field.scss +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -.field { - display: flex; - flex-direction: column; - gap: 4px 0; - - &__row { - display: flex; - gap: 0 4px; - } -} diff --git a/apps/theming/src/composables/useAdminThemingValue.ts b/apps/theming/src/composables/useAdminThemingValue.ts new file mode 100644 index 00000000000..6c71836120a --- /dev/null +++ b/apps/theming/src/composables/useAdminThemingValue.ts @@ -0,0 +1,111 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { MaybeRef, MaybeRefOrGetter, Ref } from 'vue' + +import axios, { isAxiosError } from '@nextcloud/axios' +import { showError } from '@nextcloud/dialogs' +import { generateUrl } from '@nextcloud/router' +import { watchDebounced } from '@vueuse/core' +import { isReadonly, isRef, readonly, ref, toValue } from 'vue' +import { logger } from '../utils/logger.ts' + +/** + * @param name - The property name + * @param modelValue - The model value + * @param defaultValue - The default value + */ +export function useAdminThemingValue(name: MaybeRefOrGetter, modelValue: Ref, defaultValue: MaybeRef) { + let resetted = false + const isSaving = ref(false) + const isSaved = ref(false) + + watchDebounced(modelValue, async () => { + if (isSaving.value) { + return + } + + if (resetted) { + resetted = false + return + } + + isSaving.value = true + isSaved.value = false + try { + await setValue(toValue(name), toValue(modelValue)) + isSaved.value = true + window.setTimeout(() => { + isSaved.value = false + }, 2000) + } finally { + isSaving.value = false + } + }, { debounce: 800, flush: 'sync' }) + + /** + * Reset to default value + */ + async function reset() { + isSaving.value = true + isSaved.value = false + try { + const result = await resetValue(toValue(name)) + if (result && isRef(defaultValue) && !isReadonly(defaultValue)) { + defaultValue.value = result as T + } + resetted = true + modelValue.value = toValue(defaultValue) + } finally { + isSaving.value = false + } + } + + return { + isSaving: readonly(isSaving), + isSaved: readonly(isSaved), + reset, + } +} + +/** + * @param setting - The setting name + * @param value - The setting value + */ +async function setValue(setting: string, value: unknown) { + const url = generateUrl('/apps/theming/ajax/updateStylesheet') + try { + await axios.post(url, { + setting, + value: String(value), + }) + } catch (error) { + logger.error('Failed to save changes', { error, setting, value }) + if (isAxiosError(error) && error.response?.data?.data?.message) { + showError(error.response.data.data.message) + } + throw error + } +} + +/** + * Reset theming value for a given setting + * + * @param setting - The setting name + */ +async function resetValue(setting: string) { + const url = generateUrl('/apps/theming/ajax/undoChanges') + try { + const { data } = await axios.post<{ data: { value?: string } }>(url, { setting }) + return data.data.value + } catch (error) { + logger.error('Failed to reset theming value', { error, setting }) + if (isAxiosError(error) && error.response?.data?.data?.message) { + showError(error.response.data.data.message) + return false + } + throw error + } +} diff --git a/apps/theming/src/mixins/admin/FieldMixin.js b/apps/theming/src/mixins/admin/FieldMixin.js deleted file mode 100644 index e9a2d3c3877..00000000000 --- a/apps/theming/src/mixins/admin/FieldMixin.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -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 deleted file mode 100644 index 0e83c1bd084..00000000000 --- a/apps/theming/src/mixins/admin/TextValueMixin.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' -import { logger } from '../../logger.ts' -import FieldMixin from './FieldMixin.js' - -export default { - mixins: [ - FieldMixin, - ], - - watch: { - value(value) { - this.localValue = value - }, - }, - - 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') - - try { - await axios.post(url, { - setting: this.name, - value: this.valueToPost, - }) - this.$emit('update:value', this.localValue) - this.handleSuccess() - } catch (error) { - logger.error('Failed to save changes', { error }) - this.errorMessage = error.response?.data.data?.message - } - }, - - async undo() { - this.reset() - const url = generateUrl('/apps/theming/ajax/undoChanges') - try { - const { data } = await axios.post(url, { - setting: this.name, - }) - - 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 deleted file mode 100644 index fe815752332..00000000000 --- a/apps/theming/src/personal-settings.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { getCSPNonce } from '@nextcloud/auth' -import Vue from 'vue' -import App from './UserTheming.vue' -import { refreshStyles } from './helpers/refreshStyles.js' - -__webpack_nonce__ = getCSPNonce() - -Vue.prototype.OC = OC -Vue.prototype.t = t - -const View = Vue.extend(App) -const theming = new View() -theming.$mount('#theming') -theming.$on('update:background', refreshStyles) diff --git a/apps/theming/src/settings-admin.ts b/apps/theming/src/settings-admin.ts new file mode 100644 index 00000000000..ec7a87ac9d8 --- /dev/null +++ b/apps/theming/src/settings-admin.ts @@ -0,0 +1,13 @@ +/*! + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createApp } from 'vue' +import AdminTheming from './views/AdminTheming.vue' + +import 'vite/modulepreload-polyfill' + +const app = createApp(AdminTheming) +app.config.idPrefix = 'settings' +app.mount('#settings-admin-theming') diff --git a/apps/theming/src/settings-personal.ts b/apps/theming/src/settings-personal.ts new file mode 100644 index 00000000000..bff8415c994 --- /dev/null +++ b/apps/theming/src/settings-personal.ts @@ -0,0 +1,13 @@ +/*! + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createApp } from 'vue' +import UserTheming from './views/UserTheming.vue' + +import 'vite/modulepreload-polyfill' + +const app = createApp(UserTheming) +app.config.idPrefix = 'settings' +app.mount('#settings-personal-theming') diff --git a/apps/theming/src/types.d.ts b/apps/theming/src/types.d.ts new file mode 100644 index 00000000000..1f52ace941d --- /dev/null +++ b/apps/theming/src/types.d.ts @@ -0,0 +1,38 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Modifiable parameters for the admin theming settings. + */ +export interface AdminThemingParameters { + backgroundMime: string + backgroundURL: string + backgroundColor: string + faviconMime: string + legalNoticeUrl: string + logoheaderMime: string + logoMime: string + name: string + primaryColor: string + privacyPolicyUrl: string + slogan: string + url: string + disableUserTheming: boolean + defaultApps: string[] +} + +/** + * Admin theming information. + */ +export interface AdminThemingInfo { + isThemeable: boolean + canThemeIcons: boolean + + notThemeableErrorMessage: string + defaultBackgroundURL: string + defaultBackgroundColor: string + docUrl: string + docUrlIcons: string +} diff --git a/apps/theming/src/utils/color.spec.ts b/apps/theming/src/utils/color.spec.ts new file mode 100644 index 00000000000..f72169b6780 --- /dev/null +++ b/apps/theming/src/utils/color.spec.ts @@ -0,0 +1,36 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from 'vitest' +import { getTextColor } from './color.ts' + +test('getTextColor returns black for light backgrounds', () => { + expect(getTextColor('#FFFFFF')).toBe('#000000') // white background + expect(getTextColor('#DDDDDD')).toBe('#000000') // light gray background + expect(getTextColor('#FFFFAA')).toBe('#000000') // light yellow background +}) + +test('getTextColor returns white for dark backgrounds', () => { + expect(getTextColor('#000000')).toBe('#ffffff') // black background + expect(getTextColor('#333333')).toBe('#ffffff') // dark gray background + expect(getTextColor('#0000AA')).toBe('#ffffff') // dark blue background +}) + +test('getTextColor handles edge cases', () => { + expect(getTextColor('#808080')).toBe('#ffffff') // medium gray background + expect(getTextColor('#C0C0C0')).toBe('#000000') // silver background + expect(getTextColor('#404040')).toBe('#ffffff') // dark gray background +}) + +test('getTextColor handles shorthand hex colors', () => { + expect(getTextColor('#FFF')).toBe('#000000') // white background + expect(getTextColor('#000')).toBe('#ffffff') // black background + expect(getTextColor('#888')).toBe('#ffffff') // medium gray background +}) + +test('getTextColor handles invalid hex colors', () => { + expect(getTextColor('invalid')).toBe('#ffffff') + expect(getTextColor('#GG')).toBe('#ffffff') +}) diff --git a/apps/theming/src/utils/color.ts b/apps/theming/src/utils/color.ts new file mode 100644 index 00000000000..4b060e93619 --- /dev/null +++ b/apps/theming/src/utils/color.ts @@ -0,0 +1,45 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Get the text color for a given background color + * + * @param color - The hex color + */ +export function getTextColor(color: string) { + return calculateLuma(color) > 0.6 + ? '#000000' + : '#ffffff' +} + +/** + * Calculate luminance of provided hex color + * + * @param color - The hex color + */ +function calculateLuma(color: string) { + const [red, green, blue] = hexToRGB(color) + return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 +} + +/** + * Convert hex color to RGB + * + * @param hex - The hex color + */ +function hexToRGB(hex: string): [number, number, number] { + if (hex.length < 6) { + // handle shorthand hex colors like #FFF + const result = /^#?([a-f\d])([a-f\d])([a-f\d])/i.exec(hex) + if (result) { + hex = `#${result[1]!.repeat(2)}${result[2]!.repeat(2)}${result[3]!.repeat(2)}` + } + } + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? [parseInt(result[1]!, 16), parseInt(result[2]!, 16), parseInt(result[3]!, 16)] + : [0, 0, 0] +} diff --git a/apps/theming/src/logger.ts b/apps/theming/src/utils/logger.ts similarity index 100% rename from apps/theming/src/logger.ts rename to apps/theming/src/utils/logger.ts diff --git a/apps/theming/src/helpers/refreshStyles.js b/apps/theming/src/utils/refreshStyles.ts similarity index 70% rename from apps/theming/src/helpers/refreshStyles.js rename to apps/theming/src/utils/refreshStyles.ts index ba198be0a00..37db9321815 100644 --- a/apps/theming/src/helpers/refreshStyles.js +++ b/apps/theming/src/utils/refreshStyles.ts @@ -1,4 +1,4 @@ -/** +/*! * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -8,12 +8,13 @@ * 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 themes = [...document.head.querySelectorAll('link.theme')] as HTMLLinkElement[] + const promises = themes.map((theme) => new Promise((resolve, reject) => { const url = new URL(theme.href) - url.searchParams.set('v', Date.now()) - const newTheme = theme.cloneNode() + url.searchParams.set('v', Date.now().toString()) + const newTheme = theme.cloneNode() as HTMLLinkElement newTheme.href = url.toString() + newTheme.onerror = reject newTheme.onload = () => { theme.remove() resolve() diff --git a/apps/theming/src/views/AdminTheming.vue b/apps/theming/src/views/AdminTheming.vue new file mode 100644 index 00000000000..c34f7e64f8a --- /dev/null +++ b/apps/theming/src/views/AdminTheming.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/theming/src/views/UserTheming.vue b/apps/theming/src/views/UserTheming.vue new file mode 100644 index 00000000000..96933ef901a --- /dev/null +++ b/apps/theming/src/views/UserTheming.vue @@ -0,0 +1,268 @@ + + + + + + + diff --git a/apps/theming/templates/settings-admin.php b/apps/theming/templates/settings-admin.php index 05bbd3684b5..3f689304f2c 100644 --- a/apps/theming/templates/settings-admin.php +++ b/apps/theming/templates/settings-admin.php @@ -5,4 +5,4 @@ */ ?> -
+
diff --git a/apps/theming/templates/settings-personal.php b/apps/theming/templates/settings-personal.php index 494a466c840..ba8fa04f901 100644 --- a/apps/theming/templates/settings-personal.php +++ b/apps/theming/templates/settings-personal.php @@ -6,4 +6,4 @@ ?> - \ No newline at end of file + \ No newline at end of file diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index fb461f03a28..cfac9125c10 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -89,20 +89,21 @@ class ThemingControllerTest extends TestCase { ['name', str_repeat('a', 250), 'Saved'], ['url', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'], ['slogan', str_repeat('a', 500), 'Saved'], - ['color', '#0082c9', 'Saved'], - ['color', '#0082C9', 'Saved'], - ['color', '#0082C9', 'Saved'], + ['primaryColor', '#0082c9', 'Saved', 'primary_color'], + ['primary_color', '#0082C9', 'Saved'], + ['backgroundColor', '#0082C9', 'Saved', 'background_color'], + ['background_color', '#0082C9', 'Saved'], ['imprintUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'], ['privacyUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'], ]; } #[\PHPUnit\Framework\Attributes\DataProvider('dataUpdateStylesheetSuccess')] - public function testUpdateStylesheetSuccess(string $setting, string $value, string $message): void { + public function testUpdateStylesheetSuccess(string $setting, string $value, string $message, ?string $realSetting = null): void { $this->themingDefaults ->expects($this->once()) ->method('set') - ->with($setting, $value); + ->with($realSetting ?? $setting, $value); $this->l10n ->expects($this->once()) ->method('t') @@ -149,6 +150,8 @@ class ThemingControllerTest extends TestCase { ['background_color', '#0082Z9', 'The given color is invalid'], ['background_color', 'Nextcloud', 'The given color is invalid'], + ['doesnotexist', 'value', 'Invalid setting key'], + ...$urlTests, ]; } diff --git a/apps/theming/tests/Settings/AdminTest.php b/apps/theming/tests/Settings/AdminTest.php index 277b94900a8..5a26657574e 100644 --- a/apps/theming/tests/Settings/AdminTest.php +++ b/apps/theming/tests/Settings/AdminTest.php @@ -7,7 +7,6 @@ declare(strict_types=1); */ namespace OCA\Theming\Tests\Settings; -use OCA\Theming\AppInfo\Application; use OCA\Theming\ImageManager; use OCA\Theming\Settings\Admin; use OCA\Theming\ThemingDefaults; @@ -41,7 +40,6 @@ class AdminTest extends TestCase { $this->navigationManager = $this->createMock(INavigationManager::class); $this->admin = new Admin( - Application::APP_ID, $this->config, $this->l10n, $this->themingDefaults, diff --git a/apps/theming/tests/Settings/PersonalTest.php b/apps/theming/tests/Settings/PersonalTest.php index 6fe790ceeef..6446464f02e 100644 --- a/apps/theming/tests/Settings/PersonalTest.php +++ b/apps/theming/tests/Settings/PersonalTest.php @@ -7,7 +7,6 @@ declare(strict_types=1); */ namespace OCA\Theming\Tests\Settings; -use OCA\Theming\AppInfo\Application; use OCA\Theming\ImageManager; use OCA\Theming\ITheme; use OCA\Theming\Service\BackgroundService; @@ -60,7 +59,6 @@ class PersonalTest extends TestCase { ->willReturn($this->themes); $this->admin = new Personal( - Application::APP_ID, 'admin', $this->config, $this->themesService, diff --git a/build/frontend-legacy/webpack.modules.cjs b/build/frontend-legacy/webpack.modules.cjs index 61125c271b3..cdc0f8e58b9 100644 --- a/build/frontend-legacy/webpack.modules.cjs +++ b/build/frontend-legacy/webpack.modules.cjs @@ -79,10 +79,6 @@ module.exports = { init: path.join(__dirname, 'apps/systemtags/src', 'init.ts'), admin: path.join(__dirname, 'apps/systemtags/src', 'admin.ts'), }, - theming: { - 'personal-theming': path.join(__dirname, 'apps/theming/src', 'personal-settings.js'), - 'admin-theming': path.join(__dirname, 'apps/theming/src', 'admin-settings.js'), - }, updatenotification: { init: path.join(__dirname, 'apps/updatenotification/src', 'init.ts'), 'view-changelog-page': path.join(__dirname, 'apps/updatenotification/src', 'view-changelog-page.ts'), diff --git a/build/frontend-legacy/apps/theming b/build/frontend/apps/theming similarity index 100% rename from build/frontend-legacy/apps/theming rename to build/frontend/apps/theming diff --git a/build/frontend/vite.config.mts b/build/frontend/vite.config.ts similarity index 95% rename from build/frontend/vite.config.mts rename to build/frontend/vite.config.ts index aec4990fc91..198081027a1 100644 --- a/build/frontend/vite.config.mts +++ b/build/frontend/vite.config.ts @@ -32,6 +32,10 @@ const modules = { sharebymail: { 'admin-settings': resolve(import.meta.dirname, 'apps/sharebymail/src', 'settings-admin.ts'), }, + theming: { + 'settings-personal': resolve(import.meta.dirname, 'apps/theming/src', 'settings-personal.ts'), + 'settings-admin': resolve(import.meta.dirname, 'apps/theming/src', 'settings-admin.ts'), + }, twofactor_backupcodes: { 'settings-personal': resolve(import.meta.dirname, 'apps/twofactor_backupcodes/src', 'settings-personal.ts'), }, diff --git a/package-lock.json b/package-lock.json index 9df2a9792ad..091e7a8a97c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "AGPL-3.0-or-later", "dependencies": { + "@mdi/js": "^7.4.47", "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.5.3", "@nextcloud/axios": "^2.5.2", @@ -28,8 +29,10 @@ "@nextcloud/sharing": "^0.3.0", "@nextcloud/vue": "^9.3.1", "@vueuse/core": "^14.1.0", + "@vueuse/integrations": "^14.1.0", "debounce": "^3.0.0", "pinia": "^3.0.4", + "sortablejs": "^1.15.6", "vue": "^3.5.26", "vuex": "^4.1.0", "webdav": "^5.8.0" @@ -225,6 +228,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -534,6 +538,7 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -663,6 +668,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -706,6 +712,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2328,6 +2335,7 @@ "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", "license": "GPL-3.0-or-later", + "peer": true, "dependencies": { "@nextcloud/router": "^3.0.1", "@nextcloud/typings": "^1.9.1", @@ -2516,6 +2524,7 @@ "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.3.1.tgz", "integrity": "sha512-zNit83SI7IPT5iT9QsYPCYNwBYvKEqzLvWKTeJemqg9MZ8JGIC3/jjENeXzDolrTN/PixHns5lOYVCejATE1ag==", "license": "AGPL-3.0-or-later", + "peer": true, "dependencies": { "@ckpack/vue-color": "^1.6.0", "@floating-ui/dom": "^1.7.4", @@ -3419,6 +3428,7 @@ "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -3598,6 +3608,7 @@ "integrity": "sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.46.1", @@ -4008,6 +4019,7 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -4458,6 +4470,7 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.26", @@ -4697,6 +4710,72 @@ "vue": "^3.5.0" } }, + "node_modules/@vueuse/integrations": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz", + "integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, "node_modules/@vueuse/metadata": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", @@ -4746,6 +4825,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5521,6 +5601,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6485,6 +6566,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@cypress/request": "^3.0.9", "@cypress/xvfb": "^1.2.4", @@ -7187,7 +7269,6 @@ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -7203,7 +7284,6 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -7235,8 +7315,7 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", @@ -7244,7 +7323,6 @@ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -7270,7 +7348,6 @@ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -7457,6 +7534,7 @@ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7676,6 +7754,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9320,7 +9399,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -9334,7 +9412,6 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -12789,6 +12866,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12804,7 +12882,6 @@ "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^9.0.0", @@ -12820,16 +12897,14 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.6", @@ -12844,7 +12919,6 @@ "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12.0" }, @@ -12876,7 +12950,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=12.0" }, @@ -13676,6 +13749,7 @@ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13880,6 +13954,7 @@ "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -14296,6 +14371,13 @@ "node": ">=20" } }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT", + "peer": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14832,6 +14914,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", @@ -14886,7 +14969,6 @@ "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12 || >=14" }, @@ -14914,7 +14996,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18.12.0" }, @@ -14976,7 +15057,6 @@ "integrity": "sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "css-tree": "^3.0.1", "is-plain-object": "^5.0.0", @@ -14999,16 +15079,14 @@ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz", "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/stylelint-scss/node_modules/mdn-data": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.24.0.tgz", "integrity": "sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==", "dev": true, - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/stylelint-scss/node_modules/postcss-selector-parser": { "version": "7.1.0", @@ -15016,7 +15094,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15133,6 +15210,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15780,6 +15858,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16147,6 +16226,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16760,6 +16840,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -16851,6 +16932,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", diff --git a/package.json b/package.json index c9e43a0f5fd..3f13d2440e3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "extends @nextcloud/browserslist-config" ], "dependencies": { + "@mdi/js": "^7.4.47", "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.5.3", "@nextcloud/axios": "^2.5.2", @@ -57,8 +58,10 @@ "@nextcloud/sharing": "^0.3.0", "@nextcloud/vue": "^9.3.1", "@vueuse/core": "^14.1.0", + "@vueuse/integrations": "^14.1.0", "debounce": "^3.0.0", "pinia": "^3.0.4", + "sortablejs": "^1.15.6", "vue": "^3.5.26", "vuex": "^4.1.0", "webdav": "^5.8.0"