Add setting to toggle light/dark mode

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-10-28 12:38:15 +02:00
parent 610570c795
commit e420713a6f
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
7 changed files with 122 additions and 4 deletions

View file

@ -54,6 +54,7 @@ import {
defineAsyncComponent, defineAsyncComponent,
computed, computed,
watch, watch,
onBeforeUnmount,
} from "vue"; } from "vue";
import { LocationType } from "@/types/user-location.model"; import { LocationType } from "@/types/user-location.model";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
@ -155,6 +156,7 @@ onBeforeMount(async () => {
}); });
const snackbar = inject<Snackbar>("snackbar"); const snackbar = inject<Snackbar>("snackbar");
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
onMounted(() => { onMounted(() => {
online.value = window.navigator.onLine; online.value = window.navigator.onLine;
@ -187,6 +189,7 @@ onMounted(() => {
}, },
}); });
}); });
darkModePreference.addEventListener("change", changeTheme);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -289,6 +292,23 @@ watch(config, async (configWatched: IConfig | undefined) => {
}); });
const isDemoMode = computed(() => config.value?.demoMode); const isDemoMode = computed(() => config.value?.demoMode);
const changeTheme = () => {
console.debug("changing theme");
if (
localStorage.getItem("theme") === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
onBeforeUnmount(() => {
darkModePreference.removeEventListener("change", changeTheme);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -194,6 +194,10 @@ body {
@apply pl-2; @apply pl-2;
} }
.o-field--addons .o-radio:not(:only-child) input {
@apply rounded-full;
}
/* Editor */ /* Editor */
button.menubar__button { button.menubar__button {
@apply dark:text-white; @apply dark:text-white;

View file

@ -1408,5 +1408,14 @@
"Most recently published": "Most recently published", "Most recently published": "Most recently published",
"Least recently published": "Least recently published", "Least recently published": "Least recently published",
"With the most participants": "With the most participants", "With the most participants": "With the most participants",
"Number of members": "Number of members" "Number of members": "Number of members",
} "More options": "More options",
"Reported by someone anonymously": "Reported by someone anonymously",
"Back to homepage": "Back to homepage",
"Category list": "Category list",
"No categories with public upcoming events on this instance were found.": "No categories with public upcoming events on this instance were found.",
"Theme": "Theme",
"Adapt to system theme": "Adapt to system theme",
"Light": "Light",
"Dark": "Dark"
}

View file

@ -1406,5 +1406,14 @@
"{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})", "{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})",
"{title} ({count} todos)": "{title} ({count} todos)", "{title} ({count} todos)": "{title} ({count} todos)",
"{username} was invited to {group}": "{username} a été invité à {group}", "{username} was invited to {group}": "{username} a été invité à {group}",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"More options": "Plus d'options",
"Reported by someone anonymously": "Signalé par quelqu'un anonymement",
"Back to homepage": "Retour à la page d'accueil",
"Category list": "Liste des catégories",
"No categories with public upcoming events on this instance were found.": "Aucune catégorie avec des événements publics à venir n'a été trouvée.",
"Theme": "Thème",
"Adapt to system theme": "Sadapter au thème du système",
"Light": "Clair",
"Dark": "Sombre"
} }

View file

@ -13,6 +13,36 @@
]" ]"
/> />
<div> <div>
<o-field :label="t('Theme')" addonsClass="flex flex-col">
<o-field>
<o-checkbox v-model="systemTheme">{{
t("Adapt to system theme")
}}</o-checkbox>
</o-field>
<o-field>
<fieldset>
<legend class="sr-only">{{ t("Theme") }}</legend>
<o-radio
:class="{ 'border-mbz-bluegreen border-2': theme === 'light' }"
class="p-4 bg-white text-zinc-800 rounded-md mt-2 mr-2"
:disabled="systemTheme"
v-model="theme"
name="theme"
native-value="light"
>{{ t("Light") }}</o-radio
>
<o-radio
:class="{ 'border-mbz-bluegreen border-2': theme === 'dark' }"
class="p-4 bg-zinc-800 rounded-md text-white mt-2 ml-2"
:disabled="systemTheme"
v-model="theme"
name="theme"
native-value="dark"
>{{ t("Dark") }}</o-radio
>
</fieldset>
</o-field>
</o-field>
<o-field :label="t('Language')" label-for="setting-language"> <o-field :label="t('Language')" label-for="setting-language">
<o-select <o-select
:loading="loadingTimezones || loadingUserSettings" :loading="loadingTimezones || loadingUserSettings"
@ -120,7 +150,7 @@ import { Address, IAddress } from "@/types/address.model";
import { useTimezones } from "@/composition/apollo/config"; import { useTimezones } from "@/composition/apollo/config";
import { useUserSettings } from "@/composition/apollo/user"; import { useUserSettings } from "@/composition/apollo/user";
import { useHead } from "@vueuse/head"; import { useHead } from "@vueuse/head";
import { computed, defineAsyncComponent } from "vue"; import { computed, defineAsyncComponent, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
@ -140,6 +170,44 @@ useHead({
// langs: Record<string, string> = langs; // langs: Record<string, string> = langs;
const theme = ref(localStorage.getItem("theme"));
const systemTheme = ref(!("theme" in localStorage));
watch(systemTheme, (newSystemTheme) => {
console.debug("changing system theme", newSystemTheme);
if (newSystemTheme) {
theme.value = null;
localStorage.removeItem("theme");
} else {
theme.value = "light";
localStorage.setItem("theme", theme.value);
}
changeTheme();
});
watch(theme, (newTheme) => {
console.debug("changing theme value", newTheme);
if (newTheme) {
localStorage.setItem("theme", newTheme);
}
changeTheme();
});
const changeTheme = () => {
console.debug("changing theme to apply");
if (
localStorage.getItem("theme") === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
console.debug("applying dark theme");
document.documentElement.classList.add("dark");
} else {
console.debug("removing dark theme");
document.documentElement.classList.remove("dark");
}
};
const selectedTimezone = computed({ const selectedTimezone = computed({
get() { get() {
if (loggedUser.value?.settings?.timezone) { if (loggedUser.value?.settings?.timezone) {

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
content: ["./public/**/*.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], content: ["./public/**/*.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {

View file

@ -7,6 +7,13 @@
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" /> <link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" />
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} /> <link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} />
<meta name="theme-color" content={theme_color()} /> <meta name="theme-color" content={theme_color()} />
<script>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<%= if is_root(assigns) do %> <%= if is_root(assigns) do %>
<link rel="preload" href="/img/shape-1.svg" as="image" /> <link rel="preload" href="/img/shape-1.svg" as="image" />
<link rel="preload" href="/img/shape-2.svg" as="image" /> <link rel="preload" href="/img/shape-2.svg" as="image" />