diff options
author | Christopher Ng <chrng8@gmail.com> | 2022-10-01 03:04:39 +0000 |
---|---|---|
committer | Christopher Ng <chrng8@gmail.com> | 2022-10-28 00:18:47 +0000 |
commit | 4a2bbc7af9249364ba2455f627522450262cad75 (patch) | |
tree | b0fd373e0aad0f18c35d2272c565b20bdab630a9 /apps | |
parent | d007088cf5d89e29065991e0cbe2c890dfa13d96 (diff) | |
download | nextcloud-server-4a2bbc7af9249364ba2455f627522450262cad75.tar.gz nextcloud-server-4a2bbc7af9249364ba2455f627522450262cad75.zip |
Rewrite admin theming in Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>
Diffstat (limited to 'apps')
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()); } |