Rewild Your Web
web
browser
dweb
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();