Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import {
4 getAvailableThemes,
5 getTheme,
6 setTheme,
7} from "//shared.localhost:8888/theme.js";
8
9import SearchEngines from "//shared.localhost:8888/search/engines.js";
10
11// Bi-directionial binding between a preference and an element's value.
12class PreferenceBindings {
13 constructor() {
14 navigator.embedder.addEventListener(
15 "preferencechanged",
16 this.onPrefChange.bind(this),
17 );
18
19 this.bindings = {};
20 }
21
22 add(prefName, element) {
23 this.bindings[prefName] = element;
24 element.addEventListener("input", (event) => {
25 console.log(`[PreferenceBinding] ${event.type} for ${prefName}, value is ${element.value}`);
26 navigator.servo.setStringPreference(prefName, element.value);
27 });
28 console.log(`[PreferenceBinding] ${prefName} is ${navigator.servo.getStringPreference(prefName)}`)
29 element.value = navigator.servo.getStringPreference(prefName);
30 }
31
32 addBool(prefName, element) {
33 this.bindings[prefName] = element;
34 element.addEventListener("change", (event) => {
35 console.log(`[PreferenceBinding] ${event.type} for ${prefName}, checked is ${element.checked}`);
36 navigator.servo.setBoolPreference(prefName, element.checked);
37 });
38 console.log(`[PreferenceBinding] ${prefName} is ${navigator.servo.getBoolPreference(prefName)}`)
39 element.checked = navigator.servo.getBoolPreference(prefName);
40 }
41
42 addInt(prefName, element) {
43 this.bindings[prefName] = element;
44 element.addEventListener("input", (event) => {
45 const value = parseInt(element.value, 10);
46 if (!isNaN(value)) {
47 console.log(`[PreferenceBinding] ${event.type} for ${prefName}, value is ${value}`);
48 navigator.servo.setIntPreference(prefName, value);
49 }
50 });
51 console.log(`[PreferenceBinding] ${prefName} is ${navigator.servo.getIntPreference(prefName)}`)
52 element.value = navigator.servo.getIntPreference(prefName);
53 }
54
55 onPrefChange(event) {
56 let element = this.bindings[event.detail.name];
57 if (element) {
58 if (element.type === "checkbox") {
59 element.checked = event.detail.value;
60 } else {
61 element.value = event.detail.value;
62 }
63 }
64 }
65}
66
67// Setup sidebar navigation
68function setupNavigation() {
69 const navItems = document.querySelectorAll(".nav-item");
70
71 navItems.forEach((item) => {
72 item.addEventListener("click", (event) => {
73 event.preventDefault();
74 const categoryId = item.dataset.category;
75
76 // Update active nav item
77 navItems.forEach((nav) => nav.classList.remove("active"));
78 item.classList.add("active");
79
80 // Scroll to category and open its details
81 const category = document.getElementById(categoryId);
82 if (category) {
83 category.scrollIntoView({ behavior: "smooth", block: "start" });
84 const details = category.querySelector("details");
85 if (details) {
86 details.open = true;
87 }
88 }
89 });
90 });
91}
92
93// Mobile drill-down navigation
94function setupMobileNavigation() {
95 const navItems = document.querySelectorAll(".nav-item");
96 const backButton = document.querySelector(".back-button");
97 const mobileTitle = document.querySelector(".mobile-title");
98 const categories = document.querySelectorAll(".settings-category");
99
100 // Check if we're in mobile view
101 function isMobile() {
102 return window.matchMedia("(max-width: 600px)").matches;
103 }
104
105 // Show detail view for a category
106 function showDetail(categoryId) {
107 if (!isMobile()) {
108 return;
109 }
110
111 document.body.classList.add("show-detail");
112
113 // Mark the active category
114 categories.forEach((cat) => {
115 cat.classList.remove("active");
116 if (cat.id === categoryId) {
117 cat.classList.add("active");
118 // Open the details element
119 const details = cat.querySelector("details");
120 if (details) details.open = true;
121 }
122 });
123
124 // Update title
125 const activeNav = document.querySelector(`[data-category="${categoryId}"]`);
126 if (activeNav && mobileTitle) {
127 mobileTitle.textContent = activeNav.querySelector("span").textContent;
128 }
129 }
130
131 // Return to list view
132 function showList() {
133 document.body.classList.remove("show-detail");
134 categories.forEach((cat) => cat.classList.remove("active"));
135 if (mobileTitle) mobileTitle.textContent = "Settings";
136 }
137
138 // Nav item click handler (enhanced for mobile)
139 navItems.forEach((item) => {
140 item.addEventListener("click", () => {
141 if (isMobile()) {
142 showDetail(item.dataset.category);
143 }
144 });
145 });
146
147 // Back button handler
148 if (backButton) {
149 backButton.addEventListener("click", showList);
150 }
151
152 // Handle resize: reset state when switching between mobile/desktop
153 window.addEventListener("resize", () => {
154 if (!isMobile()) {
155 document.body.classList.remove("show-detail");
156 categories.forEach((cat) => cat.classList.remove("active"));
157 }
158 });
159}
160
161// Render theme cards
162function renderThemeCards() {
163 const grid = document.getElementById("theme-grid");
164 if (!grid) {
165 return;
166 }
167
168 const currentTheme = getTheme();
169 const themes = getAvailableThemes();
170
171 grid.innerHTML = themes
172 .map(
173 (theme) => `
174 <div class="theme-card ${theme.id === currentTheme ? "selected" : ""}"
175 data-theme="${theme.id}">
176 <div class="theme-preview preview-${theme.id}">
177 <div class="theme-preview-header"></div>
178 <div class="theme-preview-main">
179 <div class="theme-preview-webview"></div>
180 </div>
181 </div>
182 <div class="theme-info">
183 <div class="theme-name">${theme.name}</div>
184 <div class="theme-description">${theme.description}</div>
185 </div>
186 </div>
187 `,
188 )
189 .join("");
190
191 // Add click handlers
192 grid.querySelectorAll(".theme-card").forEach((card) => {
193 card.addEventListener("click", () => {
194 const themeId = card.dataset.theme;
195 setTheme(themeId);
196
197 // Update selected state
198 grid
199 .querySelectorAll(".theme-card")
200 .forEach((c) => c.classList.remove("selected"));
201 card.classList.add("selected");
202 });
203 });
204}
205
206async function setupSearchEngines() {
207 let engines = new SearchEngines();
208 await engines.ensureReady();
209
210 let list = document.getElementById("search-engine-list");
211 engines.engines.forEach((engine) => {
212 console.log(`Adding search engine: ${engine.name}`);
213 let option = document.createElement("option");
214 option.setAttribute("value", engine.id);
215 if (engine.id == engines.currentEngine.id) {
216 option.setAttribute("selected", true);
217 }
218 option.textContent = engine.name;
219 list.append(option);
220 });
221}
222
223// Initialize
224setupNavigation();
225setupMobileNavigation();
226renderThemeCards();
227setupSearchEngines();
228
229const bindings = new PreferenceBindings();
230
231// Register bindings: [<element id>, <preference name>]
232[
233 ["new-view-url", "browserhtml.start_url"],
234 ["homescreen-url", "browserhtml.homescreen_url"],
235 ["search-engine-list", "browserhtml.search_engine"],
236 ["user-agent", "user_agent"],
237].forEach((item) => {
238 bindings.add(item[1], document.getElementById(item[0]));
239});
240
241// Register boolean bindings: [<element id>, <preference name>]
242[
243 ["virtual-keyboard-toggle", "browserhtml.ime_virtual_keyboard_enabled"],
244 ["mobile-simulation-toggle", "browserhtml.mobile_simulation"],
245 ["devtools-toggle", "devtools_server_enabled"],
246].forEach((item) => {
247 bindings.addBool(item[1], document.getElementById(item[0]));
248});
249
250// Register integer bindings: [<element id>, <preference name>]
251[
252 ["devtools-port", "devtools_server_port"],
253].forEach((item) => {
254 bindings.addInt(item[1], document.getElementById(item[0]));
255});