Refactor UI dark/bright mode (#2590)

- collapse pipeline configs by default, closes
https://github.com/woodpecker-ci/woodpecker/discussions/2557
- refactor ui themes: instead of just differentiating between "is dark"
or "is not dark", add a third "auto" option following the browser
settings (closes
https://github.com/woodpecker-ci/woodpecker/discussions/2204) and put
everything into an enum. also move the option from the navbar to user
settings.

---------

Co-authored-by: Anbraten <anton@ju60.de>
This commit is contained in:
qwerty287 2023-11-01 10:30:40 +01:00 committed by GitHub
parent 4198c447fb
commit 2ff916c804
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 56 additions and 62 deletions

View file

@ -12,8 +12,6 @@
"password": "Password",
"url": "URL",
"back": "Back",
"color_scheme_light": "Switch to dark mode",
"color_scheme_dark": "Switch to light mode",
"unknown_error": "An unknown error occurred",
"documentation_for": "Documentation for \"{topic}\"",
"pipeline_feed": "Pipeline feed",
@ -453,7 +451,13 @@
"settings": "User Settings",
"general": {
"general": "General",
"language": "Language"
"language": "Language",
"theme": {
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"auto": "Auto"
}
},
"secrets": {
"secrets": "Secrets",

View file

@ -39,13 +39,14 @@ import Icon from '~/components/atomic/Icon.vue';
const props = defineProps<{
title?: string;
collapsable?: boolean;
collapsedByDefault?: boolean;
}>();
/**
* _collapsed is used to store the internal state of the panel, but is
* ignored if the panel is not collapsable.
*/
const _collapsed = ref(false);
const _collapsed = ref(props.collapsedByDefault || false);
const collapsed = computed(() => props.collapsable && _collapsed.value);
</script>

View file

@ -26,13 +26,6 @@
</div>
<!-- Right Icons Box -->
<div class="flex ml-auto -m-1.5 items-center space-x-2">
<!-- Dark Mode Toggle -->
<IconButton
:icon="darkMode ? 'dark' : 'light'"
:title="$t(darkMode ? 'color_scheme_dark' : 'color_scheme_light')"
class="navbar-icon"
@click="darkMode = !darkMode"
/>
<!-- Admin Settings -->
<IconButton
v-if="user?.admin"
@ -62,7 +55,6 @@ import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useConfig from '~/compositions/useConfig';
import { useDarkMode } from '~/compositions/useDarkMode';
import ActivePipelines from './ActivePipelines.vue';
@ -70,7 +62,6 @@ const config = useConfig();
const route = useRoute();
const authentication = useAuthentication();
const { user } = authentication;
const { darkMode } = useDarkMode();
const apiUrl = `${config.rootPath ?? ''}/swagger/index.html`;
function doLogin() {

View file

@ -3,6 +3,16 @@
<InputField :label="$t('user.settings.general.language')">
<SelectField v-model="selectedLocale" :options="localeOptions" />
</InputField>
<InputField :label="$t('user.settings.general.theme.theme')">
<SelectField
v-model="storeTheme"
:options="[
{ value: 'auto', text: $t('user.settings.general.theme.auto') },
{ value: 'light', text: $t('user.settings.general.theme.light') },
{ value: 'dark', text: $t('user.settings.general.theme.dark') },
]"
/>
</InputField>
</Settings>
</template>
@ -17,8 +27,10 @@ import { useI18n } from 'vue-i18n';
import SelectField from '~/components/form/SelectField.vue';
import Settings from '~/components/layout/Settings.vue';
import { setI18nLanguage } from '~/compositions/useI18n';
import { useTheme } from '~/compositions/useTheme';
const { locale } = useI18n();
const { storeTheme } = useTheme();
const localeOptions = computed(() =>
SUPPORTED_LOCALES.map((supportedLocale) => ({

View file

@ -1,47 +0,0 @@
import { computed, ref, watch } from 'vue';
const LS_DARK_MODE = 'woodpecker:dark-mode';
const isDarkModeActive = ref(false);
watch(isDarkModeActive, (isActive) => {
if (isActive) {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#2A2E3A'); // internal-wp-secondary-600 (see windi.config.ts)
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
document.documentElement.setAttribute('data-theme', 'light');
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#369943'); // internal-wp-primary-400
}
});
function setDarkMode(isActive: boolean) {
isDarkModeActive.value = isActive;
localStorage.setItem(LS_DARK_MODE, isActive ? 'dark' : 'light');
}
function load() {
const isActive = localStorage.getItem(LS_DARK_MODE) as 'dark' | 'light' | null;
if (isActive === null) {
setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
} else {
setDarkMode(isActive === 'dark');
}
}
load();
export function useDarkMode() {
return {
darkMode: computed({
get() {
return isDarkModeActive.value;
},
set(isActive: boolean) {
setDarkMode(isActive);
},
}),
};
}

View file

@ -1,10 +1,11 @@
import { computed, ref, watch } from 'vue';
import useConfig from '~/compositions/useConfig';
import { useDarkMode } from '~/compositions/useDarkMode';
import { useTheme } from '~/compositions/useTheme';
import { PipelineStatus } from '~/lib/api/types';
const darkMode = computed(() => (useDarkMode().darkMode.value ? 'dark' : 'light'));
const { theme } = useTheme();
const darkMode = computed(() => theme.value);
type Status = 'default' | 'success' | 'pending' | 'error';
const faviconStatus = ref<Status>('default');

View file

@ -0,0 +1,31 @@
import { useColorMode } from '@vueuse/core';
import { watch } from 'vue';
const { store: storeTheme, state: resolvedTheme } = useColorMode({
storageKey: 'woodpecker:theme',
});
function updateTheme() {
if (resolvedTheme.value === 'dark') {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#2A2E3A'); // internal-wp-secondary-600 (see windi.config.ts)
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
document.documentElement.setAttribute('data-theme', 'light');
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#369943'); // internal-wp-primary-400
}
}
watch(storeTheme, updateTheme);
updateTheme();
export function useTheme() {
return {
theme: resolvedTheme,
storeTheme,
};
}

View file

@ -4,6 +4,7 @@
v-for="pipelineConfig in pipelineConfigs || []"
:key="pipelineConfig.hash"
collapsable
collapsed-by-default
:title="pipelineConfig.name"
>
<SyntaxHighlight class="font-mono whitespace-pre overflow-auto" language="yaml" :code="pipelineConfig.data" />