diff --git a/index.html b/index.html index 0356af28..c380d258 100644 --- a/index.html +++ b/index.html @@ -30,6 +30,23 @@ + + diff --git a/lib/gui.ts b/lib/gui.ts index b2609b51..2d5358b6 100644 --- a/lib/gui.ts +++ b/lib/gui.ts @@ -18,6 +18,7 @@ import { FilterGui } from "./filters/filtergui.js"; import { HostnameFilter } from "./filters/hostname.js"; import * as helper from "./utils/helper.js"; import { Language } from "./utils/language.js"; +import { cycleTheme, getTheme, initTheme, themeIconSVG } from "./theme.js"; export const Gui = function (language: ReturnType) { const self = { @@ -78,6 +79,21 @@ export const Gui = function (language: ReturnType) { contentDiv.appendChild(buttons); + initTheme(); + let buttonTheme = document.createElement("button"); + buttonTheme.classList.add("theme-toggle"); + function refreshThemeButton() { + let current = getTheme(); + buttonTheme.innerHTML = themeIconSVG(current); + buttonTheme.setAttribute("aria-label", _.t("button.theme." + current)); + } + buttonTheme.onclick = function onclick() { + cycleTheme(); + refreshThemeButton(); + }; + refreshThemeButton(); + buttons.appendChild(buttonTheme); + let buttonToggle = document.createElement("button"); buttonToggle.classList.add("ion-eye"); buttonToggle.setAttribute("aria-label", _.t("button.switchView")); diff --git a/lib/map.ts b/lib/map.ts index 71a5d5b1..a9d4117c 100644 --- a/lib/map.ts +++ b/lib/map.ts @@ -160,19 +160,6 @@ export const Map = function (linkScale: (t: any) => any, sidebar: ReturnType this.updateLayer(); + document.documentElement.addEventListener("themechange", this._onThemeChange); + }, + onRemove: function (map) { + if (this._onThemeChange) { + document.documentElement.removeEventListener("themechange", this._onThemeChange); + this._onThemeChange = null; + } + L.GridLayer.prototype.onRemove.call(this, map); }, setData: function (data, map, nodeDict, linkDict, linkScale) { let config = window.config; diff --git a/lib/theme.ts b/lib/theme.ts new file mode 100644 index 00000000..f38c2173 --- /dev/null +++ b/lib/theme.ts @@ -0,0 +1,114 @@ +export type Theme = "light" | "dark" | "auto"; + +// Keep STORAGE_KEY and DARK_CLASS in sync with the pre-paint inline script in index.html. +const STORAGE_KEY = "meshviewer.theme"; +const DARK_CLASS = "theme_night"; +const THEMES: Theme[] = ["light", "dark", "auto"]; + +let inMemory: Theme = "auto"; + +const darkQuery = + typeof window !== "undefined" && window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null; + +function isTheme(value: string | null): value is Theme { + return value === "light" || value === "dark" || value === "auto"; +} + +function read(): Theme { + try { + let v = localStorage.getItem(STORAGE_KEY); + if (isTheme(v)) { + return v; + } + } catch (e) { + /* localStorage inaccessible (private mode, sandboxed iframe, file://) */ + } + return inMemory; +} + +function write(value: Theme): void { + inMemory = value; + try { + localStorage.setItem(STORAGE_KEY, value); + } catch (e) { + /* keep in-memory only */ + } +} + +export function resolveTheme(): "light" | "dark" { + let current = read(); + if (current === "dark") { + return "dark"; + } + if (current === "auto" && darkQuery && darkQuery.matches) { + return "dark"; + } + return "light"; +} + +function apply(): void { + let dark = resolveTheme() === "dark"; + document.documentElement.classList.toggle(DARK_CLASS, dark); + document.documentElement.dispatchEvent(new CustomEvent("themechange")); +} + +export function getTheme(): Theme { + return read(); +} + +export function cycleTheme(): Theme { + let next = THEMES[(THEMES.indexOf(read()) + 1) % THEMES.length]; + write(next); + apply(); + return next; +} + +export function initTheme(): void { + inMemory = read(); + apply(); + if (darkQuery) { + darkQuery.addEventListener("change", function () { + if (read() === "auto") { + apply(); + } + }); + } +} + +const SVG_OPEN = '"; + +const SUN_RAYS = + '' + + '' + + '' + + ''; + +const SUN = + SVG_OPEN + + '' + + "" + + SUN_RAYS + + "" + + '' + + SUN_RAYS + + "" + + SVG_CLOSE; + +const MOON = SVG_OPEN + '' + SVG_CLOSE; + +const AUTO = + SVG_OPEN + + '' + + '' + + SVG_CLOSE; + +export function themeIconSVG(theme: Theme): string { + if (theme === "dark") { + return MOON; + } + if (theme === "auto") { + return AUTO; + } + return SUN; +} diff --git a/public/locale/cz.json b/public/locale/cz.json index 7a470c29..f5c239f2 100644 --- a/public/locale/cz.json +++ b/public/locale/cz.json @@ -63,7 +63,12 @@ "button": { "switchView": "Přepnout zobrazení", "location": "Vybrat souřadnice", - "tracking": "Lokalizace" + "tracking": "Lokalizace", + "theme": { + "light": "Světlý (další: tmavý)", + "dark": "Tmavý (další: auto)", + "auto": "Auto (další: světlý)" + } }, "momentjs": { "calendar": { diff --git a/public/locale/de.json b/public/locale/de.json index c0d18f54..ffa8ce1a 100644 --- a/public/locale/de.json +++ b/public/locale/de.json @@ -66,7 +66,12 @@ "fullscreen": "Vollbildmodus wechseln", "tracking": "Lokalisierung", "location": "Koordinaten wählen", - "ruler": "Lineal" + "ruler": "Lineal", + "theme": { + "light": "Hell (nächstes: dunkel)", + "dark": "Dunkel (nächstes: auto)", + "auto": "Auto (nächstes: hell)" + } }, "momentjs": { "calendar": { diff --git a/public/locale/en.json b/public/locale/en.json index 93403aa6..d9ace7cf 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -66,7 +66,12 @@ "fullscreen": "Toggle fullscreen", "tracking": "Localisation", "location": "Pick coordinates", - "ruler": "Ruler" + "ruler": "Ruler", + "theme": { + "light": "Light (next: dark)", + "dark": "Dark (next: auto)", + "auto": "Auto (next: light)" + } }, "momentjs": { "calendar": { diff --git a/public/locale/fr.json b/public/locale/fr.json index 71945beb..40e76ccc 100644 --- a/public/locale/fr.json +++ b/public/locale/fr.json @@ -65,7 +65,12 @@ "fullscreen": "Vollbildmodus wechseln", "tracking": "Localisation", "location": "Choisir les coordonnées", - "ruler": "Règle" + "ruler": "Règle", + "theme": { + "light": "Clair (suivant : sombre)", + "dark": "Sombre (suivant : auto)", + "auto": "Auto (suivant : clair)" + } }, "momentjs": { "calendar": { diff --git a/public/locale/ru.json b/public/locale/ru.json index 247a0975..eebdd472 100644 --- a/public/locale/ru.json +++ b/public/locale/ru.json @@ -63,7 +63,12 @@ "button": { "switchView": "Переключить вид", "location": "Взять координаты", - "tracking": "Локализация" + "tracking": "Локализация", + "theme": { + "light": "Светлая (далее: тёмная)", + "dark": "Тёмная (далее: авто)", + "auto": "Авто (далее: светлая)" + } }, "momentjs": { "calendar": { diff --git a/public/locale/tr.json b/public/locale/tr.json index 43a0ca01..7cd47387 100644 --- a/public/locale/tr.json +++ b/public/locale/tr.json @@ -63,7 +63,12 @@ "button": { "switchView": "Görünümü Değiştir", "location": "Koordinatları seç", - "tracking": "Yerelleştirme" + "tracking": "Yerelleştirme", + "theme": { + "light": "Açık (sonraki: koyu)", + "dark": "Koyu (sonraki: oto)", + "auto": "Oto (sonraki: açık)" + } }, "momentjs": { "calendar": { diff --git a/scss/modules/_button.scss b/scss/modules/_button.scss index 7f7de877..c60e45c6 100644 --- a/scss/modules/_button.scss +++ b/scss/modules/_button.scss @@ -79,6 +79,16 @@ button { margin: variables.$button-distance; width: auto; } + + &.theme-toggle { + text-align: center; + + svg { + height: 1em; + vertical-align: middle; + width: 1em; + } + } } // Tooltip