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