Rewild Your Web
web browser dweb
at main 255 lines 7.8 kB view raw
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});