Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorChristopher Ng <chrng8@gmail.com>2022-10-01 06:04:39 +0300
committerChristopher Ng <chrng8@gmail.com>2022-10-28 03:18:47 +0300
commit4a2bbc7af9249364ba2455f627522450262cad75 (patch)
treeb0fd373e0aad0f18c35d2272c565b20bdab630a9 /apps
parentd007088cf5d89e29065991e0cbe2c890dfa13d96 (diff)
Rewrite admin theming in Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>
Diffstat (limited to 'apps')
-rw-r--r--apps/theming/css/default.css2
-rw-r--r--apps/theming/lib/ImageManager.php15
-rw-r--r--apps/theming/lib/Settings/Admin.php31
-rw-r--r--apps/theming/lib/Settings/Personal.php3
-rw-r--r--apps/theming/lib/Themes/CommonThemeTrait.php11
-rw-r--r--apps/theming/lib/Themes/DefaultTheme.php1
-rw-r--r--apps/theming/lib/ThemingDefaults.php2
-rw-r--r--apps/theming/src/AdminTheming.vue303
-rw-r--r--apps/theming/src/admin-settings.js33
-rw-r--r--apps/theming/src/components/admin/CheckboxField.vue102
-rw-r--r--apps/theming/src/components/admin/ColorPickerField.vue121
-rw-r--r--apps/theming/src/components/admin/FileInputField.vue248
-rw-r--r--apps/theming/src/components/admin/TextField.vue98
-rw-r--r--apps/theming/src/components/admin/shared/field.scss32
-rw-r--r--apps/theming/src/helpers/refreshStyles.js33
-rw-r--r--apps/theming/src/mixins/admin/FieldMixin.js64
-rw-r--r--apps/theming/src/mixins/admin/TextValueMixin.js77
-rw-r--r--apps/theming/src/personal-settings.js (renamed from apps/theming/src/settings.js)15
-rw-r--r--apps/theming/templates/settings-admin.php131
-rw-r--r--apps/theming/tests/Settings/AdminTest.php66
20 files changed, 1162 insertions, 226 deletions
diff --git a/apps/theming/css/default.css b/apps/theming/css/default.css
index 3bc1fd974cc..0d3f3402fce 100644
--- a/apps/theming/css/default.css
+++ b/apps/theming/css/default.css
@@ -55,6 +55,7 @@
--background-invert-if-bright: invert(100%);
--background-image-invert-if-bright: no;
--image-background: url('/core/img/app-background.jpg');
+ --image-background-default: url('/core/img/app-background.jpg');
--color-background-plain: #0082c9;
--primary-invert-if-bright: no;
--color-primary: #00639a;
@@ -66,6 +67,7 @@
--color-primary-light-hover: #dbe4e9;
--color-primary-text-dark: #ededed;
--color-primary-element: #00639a;
+ --color-primary-element-default-hover: #329bd3;
--color-primary-element-text: #ffffff;
--color-primary-element-hover: #3282ae;
--color-primary-element-light: #e5eff4;
diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php
index 560a4c981fe..88a733580fc 100644
--- a/apps/theming/lib/ImageManager.php
+++ b/apps/theming/lib/ImageManager.php
@@ -53,7 +53,6 @@ class ImageManager {
private $appData;
/** @var IURLGenerator */
private $urlGenerator;
- /** @var array */
/** @var ICacheFactory */
private $cacheFactory;
/** @var ILogger */
@@ -138,20 +137,6 @@ class ImageManager {
}
/**
- * @return array<string, array{mime: string, url: string}>
- */
- public function getCustomImages(): array {
- $images = [];
- foreach ($this::SupportedImageKeys as $key) {
- $images[$key] = [
- 'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
- 'url' => $this->getImageUrl($key),
- ];
- }
- return $images;
- }
-
- /**
* Get folder for current theming files
*
* @return ISimpleFolder
diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php
index 4576bea1df4..0f0d85c147d 100644
--- a/apps/theming/lib/Settings/Admin.php
+++ b/apps/theming/lib/Settings/Admin.php
@@ -27,19 +27,23 @@
*/
namespace OCA\Theming\Settings;
+use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
+use OCP\Util;
class Admin implements IDelegatedSettings {
private string $appName;
private IConfig $config;
private IL10N $l;
private ThemingDefaults $themingDefaults;
+ private IInitialState $initialState;
private IURLGenerator $urlGenerator;
private ImageManager $imageManager;
@@ -47,12 +51,14 @@ class Admin implements IDelegatedSettings {
IConfig $config,
IL10N $l,
ThemingDefaults $themingDefaults,
+ IInitialState $initialState,
IURLGenerator $urlGenerator,
ImageManager $imageManager) {
$this->appName = $appName;
$this->config = $config;
$this->l = $l;
$this->themingDefaults = $themingDefaults;
+ $this->initialState = $initialState;
$this->urlGenerator = $urlGenerator;
$this->imageManager = $imageManager;
}
@@ -69,23 +75,28 @@ class Admin implements IDelegatedSettings {
$errorMessage = $this->l->t('You are already using a custom theme. Theming app settings might be overwritten by that.');
}
- $parameters = [
- 'themable' => $themable,
- 'errorMessage' => $errorMessage,
+ $this->initialState->provideInitialState('adminThemingParameters', [
+ 'isThemable' => $themable,
+ 'notThemableErrorMessage' => $errorMessage,
'name' => $this->themingDefaults->getEntity(),
'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
'color' => $this->themingDefaults->getDefaultColorPrimary(),
- 'uploadLogoRoute' => $this->urlGenerator->linkToRoute('theming.Theming.uploadImage'),
+ 'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
+ '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(),
- 'iconDocs' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
- 'images' => $this->imageManager->getCustomImages(),
- 'imprintUrl' => $this->themingDefaults->getImprintUrl(),
- 'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
- ];
+ ]);
+
+ Util::addScript($this->appName, 'admin-theming');
- return new TemplateResponse($this->appName, 'settings-admin', $parameters, '');
+ return new TemplateResponse($this->appName, 'settings-admin');
}
/**
diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php
index 7ba4da15191..5b0dc742574 100644
--- a/apps/theming/lib/Settings/Personal.php
+++ b/apps/theming/lib/Settings/Personal.php
@@ -77,7 +77,8 @@ class Personal implements ISettings {
$this->initialStateService->provideInitialState('themes', array_values($themes));
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
- Util::addScript($this->appName, 'theming-settings');
+
+ Util::addScript($this->appName, 'personal-theming');
return new TemplateResponse($this->appName, 'settings-personal');
}
diff --git a/apps/theming/lib/Themes/CommonThemeTrait.php b/apps/theming/lib/Themes/CommonThemeTrait.php
index c203b35ed44..620c40199db 100644
--- a/apps/theming/lib/Themes/CommonThemeTrait.php
+++ b/apps/theming/lib/Themes/CommonThemeTrait.php
@@ -40,6 +40,7 @@ trait CommonThemeTrait {
protected function generatePrimaryVariables(string $colorMainBackground, string $colorMainText): array {
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
+ $colorPrimaryElementDefault = $this->util->elementColor($this->defaultPrimaryColor);
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
// primary related colours
@@ -64,6 +65,7 @@ trait CommonThemeTrait {
// used for buttons, inputs...
'--color-primary-element' => $colorPrimaryElement,
+ '--color-primary-element-default-hover' => $this->util->mix($colorPrimaryElementDefault, $colorMainBackground, 60),
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
'--color-primary-element-light' => $colorPrimaryElementLight,
@@ -80,6 +82,7 @@ trait CommonThemeTrait {
* Generate admin theming background-related variables
*/
protected function generateGlobalBackgroundVariables(): array {
+ $user = $this->userSession->getUser();
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$hasCustomLogoHeader = $this->imageManager->hasImage('logo') || $this->imageManager->hasImage('logoheader');
@@ -87,9 +90,11 @@ trait CommonThemeTrait {
// If primary as background has been request or if we have a custom primary colour
// let's not define the background image
- if ($backgroundDeleted && $this->themingDefaults->isUserThemingDisabled()) {
- $variables['--image-background-plain'] = 'true';
+ if ($backgroundDeleted) {
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary();
+ if ($this->themingDefaults->isUserThemingDisabled() || $user === null) {
+ $variables['--image-background-plain'] = 'true';
+ }
}
// Register image variables only if custom-defined
@@ -99,9 +104,11 @@ trait CommonThemeTrait {
if ($image === 'background') {
// If background deleted is set, ignoring variable
if ($backgroundDeleted) {
+ $variables['--image-background-default'] = 'no';
continue;
}
$variables['--image-background-size'] = 'cover';
+ $variables['--image-background-default'] = "url('" . $imageUrl . "')";
}
$variables["--image-$image"] = "url('" . $imageUrl . "')";
}
diff --git a/apps/theming/lib/Themes/DefaultTheme.php b/apps/theming/lib/Themes/DefaultTheme.php
index bb24bb4566b..94b71eb9d12 100644
--- a/apps/theming/lib/Themes/DefaultTheme.php
+++ b/apps/theming/lib/Themes/DefaultTheme.php
@@ -193,6 +193,7 @@ class DefaultTheme implements ITheme {
// Default last fallback values
'--image-background' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')",
+ '--image-background-default' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')",
'--color-background-plain' => $this->defaultPrimaryColor,
];
diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php
index eee44e81fda..f32faa5a8ef 100644
--- a/apps/theming/lib/ThemingDefaults.php
+++ b/apps/theming/lib/ThemingDefaults.php
@@ -224,7 +224,7 @@ class ThemingDefaults extends \OC_Defaults {
if ($this->isUserThemingDisabled()) {
return $defaultColor;
}
-
+
// user-defined primary color
$themingBackground = '';
if (!empty($user)) {
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue
new file mode 100644
index 00000000000..1d9f5b69512
--- /dev/null
+++ b/apps/theming/src/AdminTheming.vue
@@ -0,0 +1,303 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <section>
+ <NcSettingsSection :title="t('theming', 'Theming')"
+ :description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')"
+ :doc-url="docUrl">
+ <div class="admin-theming">
+ <NcNoteCard v-if="!isThemable"
+ type="error"
+ :show-alert="true">
+ <p>{{ notThemableErrorMessage }}</p>
+ </NcNoteCard>
+ <TextField v-for="field in textFields"
+ :key="field.name"
+ :name="field.name"
+ :value.sync="field.value"
+ :default-value="field.defaultValue"
+ :type="field.type"
+ :display-name="field.displayName"
+ :placeholder="field.placeholder"
+ :maxlength="field.maxlength"
+ @update:theming="$emit('update:theming')" />
+ <ColorPickerField :name="colorPickerField.name"
+ :value.sync="colorPickerField.value"
+ :default-value="colorPickerField.defaultValue"
+ :display-name="colorPickerField.displayName"
+ @update:theming="$emit('update:theming')" />
+ <FileInputField v-for="field in fileInputFields"
+ :key="field.name"
+ :name="field.name"
+ :mime-name="field.mimeName"
+ :mime-value.sync="field.mimeValue"
+ :default-mime-value="field.defaultMimeValue"
+ :display-name="field.displayName"
+ :aria-label="field.ariaLabel"
+ @update:theming="$emit('update:theming')" />
+ <div class="admin-theming__preview">
+ <div class="admin-theming__preview-logo" />
+ </div>
+ </div>
+ </NcSettingsSection>
+ <NcSettingsSection :title="t('theming', 'Advanced options')">
+ <div class="admin-theming-advanced">
+ <TextField v-for="field in advancedTextFields"
+ :key="field.name"
+ :name="field.name"
+ :value.sync="field.value"
+ :default-value="field.defaultValue"
+ :type="field.type"
+ :display-name="field.displayName"
+ :placeholder="field.placeholder"
+ :maxlength="field.maxlength"
+ @update:theming="$emit('update:theming')" />
+ <FileInputField v-for="field in advancedFileInputFields"
+ :key="field.name"
+ :name="field.name"
+ :mime-name="field.mimeName"
+ :mime-value.sync="field.mimeValue"
+ :default-mime-value="field.defaultMimeValue"
+ :display-name="field.displayName"
+ :aria-label="field.ariaLabel"
+ @update:theming="$emit('update:theming')" />
+ <CheckboxField :name="userThemingField.name"
+ :value="userThemingField.value"
+ :default-value="userThemingField.defaultValue"
+ :display-name="userThemingField.displayName"
+ :label="userThemingField.label"
+ :description="userThemingField.description"
+ @update:theming="$emit('update:theming')" />
+ <a v-if="!canThemeIcons"
+ :href="docUrlIcons"
+ rel="noreferrer noopener">
+ <em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em>
+ </a>
+ </div>
+ </NcSettingsSection>
+ </section>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+
+import {
+ NcNoteCard,
+ NcSettingsSection,
+} from '@nextcloud/vue'
+import CheckboxField from './components/admin/CheckboxField.vue'
+import ColorPickerField from './components/admin/ColorPickerField.vue'
+import FileInputField from './components/admin/FileInputField.vue'
+import TextField from './components/admin/TextField.vue'
+
+const {
+ backgroundMime,
+ canThemeIcons,
+ color,
+ docUrl,
+ docUrlIcons,
+ faviconMime,
+ isThemable,
+ legalNoticeUrl,
+ logoheaderMime,
+ logoMime,
+ name,
+ notThemableErrorMessage,
+ privacyPolicyUrl,
+ slogan,
+ url,
+ userThemingDisabled,
+} = loadState('theming', 'adminThemingParameters')
+
+const textFields = [
+ {
+ name: 'name',
+ value: name,
+ defaultValue: 'Nextcloud',
+ type: 'text',
+ displayName: t('theming', 'Name'),
+ placeholder: t('theming', 'Name'),
+ maxlength: 250,
+ },
+ {
+ name: 'url',
+ value: url,
+ defaultValue: 'https://nextcloud.com',
+ type: 'url',
+ displayName: t('theming', 'Web link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+ {
+ name: 'slogan',
+ value: slogan,
+ defaultValue: t('theming', 'a safe home for all your data'),
+ type: 'text',
+ displayName: t('theming', 'Slogan'),
+ placeholder: t('theming', 'Slogan'),
+ maxlength: 500,
+ },
+]
+
+const colorPickerField = {
+ name: 'color',
+ value: color,
+ defaultValue: '#0082c9',
+ displayName: t('theming', 'Color'),
+}
+
+const fileInputFields = [
+ {
+ name: 'logo',
+ mimeName: 'logoMime',
+ mimeValue: logoMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Logo'),
+ ariaLabel: t('theming', 'Upload new logo'),
+ },
+ {
+ name: 'background',
+ mimeName: 'backgroundMime',
+ mimeValue: backgroundMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Background and login image'),
+ ariaLabel: t('theming', 'Upload new background and login image'),
+ },
+]
+
+const advancedTextFields = [
+ {
+ name: 'imprintUrl',
+ value: legalNoticeUrl,
+ defaultValue: '',
+ type: 'url',
+ displayName: t('theming', 'Legal notice link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+ {
+ name: 'privacyUrl',
+ value: privacyPolicyUrl,
+ defaultValue: '',
+ type: 'url',
+ displayName: t('theming', 'Privacy policy link'),
+ placeholder: 'https://…',
+ maxlength: 500,
+ },
+]
+
+const advancedFileInputFields = [
+ {
+ name: 'logoheader',
+ mimeName: 'logoheaderMime',
+ mimeValue: logoheaderMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Header logo'),
+ ariaLabel: t('theming', 'Upload new header logo'),
+ },
+ {
+ name: 'favicon',
+ mimeName: 'faviconMime',
+ mimeValue: faviconMime,
+ defaultMimeValue: '',
+ displayName: t('theming', 'Favicon'),
+ ariaLabel: t('theming', 'Upload new favicon'),
+ },
+]
+
+const userThemingField = {
+ name: 'disable-user-theming',
+ value: userThemingDisabled,
+ defaultValue: false,
+ displayName: t('theming', 'User settings'),
+ label: t('theming', 'Disable user theming'),
+ description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'),
+}
+
+export default {
+ name: 'AdminTheming',
+
+ components: {
+ CheckboxField,
+ ColorPickerField,
+ FileInputField,
+ NcNoteCard,
+ NcSettingsSection,
+ TextField,
+ },
+
+ emits: [
+ 'update:theming',
+ ],
+
+ data() {
+ return {
+ textFields,
+ colorPickerField,
+ fileInputFields,
+ advancedTextFields,
+ advancedFileInputFields,
+ userThemingField,
+
+ canThemeIcons,
+ docUrl,
+ docUrlIcons,
+ isThemable,
+ notThemableErrorMessage,
+ }
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.admin-theming,
+.admin-theming-advanced {
+ display: flex;
+ flex-direction: column;
+ gap: 8px 0;
+}
+
+.admin-theming {
+ &__preview {
+ width: 230px;
+ height: 140px;
+ background-size: cover;
+ background-position: center;
+ text-align: center;
+ margin-top: 10px;
+ background-color: var(--color-primary-default);
+ background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
+
+ &-logo {
+ width: 20%;
+ height: 20%;
+ margin-top: 20px;
+ display: inline-block;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
+ }
+ }
+}
+</style>
diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js
new file mode 100644
index 00000000000..9fce526c463
--- /dev/null
+++ b/apps/theming/src/admin-settings.js
@@ -0,0 +1,33 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import App from './AdminTheming.vue'
+import { refreshStyles } from './helpers/refreshStyles.js'
+
+Vue.prototype.OC = OC
+Vue.prototype.t = t
+
+const View = Vue.extend(App)
+const theming = new View()
+theming.$mount('#admin-theming')
+theming.$on('update:theming', refreshStyles)
diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue
new file mode 100644
index 00000000000..5877614717e
--- /dev/null
+++ b/apps/theming/src/components/admin/CheckboxField.vue
@@ -0,0 +1,102 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcCheckboxRadioSwitch type="switch"
+ :id="id"
+ :checked.sync="localValue"
+ @update:checked="save">
+ {{ label }}
+ </NcCheckboxRadioSwitch>
+ </div>
+
+ <p class="field__description">{{ description }}</p>
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+ </div>
+</template>
+
+<script>
+import {
+ NcCheckboxRadioSwitch,
+ NcNoteCard,
+} from '@nextcloud/vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'CheckboxField',
+
+ components: {
+ NcCheckboxRadioSwitch,
+ NcNoteCard,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ defaultValue: {
+ type: Boolean,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ &__description {
+ color: var(--color-text-maxcontrast);
+ }
+}
+</style>
diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue
new file mode 100644
index 00000000000..2e6ee99a75d
--- /dev/null
+++ b/apps/theming/src/components/admin/ColorPickerField.vue
@@ -0,0 +1,121 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcColorPicker :value.sync="localValue"
+ :advanced-fields="true"
+ @update:value="debounceSave">
+ <NcButton class="field__button"
+ type="primary"
+ :id="id"
+ :aria-label="t('theming', 'Select a custom color')">
+ {{ value }}
+ </NcButton>
+ </NcColorPicker>
+ <NcButton v-if="value !== defaultValue"
+ type="tertiary"
+ :aria-label="t('theming', 'Reset to default')"
+ @click="undo">
+ <template #icon>
+ <Undo :size="20" />
+ </template>
+ </NcButton>
+ </div>
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+ </div>
+</template>
+
+<script>
+import { debounce } from 'debounce'
+import {
+ NcButton,
+ NcColorPicker,
+ NcNoteCard,
+} from '@nextcloud/vue'
+import Undo from 'vue-material-design-icons/UndoVariant.vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'ColorPickerField',
+
+ components: {
+ NcButton,
+ NcColorPicker,
+ NcNoteCard,
+ Undo,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ },
+
+ methods: {
+ debounceSave: debounce(async function() {
+ await this.save()
+ }, 200),
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ // Override default NcButton styles
+ &__button {
+ width: 230px !important;
+ border-radius: var(--border-radius-large) !important;
+ background-color: var(--color-primary-default) !important;
+ &:hover {
+ background-color: var(--color-primary-element-default-hover) !important;
+ }
+ }
+}
+</style>
diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue
new file mode 100644
index 00000000000..537970cc0cc
--- /dev/null
+++ b/apps/theming/src/components/admin/FileInputField.vue
@@ -0,0 +1,248 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <label :for="id">{{ displayName }}</label>
+ <div class="field__row">
+ <NcButton type="secondary"
+ :id="id"
+ :aria-label="ariaLabel"
+ @click="activateLocalFilePicker">
+ <template #icon>
+ <Upload :size="20" />
+ </template>
+ {{ t('theming', 'Upload') }}
+ </NcButton>
+ <NcButton v-if="showReset"
+ type="tertiary"
+ :aria-label="t('theming', 'Reset to default')"
+ @click="undo">
+ <template #icon>
+ <Undo :size="20" />
+ </template>
+ </NcButton>
+ <NcButton v-if="showRemove"
+ type="tertiary"
+ :aria-label="t('theming', 'Remove background image')"
+ @click="removeBackground">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </NcButton>
+ <NcLoadingIcon v-if="showLoading"
+ class="field__loading-icon"
+ :size="20" />
+ </div>
+
+ <div v-if="(name === 'logoheader' || name === 'favicon') && mimeValue !== defaultMimeValue"
+ class="field__preview"
+ :class="{
+ 'field__preview--logoheader': name === 'logoheader',
+ 'field__preview--favicon': name === 'favicon',
+ }" />
+
+ <NcNoteCard v-if="errorMessage"
+ type="error"
+ :show-alert="true">
+ <p>{{ errorMessage }}</p>
+ </NcNoteCard>
+
+ <input ref="input"
+ type="file"
+ @change="onChange">
+ </div>
+</template>
+
+<script>
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+import {
+ NcButton,
+ NcLoadingIcon,
+ NcNoteCard,
+} from '@nextcloud/vue'
+import Delete from 'vue-material-design-icons/Delete.vue'
+import Undo from 'vue-material-design-icons/UndoVariant.vue'
+import Upload from 'vue-material-design-icons/Upload.vue'
+
+import FieldMixin from '../../mixins/admin/FieldMixin.js'
+
+export default {
+ name: 'FileInputField',
+
+ components: {
+ Delete,
+ NcButton,
+ NcLoadingIcon,
+ NcNoteCard,
+ Undo,
+ Upload,
+ },
+
+ mixins: [
+ FieldMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ mimeName: {
+ type: String,
+ required: true,
+ },
+ mimeValue: {
+ type: String,
+ required: true,
+ },
+ defaultMimeValue: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ ariaLabel: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ showLoading: false,
+ }
+ },
+
+ computed: {
+ showReset() {
+ return this.mimeValue !== this.defaultMimeValue
+ },
+
+ showRemove() {
+ if (this.name === 'background') {
+ if (this.mimeValue.startsWith('image/')) {
+ return true
+ }
+ if (this.mimeValue === this.defaultMimeValue) {
+ return true
+ }
+ }
+ return false
+ },
+ },
+
+ methods: {
+ activateLocalFilePicker() {
+ this.reset()
+ // Set to null so that selecting the same file will trigger the change event
+ this.$refs.input.value = null
+ this.$refs.input.click()
+ },
+
+ async onChange(e) {
+ const file = e.target.files[0]
+
+ const formData = new FormData()
+ formData.append('key', this.name)
+ formData.append('image', file)
+
+ const url = generateUrl('/apps/theming/ajax/uploadImage')
+ try {
+ this.showLoading = true
+ await axios.post(url, formData)
+ this.showLoading = false
+ this.$emit('update:mime-value', file.type)
+ this.handleSuccess()
+ } catch (e) {
+ this.showLoading = false
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async undo() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/undoChanges')
+ try {
+ await axios.post(url, {
+ setting: this.mimeName,
+ })
+ this.$emit('update:mime-value', this.defaultMimeValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async removeBackground() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/updateStylesheet')
+ try {
+ await axios.post(url, {
+ setting: this.mimeName,
+ value: 'backgroundColor',
+ })
+ this.$emit('update:mime-value', 'backgroundColor')
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import './shared/field.scss';
+
+.field {
+ &__loading-icon {
+ width: 44px;
+ height: 44px;
+ }
+
+ &__preview {
+ width: 70px;
+ height: 70px;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin: 10px 0;
+
+ &--logoheader {
+ background-image: var(--image-logoheader);
+ }
+
+ &--favicon {
+ background-image: var(--image-favicon);
+ }
+ }
+}
+
+input[type="file"] {
+ display: none;
+}
+</style>
diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue
new file mode 100644
index 00000000000..df82415e48a
--- /dev/null
+++ b/apps/theming/src/components/admin/TextField.vue
@@ -0,0 +1,98 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <div class="field">
+ <!-- PENDING undo trailing button icon requires @nextcloud/vue release and bump -->
+ <!-- PENDING custom maxlength requires @nextcloud/vue release and bump -->
+ <NcTextField :value.sync="localValue"
+ :label="displayName"
+ :label-visible="true"
+ :placeholder="placeholder"
+ :type="type"
+ :maxlength="maxlength"
+ :spellcheck="false"
+ :success="showSuccess"
+ :error="Boolean(errorMessage)"
+ :helper-text="errorMessage"
+ :show-trailing-button="value !== defaultValue"
+ trailing-button-icon="undo"
+ @trailing-button-click="undo"
+ @keydown.enter="save"
+ @blur="save" />
+ </div>
+</template>
+
+<script>
+import { NcTextField } from '@nextcloud/vue'
+
+import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
+
+export default {
+ name: 'TextField',
+
+ components: {
+ NcTextField,
+ },
+
+ mixins: [
+ TextValueMixin,
+ ],
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ defaultValue: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ displayName: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ maxlength: {
+ type: Number,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.field {
+ max-width: 400px;
+}
+</style>
diff --git a/apps/theming/src/components/admin/shared/field.scss b/apps/theming/src/components/admin/shared/field.scss
new file mode 100644
index 00000000000..54fc57b3ee5
--- /dev/null
+++ b/apps/theming/src/components/admin/shared/field.scss
@@ -0,0 +1,32 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px 0;
+
+ &__row {
+ display: flex;
+ gap: 0 4px;
+ }
+}
diff --git a/apps/theming/src/helpers/refreshStyles.js b/apps/theming/src/helpers/refreshStyles.js
new file mode 100644
index 00000000000..0c4a7cea22b
--- /dev/null
+++ b/apps/theming/src/helpers/refreshStyles.js
@@ -0,0 +1,33 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export const refreshStyles = () => {
+ // Refresh server-side generated theming CSS
+ [...document.head.querySelectorAll('link.theme')].forEach(theme => {
+ const url = new URL(theme.href)
+ url.searchParams.set('v', Date.now())
+ const newTheme = theme.cloneNode()
+ newTheme.href = url.toString()
+ newTheme.onload = () => theme.remove()
+ document.head.append(newTheme)
+ })
+}
diff --git a/apps/theming/src/mixins/admin/FieldMixin.js b/apps/theming/src/mixins/admin/FieldMixin.js
new file mode 100644
index 00000000000..811fa0c0bba
--- /dev/null
+++ b/apps/theming/src/mixins/admin/FieldMixin.js
@@ -0,0 +1,64 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+const styleRefreshFields = [
+ 'color',
+ 'logo',
+ 'background',
+ 'logoheader',
+ 'favicon',
+ 'disable-user-theming',
+]
+
+export default {
+ emits: [
+ 'update:theming',
+ ],
+
+ data() {
+ return {
+ showSuccess: false,
+ errorMessage: '',
+ }
+ },
+
+ computed: {
+ id() {
+ return `admin-theming-${this.name}`
+ },
+ },
+
+ methods: {
+ reset() {
+ this.showSuccess = false
+ this.errorMessage = ''
+ },
+
+ handleSuccess() {
+ this.showSuccess = true
+ setTimeout(() => { this.showSuccess = false }, 2000)
+ if (styleRefreshFields.includes(this.name)) {
+ this.$emit('update:theming')
+ }
+ },
+ },
+}
diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js
new file mode 100644
index 00000000000..4cce8bb301a
--- /dev/null
+++ b/apps/theming/src/mixins/admin/TextValueMixin.js
@@ -0,0 +1,77 @@
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+import FieldMixin from './FieldMixin.js'
+
+export default {
+ mixins: [
+ FieldMixin,
+ ],
+
+ watch: {
+ value(value) {
+ this.localValue = value
+ },
+ },
+
+ data() {
+ return {
+ localValue: this.value,
+ }
+ },
+
+ methods: {
+ async save() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/updateStylesheet')
+ // Convert boolean to string as server expects string value
+ const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue
+ try {
+ await axios.post(url, {
+ setting: this.name,
+ value: valueToPost,
+ })
+ this.$emit('update:value', this.localValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+
+ async undo() {
+ this.reset()
+ const url = generateUrl('/apps/theming/ajax/undoChanges')
+ try {
+ await axios.post(url, {
+ setting: this.name,
+ })
+ this.$emit('update:value', this.defaultValue)
+ this.handleSuccess()
+ } catch (e) {
+ this.errorMessage = e.response.data.data?.message
+ }
+ },
+ },
+}
diff --git a/apps/theming/src/settings.js b/apps/theming/src/personal-settings.js
index 9b846117947..97f5e75e27a 100644
--- a/apps/theming/src/settings.js
+++ b/apps/theming/src/personal-settings.js
@@ -22,23 +22,12 @@
import Vue from 'vue'
import App from './UserThemes.vue'
+import { refreshStyles } from './helpers/refreshStyles.js'
-// bind to window
Vue.prototype.OC = OC
Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#theming')
-
-theming.$on('update:background', () => {
- // Refresh server-side generated theming CSS
- [...document.head.querySelectorAll('link.theme')].forEach(theme => {
- const url = new URL(theme.href)
- url.searchParams.set('v', Date.now())
- const newTheme = theme.cloneNode()
- newTheme.href = url.toString()
- newTheme.onload = () => theme.remove()
- document.head.append(newTheme)
- })
-})
+theming.$on('update:background', refreshStyles)
diff --git a/apps/theming/templates/settings-admin.php b/apps/theming/templates/settings-admin.php
index acaa7b168e8..0724d4fd55b 100644
--- a/apps/theming/templates/settings-admin.php
+++ b/apps/theming/templates/settings-admin.php
@@ -22,135 +22,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-script('theming', 'settings-admin');
-script('theming', '3rdparty/jscolor/jscolor');
-style('theming', 'settings-admin');
?>
-<div id="theming" class="section">
- <h2 class="inlineblock"><?php p($l->t('Theming')); ?></h2>
- <a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation'));?>" href="<?php p(link_to_docs('admin-theming')); ?>"></a>
- <p class="settings-hint"><?php p($l->t('Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')); ?></p>
- <div id="theming_settings_status">
- <div id="theming_settings_loading" class="icon-loading-small" style="display: none;"></div>
- <span id="theming_settings_msg" class="msg success" style="display: none;">Saved</span>
- </div>
- <?php if ($_['themable'] === false) { ?>
- <p>
- <?php p($_['errorMessage']) ?>
- </p>
- <?php } ?>
- <div>
- <label>
- <span><?php p($l->t('Name')) ?></span>
- <input id="theming-name" type="text" placeholder="<?php p($l->t('Name')); ?>" value="<?php p($_['name']) ?>" maxlength="250" />
- <div data-setting="name" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div>
- <label>
- <span><?php p($l->t('Web link')) ?></span>
- <input id="theming-url" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['url']) ?>" maxlength="500" />
- <div data-setting="url" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div>
- <label>
- <span><?php p($l->t('Slogan')) ?></span>
- <input id="theming-slogan" type="text" placeholder="<?php p($l->t('Slogan')); ?>" value="<?php p($_['slogan']) ?>" maxlength="500" />
- <div data-setting="slogan" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div>
- <label>
- <span><?php p($l->t('Color')) ?></span>
- <input id="theming-color" type="text" maxlength="7" value="<?php p($_['color']) ?>" />
- <div data-setting="color" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div>
- <form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="logo">
- <input type="hidden" id="theming-logoMime" value="<?php p($_['images']['logo']['mime']); ?>" />
- <input type="hidden" name="key" value="logo" />
- <label for="uploadlogo"><span><?php p($l->t('Logo')) ?></span></label>
- <input id="uploadlogo" class="fileupload" name="image" type="file" />
- <label for="uploadlogo" class="button icon-upload svg" id="uploadlogo" title="<?php p($l->t('Upload new logo')) ?>"></label>
- <div data-setting="logoMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </form>
- </div>
- <div>
- <form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="background">
- <input type="hidden" id="theming-backgroundMime" value="<?php p($_['images']['background']['mime']); ?>" />
- <input type="hidden" name="key" value="background" />
- <label for="upload-login-background"><span><?php p($l->t('Background and login image')) ?></span></label>
- <input id="upload-login-background" class="fileupload" name="image" type="file">
- <label for="upload-login-background" class="button icon-upload svg" id="upload-login-background" title="<?php p($l->t("Upload new login background")) ?>"></label>
- <div data-setting="backgroundMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- <div class="theme-remove-bg icon icon-delete" data-toggle="tooltip" data-original-title="<?php p($l->t('Remove background image')); ?>"></div>
- </form>
- </div>
- <div id="theming-preview">
- <div id="theming-preview-logo"></div>
- </div>
- <h3 class="inlineblock"><?php p($l->t('Advanced options')); ?></h3>
- <div class="advanced-options">
- <div>
- <label>
- <span><?php p($l->t('Legal notice link')) ?></span>
- <input id="theming-imprintUrl" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['imprintUrl']) ?>" maxlength="500" />
- <div data-setting="imprintUrl" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div>
- <label>
- <span><?php p($l->t('Privacy policy link')) ?></span>
- <input id="theming-privacyUrl" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['privacyUrl']) ?>" maxlength="500" />
- <div data-setting="privacyUrl" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </label>
- </div>
- <div class="advanced-option-logoheader">
- <form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="logoheader">
- <input type="hidden" id="theming-logoheaderMime" value="<?php p($_['images']['logoheader']['mime']); ?>" />
- <input type="hidden" name="key" value="logoheader" />
- <label for="upload-login-logoheader"><span><?php p($l->t('Header logo')) ?></span></label>
- <input id="upload-login-logoheader" class="fileupload" name="image" type="file">
- <label for="upload-login-logoheader" class="button icon-upload svg" id="upload-login-logoheader" title="<?php p($l->t("Upload new header logo")) ?>"></label>
- <div id="theming-preview-logoheader" class="image-preview"></div>
- <div data-setting="logoheaderMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </form>
- </div>
- <div class="advanced-option-favicon">
- <form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="favicon">
- <input type="hidden" id="theming-faviconMime" value="<?php p($_['images']['favicon']['mime']); ?>" />
- <input type="hidden" name="key" value="favicon" />
- <label for="upload-login-favicon"><span><?php p($l->t('Favicon')) ?></span></label>
- <input id="upload-login-favicon" class="fileupload" name="image" type="file">
- <label for="upload-login-favicon" class="button icon-upload svg" id="upload-login-favicon" title="<?php p($l->t("Upload new favicon")) ?>"></label>
- <div id="theming-preview-favicon" class="image-preview"></div>
- <div data-setting="faviconMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
- </form>
- </div>
- <div class="advanced-options" id="user-theming">
- <label><span><?php p($l->t('User settings')); ?></span></label>
- <div>
- <p class="info">
- <?php p($l->t('Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can check this box.')); ?>
- </p>
- <input id="userThemingDisabled" class="checkbox" type="checkbox" <?php p($_['userThemingDisabled'] ? 'checked="checked"' : ''); ?> />
- <label for="userThemingDisabled"><?php p($l->t('Disable user theming')) ?></label>
- </div>
- </div>
- </div>
-
- <div class="theming-hints">
- <?php if (!$_['canThemeIcons']) { ?>
- <p class="info">
- <a href="<?php p($_['iconDocs']); ?> target="_blank" rel="noreferrer noopener">
- <em>
- <?php p($l->t('Install the Imagemagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.')); ?> ↗
- </em>
- </a>
- </p>
- <?php } ?>
- </div>
-</div>
+<div id="admin-theming"></div>
diff --git a/apps/theming/tests/Settings/AdminTest.php b/apps/theming/tests/Settings/AdminTest.php
index df884f4f803..8d59ea014a4 100644
--- a/apps/theming/tests/Settings/AdminTest.php
+++ b/apps/theming/tests/Settings/AdminTest.php
@@ -32,30 +32,27 @@ use OCA\Theming\ImageManager;
use OCA\Theming\Settings\Admin;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use Test\TestCase;
class AdminTest extends TestCase {
- /** @var Admin */
- private $admin;
- /** @var IConfig */
- private $config;
- /** @var ThemingDefaults */
- private $themingDefaults;
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var ImageManager */
- private $imageManager;
- /** @var IL10N */
- private $l10n;
+ private Admin $admin;
+ private IConfig $config;
+ private ThemingDefaults $themingDefaults;
+ private IInitialState $initialState;
+ private IURLGenerator $urlGenerator;
+ private ImageManager $imageManager;
+ private IL10N $l10n;
protected function setUp(): void {
parent::setUp();
$this->config = $this->createMock(IConfig::class);
$this->l10n = $this->createMock(IL10N::class);
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
+ $this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
@@ -64,6 +61,7 @@ class AdminTest extends TestCase {
$this->config,
$this->l10n,
$this->themingDefaults,
+ $this->initialState,
$this->urlGenerator,
$this->imageManager
);
@@ -99,28 +97,8 @@ class AdminTest extends TestCase {
->expects($this->once())
->method('getDefaultColorPrimary')
->willReturn('#fff');
- $this->urlGenerator
- ->expects($this->once())
- ->method('linkToRoute')
- ->with('theming.Theming.uploadImage')
- ->willReturn('/my/route');
- $params = [
- 'themable' => true,
- 'errorMessage' => '',
- 'name' => 'MyEntity',
- 'url' => 'https://example.com',
- 'slogan' => 'MySlogan',
- 'color' => '#fff',
- 'uploadLogoRoute' => '/my/route',
- 'canThemeIcons' => null,
- 'iconDocs' => null,
- 'images' => [],
- 'imprintUrl' => '',
- 'privacyUrl' => '',
- 'userThemingDisabled' => false,
- ];
- $expected = new TemplateResponse('theming', 'settings-admin', $params, '');
+ $expected = new TemplateResponse('theming', 'settings-admin');
$this->assertEquals($expected, $this->admin->getForm());
}
@@ -159,28 +137,8 @@ class AdminTest extends TestCase {
->expects($this->once())
->method('getDefaultColorPrimary')
->willReturn('#fff');
- $this->urlGenerator
- ->expects($this->once())
- ->method('linkToRoute')
- ->with('theming.Theming.uploadImage')
- ->willReturn('/my/route');
- $params = [
- 'themable' => false,
- 'errorMessage' => 'You are already using a custom theme. Theming app settings might be overwritten by that.',
- 'name' => 'MyEntity',
- 'url' => 'https://example.com',
- 'slogan' => 'MySlogan',
- 'color' => '#fff',
- 'uploadLogoRoute' => '/my/route',
- 'canThemeIcons' => null,
- 'iconDocs' => '',
- 'images' => [],
- 'imprintUrl' => '',
- 'privacyUrl' => '',
- 'userThemingDisabled' => false
- ];
- $expected = new TemplateResponse('theming', 'settings-admin', $params, '');
+ $expected = new TemplateResponse('theming', 'settings-admin');
$this->assertEquals($expected, $this->admin->getForm());
}