Network & Internet
Wi-Fi
Status: Disconnected
Current connection
Network: β
IP: β Β· Signal: β
Troubleshoot
Simulated networking (browser-safe). Connect/disconnect works within GingerOS UI.
Personalization
Theme
More theme options will be wired up in JS.
Apps
Installed apps
Ginger Browser Β· File Explorer Β· Notes Β· Settings
About GingerOS
GingerOS
Windows-10-style desktop shell for the web.
Version: 0.1.0
Desktop
This is a UI shell (no real filesystem access in the browser).
Welcome
Double-click icons, drag windows, use taskbar & tray.
Ginger Browser
This is a simulated browser tab. In JS, the address bar can load a few built-in pages (no cross-origin fetch by default).
Tip: Try opening Settings and connect to Wi-Fi.
Autosave will be wired up in JS.
GingerOS
A Windows-10-style web desktop
Everything is simulated in-browser. Networking is a UI model: you can scan networks, connect, disconnect, and show status.
Build: ginger-2026.02
Shell: Explorer-like
or inline after the HTML.
*/
(() => {
/* =========================
HELPERS
========================== */
const qs = (s, p = document) => p.querySelector(s);
const qsa = (s, p = document) => [...p.querySelectorAll(s)];
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
const STORAGE_KEYS = {
NOTES: "ginger_notes",
WIFI_PROFILES: "ginger_wifi_profiles",
WIFI_ENABLED: "ginger_wifi_enabled",
WIFI_LAST: "ginger_wifi_last",
BROWSER_STATE: "ginger_browser_state"
};
/* =========================
STATE
========================== */
const state = {
z: 100,
windows: {}, // { id: { el, minimized, maximized, prevRect } }
activeWinId: null,
notes: {
content: localStorage.getItem(STORAGE_KEYS.NOTES) || ""
},
wifi: {
enabled: (localStorage.getItem(STORAGE_KEYS.WIFI_ENABLED) || "true") === "true",
selected: null,
connected: null,
connecting: false,
autoConnect: false,
networks: [],
profiles: safeParseJSON(localStorage.getItem(STORAGE_KEYS.WIFI_PROFILES), {}) // {ssid:{password,auto}}
},
browser: {
history: [],
index: -1,
current: null
}
};
function safeParseJSON(raw, fallback) {
try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; }
}
function saveWifiProfiles() {
localStorage.setItem(STORAGE_KEYS.WIFI_PROFILES, JSON.stringify(state.wifi.profiles));
}
function saveWifiEnabled() {
localStorage.setItem(STORAGE_KEYS.WIFI_ENABLED, String(state.wifi.enabled));
}
/* =========================
ROOT ELEMENTS
========================== */
const overlay = qs("#overlay");
const startMenu = qs("#start-menu");
const actionCenter = qs("#action-center");
const wifiFlyout = qs("#wifi-flyout");
const desktop = qs("#desktop");
const btnStart = qs("#btn-start");
const trayWifiBtn = qs('[data-tray="wifi"]');
const trayActionBtn = qs('[data-tray="action"]');
/* =========================
INIT
========================== */
document.addEventListener("DOMContentLoaded", init);
function init() {
registerWindows();
bindOpeners();
bindStartMenu();
bindActionCenter();
bindWifi();
bindDesktopContextMenu();
bindGlobalShortcuts();
initClock();
initNotes();
initBrowser();
fakeScanNetworks();
restoreWifiLastConnection();
updateWifiUI();
}
/* =========================
WINDOWS: open/close/min/max, focus, drag
========================== */
function registerWindows() {
qsa(".win").forEach(win => {
const id = win.dataset.app;
state.windows[id] = { el: win, minimized: false, maximized: false, prevRect: null };
// baseline z-index
win.style.zIndex = ++state.z;
// Focus on click
win.addEventListener("mousedown", () => activateWindow(id));
// Controls
const closeBtn = qs("[data-win-close]", win);
const minBtn = qs("[data-win-min]", win);
const maxBtn = qs("[data-win-max]", win);
closeBtn?.addEventListener("click", () => closeWindow(id));
minBtn?.addEventListener("click", () => minimizeWindow(id));
maxBtn?.addEventListener("click", () => toggleMaximize(id));
makeDraggable(win, id);
});
}
function openWindow(id) {
const w = state.windows[id];
if (!w) return;
w.el.classList.remove("hidden");
w.minimized = false;
activateWindow(id);
}
function closeWindow(id) {
const w = state.windows[id];
if (!w) return;
w.el.classList.add("hidden");
w.minimized = false;
if (state.activeWinId === id) state.activeWinId = null;
}
function minimizeWindow(id) {
const w = state.windows[id];
if (!w) return;
w.el.classList.add("hidden");
w.minimized = true;
if (state.activeWinId === id) state.activeWinId = null;
}
function toggleMaximize(id) {
const w = state.windows[id];
if (!w) return;
w.maximized = !w.maximized;
if (w.maximized) {
// β
FIX: store actual computed position/size reliably
w.prevRect = {
top: w.el.offsetTop + "px",
left: w.el.offsetLeft + "px",
width: w.el.offsetWidth + "px",
height: w.el.offsetHeight + "px"
};
w.el.style.top = "0px";
w.el.style.left = "0px";
w.el.style.width = "100%";
w.el.style.height = "calc(100% - 44px)";
} else if (w.prevRect) {
Object.assign(w.el.style, w.prevRect);
}
}
function activateWindow(id) {
const w = state.windows[id];
if (!w) return;
qsa(".win").forEach(x => x.classList.remove("active"));
w.el.classList.add("active");
w.el.style.zIndex = ++state.z;
state.activeWinId = id;
}
function makeDraggable(win, id) {
const bar = qs("[data-drag-handle]", win);
if (!bar) return;
let dragging = false;
let offsetX = 0;
let offsetY = 0;
bar.addEventListener("mousedown", e => {
const w = state.windows[id];
if (!w || w.maximized) return;
dragging = true;
activateWindow(id);
offsetX = e.clientX - win.offsetLeft;
offsetY = e.clientY - win.offsetTop;
});
document.addEventListener("mousemove", e => {
if (!dragging) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const maxX = window.innerWidth - 120;
const maxY = window.innerHeight - 120 - 44; // taskbar
win.style.left = clamp(x, -80, maxX) + "px";
win.style.top = clamp(y, 0, maxY) + "px";
});
document.addEventListener("mouseup", () => (dragging = false));
// Double-click titlebar to maximize/restore
bar.addEventListener("dblclick", () => toggleMaximize(id));
}
/* =========================
OPENERS (desktop icons, pins, start items)
========================== */
function bindOpeners() {
// Anything with data-open opens window and closes overlays
qsa("[data-open]").forEach(btn => {
btn.addEventListener("click", () => {
openWindow(btn.dataset.open);
closeAllMenus();
});
btn.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
btn.click();
}
});
});
// β
FIX: proper double-click for desktop icons (no recursion)
qsa(".desk-icon").forEach(icon => {
icon.addEventListener("dblclick", () => {
const id = icon.dataset.open;
if (id) {
openWindow(id);
closeAllMenus();
}
});
});
}
/* =========================
MENUS / OVERLAY
========================== */
function toggleMenu(el) {
const isOpen = el.classList.contains("show");
closeAllMenus();
if (!isOpen) {
el.classList.add("show");
overlay.classList.add("show");
updateAriaExpanded();
}
}
function closeAllMenus() {
[startMenu, actionCenter, wifiFlyout].forEach(m => m.classList.remove("show"));
overlay.classList.remove("show");
updateAriaExpanded();
}
function updateAriaExpanded() {
btnStart?.setAttribute("aria-expanded", startMenu.classList.contains("show") ? "true" : "false");
trayWifiBtn?.setAttribute("aria-expanded", wifiFlyout.classList.contains("show") ? "true" : "false");
trayActionBtn?.setAttribute("aria-expanded", actionCenter.classList.contains("show") ? "true" : "false");
}
overlay.addEventListener("click", closeAllMenus);
/* =========================
START MENU
========================== */
function bindStartMenu() {
btnStart?.addEventListener("click", () => toggleMenu(startMenu));
// Power button (placeholder)
qsa("[data-power]").forEach(btn => {
btn.addEventListener("click", () => {
pushNotif("Power", "Sleep / Shut down is simulated in GingerOS.");
});
});
}
/* =========================
ACTION CENTER
========================== */
function bindActionCenter() {
trayActionBtn?.addEventListener("click", () => toggleMenu(actionCenter));
qs("[data-clear-notifs]")?.addEventListener("click", () => {
// Keep the welcome notif (first) and template; remove others
const body = qs(".ac-body");
if (!body) return;
qsa(".notif", body).forEach((n, idx) => {
if (n.id === "wifi-notif-template") return;
if (idx === 0) return;
n.remove();
});
});
// Quick settings toggles
qsa("[data-qs]").forEach(btn => {
btn.addEventListener("click", () => {
const on = btn.classList.toggle("active");
btn.setAttribute("aria-pressed", on ? "true" : "false");
if (btn.dataset.qs === "wifi") {
state.wifi.enabled = on;
saveWifiEnabled();
if (!state.wifi.enabled) {
if (state.wifi.connected) disconnectWifi(true);
}
updateWifiUI();
pushNotif("Wi-Fi", state.wifi.enabled ? "Wi-Fi turned on." : "Wi-Fi turned off.");
}
});
});
}
function pushNotif(title, body) {
const acBody = qs(".ac-body");
if (!acBody) return;
const tmpl = qs("#wifi-notif-template");
let n;
if (tmpl) {
n = tmpl.cloneNode(true);
n.hidden = false;
n.removeAttribute("id");
qs(".nt", n).textContent = title;
const b = qs("[data-wifi-notif-text]", n) || qs(".nb", n);
if (b) b.textContent = body;
} else {
n = document.createElement("div");
n.className = "notif";
n.innerHTML = ``;
qs(".nt", n).textContent = title;
qs(".nb", n).textContent = body;
}
acBody.prepend(n);
}
/* =========================
WI-FI (simulated) β WITH PASSWORD INPUT
========================== */
function bindWifi() {
// open flyout
qsa("[data-open-wifi-flyout]").forEach(b => b.addEventListener("click", () => toggleMenu(wifiFlyout)));
trayWifiBtn?.addEventListener("click", () => toggleMenu(wifiFlyout));
// toggle wifi (buttons exist in both Settings + Flyout)
qsa("[data-wifi-toggle]").forEach(btn => {
btn.addEventListener("click", () => {
state.wifi.enabled = !state.wifi.enabled;
saveWifiEnabled();
if (!state.wifi.enabled && state.wifi.connected) disconnectWifi(true);
updateWifiUI();
pushNotif("Wi-Fi", state.wifi.enabled ? "Wi-Fi is on." : "Wi-Fi is off.");
});
});
// refresh networks
qs("[data-wifi-refresh]")?.addEventListener("click", () => {
fakeScanNetworks();
pushNotif("Wi-Fi", "Scanning for networksβ¦");
});
// auto-connect checkbox
qs("[data-auto-connect]")?.addEventListener("change", e => {
state.wifi.autoConnect = !!e.target.checked;
const sel = state.wifi.selected;
if (sel && state.wifi.profiles[sel]) {
state.wifi.profiles[sel].auto = state.wifi.autoConnect;
saveWifiProfiles();
}
});
// select network
qs("#net-list")?.addEventListener("click", e => {
const net = e.target.closest(".net");
if (!net) return;
selectNetwork(net.dataset.ssid);
});
// connect / disconnect buttons
qsa("[data-connect]").forEach(b => b.addEventListener("click", () => connectWifi()));
qsa("[data-disconnect]").forEach(b => b.addEventListener("click", () => disconnectWifi(false)));
// password input should enable/disable connect when secured
qs("[data-wifi-pass]")?.addEventListener("input", () => updateWifiUI());
}
function selectNetwork(ssid) {
state.wifi.selected = ssid;
const net = state.wifi.networks.find(n => n.ssid === ssid);
const secured = !!net?.secured;
qsa("[data-selected-ssid]").forEach(el => (el.textContent = ssid || "β"));
const passEl = qs("[data-wifi-pass]");
if (passEl) {
passEl.value = state.wifi.profiles[ssid]?.password || "";
passEl.placeholder = secured ? "Network password" : "Not required (open network)";
passEl.disabled = !secured;
}
const auto = !!state.wifi.profiles[ssid]?.auto;
state.wifi.autoConnect = auto;
const autoEl = qs("[data-auto-connect]");
if (autoEl) autoEl.checked = auto;
updateWifiUI();
}
function fakeScanNetworks() {
const base = [
{ ssid: "GingerNet", secured: true, band: "2.4GHz" },
{ ssid: "Cafe-WiFi", secured: false, band: "5GHz" },
{ ssid: "DevLab_5G", secured: true, band: "5GHz" },
{ ssid: "Public_Free", secured: false, band: "2.4GHz" },
{ ssid: "Neighbours_5G", secured: true, band: "5GHz" },
{ ssid: "Hidden Network", secured: true, band: "β" }
];
state.wifi.networks = base.map(n => ({
...n,
signal: clamp(Math.floor(Math.random() * 5) + 1, 1, 5)
}));
renderNetworks();
if (state.wifi.selected && state.wifi.networks.some(n => n.ssid === state.wifi.selected)) {
selectNetwork(state.wifi.selected);
} else {
state.wifi.selected = null;
qsa("[data-selected-ssid]").forEach(el => (el.textContent = "β"));
updateWifiUI();
}
}
function renderNetworks() {
const list = qs("#net-list");
if (!list) return;
list.innerHTML = "";
state.wifi.networks.forEach(n => {
const div = document.createElement("div");
div.className = "net";
div.dataset.ssid = n.ssid;
div.tabIndex = 0;
div.setAttribute("role", "button");
div.innerHTML = `
${escapeHtml(n.ssid)}
${n.secured ? "Secured" : "Open"}
Signal: ${"β".repeat(n.signal)}${"β".repeat(5 - n.signal)}
${escapeHtml(n.band || "")}
`;
div.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectNetwork(n.ssid);
}
});
list.appendChild(div);
});
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function connectWifi() {
if (!state.wifi.enabled) {
pushNotif("Wi-Fi", "Turn Wi-Fi on to connect.");
return;
}
if (!state.wifi.selected) return;
if (state.wifi.connecting) return;
const net = state.wifi.networks.find(n => n.ssid === state.wifi.selected);
if (!net) return;
const secured = !!net.secured;
const passEl = qs("[data-wifi-pass]");
const password = passEl ? passEl.value : "";
if (secured && (!password || password.trim().length < 8)) {
pushNotif("Wi-Fi", "Password required (min 8 characters) for secured networks.");
setConnectionBadge("Password required");
updateWifiUI();
return;
}
state.wifi.connecting = true;
setConnectionBadge("Connectingβ¦");
updateWifiUI();
setTimeout(() => {
state.wifi.connecting = false;
state.wifi.connected = state.wifi.selected;
if (secured) {
state.wifi.profiles[state.wifi.connected] = {
password,
auto: !!state.wifi.autoConnect
};
saveWifiProfiles();
}
localStorage.setItem(STORAGE_KEYS.WIFI_LAST, state.wifi.connected);
updateWifiUI();
setConnectionBadge("Connected");
pushNotif("Wi-Fi", `Connected to ${state.wifi.connected}.`);
}, 1100);
}
function disconnectWifi(silent) {
if (!state.wifi.connected) return;
const name = state.wifi.connected;
state.wifi.connected = null;
localStorage.setItem(STORAGE_KEYS.WIFI_LAST, "");
updateWifiUI();
setConnectionBadge("Idle");
if (!silent) pushNotif("Wi-Fi", `Disconnected from ${name}.`);
}
function restoreWifiLastConnection() {
const last = (localStorage.getItem(STORAGE_KEYS.WIFI_LAST) || "").trim();
if (!last) return;
const profile = state.wifi.profiles[last];
if (state.wifi.enabled && profile && profile.auto) {
state.wifi.selected = last;
const passEl = qs("[data-wifi-pass]");
if (passEl) passEl.value = profile.password || "";
selectNetwork(last);
connectWifi();
}
}
function setConnectionBadge(text) {
qsa("[data-connection-state]").forEach(el => (el.textContent = text));
}
function updateWifiUI() {
const enabled = state.wifi.enabled;
const connected = state.wifi.connected;
const selected = state.wifi.selected;
const connecting = state.wifi.connecting;
qsa("[data-wifi-on]").forEach(el => (el.textContent = enabled ? "On" : "Off"));
qsa("[data-wifi-toggle]").forEach(btn => {
btn.setAttribute("aria-pressed", enabled ? "true" : "false");
if (btn.textContent.trim().toLowerCase().includes("turn")) {
btn.textContent = enabled ? "Turn off" : "Turn on";
} else {
btn.textContent = enabled ? "On" : "Off";
}
});
const statusText = connected ? `Connected to ${connected}` : "Disconnected";
qsa("[data-wifi-status]").forEach(el => (el.textContent = statusText));
qsa("[data-current-ssid]").forEach(el => (el.textContent = connected || "β"));
qsa("[data-ip]").forEach(el => (el.textContent = connected ? `192.168.0.${Math.floor(Math.random() * 200 + 20)}` : "β"));
const sig = connected
? (state.wifi.networks.find(n => n.ssid === connected)?.signal ?? 3)
: null;
qsa("[data-signal]").forEach(el => (el.textContent = connected ? `${"β".repeat(sig)}${"β".repeat(5 - sig)}` : "β"));
const net = selected ? state.wifi.networks.find(n => n.ssid === selected) : null;
const secured = !!net?.secured;
const passEl = qs("[data-wifi-pass]");
const passwordOk = secured ? !!(passEl?.value && passEl.value.trim().length >= 8) : true;
qsa("[data-connect]").forEach(btn => {
btn.disabled = !enabled || !selected || connecting || (secured && !passwordOk);
});
qsa("[data-disconnect]").forEach(btn => {
btn.disabled = !enabled || !connected || connecting;
});
if (passEl) {
passEl.disabled = !enabled || !secured;
// β
FIX: do NOT wipe password when switching to open networks
// (leave existing value alone; selectNetwork() handles hydration)
}
const wifiQS = qs('[data-qs="wifi"]');
if (wifiQS) {
wifiQS.classList.toggle("active", enabled);
wifiQS.setAttribute("aria-pressed", enabled ? "true" : "false");
}
if (trayWifiBtn) trayWifiBtn.classList.toggle("active", !!connected);
}
/* =========================
NOTES
========================== */
function initNotes() {
const area = qs("[data-note-area]");
if (!area) return;
area.value = state.notes.content;
let t = null;
area.addEventListener("input", () => {
if (t) clearTimeout(t);
t = setTimeout(() => {
localStorage.setItem(STORAGE_KEYS.NOTES, area.value);
}, 700);
});
qs("[data-note-save]")?.addEventListener("click", () => {
localStorage.setItem(STORAGE_KEYS.NOTES, area.value);
pushNotif("Notes", "Saved locally.");
});
qs("[data-note-new]")?.addEventListener("click", () => {
area.value = "";
localStorage.setItem(STORAGE_KEYS.NOTES, "");
});
}
/* =========================
CLOCK
========================== */
function initClock() {
const t = qs("[data-time]");
const d = qs("[data-date]");
if (!t || !d) return;
const pad2 = n => String(n).padStart(2, "0");
function update() {
const now = new Date();
t.textContent = `${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
d.textContent = now.toLocaleDateString();
}
update();
setInterval(update, 1000);
qs("#clock")?.addEventListener("click", () => toggleMenu(actionCenter));
}
/* =========================
DESKTOP CONTEXT MENU (basic)
========================== */
function bindDesktopContextMenu() {
const menu = qs("#context-menu");
if (!menu) return;
desktop?.addEventListener("contextmenu", e => {
e.preventDefault();
// show first so it has a measurable size
menu.classList.add("show");
// β
FIX: clamp within viewport
const rect = menu.getBoundingClientRect();
const x = clamp(e.clientX, 0, window.innerWidth - rect.width);
const y = clamp(e.clientY, 0, window.innerHeight - rect.height);
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
closeAllMenus(); // context menu uses no overlay
});
document.addEventListener("click", () => menu.classList.remove("show"));
menu.addEventListener("click", e => {
const item = e.target.closest("[data-cm]");
if (!item) return;
const cmd = item.dataset.cm;
menu.classList.remove("show");
if (cmd === "refresh") {
fakeScanNetworks();
pushNotif("Desktop", "Refreshed.");
} else if (cmd === "display") {
openWindow("settings");
activateSettingsTab("network");
} else if (cmd === "personalize") {
openWindow("settings");
activateSettingsTab("personalization");
} else {
pushNotif("Desktop", `β${cmd}β is a UI placeholder.`);
}
});
}
/* =========================
SETTINGS TABS
========================== */
function activateSettingsTab(name) {
const win = qs("#win-settings");
if (!win) return;
qsa("[data-settings-tab]", win).forEach(n => n.classList.toggle("active", n.dataset.settingsTab === name));
qsa("[data-pane]", win).forEach(p => (p.hidden = p.dataset.pane !== name));
}
document.addEventListener("click", e => {
const nav = e.target.closest("[data-settings-tab]");
if (!nav) return;
activateSettingsTab(nav.dataset.settingsTab);
});
/* =========================
GLOBAL SHORTCUTS
========================== */
function bindGlobalShortcuts() {
document.addEventListener("keydown", e => {
if (e.key === "Escape") {
closeAllMenus();
qs("#context-menu")?.classList.remove("show");
}
// β
FIX: ignore repeats so holding Meta doesnβt spam-toggle
if (e.key === "Meta" && !e.repeat) {
e.preventDefault();
toggleMenu(startMenu);
}
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === "w") {
e.preventDefault();
toggleMenu(wifiFlyout);
}
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === "a") {
e.preventDefault();
toggleMenu(actionCenter);
}
if (e.altKey && e.key === "F4") {
e.preventDefault();
if (state.activeWinId) closeWindow(state.activeWinId);
}
});
desktop?.addEventListener("mousedown", e => {
if (e.target.closest(".desk-icon")) return;
closeAllMenus();
});
}
/* =========================
GINGER BROWSER
========================== */
function initBrowser() {
const win = qs("#win-edge");
if (!win) return;
const addr = qs("[data-addr]", win);
const btnGo = qs("[data-go]", win);
const btns = qsa(".btn", win).slice(0, 3);
const [btnBack, btnFwd, btnRef] = btns;
const contentHost = win.querySelector(".content > div:last-child") || win.querySelector(".content");
const viewport = document.createElement("div");
viewport.style.minHeight = "100%";
viewport.style.display = "block";
contentHost.innerHTML = "";
contentHost.appendChild(viewport);
const toolbar = win.querySelector(".content > div:first-child");
if (toolbar) {
const bm = document.createElement("div");
bm.style.display = "flex";
bm.style.gap = "8px";
bm.style.padding = "10px";
bm.style.borderBottom = "1px solid rgba(255,255,255,.10)";
bm.style.background = "rgba(10,11,13,.35)";
bm.innerHTML = `
Tip: external sites may open in a new tab (X-Frame-Options).
`;
toolbar.insertAdjacentElement("afterend", bm);
bm.addEventListener("click", e => {
const b = e.target.closest("[data-bm]");
if (!b) return;
navigateTo(b.dataset.bm, { push: true });
if (addr) addr.value = normalizeUrl(b.dataset.bm);
});
}
const saved = safeParseJSON(localStorage.getItem(STORAGE_KEYS.BROWSER_STATE), null);
if (saved?.history?.length) {
state.browser.history = saved.history;
state.browser.index = clamp(saved.index ?? (saved.history.length - 1), 0, saved.history.length - 1);
const cur = state.browser.history[state.browser.index];
if (addr) addr.value = cur;
navigateTo(cur, { push: false });
} else {
navigateTo("ginger://home", { push: true });
if (addr) addr.value = "ginger://home";
}
function saveBrowserState() {
localStorage.setItem(
STORAGE_KEYS.BROWSER_STATE,
JSON.stringify({ history: state.browser.history, index: state.browser.index })
);
}
function normalizeUrl(input) {
const s = (input || "").trim();
if (!s) return "ginger://home";
if (s.startsWith("ginger://")) return s;
if (/^https?:\/\//i.test(s)) return s;
if (s.includes("://")) return s;
if (s.includes(".") || s.includes("localhost")) return "https://" + s;
return "ginger://search?q=" + encodeURIComponent(s);
}
function setNavButtons() {
if (btnBack) btnBack.disabled = state.browser.index <= 0;
if (btnFwd) btnFwd.disabled = state.browser.index >= state.browser.history.length - 1;
}
function pushHistory(url) {
if (state.browser.index < state.browser.history.length - 1) {
state.browser.history = state.browser.history.slice(0, state.browser.index + 1);
}
state.browser.history.push(url);
state.browser.index = state.browser.history.length - 1;
saveBrowserState();
setNavButtons();
}
function renderBuiltInPage(url) {
const u = new URL(url.replace("ginger://", "https://ginger.local/"));
const path = u.pathname.replace(/^\//, "");
const q = u.searchParams;
const page = document.createElement("div");
page.style.padding = "14px";
page.style.display = "grid";
page.style.gap = "10px";
const card = (title, body) => {
const c = document.createElement("div");
c.className = "card";
c.innerHTML = `${escapeHtml(title)}
${body}
`;
return c;
};
if (path === "" || path === "home") {
page.appendChild(card("Ginger Browser β Home",
`Welcome. Try ginger://apps or paste a URL.
External sites might open in a new tab if they block embedding.`));
const quick = document.createElement("div");
quick.className = "card";
quick.innerHTML = `
Quick links
`;
quick.addEventListener("click", e => {
const b = e.target.closest("[data-nav]");
if (!b) return;
navigateTo(b.dataset.nav, { push: true });
if (addr) addr.value = b.dataset.nav;
});
page.appendChild(quick);
} else if (path === "apps") {
page.appendChild(card("Apps", `Open GingerOS apps directly from the browser:`));
const grid = document.createElement("div");
grid.className = "card";
grid.innerHTML = `
`;
grid.addEventListener("click", e => {
const b = e.target.closest("[data-appopen]");
if (!b) return;
openWindow(b.dataset.appopen);
});
page.appendChild(grid);
} else if (path === "wifi") {
const status = state.wifi.connected ? `Connected to ${escapeHtml(state.wifi.connected)}` : "Disconnected";
page.appendChild(card("Wi-Fi",
`Status: ${status}
Wi-Fi is ${state.wifi.enabled ? "On" : "Off"}.`));
const actions = document.createElement("div");
actions.className = "card";
actions.innerHTML = `
Connect from the Wi-Fi flyout to enter passwords.
`;
actions.addEventListener("click", e => {
if (e.target.closest("[data-openfly]")) toggleMenu(wifiFlyout);
if (e.target.closest("[data-open-settings]")) { openWindow("settings"); activateSettingsTab("network"); }
if (e.target.closest("[data-togglewifi]")) {
state.wifi.enabled = !state.wifi.enabled;
saveWifiEnabled();
if (!state.wifi.enabled && state.wifi.connected) disconnectWifi(true);
updateWifiUI();
navigateTo("ginger://wifi", { push: false });
}
});
page.appendChild(actions);
} else if (path === "notes") {
page.appendChild(card("Notes", `Your notes are stored locally in this browser.`));
const c = document.createElement("div");
c.className = "card";
c.innerHTML = `
Preview:
${escapeHtml(localStorage.getItem(STORAGE_KEYS.NOTES) || "") || "(empty)"}
`;
c.addEventListener("click", e => {
if (e.target.closest("[data-open-notes]")) openWindow("notes");
if (e.target.closest("[data-clear-notes]")) {
localStorage.setItem(STORAGE_KEYS.NOTES, "");
const area = qs("[data-note-area]");
if (area) area.value = "";
navigateTo("ginger://notes", { push: false });
pushNotif("Notes", "Cleared.");
}
});
page.appendChild(c);
} else if (path === "search") {
const term = q.get("q") || "";
page.appendChild(card("Search", `Results for: ${escapeHtml(term)}`));
const res = document.createElement("div");
res.className = "card";
const items = [
{ t: "GingerOS docs (built-in)", u: "ginger://home" },
{ t: "Open Settings", u: "ginger://apps" },
{ t: "Example.com (external)", u: "https://example.com" }
].filter(x => x.t.toLowerCase().includes(term.toLowerCase()) || !term);
res.innerHTML = `
This is a built-in demo search (not web search).
${items.map(i => `
${escapeHtml(i.t)}
${escapeHtml(i.u)}
`).join("")}
`;
res.addEventListener("click", e => {
const b = e.target.closest("[data-openurl]");
if (!b) return;
navigateTo(b.dataset.openurl, { push: true });
if (addr) addr.value = b.dataset.openurl;
});
page.appendChild(res);
} else {
page.appendChild(card("Page not found", `No built-in page at ${escapeHtml("ginger://" + path)}.`));
}
viewport.innerHTML = "";
viewport.appendChild(page);
}
function renderExternal(url) {
viewport.innerHTML = "";
const wrap = document.createElement("div");
wrap.style.padding = "14px";
wrap.style.display = "grid";
wrap.style.gap = "10px";
const info = document.createElement("div");
info.className = "card";
info.innerHTML = `
External site
Many sites block being shown inside other pages (X-Frame-Options / CSP).
Ginger Browser will try to embed it; if it fails, use βOpen in new tabβ.
`;
wrap.appendChild(info);
const frameCard = document.createElement("div");
frameCard.className = "card";
frameCard.innerHTML = `
Embedded view (may be blank if blocked):
If you see an error/blank page, use βOpen in new tabβ.
`;
wrap.appendChild(frameCard);
const iframe = qs("[data-frame]", frameCard);
function tryEmbed() {
iframe.src = url;
}
info.addEventListener("click", e => {
if (e.target.closest("[data-open-newtab]")) {
window.open(url, "_blank", "noopener,noreferrer");
pushNotif("Ginger Browser", "Opened in a new tab.");
}
if (e.target.closest("[data-try-embed]")) {
tryEmbed();
}
});
viewport.appendChild(wrap);
tryEmbed();
}
function navigateTo(input, { push }) {
const url = normalizeUrl(input);
if (url.startsWith("ginger://")) {
state.browser.current = url;
if (push) pushHistory(url);
else setNavButtons();
renderBuiltInPage(url);
return;
}
state.browser.current = url;
if (push) pushHistory(url);
else setNavButtons();
renderExternal(url);
}
btnGo?.addEventListener("click", () => {
const url = (addr?.value || "").trim();
navigateTo(url, { push: true });
if (addr) addr.value = normalizeUrl(url);
});
addr?.addEventListener("keydown", e => {
if (e.key === "Enter") {
e.preventDefault();
btnGo?.click();
}
});
btnBack?.addEventListener("click", () => {
if (state.browser.index <= 0) return;
state.browser.index--;
const url = state.browser.history[state.browser.index];
if (addr) addr.value = url;
navigateTo(url, { push: false });
localStorage.setItem(STORAGE_KEYS.BROWSER_STATE, JSON.stringify({ history: state.browser.history, index: state.browser.index }));
setNavButtons();
});
btnFwd?.addEventListener("click", () => {
if (state.browser.index >= state.browser.history.length - 1) return;
state.browser.index++;
const url = state.browser.history[state.browser.index];
if (addr) addr.value = url;
navigateTo(url, { push: false });
localStorage.setItem(STORAGE_KEYS.BROWSER_STATE, JSON.stringify({ history: state.browser.history, index: state.browser.index }));
setNavButtons();
});
btnRef?.addEventListener("click", () => {
const cur = state.browser.current || (addr?.value || "ginger://home");
navigateTo(cur, { push: false });
pushNotif("Ginger Browser", "Refreshed.");
});
const _ = 0;
}
/* =========================
MISC CLICK HELPERS
========================== */
document.addEventListener("click", e => {
const t = e.target.closest("[data-run-troubleshooter]");
if (t) {
pushNotif("Troubleshooter", "No issues found (simulated).");
return;
}
const sw = e.target.closest("[data-open-wifi-flyout]");
if (sw) {
updateWifiUI();
return;
}
});
})();