aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/theme.js
blob: 7382d3ea097cb435c744389271238db9c1cc5115 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Copyright Epic Games, Inc. All Rights Reserved.

// Theme toggle: cycles system → light → dark → system.
// Persists choice in localStorage. Applies data-theme attribute on <html>.

(function() {
	// Wrap localStorage so a single key's get/set/clear all swallow the
	// SecurityError that fires in private-mode / cookies-disabled browsers.
	// `clear` removes the key entirely (used for theme to reset to system
	// preference); `set` stores the raw value passed (callers serialize).
	function safeStorage(key) {
		return {
			get: function() {
				try { return localStorage.getItem(key); } catch (e) { return null; }
			},
			set: function(value) {
				try { localStorage.setItem(key, value); } catch (e) {}
			},
			clear: function() {
				try { localStorage.removeItem(key); } catch (e) {}
			},
		};
	}

	var themeStore = safeStorage('zen-theme');

	function apply(theme) {
		if (theme)
			document.documentElement.setAttribute('data-theme', theme);
		else
			document.documentElement.removeAttribute('data-theme');
	}

	function getEffective(stored) {
		if (stored) return stored;
		return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
	}

	// Apply stored preference immediately (before paint)
	apply(themeStore.get());

	// Wide-mode preference: persisted across sessions, applied before paint
	// so the layout doesn't flash at the default width on reload. Lifts the
	// 1400px #container cap and the body's horizontal padding so the main
	// content fills the viewport edge-to-edge.
	var wideStore = safeStorage('zen-wide');
	function getWide() { return wideStore.get() === 'true'; }
	function setWide(value) {
		if (value) wideStore.set('true');
		else wideStore.clear();
	}
	function applyWide(wide) {
		if (wide) document.documentElement.setAttribute('data-wide', 'true');
		else document.documentElement.removeAttribute('data-wide');
	}
	applyWide(getWide());

	// Double-chevron SVGs for the wide toggle — outward when content is
	// narrow (click to fill the viewport), inward when wide (click to snap
	// back to the 1400px cap). currentColor so button styles tint it.
	var ICON_WIDEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="8 6 2 12 8 18"/><polyline points="16 6 22 12 16 18"/></svg>';
	var ICON_NARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 6 10 12 4 18"/><polyline points="20 6 14 12 20 18"/></svg>';

	// Create toggle button once DOM is ready
	function createToggle() {
		var btn = document.createElement('button');
		btn.id = 'zen_theme_toggle';
		btn.className = 'zen-floating-toggle';
		btn.title = 'Toggle theme';

		function updateIcon() {
			var effective = getEffective(themeStore.get());
			// Show sun in dark mode (click to go light), moon in light mode (click to go dark)
			btn.textContent = effective === 'dark' ? '\u2600' : '\u263E';

			var isManual = themeStore.get() != null;
			btn.title = isManual
				? 'Theme: ' + effective + ' (click to change, double-click for system)'
				: 'Theme: system (click to change)';
		}

		btn.addEventListener('click', function() {
			var effective = getEffective(themeStore.get());
			// Toggle to the opposite
			var next = effective === 'dark' ? 'light' : 'dark';
			themeStore.set(next);
			apply(next);
			updateIcon();
		});

		btn.addEventListener('dblclick', function(e) {
			e.preventDefault();
			// Reset to system preference
			themeStore.clear();
			apply(null);
			updateIcon();
		});

		// Update icon when system preference changes
		window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
			if (!themeStore.get()) updateIcon();
		});

		updateIcon();
		document.body.appendChild(btn);

		// WebSocket pause/play toggle
		var wsStore = safeStorage('zen-ws-paused');
		var wsBtn = document.createElement('button');
		wsBtn.id = 'zen_ws_toggle';
		wsBtn.className = 'zen-floating-toggle';

		var initialPaused = wsStore.get() === 'true';

		function updateWsIcon(paused) {
			wsBtn.dataset.paused = paused ? 'true' : 'false';
			wsBtn.textContent = paused ? '\u25B6' : '\u23F8';
			wsBtn.title = paused ? 'Resume live updates' : 'Pause live updates';
		}

		updateWsIcon(initialPaused);

		// No initial event is dispatched: createToggle runs at (or after)
		// DOMContentLoaded, so any listener gated on DOMContentLoaded would
		// not fire. Page scripts read localStorage('zen-ws-paused') directly
		// for their initial paused state and subscribe to zen-ws-toggle for
		// subsequent transitions.

		wsBtn.addEventListener('click', function() {
			var paused = wsBtn.dataset.paused !== 'true';
			wsStore.set(paused ? 'true' : 'false');
			updateWsIcon(paused);
			document.dispatchEvent(new CustomEvent('zen-ws-toggle', { detail: { paused: paused } }));
		});

		document.body.appendChild(wsBtn);

		// Wide-mode toggle. Sits to the left of the pause and theme toggles.
		var wideBtn = document.createElement('button');
		wideBtn.id = 'zen_wide_toggle';
		wideBtn.className = 'zen-floating-toggle';

		function updateWideIcon(wide) {
			wideBtn.dataset.wide = wide ? 'true' : 'false';
			wideBtn.innerHTML = wide ? ICON_NARROW : ICON_WIDEN;
			wideBtn.title = wide ? 'Narrow the main content' : 'Fill the viewport width';
			wideBtn.setAttribute('aria-label', wide ? 'Narrow content' : 'Widen content');
		}

		updateWideIcon(getWide());

		wideBtn.addEventListener('click', function() {
			var wide = !getWide();
			setWide(wide);
			applyWide(wide);
			updateWideIcon(wide);
		});

		document.body.appendChild(wideBtn);
	}

	if (document.readyState === 'loading')
		document.addEventListener('DOMContentLoaded', createToggle);
	else
		createToggle();
})();