Skip to content

Commit cb20e4e

Browse files
committed
feat(theming): allow to define supplementary themes
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent a1ba0bb commit cb20e4e

File tree

9 files changed

+262
-191
lines changed

9 files changed

+262
-191
lines changed

apps/theming/REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ SPDX-FileCopyrightText = "2012-2019 Abbie Gonzalez <https://abbiecod.es|support@
1616
SPDX-License-Identifier = "OFL-1.1-RFN"
1717

1818
[[annotations]]
19-
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"]
19+
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"]
2020
precedence = "aggregate"
2121
SPDX-FileCopyrightText = "2022 Nextcloud GmbH and Nextcloud contributors"
2222
SPDX-License-Identifier = "AGPL-3.0-or-later"
18.5 KB
Loading

apps/theming/lib/ITheme.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ interface ITheme {
1717

1818
public const TYPE_THEME = 1;
1919
public const TYPE_FONT = 2;
20+
/**
21+
* A supplementary theme where multiple can be active at the same time.
22+
* @since 33.0.0
23+
*/
24+
public const TYPE_SUPPLEMENTARY = 3;
2025

2126
/**
2227
* Unique theme id

apps/theming/lib/Service/ThemesService.php

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -87,33 +87,22 @@ public function getThemes(): array {
8787
* @return string[] the enabled themes
8888
*/
8989
public function enableTheme(ITheme $theme): array {
90-
$themesIds = $this->getEnabledThemes();
90+
$enabledThemeIds = $this->getEnabledThemes();
9191

9292
// If already enabled, ignore
93-
if (in_array($theme->getId(), $themesIds)) {
94-
return $themesIds;
93+
if (in_array($theme->getId(), $enabledThemeIds)) {
94+
return $enabledThemeIds;
9595
}
9696

97-
/** @var ITheme[] */
98-
$themes = array_filter(array_map(function ($themeId) {
99-
return $this->getThemes()[$themeId];
100-
}, $themesIds));
101-
102-
// Filtering all themes with the same type
103-
$filteredThemes = array_filter($themes, function (ITheme $t) use ($theme) {
104-
return $theme->getType() === $t->getType();
105-
});
106-
107-
// Retrieve IDs only
108-
/** @var string[] */
109-
$filteredThemesIds = array_map(function (ITheme $t) {
110-
return $t->getId();
111-
}, array_values($filteredThemes));
112-
113-
$enabledThemes = array_merge(array_diff($themesIds, $filteredThemesIds), [$theme->getId()]);
114-
$this->setEnabledThemes($enabledThemes);
97+
// for other types then supplementary themes we need to filter out themes with the same type
98+
if ($theme->getType() !== ITheme::TYPE_SUPPLEMENTARY) {
99+
$allThemes = $this->getThemes();
100+
$enabledThemeIds = array_filter($enabledThemeIds, fn (string $themeId) => $allThemes[$themeId]->gettype() !== $theme->gettype());
101+
}
115102

116-
return $enabledThemes;
103+
$enabledThemeIds[] = $theme->getId();
104+
$this->setEnabledThemes($enabledThemeIds);
105+
return array_values($enabledThemeIds);
117106
}
118107

119108
/**
@@ -127,7 +116,7 @@ public function disableTheme(ITheme $theme): array {
127116

128117
// If enabled, removing it
129118
if (in_array($theme->getId(), $themesIds)) {
130-
$enabledThemes = array_diff($themesIds, [$theme->getId()]);
119+
$enabledThemes = array_values(array_diff($themesIds, [$theme->getId()]));
131120
$this->setEnabledThemes($enabledThemes);
132121
return $enabledThemes;
133122
}

apps/theming/lib/Themes/ReducedMotion.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ public function __construct(
1717
) {
1818
}
1919

20-
public function getCustomCss(): string
21-
{
20+
public function getCustomCss(): string {
2221
return '';
2322
}
2423

25-
public function getMeta(): array
26-
{
24+
public function getMeta(): array {
2725
return [];
2826
}
2927

@@ -32,7 +30,7 @@ public function getId(): string {
3230
}
3331

3432
public function getType(): int {
35-
return ITheme::TYPE_FONT;
33+
return ITheme::TYPE_SUPPLEMENTARY;
3634
}
3735

3836
public function getTitle(): string {

apps/theming/src/UserTheming.vue

Lines changed: 74 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,21 @@
1313
<!-- eslint-disable-next-line vue/no-v-html -->
1414
<p v-html="descriptionDetail" />
1515

16-
<div class="theming__preview-list">
17-
<ItemPreview
18-
v-for="theme in themes"
19-
:key="theme.id"
20-
:enforced="theme.id === enforceTheme"
21-
:selected="selectedTheme.id === theme.id"
22-
:theme="theme"
23-
:unique="themes.length === 1"
24-
type="theme"
25-
@change="changeTheme" />
26-
</div>
27-
28-
<div class="theming__preview-list">
29-
<ItemPreview
30-
v-for="theme in fonts"
31-
:key="theme.id"
32-
:selected="theme.enabled"
33-
:theme="theme"
34-
:unique="fonts.length === 1"
35-
type="font"
36-
@change="changeFont" />
37-
</div>
16+
<ThemeList
17+
v-model="selectedMainThemes"
18+
:label="t('theming', 'Themes')"
19+
:themes="mainThemes" />
20+
21+
<ThemeList
22+
v-model="selectedSupplementaryThemes"
23+
:label="t('theming', 'Supplementary themes')"
24+
:themes="supplementaryThemes"
25+
multiple />
26+
27+
<ThemeList
28+
v-model="selectedFontThemes"
29+
:label="t('theming', 'Fonts')"
30+
:themes="fontThemes" />
3831

3932
<h3>{{ t('theming', 'Misc accessibility options') }}</h3>
4033
<NcCheckboxRadioSwitch
@@ -86,21 +79,19 @@
8679
</template>
8780

8881
<script>
89-
import axios, { isAxiosError } from '@nextcloud/axios'
90-
import { showError } from '@nextcloud/dialogs'
82+
import axios from '@nextcloud/axios'
9183
import { loadState } from '@nextcloud/initial-state'
9284
import { generateOcsUrl } from '@nextcloud/router'
9385
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
9486
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
9587
import BackgroundSettings from './components/BackgroundSettings.vue'
96-
import ItemPreview from './components/ItemPreview.vue'
88+
import ThemeList from './components/ThemeList.vue'
9789
import UserAppMenuSection from './components/UserAppMenuSection.vue'
9890
import UserPrimaryColor from './components/UserPrimaryColor.vue'
9991
import { refreshStyles } from './helpers/refreshStyles.js'
10092
import { logger } from './logger.js'
10193
10294
const availableThemes = loadState('theming', 'themes', [])
103-
const enforceTheme = loadState('theming', 'enforceTheme', '')
10495
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
10596
const enableBlurFilter = loadState('theming', 'enableBlurFilter', '')
10697
@@ -110,7 +101,7 @@ export default {
110101
name: 'UserTheming',
111102
112103
components: {
113-
ItemPreview,
104+
ThemeList,
114105
NcCheckboxRadioSwitch,
115106
NcSettingsSection,
116107
BackgroundSettings,
@@ -119,30 +110,62 @@ export default {
119110
},
120111
121112
data() {
113+
if (availableThemes.every(({ type, enabled }) => type !== 1 || !enabled)) {
114+
availableThemes.find(({ id, type }) => id === 'default' && type === 1).enabled = true
115+
}
116+
122117
return {
123118
availableThemes,
124119
125120
// Admin defined configs
126-
enforceTheme,
127121
shortcutsDisabled,
128122
isUserThemingDisabled,
129-
130123
enableBlurFilter,
131124
}
132125
},
133126
134127
computed: {
135-
themes() {
128+
mainThemes() {
136129
return this.availableThemes.filter((theme) => theme.type === 1)
137130
},
138131
139-
fonts() {
132+
fontThemes() {
140133
return this.availableThemes.filter((theme) => theme.type === 2)
141134
},
142135
143-
// Selected theme, fallback on first (default) if none
144-
selectedTheme() {
145-
return this.themes.find((theme) => theme.enabled === true) || this.themes[0]
136+
supplementaryThemes() {
137+
return this.availableThemes.filter((theme) => theme.type === 3)
138+
},
139+
140+
selectedMainThemes: {
141+
get() {
142+
return this.mainThemes.filter(({ enabled }) => enabled)
143+
},
144+
145+
set(themes) {
146+
logger.debug('SETTING main', { themes })
147+
this.updateThemes(this.mainThemes, themes)
148+
},
149+
},
150+
151+
selectedFontThemes: {
152+
get() {
153+
return this.fontThemes.filter(({ enabled }) => enabled)
154+
},
155+
156+
set(themes) {
157+
this.updateThemes(this.fontThemes, themes)
158+
},
159+
},
160+
161+
selectedSupplementaryThemes: {
162+
get() {
163+
return this.supplementaryThemes.filter(({ enabled }) => enabled)
164+
},
165+
166+
set(themes) {
167+
this.updateThemes(this.supplementaryThemes, themes)
168+
},
146169
},
147170
148171
description() {
@@ -182,38 +205,19 @@ export default {
182205
},
183206
184207
methods: {
208+
t,
209+
185210
// Refresh server-side generated theming CSS
186211
async refreshGlobalStyles() {
187212
await refreshStyles()
188213
this.$nextTick(() => this.$refs.primaryColor.reload())
189214
},
190215
191-
changeTheme({ enabled, id }) {
192-
// Reset selected and select new one
193-
this.themes.forEach((theme) => {
194-
if (theme.id === id && enabled) {
195-
theme.enabled = true
196-
return
197-
}
198-
theme.enabled = false
199-
})
200-
201-
this.updateBodyAttributes()
202-
this.selectItem(enabled, id)
203-
},
204-
205-
changeFont({ enabled, id }) {
206-
// Reset selected and select new one
207-
this.fonts.forEach((font) => {
208-
if (font.id === id && enabled) {
209-
font.enabled = true
210-
return
211-
}
212-
font.enabled = false
213-
})
214-
216+
updateThemes(allThemes, selectedThemes) {
217+
for (const theme of allThemes) {
218+
theme.enabled = selectedThemes.includes(theme)
219+
}
215220
this.updateBodyAttributes()
216-
this.selectItem(enabled, id)
217221
},
218222
219223
async changeShortcutsDisabled(newState) {
@@ -256,47 +260,23 @@ export default {
256260
},
257261
258262
updateBodyAttributes() {
259-
const enabledThemesIDs = this.themes.filter((theme) => theme.enabled === true).map((theme) => theme.id)
260-
const enabledFontsIDs = this.fonts.filter((font) => font.enabled === true).map((font) => font.id)
263+
const enabledThemesIDs = [
264+
...this.selectedMainThemes.map((theme) => theme.id),
265+
...this.selectedSupplementaryThemes.map((theme) => theme.id),
266+
...this.selectedFontThemes.map((theme) => theme.id),
267+
]
261268
262-
this.themes.forEach((theme) => {
269+
this.mainThemes.forEach((theme) => {
270+
document.body.toggleAttribute(`data-theme-${theme.id}`, theme.enabled)
271+
})
272+
this.supplementaryThemes.forEach((theme) => {
263273
document.body.toggleAttribute(`data-theme-${theme.id}`, theme.enabled)
264274
})
265-
this.fonts.forEach((font) => {
275+
this.fontThemes.forEach((font) => {
266276
document.body.toggleAttribute(`data-theme-${font.id}`, font.enabled)
267277
})
268278
269-
document.body.setAttribute('data-themes', [...enabledThemesIDs, ...enabledFontsIDs].join(','))
270-
},
271-
272-
/**
273-
* Commit a change and force reload css
274-
* Fetching the file again will trigger the server update
275-
*
276-
* @param {boolean} enabled the theme state
277-
* @param {string} themeId the theme ID to change
278-
*/
279-
async selectItem(enabled, themeId) {
280-
try {
281-
if (enabled) {
282-
await axios({
283-
url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}/enable', { themeId }),
284-
method: 'PUT',
285-
})
286-
} else {
287-
await axios({
288-
url: generateOcsUrl('apps/theming/api/v1/theme/{themeId}', { themeId }),
289-
method: 'DELETE',
290-
})
291-
}
292-
} catch (error) {
293-
logger.error('theming: Unable to apply setting.', { error })
294-
let message = t('theming', 'Unable to apply the setting.')
295-
if (isAxiosError(error) && error.response.data.ocs?.meta?.message) {
296-
message = `${error.response.data.ocs.meta.message}. ${message}`
297-
}
298-
showError(message)
299-
}
279+
document.body.setAttribute('data-themes', enabledThemesIDs.join(','))
300280
},
301281
},
302282
}
@@ -318,26 +298,11 @@ export default {
318298
text-decoration: underline;
319299
}
320300
}
321-
322-
&__preview-list {
323-
--gap: 30px;
324-
display: grid;
325-
margin-top: var(--gap);
326-
column-gap: var(--gap);
327-
row-gap: var(--gap);
328-
}
329301
}
330302
331303
.background {
332304
&__grid {
333305
margin-top: 30px;
334306
}
335307
}
336-
337-
@media (max-width: 1440px) {
338-
.theming__preview-list {
339-
display: flex;
340-
flex-direction: column;
341-
}
342-
}
343308
</style>

0 commit comments

Comments
 (0)