Rewild Your Web
web browser dweb
at main 200 lines 5.3 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3// DOM elements 4const bookmarksGrid = document.getElementById("bookmarks-grid"); 5const resultsArea = document.getElementById("results-area"); 6const resultsList = document.getElementById("results-list"); 7const searchInput = document.getElementById("search-input"); 8const widgetsArea = document.getElementById("widgets-area"); 9 10// Load widgets from JSON 11async function loadWidgets() { 12 try { 13 const { default: widgets } = await import("./resources/widgets.json", { 14 with: { type: "json" }, 15 }); 16 renderWidgets(widgets); 17 } catch (e) { 18 console.log("[Homescreen] No widgets configured"); 19 } 20} 21 22// Render widgets as iframes 23function renderWidgets(widgets) { 24 for (const widget of widgets) { 25 const iframe = document.createElement("iframe"); 26 iframe.className = "widget-frame"; 27 iframe.src = widget.url; 28 iframe.style.width = widget.width || "100%"; 29 iframe.style.height = widget.height || "100px"; 30 widgetsArea.appendChild(iframe); 31 } 32} 33 34// Load bookmarks from JSON 35async function loadBookmarks() { 36 try { 37 const { default: bookmarks } = await import("./resources/bookmarks.json", { 38 with: { type: "json" }, 39 }); 40 renderBookmarks(bookmarks); 41 } catch (e) { 42 console.error("[Homescreen] Failed to load bookmarks:", e); 43 } 44} 45 46// Render bookmarks to the grid 47function renderBookmarks(bookmarks) { 48 bookmarksGrid.innerHTML = ""; 49 50 for (const bookmark of bookmarks) { 51 const item = document.createElement("div"); 52 item.className = "bookmark-item"; 53 item.innerHTML = ` 54 <div class="bookmark-icon"> 55 <img src="resources/${bookmark.icon}" alt="" onerror="this.style.display='none'" /> 56 </div> 57 <span class="bookmark-title">${bookmark.title}</span> 58 `; 59 // TODO: replace with <a target="_blank"> 60 item.addEventListener("click", () => { 61 navigate(bookmark.url); 62 }); 63 bookmarksGrid.appendChild(item); 64 } 65} 66 67// Reset search UI to initial state 68function resetSearchState() { 69 searchInput.value = ""; 70 searchInput.blur(); 71 document.body.classList.remove("searching"); 72 controller.clear(); 73} 74 75// Navigate to a URL 76function navigate(url) { 77 resetSearchState(); 78 window.open(url, "_blank"); 79} 80 81class Controller { 82 constructor() { 83 this.inner = null; 84 } 85 86 async ensureController() { 87 if (this.inner) { 88 return; 89 } 90 91 let mod = await import("//shared.localhost:8888/search/controller.js"); 92 this.inner = new mod.SearchController({ 93 onNavigate: navigate, 94 onResultsChanged: renderResults, 95 debounceDelay: 150, 96 }); 97 } 98 99 async handleResultClick(result) { 100 await this.ensureController(); 101 this.inner.handleResultClick(result); 102 } 103 104 async handleSubmit(data) { 105 await this.ensureController(); 106 this.inner.handleSubmit(data); 107 } 108 109 async query(query) { 110 await this.ensureController(); 111 this.inner.query(query); 112 } 113 114 async clear() { 115 await this.ensureController(); 116 this.inner.clear(); 117 } 118} 119 120// Initialize search controller 121const controller = new Controller(); 122 123// Render search results (adapted from new_view.js) 124function renderResults(results, groups) { 125 resultsList.innerHTML = ""; 126 127 if (results.length === 0) { 128 return; 129 } 130 131 for (const group of groups) { 132 const groupDiv = document.createElement("div"); 133 groupDiv.className = "result-group"; 134 135 // Icon column 136 const iconDiv = document.createElement("div"); 137 iconDiv.className = "result-group-icon"; 138 if (group.providerIcon) { 139 const icon = document.createElement("lucide-icon"); 140 icon.setAttribute("name", group.providerIcon); 141 iconDiv.appendChild(icon); 142 } 143 groupDiv.appendChild(iconDiv); 144 145 // Items column 146 const itemsDiv = document.createElement("div"); 147 itemsDiv.className = "result-group-items"; 148 149 for (const result of group.items) { 150 const itemDiv = document.createElement("div"); 151 itemDiv.className = "result-item"; 152 itemDiv.dataset.kind = result.kind; 153 154 if (result.kind === "link" || result.kind === "webview") { 155 const link = document.createElement("a"); 156 link.href = result.kind === "link" ? result.value.url : "#"; 157 link.className = "result-link"; 158 link.textContent = result.value.title; 159 link.addEventListener("click", (e) => { 160 e.preventDefault(); 161 controller.handleResultClick(result); 162 }); 163 itemDiv.appendChild(link); 164 } else if (result.kind === "text") { 165 const text = document.createElement("span"); 166 text.className = "result-text"; 167 text.textContent = result.value; 168 itemDiv.appendChild(text); 169 } 170 171 itemsDiv.appendChild(itemDiv); 172 } 173 174 groupDiv.appendChild(itemsDiv); 175 resultsList.appendChild(groupDiv); 176 } 177} 178 179// Event listeners 180searchInput.addEventListener("input", () => { 181 const query = searchInput.value.trim(); 182 if (query) { 183 document.body.classList.add("searching"); 184 controller.query(query); 185 } else { 186 document.body.classList.remove("searching"); 187 controller.clear(); 188 } 189}); 190 191searchInput.addEventListener("keydown", (e) => { 192 if (e.key === "Enter") { 193 e.preventDefault(); 194 controller.handleSubmit(searchInput.value); 195 } 196}); 197 198// Initialize 199loadWidgets(); 200loadBookmarks();