@axis-network/butter-col 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/bin/butter-col.js +4 -0
- package/dist/bootstrap.html +67 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +7 -0
- package/dist/controller.mjs +2 -0
- package/dist/controller.mjs.map +7 -0
- package/dist/formatter.mjs +2 -0
- package/dist/formatter.mjs.map +7 -0
- package/dist/sw.js +30 -0
- package/dist/sw.js.map +7 -0
- package/dist/transports/butter.js +2 -0
- package/dist/transports/butter.js.map +7 -0
- package/dist/types/controller/index.d.ts +65 -0
- package/dist/types/formatter/codecs.d.ts +17 -0
- package/dist/types/formatter/css.d.ts +10 -0
- package/dist/types/formatter/html.d.ts +61 -0
- package/dist/types/formatter/index.d.ts +77 -0
- package/dist/types/formatter/js.d.ts +27 -0
- package/dist/types/formatter/url.d.ts +40 -0
- package/lib/cli.js +257 -0
- package/lib/index.d.ts +68 -0
- package/lib/index.js +166 -0
- package/package.json +77 -4
- package/server/conf/butter-proxy.conf.template +29 -0
- package/server/conf/mime.types +19 -0
- package/server/conf/nginx.conf.template +152 -0
- package/server/config.lua +34 -0
- package/server/lua/butter/config.lua +160 -0
- package/server/lua/butter/cookiejar.lua +197 -0
- package/server/lua/butter/dns.lua +67 -0
- package/server/lua/butter/guard.lua +78 -0
- package/server/lua/butter/headers.lua +83 -0
- package/server/lua/butter/init.lua +18 -0
- package/server/lua/butter/ip.lua +165 -0
- package/server/lua/butter/limit.lua +18 -0
- package/server/lua/butter/session.lua +42 -0
- package/server/lua/butter/store.lua +77 -0
- package/server/lua/butter/transport.lua +248 -0
- package/server/lua/butter/url.lua +78 -0
- package/site/app.css +373 -0
- package/site/app.js +335 -0
- package/site/butter-col.png +0 -0
- package/site/index.html +139 -0
package/site/app.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// butter-col frontend: a launcher plus an in-page browser around the
|
|
2
|
+
// proxied site. Everything proxy-related goes through ButterController.
|
|
3
|
+
import { ButterController } from "/butter/controller.mjs";
|
|
4
|
+
|
|
5
|
+
const $ = (selector) => document.querySelector(selector);
|
|
6
|
+
|
|
7
|
+
// ---- preferences (per browser, best effort) ----------------------------------
|
|
8
|
+
|
|
9
|
+
const PREFS_KEY = "butter-col:frontend";
|
|
10
|
+
const defaults = { searchEngine: "https://duckduckgo.com/?q=%s", theme: "system", recent: [] };
|
|
11
|
+
|
|
12
|
+
function loadPrefs() {
|
|
13
|
+
try {
|
|
14
|
+
return { ...defaults, ...JSON.parse(localStorage.getItem(PREFS_KEY) || "{}") };
|
|
15
|
+
} catch {
|
|
16
|
+
return { ...defaults };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const prefs = loadPrefs();
|
|
20
|
+
function savePrefs() {
|
|
21
|
+
try {
|
|
22
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
|
23
|
+
} catch {
|
|
24
|
+
// storage unavailable (private mode): preferences last for this visit only
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function applyTheme() {
|
|
29
|
+
document.documentElement.dataset.theme = prefs.theme;
|
|
30
|
+
}
|
|
31
|
+
applyTheme();
|
|
32
|
+
|
|
33
|
+
// ---- elements ---------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const home = $("#home");
|
|
36
|
+
const browser = $("#browser");
|
|
37
|
+
const frame = $("#frame");
|
|
38
|
+
const homeForm = $("#home-form");
|
|
39
|
+
const homeInput = $("#home-input");
|
|
40
|
+
const homeNotice = $("#home-notice");
|
|
41
|
+
const addressForm = $("#address-form");
|
|
42
|
+
const addressInput = $("#address-input");
|
|
43
|
+
const addressLock = $("#address-lock");
|
|
44
|
+
const progress = $("#progress");
|
|
45
|
+
const settings = $("#settings");
|
|
46
|
+
const settingsNotice = $("#settings-notice");
|
|
47
|
+
const transportSelect = $("#setting-transport");
|
|
48
|
+
const transportName = $("#transport-name");
|
|
49
|
+
|
|
50
|
+
function showNotice(element, text, kind = "error") {
|
|
51
|
+
element.textContent = text;
|
|
52
|
+
element.dataset.kind = kind;
|
|
53
|
+
element.hidden = !text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- butter-col ------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const butter = new ButterController({ searchEngine: prefs.searchEngine });
|
|
59
|
+
const ready = butter.init().then(refreshTransport, (err) => {
|
|
60
|
+
showNotice(homeNotice, explain(err));
|
|
61
|
+
homeForm.querySelector("button").disabled = true;
|
|
62
|
+
transportName.textContent = "nothing yet";
|
|
63
|
+
throw err;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function explain(err) {
|
|
67
|
+
const message = String((err && err.message) || err);
|
|
68
|
+
if (/no service workers/i.test(message)) {
|
|
69
|
+
return "This browser has service workers turned off, so butter-col can't run here. Private windows often turn them off; try a normal window.";
|
|
70
|
+
}
|
|
71
|
+
if (/HTTPS/i.test(message)) {
|
|
72
|
+
return "butter-col needs a secure connection. Open this page over https:// (or on localhost).";
|
|
73
|
+
}
|
|
74
|
+
return message;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function refreshTransport() {
|
|
78
|
+
const { active, available } = await butter.getTransport();
|
|
79
|
+
transportName.textContent = active.name;
|
|
80
|
+
transportSelect.replaceChildren(
|
|
81
|
+
...available.map((t) => {
|
|
82
|
+
const option = new Option(t.loaded ? t.name : `${t.name} (didn't load)`, t.name, false, t.name === active.name);
|
|
83
|
+
option.disabled = !t.loaded;
|
|
84
|
+
return option;
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
transportSelect.disabled = available.length < 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- recent sites ---------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
function remember(url, title) {
|
|
93
|
+
let host;
|
|
94
|
+
try {
|
|
95
|
+
host = new URL(url).hostname;
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
prefs.recent = [{ url, title: title || host, host }, ...prefs.recent.filter((r) => r.url !== url)].slice(0, 8);
|
|
100
|
+
savePrefs();
|
|
101
|
+
renderRecent();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderRecent() {
|
|
105
|
+
const list = $("#recent-list");
|
|
106
|
+
list.replaceChildren(
|
|
107
|
+
...prefs.recent.map((item) => {
|
|
108
|
+
const li = document.createElement("li");
|
|
109
|
+
const button = document.createElement("button");
|
|
110
|
+
button.type = "button";
|
|
111
|
+
button.addEventListener("click", () => open(item.url));
|
|
112
|
+
const badge = document.createElement("span");
|
|
113
|
+
badge.className = "recent-badge";
|
|
114
|
+
badge.textContent = item.host.replace(/^www\./, "").charAt(0) || "?";
|
|
115
|
+
const title = document.createElement("span");
|
|
116
|
+
title.className = "recent-title";
|
|
117
|
+
title.textContent = item.title;
|
|
118
|
+
const host = document.createElement("span");
|
|
119
|
+
host.className = "recent-host";
|
|
120
|
+
host.textContent = item.host;
|
|
121
|
+
button.append(badge, title, host);
|
|
122
|
+
li.append(button);
|
|
123
|
+
return li;
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
$("#recent-empty").hidden = prefs.recent.length > 0;
|
|
127
|
+
}
|
|
128
|
+
renderRecent();
|
|
129
|
+
|
|
130
|
+
// ---- browsing ---------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
let current = ""; // target URL currently shown in the frame
|
|
133
|
+
let watcher = 0;
|
|
134
|
+
|
|
135
|
+
function setLoading(loading) {
|
|
136
|
+
progress.dataset.state = loading ? "loading" : "done";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function showAddress(url) {
|
|
140
|
+
current = url;
|
|
141
|
+
if (document.activeElement !== addressInput) addressInput.value = url;
|
|
142
|
+
const insecure = url.startsWith("http:");
|
|
143
|
+
addressLock.toggleAttribute("data-insecure", insecure);
|
|
144
|
+
addressLock.setAttribute("aria-label", insecure ? "Not encrypted" : "Encrypted");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function open(input) {
|
|
148
|
+
const value = String(input || "").trim();
|
|
149
|
+
if (!value) return;
|
|
150
|
+
try {
|
|
151
|
+
await ready;
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const url = butter.resolveInput(value);
|
|
156
|
+
home.hidden = true;
|
|
157
|
+
browser.hidden = false;
|
|
158
|
+
showAddress(url);
|
|
159
|
+
setLoading(true);
|
|
160
|
+
frame.src = butter.encodeUrl(url);
|
|
161
|
+
history.replaceState(null, "", "#" + encodeURIComponent(url));
|
|
162
|
+
if (!watcher) watcher = setInterval(watchFrame, 500);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function goHome() {
|
|
166
|
+
clearInterval(watcher);
|
|
167
|
+
watcher = 0;
|
|
168
|
+
frame.src = "about:blank";
|
|
169
|
+
current = "";
|
|
170
|
+
browser.hidden = true;
|
|
171
|
+
home.hidden = false;
|
|
172
|
+
document.title = "butter-col";
|
|
173
|
+
history.replaceState(null, "", location.pathname + location.search);
|
|
174
|
+
homeInput.select();
|
|
175
|
+
homeInput.focus();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The proxied page's real URL, or null while it isn't readable. */
|
|
179
|
+
function frameUrl() {
|
|
180
|
+
try {
|
|
181
|
+
const href = frame.contentWindow.location.href;
|
|
182
|
+
if (!href || href === "about:blank") return null;
|
|
183
|
+
const target = butter.decodeUrl(href);
|
|
184
|
+
return target === href ? null : target;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Single-page apps change their URL without loading: keep the address bar in step.
|
|
191
|
+
function watchFrame() {
|
|
192
|
+
const url = frameUrl();
|
|
193
|
+
if (url && url !== current) {
|
|
194
|
+
showAddress(url);
|
|
195
|
+
history.replaceState(null, "", "#" + encodeURIComponent(url));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
frame.addEventListener("load", () => {
|
|
200
|
+
setLoading(false);
|
|
201
|
+
const url = frameUrl();
|
|
202
|
+
if (!url) return;
|
|
203
|
+
showAddress(url);
|
|
204
|
+
let title = "";
|
|
205
|
+
try {
|
|
206
|
+
title = frame.contentDocument.title;
|
|
207
|
+
// Show the progress bar again as soon as the page starts leaving.
|
|
208
|
+
frame.contentWindow.addEventListener("pagehide", () => setLoading(true), { once: true });
|
|
209
|
+
} catch {
|
|
210
|
+
// not readable
|
|
211
|
+
}
|
|
212
|
+
document.title = title ? `${title} - butter-col` : "butter-col";
|
|
213
|
+
remember(url, title);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---- controls -----------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
homeForm.addEventListener("submit", (event) => {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
open(homeInput.value);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
addressForm.addEventListener("submit", (event) => {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
addressInput.blur();
|
|
226
|
+
open(addressInput.value);
|
|
227
|
+
});
|
|
228
|
+
addressInput.addEventListener("focus", () => addressInput.select());
|
|
229
|
+
addressInput.addEventListener("keydown", (event) => {
|
|
230
|
+
if (event.key === "Escape") {
|
|
231
|
+
addressInput.value = current;
|
|
232
|
+
addressInput.blur();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
function openBlankTab() {
|
|
237
|
+
const tab = window.open("about:blank", "_blank");
|
|
238
|
+
if (!tab) {
|
|
239
|
+
showNotice(settingsNotice, "Your browser blocked the new tab. Allow pop-ups for this site and try again.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const doc = tab.document;
|
|
243
|
+
doc.title = "about:blank";
|
|
244
|
+
const style = doc.createElement("style");
|
|
245
|
+
style.textContent = "html,body{margin:0;height:100%;overflow:hidden}iframe{border:0;width:100%;height:100%}";
|
|
246
|
+
const embed = doc.createElement("iframe");
|
|
247
|
+
embed.src = location.href;
|
|
248
|
+
embed.allow = "fullscreen; clipboard-read; clipboard-write; autoplay; picture-in-picture";
|
|
249
|
+
doc.head.append(style);
|
|
250
|
+
doc.body.append(embed);
|
|
251
|
+
showNotice(settingsNotice, "Opened in a new about:blank tab.", "info");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const actions = {
|
|
255
|
+
back: () => frame.contentWindow?.history.back(),
|
|
256
|
+
forward: () => frame.contentWindow?.history.forward(),
|
|
257
|
+
reload: () => {
|
|
258
|
+
setLoading(true);
|
|
259
|
+
try {
|
|
260
|
+
frame.contentWindow.location.reload();
|
|
261
|
+
} catch {
|
|
262
|
+
frame.src = frame.src;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
home: goHome,
|
|
266
|
+
fullscreen: () => frame.requestFullscreen?.().catch(() => {}),
|
|
267
|
+
popout: () => {
|
|
268
|
+
if (current) window.open(butter.encodeUrl(current), "_blank", "noopener");
|
|
269
|
+
},
|
|
270
|
+
settings: () => {
|
|
271
|
+
showNotice(settingsNotice, "");
|
|
272
|
+
refreshTransport().catch(() => {});
|
|
273
|
+
settings.showModal();
|
|
274
|
+
},
|
|
275
|
+
blank: openBlankTab,
|
|
276
|
+
"clear-recent": () => {
|
|
277
|
+
prefs.recent = [];
|
|
278
|
+
savePrefs();
|
|
279
|
+
renderRecent();
|
|
280
|
+
showNotice(settingsNotice, "Recent sites cleared.", "info");
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
document.addEventListener("click", (event) => {
|
|
285
|
+
const button = event.target.closest("[data-action]");
|
|
286
|
+
if (button && actions[button.dataset.action]) actions[button.dataset.action]();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Settings
|
|
290
|
+
const searchSelect = $("#setting-search");
|
|
291
|
+
const themeSelect = $("#setting-theme");
|
|
292
|
+
searchSelect.value = prefs.searchEngine;
|
|
293
|
+
themeSelect.value = prefs.theme;
|
|
294
|
+
|
|
295
|
+
searchSelect.addEventListener("change", () => {
|
|
296
|
+
prefs.searchEngine = searchSelect.value;
|
|
297
|
+
butter.options.searchEngine = searchSelect.value;
|
|
298
|
+
savePrefs();
|
|
299
|
+
});
|
|
300
|
+
themeSelect.addEventListener("change", () => {
|
|
301
|
+
prefs.theme = themeSelect.value;
|
|
302
|
+
savePrefs();
|
|
303
|
+
applyTheme();
|
|
304
|
+
});
|
|
305
|
+
transportSelect.addEventListener("change", async () => {
|
|
306
|
+
try {
|
|
307
|
+
const active = await butter.setTransport(transportSelect.value);
|
|
308
|
+
transportName.textContent = active.name;
|
|
309
|
+
showNotice(settingsNotice, `Now browsing through ${active.name}. Pages already open switch when they reload.`, "info");
|
|
310
|
+
} catch (err) {
|
|
311
|
+
showNotice(settingsNotice, String((err && err.message) || err));
|
|
312
|
+
refreshTransport().catch(() => {});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Keyboard: "/" focuses the search box at home; Ctrl/Cmd+L or F6 the address bar.
|
|
317
|
+
document.addEventListener("keydown", (event) => {
|
|
318
|
+
const typing = event.target.closest?.("input, select, textarea");
|
|
319
|
+
if (!browser.hidden && (event.key === "F6" || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "l"))) {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
addressInput.focus();
|
|
322
|
+
} else if (browser.hidden && event.key === "/" && !typing) {
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
homeInput.focus();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Reopen what was open before a reload (#<url>).
|
|
329
|
+
if (location.hash.length > 1) {
|
|
330
|
+
try {
|
|
331
|
+
open(decodeURIComponent(location.hash.slice(1)));
|
|
332
|
+
} catch {
|
|
333
|
+
// malformed hash: stay home
|
|
334
|
+
}
|
|
335
|
+
}
|
|
Binary file
|
package/site/index.html
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="system">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>butter-col</title>
|
|
7
|
+
<meta name="description" content="Browse any site through butter-col.">
|
|
8
|
+
<meta name="color-scheme" content="dark light">
|
|
9
|
+
<link rel="icon" type="image/png" href="butter-col.png">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&family=Fredoka:wght@500;600;700&display=swap">
|
|
13
|
+
<link rel="stylesheet" href="app.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
|
|
17
|
+
<!-- Home: the launcher -->
|
|
18
|
+
<main id="home" class="home">
|
|
19
|
+
<header class="home-bar">
|
|
20
|
+
<button class="icon-button" type="button" data-action="settings" aria-label="Settings" title="Settings">
|
|
21
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.6-1.1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg>
|
|
22
|
+
</button>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<section class="hero" aria-labelledby="hero-title">
|
|
26
|
+
<h1 id="hero-title" class="visually-hidden">butter-col</h1>
|
|
27
|
+
<img class="hero-logo" src="butter-col.png" alt="butter-col" width="694" height="359">
|
|
28
|
+
|
|
29
|
+
<form class="stick" id="home-form" autocomplete="off" role="search">
|
|
30
|
+
<label class="visually-hidden" for="home-input">Search or type an address</label>
|
|
31
|
+
<input id="home-input" class="stick-input" type="text" inputmode="url" enterkeyhint="go" spellcheck="false"
|
|
32
|
+
placeholder="Search or type an address" autofocus>
|
|
33
|
+
<button class="stick-go" type="submit">Go</button>
|
|
34
|
+
</form>
|
|
35
|
+
|
|
36
|
+
<p class="transport-line">
|
|
37
|
+
Browsing through <strong id="transport-name">…</strong>
|
|
38
|
+
<button class="text-button" type="button" data-action="settings">Change</button>
|
|
39
|
+
</p>
|
|
40
|
+
<p class="notice" id="home-notice" role="alert" hidden></p>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<section class="recent" aria-labelledby="recent-title">
|
|
44
|
+
<h2 id="recent-title">Recent</h2>
|
|
45
|
+
<ol class="recent-list" id="recent-list"></ol>
|
|
46
|
+
<p class="recent-empty" id="recent-empty">Sites you visit show up here.</p>
|
|
47
|
+
</section>
|
|
48
|
+
</main>
|
|
49
|
+
|
|
50
|
+
<!-- Browser: the toolbar and the proxied page -->
|
|
51
|
+
<div id="browser" class="browser" hidden>
|
|
52
|
+
<nav class="toolbar" aria-label="Page controls">
|
|
53
|
+
<button class="icon-button" type="button" data-action="back" aria-label="Back" title="Back">
|
|
54
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
|
|
55
|
+
</button>
|
|
56
|
+
<button class="icon-button" type="button" data-action="forward" aria-label="Forward" title="Forward">
|
|
57
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
58
|
+
</button>
|
|
59
|
+
<button class="icon-button" type="button" data-action="reload" aria-label="Reload" title="Reload">
|
|
60
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 4v7h-7"/></svg>
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<form class="address" id="address-form" autocomplete="off" role="search">
|
|
64
|
+
<label class="visually-hidden" for="address-input">Address</label>
|
|
65
|
+
<svg class="address-lock" id="address-lock" viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>
|
|
66
|
+
<input id="address-input" class="address-input" type="text" inputmode="url" enterkeyhint="go" spellcheck="false">
|
|
67
|
+
</form>
|
|
68
|
+
|
|
69
|
+
<button class="icon-button" type="button" data-action="home" aria-label="Home" title="Home">
|
|
70
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11l8-7 8 7"/><path d="M6 10v10h12V10"/></svg>
|
|
71
|
+
</button>
|
|
72
|
+
<button class="icon-button" type="button" data-action="fullscreen" aria-label="Full screen" title="Full screen">
|
|
73
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>
|
|
74
|
+
</button>
|
|
75
|
+
<button class="icon-button" type="button" data-action="popout" aria-label="Open in a new tab" title="Open in a new tab">
|
|
76
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6"/><path d="M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>
|
|
77
|
+
</button>
|
|
78
|
+
<button class="icon-button" type="button" data-action="settings" aria-label="Settings" title="Settings">
|
|
79
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg>
|
|
80
|
+
</button>
|
|
81
|
+
<div class="progress" id="progress" aria-hidden="true"></div>
|
|
82
|
+
</nav>
|
|
83
|
+
<iframe id="frame" class="frame" title="Proxied page" allow="fullscreen; clipboard-read; clipboard-write; autoplay; picture-in-picture"></iframe>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Settings -->
|
|
87
|
+
<dialog id="settings" class="settings" aria-labelledby="settings-title">
|
|
88
|
+
<form method="dialog" class="settings-body">
|
|
89
|
+
<header class="settings-head">
|
|
90
|
+
<h2 id="settings-title">Settings</h2>
|
|
91
|
+
<button class="icon-button" value="close" aria-label="Close settings" title="Close">
|
|
92
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
|
93
|
+
</button>
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
<div class="field">
|
|
97
|
+
<label for="setting-transport">Transport</label>
|
|
98
|
+
<select id="setting-transport"></select>
|
|
99
|
+
<p class="hint" id="transport-hint">How pages reach the internet. Switching may sign you out of sites.</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="field">
|
|
103
|
+
<label for="setting-search">Search with</label>
|
|
104
|
+
<select id="setting-search">
|
|
105
|
+
<option value="https://duckduckgo.com/?q=%s">DuckDuckGo</option>
|
|
106
|
+
<option value="https://www.google.com/search?q=%s">Google</option>
|
|
107
|
+
<option value="https://www.bing.com/search?q=%s">Bing</option>
|
|
108
|
+
<option value="https://search.brave.com/search?q=%s">Brave Search</option>
|
|
109
|
+
<option value="https://www.startpage.com/do/search?q=%s">Startpage</option>
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="field">
|
|
114
|
+
<label for="setting-theme">Theme</label>
|
|
115
|
+
<select id="setting-theme">
|
|
116
|
+
<option value="system">Match my device</option>
|
|
117
|
+
<option value="dark">Dark</option>
|
|
118
|
+
<option value="light">Light</option>
|
|
119
|
+
</select>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="field">
|
|
123
|
+
<span class="field-label">Private tab</span>
|
|
124
|
+
<button class="secondary-button" type="button" data-action="blank">Open butter-col in an about:blank tab</button>
|
|
125
|
+
<p class="hint">The new tab's address stays about:blank, so the sites you browse there stay out of your history.</p>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="field">
|
|
129
|
+
<span class="field-label">Recent sites</span>
|
|
130
|
+
<button class="secondary-button" type="button" data-action="clear-recent">Clear recent sites</button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<p class="notice" id="settings-notice" role="status" hidden></p>
|
|
134
|
+
</form>
|
|
135
|
+
</dialog>
|
|
136
|
+
|
|
137
|
+
<script type="module" src="app.js"></script>
|
|
138
|
+
</body>
|
|
139
|
+
</html>
|