diff --git a/web/src/components/Base.vue b/web/src/components/Base.vue index 2c7a2ed5..c7972dd7 100644 --- a/web/src/components/Base.vue +++ b/web/src/components/Base.vue @@ -3,7 +3,6 @@ diff --git a/web/src/components/Graph.vue b/web/src/components/Graph.vue index b2eacf66..42866740 100644 --- a/web/src/components/Graph.vue +++ b/web/src/components/Graph.vue @@ -65,6 +65,7 @@ }}
import { mapActions, mapState } from "vuex"; import ToolBar from "./ToolBar.vue"; +import { getColorScheme, onColorSchemeChange } from "../lib/darkmode"; export default { name: "Graph", @@ -552,6 +554,9 @@ export default { } }; this.chartOptions = { + theme: { + mode: getColorScheme(), + }, dataLabels: { enabled: false, }, @@ -589,6 +594,22 @@ export default { }, }, }; + onColorSchemeChange(() => { + if (!this.$refs.chart) { + return; + } + this.$refs.chart.updateOptions({ + theme: { + mode: getColorScheme(), + }, + chart: { + foreColor: getColorScheme() === 'dark' ? '#f6f7f8' : '#373d3f' + }, + tooltip: { + theme: getColorScheme() + } + }); + }); this.chartData = []; obj.build(this.chartData, this.chartOptions); }, diff --git a/web/src/components/Navigation.vue b/web/src/components/Navigation.vue index c9861ca6..538b0c97 100644 --- a/web/src/components/Navigation.vue +++ b/web/src/components/Navigation.vue @@ -189,11 +189,29 @@ Manage Converters - + + + + mdi-weather-sunny + + + mdi-cog-outline + + + mdi-weather-night + + + diff --git a/web/src/lib/darkmode.js b/web/src/lib/darkmode.js new file mode 100644 index 00000000..4ec030e5 --- /dev/null +++ b/web/src/lib/darkmode.js @@ -0,0 +1,75 @@ +const STORAGE_KEY = "colorScheme"; + +/** + * @type {Array} + */ +const colorSchemeChangeListeners = []; + +/** @type {import("vuetify").Framework|null} */ +let registeredVuetify = null; + +export function registerVuetifyTheme(vuetify) { + registeredVuetify = vuetify; + vuetify.theme.dark = getColorScheme() === "dark"; + onSystemColorSchemeChange((colorScheme) => { + vuetify.theme.dark = colorScheme === "dark"; + }); +} + +export function onSystemColorSchemeChange(callback) { + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => { + callback(getColorScheme()); + }); +} + +export function onColorSchemeChange(callback) { + colorSchemeChangeListeners.push(callback); + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => { + callback(getColorScheme()); + }); +} + +/** + * @returns {"dark"|"light"} + */ +export function getSystemColorScheme() { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light"; +} + +/** + * @returns {"dark"|"light"|"system"} + */ +export function getColorSchemeFromStorage() { + const scheme = localStorage.getItem(STORAGE_KEY); + if (!['dark', 'light', 'system'].includes(scheme)) { + return 'system'; + } + return scheme; +} + +/** + * @returns {"dark"|"light"} + */ +export function getColorScheme() { + const scheme = getColorSchemeFromStorage(); + if (scheme === 'system') { + return getSystemColorScheme(); + } + return scheme; +} + +/** + * @param {"dark"|"light"|"system"} scheme + */ +export function setColorScheme(scheme) { + if (!['dark', 'light', 'system'].includes(scheme)) { + throw new Error(`Invalid color scheme: ${scheme}`); + } + localStorage.setItem(STORAGE_KEY, scheme); + colorSchemeChangeListeners.forEach((callback) => callback(scheme)); + if (null !== registeredVuetify) { + registeredVuetify.theme.dark = getColorScheme() === "dark"; + } +} diff --git a/web/src/main.js b/web/src/main.js index 33d9c484..0d3831a9 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -1,10 +1,12 @@ +import { getColorScheme } from "@/lib/darkmode"; import Vue from "vue"; +import VueApexCharts from "vue-apexcharts"; +import vueFilterPrettyBytes from "vue-filter-pretty-bytes"; import Vuetify from "vuetify"; import App from "./App.vue"; -import store from "./store"; +import { registerVuetifyTheme } from "./lib/darkmode"; import router from "./routes"; -import VueApexCharts from "vue-apexcharts"; -import vueFilterPrettyBytes from "vue-filter-pretty-bytes"; +import store from "./store"; Vue.config.productionTip = process.env.NODE_ENV == "production"; @@ -16,12 +18,14 @@ Vue.use(vueFilterPrettyBytes); Vue.component("Apexchart", VueApexCharts); const vue = new Vue({ - vuetify: new Vuetify(), + vuetify: new Vuetify({theme: {dark: getColorScheme(),},}), store, router, render: (h) => h(App), }); +registerVuetifyTheme(vue.$vuetify); + Vue.filter("capitalize", function (value) { if (!value) return ""; value = value.toString();