mirror of
https://github.com/nextcloud/server.git
synced 2026-06-12 18:21:40 -04:00
feat(theming): allow to define supplementary themes
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
a1ba0bb01f
commit
cb20e4e19d
9 changed files with 261 additions and 190 deletions
|
|
@ -16,7 +16,7 @@ SPDX-FileCopyrightText = "2012-2019 Abbie Gonzalez <https://abbiecod.es|support@
|
|||
SPDX-License-Identifier = "OFL-1.1-RFN"
|
||||
|
||||
[[annotations]]
|
||||
path = ["img/dark-highcontrast.jpg", "img/dark.jpg", "img/default-source.svg", "img/default.jpg", "img/light-highcontrast.jpg", "img/light.jpg", "img/opendyslexic.jpg"]
|
||||
path = ["img/dark-highcontrast.jpg", "img/dark.jpg", "img/default-source.svg", "img/default.jpg", "img/light-highcontrast.jpg", "img/light.jpg", "img/opendyslexic.jpg", "img/reduced-motion.jpg"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2022 Nextcloud GmbH and Nextcloud contributors"
|
||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||
|
|
|
|||
BIN
apps/theming/img/reduced-motion.jpg
Normal file
BIN
apps/theming/img/reduced-motion.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -17,6 +17,11 @@ interface ITheme {
|
|||
|
||||
public const TYPE_THEME = 1;
|
||||
public const TYPE_FONT = 2;
|
||||
/**
|
||||
* A supplementary theme where multiple can be active at the same time.
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public const TYPE_SUPPLEMENTARY = 3;
|
||||
|
||||
/**
|
||||
* Unique theme id
|
||||
|
|
|
|||
|
|
@ -87,33 +87,22 @@ class ThemesService {
|
|||
* @return string[] the enabled themes
|
||||
*/
|
||||
public function enableTheme(ITheme $theme): array {
|
||||
$themesIds = $this->getEnabledThemes();
|
||||
$enabledThemeIds = $this->getEnabledThemes();
|
||||
|
||||
// If already enabled, ignore
|
||||
if (in_array($theme->getId(), $themesIds)) {
|
||||
return $themesIds;
|
||||
if (in_array($theme->getId(), $enabledThemeIds)) {
|
||||
return $enabledThemeIds;
|
||||
}
|
||||
|
||||
/** @var ITheme[] */
|
||||
$themes = array_filter(array_map(function ($themeId) {
|
||||
return $this->getThemes()[$themeId];
|
||||
}, $themesIds));
|
||||
// for other types then supplementary themes we need to filter out themes with the same type
|
||||
if ($theme->getType() !== ITheme::TYPE_SUPPLEMENTARY) {
|
||||
$allThemes = $this->getThemes();
|
||||
$enabledThemeIds = array_filter($enabledThemeIds, fn (string $themeId) => $allThemes[$themeId]->gettype() !== $theme->gettype());
|
||||
}
|
||||
|
||||
// Filtering all themes with the same type
|
||||
$filteredThemes = array_filter($themes, function (ITheme $t) use ($theme) {
|
||||
return $theme->getType() === $t->getType();
|
||||
});
|
||||
|
||||
// Retrieve IDs only
|
||||
/** @var string[] */
|
||||
$filteredThemesIds = array_map(function (ITheme $t) {
|
||||
return $t->getId();
|
||||
}, array_values($filteredThemes));
|
||||
|
||||
$enabledThemes = array_merge(array_diff($themesIds, $filteredThemesIds), [$theme->getId()]);
|
||||
$this->setEnabledThemes($enabledThemes);
|
||||
|
||||
return $enabledThemes;
|
||||
$enabledThemeIds[] = $theme->getId();
|
||||
$this->setEnabledThemes($enabledThemeIds);
|
||||
return array_values($enabledThemeIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,7 +116,7 @@ class ThemesService {
|
|||
|
||||
// If enabled, removing it
|
||||
if (in_array($theme->getId(), $themesIds)) {
|
||||
$enabledThemes = array_diff($themesIds, [$theme->getId()]);
|
||||
$enabledThemes = array_values(array_diff($themesIds, [$theme->getId()]));
|
||||
$this->setEnabledThemes($enabledThemes);
|
||||
return $enabledThemes;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ class ReducedMotion implements ITheme {
|
|||
) {
|
||||
}
|
||||
|
||||
public function getCustomCss(): string
|
||||
{
|
||||
public function getCustomCss(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getMeta(): array
|
||||
{
|
||||
public function getMeta(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +30,7 @@ class ReducedMotion implements ITheme {
|
|||
}
|
||||
|
||||
public function getType(): int {
|
||||
return ITheme::TYPE_FONT;
|
||||
return ITheme::TYPE_SUPPLEMENTARY;
|
||||
}
|
||||
|
||||
public function getTitle(): string {
|
||||
|
|
|
|||
|
|
@ -13,28 +13,21 @@
|
|||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="descriptionDetail" />
|
||||
|
||||
<div class="theming__preview-list">
|
||||
<ItemPreview
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:enforced="theme.id === enforceTheme"
|
||||
:selected="selectedTheme.id === theme.id"
|
||||
:theme="theme"
|
||||
:unique="themes.length === 1"
|
||||
type="theme"
|
||||
@change="changeTheme" />
|
||||
</div>
|
||||
<ThemeList
|
||||
v-model="selectedMainThemes"
|
||||
:label="t('theming', 'Themes')"
|
||||
:themes="mainThemes" />
|
||||
|
||||
<div class="theming__preview-list">
|
||||
<ItemPreview
|
||||
v-for="theme in fonts"
|
||||
:key="theme.id"
|
||||
:selected="theme.enabled"
|
||||
:theme="theme"
|
||||
:unique="fonts.length === 1"
|
||||
type="font"
|
||||
@change="changeFont" />
|
||||
</div>
|
||||
<ThemeList
|
||||
v-model="selectedSupplementaryThemes"
|
||||
:label="t('theming', 'Supplementary themes')"
|
||||
:themes="supplementaryThemes"
|
||||
multiple />
|
||||
|
||||
<ThemeList
|
||||
v-model="selectedFontThemes"
|
||||
:label="t('theming', 'Fonts')"
|
||||
:themes="fontThemes" />
|
||||
|
||||
<h3>{{ t('theming', 'Misc accessibility options') }}</h3>
|
||||
<NcCheckboxRadioSwitch
|
||||
|
|
@ -86,21 +79,19 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import BackgroundSettings from './components/BackgroundSettings.vue'
|
||||
import ItemPreview from './components/ItemPreview.vue'
|
||||
import ThemeList from './components/ThemeList.vue'
|
||||
import UserAppMenuSection from './components/UserAppMenuSection.vue'
|
||||
import UserPrimaryColor from './components/UserPrimaryColor.vue'
|
||||
import { refreshStyles } from './helpers/refreshStyles.js'
|
||||
import { logger } from './logger.js'
|
||||
|
||||
const availableThemes = loadState('theming', 'themes', [])
|
||||
const enforceTheme = loadState('theming', 'enforceTheme', '')
|
||||
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
|
||||
const enableBlurFilter = loadState('theming', 'enableBlurFilter', '')
|
||||
|
||||
|
|
@ -110,7 +101,7 @@ export default {
|
|||
name: 'UserTheming',
|
||||
|
||||
components: {
|
||||
ItemPreview,
|
||||
ThemeList,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcSettingsSection,
|
||||
BackgroundSettings,
|
||||
|
|
@ -119,30 +110,62 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
if (availableThemes.every(({ type, enabled }) => type !== 1 || !enabled)) {
|
||||
availableThemes.find(({ id, type }) => id === 'default' && type === 1).enabled = true
|
||||
}
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
|
||||
// Admin defined configs
|
||||
enforceTheme,
|
||||
shortcutsDisabled,
|
||||
isUserThemingDisabled,
|
||||
|
||||
enableBlurFilter,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
themes() {
|
||||
mainThemes() {
|
||||
return this.availableThemes.filter((theme) => theme.type === 1)
|
||||
},
|
||||
|
||||
fonts() {
|
||||
fontThemes() {
|
||||
return this.availableThemes.filter((theme) => theme.type === 2)
|
||||
},
|
||||
|
||||
// Selected theme, fallback on first (default) if none
|
||||
selectedTheme() {
|
||||
return this.themes.find((theme) => theme.enabled === true) || this.themes[0]
|
||||
supplementaryThemes() {
|
||||
return this.availableThemes.filter((theme) => theme.type === 3)
|
||||
},
|
||||
|
||||
selectedMainThemes: {
|
||||
get() {
|
||||
return this.mainThemes.filter(({ enabled }) => enabled)
|
||||
},
|
||||
|
||||
set(themes) {
|
||||
logger.debug('SETTING main', { themes })
|
||||
this.updateThemes(this.mainThemes, themes)
|
||||
},
|
||||
},
|
||||
|
||||
selectedFontThemes: {
|
||||
get() {
|
||||
return this.fontThemes.filter(({ enabled }) => enabled)
|
||||
},
|
||||
|
||||
set(themes) {
|
||||
this.updateThemes(this.fontThemes, themes)
|
||||
},
|
||||
},
|
||||
|
||||
selectedSupplementaryThemes: {
|
||||
get() {
|
||||
return this.supplementaryThemes.filter(({ enabled }) => enabled)
|
||||
},
|
||||
|
||||
set(themes) {
|
||||
this.updateThemes(this.supplementaryThemes, themes)
|
||||
},
|
||||
},
|
||||
|
||||
description() {
|
||||
|
|
@ -182,38 +205,19 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
// Refresh server-side generated theming CSS
|
||||
async refreshGlobalStyles() {
|
||||
await refreshStyles()
|
||||
this.$nextTick(() => this.$refs.primaryColor.reload())
|
||||
},
|
||||
|
||||
changeTheme({ enabled, id }) {
|
||||
// Reset selected and select new one
|
||||
this.themes.forEach((theme) => {
|
||||
if (theme.id === id && enabled) {
|
||||
theme.enabled = true
|
||||
return
|
||||
}
|
||||
theme.enabled = false
|
||||
})
|
||||
|
||||
updateThemes(allThemes, selectedThemes) {
|
||||
for (const theme of allThemes) {
|
||||
theme.enabled = selectedThemes.includes(theme)
|
||||
}
|
||||
this.updateBodyAttributes()
|
||||
this.selectItem(enabled, id)
|
||||
},
|
||||
|
||||
changeFont({ enabled, id }) {
|
||||
// Reset selected and select new one
|
||||
this.fonts.forEach((font) => {
|
||||
if (font.id === id && enabled) {
|
||||
font.enabled = true
|
||||
return
|
||||
}
|
||||
font.enabled = false
|
||||
})
|
||||
|
||||
this.updateBodyAttributes()
|
||||
this.selectItem(enabled, id)
|
||||
},
|
||||
|
||||
async changeShortcutsDisabled(newState) {
|
||||
|
|
@ -256,47 +260,23 @@ export default {
|
|||
},
|
||||
|
||||
updateBodyAttributes() {
|
||||
const enabledThemesIDs = this.themes.filter((theme) => theme.enabled === true).map((theme) => theme.id)
|
||||
const enabledFontsIDs = this.fonts.filter((font) => font.enabled === true).map((font) => font.id)
|
||||
const enabledThemesIDs = [
|
||||
...this.selectedMainThemes.map((theme) => theme.id),
|
||||
...this.selectedSupplementaryThemes.map((theme) => theme.id),
|
||||
...this.selectedFontThemes.map((theme) => theme.id),
|
||||
]
|
||||
|
||||
this.themes.forEach((theme) => {
|
||||
this.mainThemes.forEach((theme) => {
|
||||
document.body.toggleAttribute(`data-theme-${theme.id}`, theme.enabled)
|
||||
})
|
||||
this.fonts.forEach((font) => {
|
||||
this.supplementaryThemes.forEach((theme) => {
|
||||
document.body.toggleAttribute(`data-theme-${theme.id}`, theme.enabled)
|
||||
})
|
||||
this.fontThemes.forEach((font) => {
|
||||
document.body.toggleAttribute(`data-theme-${font.id}`, font.enabled)
|
||||
})
|
||||
|
||||
document.body.setAttribute('data-themes', [...enabledThemesIDs, ...enabledFontsIDs].join(','))
|
||||
},
|
||||
|
||||
/**
|
||||
* Commit a change and force reload css
|
||||
* Fetching the file again will trigger the server update
|
||||
*
|
||||
* @param {boolean} enabled the theme state
|
||||
* @param {string} themeId the theme ID to change
|
||||
*/
|
||||
async selectItem(enabled, themeId) {
|
||||
try {
|
||||
if (enabled) {
|
||||
await axios({
|
||||
url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}/enable', { themeId }),
|
||||
method: 'PUT',
|
||||
})
|
||||
} else {
|
||||
await axios({
|
||||
url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}', { themeId }),
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('theming: Unable to apply setting.', { error })
|
||||
let message = t('theming', 'Unable to apply the setting.')
|
||||
if (isAxiosError(error) && error.response.data.ocs?.meta?.message) {
|
||||
message = `${error.response.data.ocs.meta.message}. ${message}`
|
||||
}
|
||||
showError(message)
|
||||
}
|
||||
document.body.setAttribute('data-themes', enabledThemesIDs.join(','))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -318,14 +298,6 @@ export default {
|
|||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview-list {
|
||||
--gap: 30px;
|
||||
display: grid;
|
||||
margin-top: var(--gap);
|
||||
column-gap: var(--gap);
|
||||
row-gap: var(--gap);
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
|
|
@ -333,11 +305,4 @@ export default {
|
|||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.theming__preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
122
apps/theming/src/components/ThemeList.vue
Normal file
122
apps/theming/src/components/ThemeList.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import ThemeListItem from './ThemeListItem.vue'
|
||||
import { logger } from '../logger.ts'
|
||||
|
||||
interface ITheme {
|
||||
id: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* Label of this list
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Allow to select multiple themes
|
||||
*/
|
||||
multiple?: boolean
|
||||
|
||||
/**
|
||||
* The list of available themes
|
||||
*/
|
||||
themes: ITheme[]
|
||||
|
||||
/**
|
||||
* The selected themes
|
||||
*/
|
||||
modelValue: ITheme[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:model-value', v: ITheme[])
|
||||
}>()
|
||||
|
||||
const name = 'themes-' + Math.random().toString(16).slice(6)
|
||||
const enforcedTheme = loadState('theming', 'enforceTheme', '')
|
||||
|
||||
/**
|
||||
* @param theme - The theme to check if selected
|
||||
*/
|
||||
function isSelected(theme: ITheme) {
|
||||
return props.modelValue.includes(theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theme - The theme to toggle
|
||||
*/
|
||||
async function toggleSelected(theme: ITheme) {
|
||||
logger.debug('Toggle theme ' + theme.id, { theme })
|
||||
try {
|
||||
if (isSelected(theme)) {
|
||||
await axios.delete(generateOcsUrl('apps/theming/api/v1/theme/{themeId}', { themeId: theme.id }))
|
||||
emit('update:model-value', props.modelValue.filter(({ id }) => id !== theme.id))
|
||||
} else {
|
||||
await axios.put(generateOcsUrl('apps/theming/api/v1/theme/{themeId}/enable', { themeId: theme.id }))
|
||||
if (props.multiple) {
|
||||
emit('update:model-value', [...props.modelValue, theme])
|
||||
} else {
|
||||
emit('update:model-value', [theme])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('theming: Unable to apply setting.', { error })
|
||||
let message = t('theming', 'Unable to apply the setting.')
|
||||
if (isAxiosError(error) && error.response?.data.ocs?.meta?.message) {
|
||||
message = `${error.response.data.ocs.meta.message}. ${message}`
|
||||
}
|
||||
showError(message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// todo: remove with Vue 3
|
||||
export default {
|
||||
model: {
|
||||
event: 'update:model-value',
|
||||
prop: 'model-value',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul :aria-label="label" class="theme-list">
|
||||
<ThemeListItem
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:enforced="theme.id === enforcedTheme"
|
||||
:name="name"
|
||||
:theme="theme"
|
||||
:is-switch="themes.length === 1 || multiple"
|
||||
@selected="toggleSelected(theme)" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-list {
|
||||
--gap: 30px;
|
||||
display: grid;
|
||||
margin-top: var(--gap);
|
||||
column-gap: var(--gap);
|
||||
row-gap: var(--gap);
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.theme-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,8 +3,12 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div :class="'theming__preview--' + theme.id" class="theming__preview">
|
||||
<div class="theming__preview-image" :style="{ backgroundImage: 'url(' + img + ')' }" @click="onToggle" />
|
||||
<li :class="'theming__preview--' + theme.id" class="theming__preview">
|
||||
<img
|
||||
alt=""
|
||||
class="theming__preview-image"
|
||||
:src="imageUrl"
|
||||
@click="onToggle">
|
||||
<div class="theming__preview-description">
|
||||
<h3>{{ theme.title }}</h3>
|
||||
<p class="theming__preview-explanation">
|
||||
|
|
@ -18,104 +22,88 @@
|
|||
<NcCheckboxRadioSwitch
|
||||
v-show="!enforced"
|
||||
class="theming__preview-toggle"
|
||||
:checked.sync="checked"
|
||||
:model-value="checkboxModelValue"
|
||||
:disabled="enforced"
|
||||
:name="name"
|
||||
:type="switchType">
|
||||
:name="isSwitch ? undefined : name"
|
||||
:type="isSwitch ? 'switch' : 'radio'"
|
||||
:value="isSwitch ? undefined : theme.id"
|
||||
@update:model-value="onToggle">
|
||||
{{ theme.enableLabel }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateFilePath } from '@nextcloud/router'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import { logger } from '../logger.ts'
|
||||
|
||||
export default {
|
||||
name: 'ItemPreview',
|
||||
name: 'ThemeListItem',
|
||||
components: {
|
||||
NcCheckboxRadioSwitch,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* If the theme is enforced by the admin
|
||||
*/
|
||||
enforced: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* The theme object
|
||||
*/
|
||||
theme: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
type: {
|
||||
/**
|
||||
* The name for the radio input to group them.
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
unique: {
|
||||
/**
|
||||
* Whether to use a switch instead of a radio
|
||||
*/
|
||||
isSwitch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['selected'],
|
||||
|
||||
computed: {
|
||||
switchType() {
|
||||
return this.unique ? 'switch' : 'radio'
|
||||
checkboxModelValue() {
|
||||
if (this.isSwitch) {
|
||||
return this.enforced || this.theme.enabled
|
||||
}
|
||||
if (this.enforced || this.theme.enabled) {
|
||||
return this.theme.id
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
name() {
|
||||
return !this.unique ? this.type : null
|
||||
},
|
||||
|
||||
img() {
|
||||
imageUrl() {
|
||||
return generateFilePath('theming', 'img', this.theme.id + '.jpg')
|
||||
},
|
||||
|
||||
checked: {
|
||||
get() {
|
||||
return this.selected
|
||||
},
|
||||
|
||||
set(checked) {
|
||||
if (this.enforced) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Changed theme', this.theme.id, checked)
|
||||
|
||||
// If this is a radio, we can only enable
|
||||
if (!this.unique) {
|
||||
this.$emit('change', { enabled: true, id: this.theme.id })
|
||||
return
|
||||
}
|
||||
|
||||
// If this is a switch, we can disable the theme
|
||||
this.$emit('change', { enabled: checked === true, id: this.theme.id })
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onToggle() {
|
||||
onToggle(value) {
|
||||
if (this.enforced) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.switchType === 'radio') {
|
||||
this.checked = true
|
||||
return
|
||||
if (this.isSwitch || (value === this.theme.id) !== this.theme.enabled) {
|
||||
this.$emit('selected')
|
||||
}
|
||||
|
||||
// Invert state
|
||||
this.checked = !this.checked
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use OCA\Theming\Themes\DefaultTheme;
|
|||
use OCA\Theming\Themes\DyslexiaFont;
|
||||
use OCA\Theming\Themes\HighContrastTheme;
|
||||
use OCA\Theming\Themes\LightTheme;
|
||||
use OCA\Theming\Themes\ReducedMotion;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
use OCP\App\IAppManager;
|
||||
|
|
@ -74,6 +75,7 @@ class ThemesServiceTest extends TestCase {
|
|||
'light-highcontrast',
|
||||
'dark-highcontrast',
|
||||
'opendyslexic',
|
||||
'reduced-motion',
|
||||
];
|
||||
$this->assertEquals($expected, array_keys($this->themesService->getThemes()));
|
||||
}
|
||||
|
|
@ -110,6 +112,7 @@ class ThemesServiceTest extends TestCase {
|
|||
'light-highcontrast',
|
||||
'dark-highcontrast',
|
||||
'opendyslexic',
|
||||
'reduced-motion',
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, array_keys($this->themesService->getThemes()));
|
||||
|
|
@ -364,6 +367,7 @@ class ThemesServiceTest extends TestCase {
|
|||
$appManager,
|
||||
null,
|
||||
),
|
||||
'reduced-motion' => new ReducedMotion($l10n),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue