diff --git a/src/webview.rs b/src/webview.rs index f682850..6de827d 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -143,92 +143,293 @@ const CHROME_HTML: &str = r#" + -
-
-
- - - +
+
+
+
-
- @@ -248,6 +449,9 @@ const normalize = (raw) => { }; const input = document.getElementById('url-input'); +const tabStrip = document.getElementById('tabs'); +const siteIcon = document.getElementById('site-icon'); +let currentUrl = ''; const isEditableTarget = (target) => { if (!target || !(target instanceof Element)) return false; @@ -262,70 +466,164 @@ document.getElementById('reload').addEventListener('click', () => post('reload') document.getElementById('props-btn').addEventListener('click', () => post('open_properties')); document.getElementById('adblock-btn').addEventListener('click', () => post('toggle_adblock')); document.getElementById('downloads-btn').addEventListener('click', () => post('toggle_downloads')); +document.getElementById('add-btn').addEventListener('click', () => post('new_tab')); document.getElementById('popup-yes').addEventListener('click', () => post('popup_allow_yes')); document.getElementById('popup-no').addEventListener('click', () => post('popup_allow_no')); +input.addEventListener('focus', () => input.select()); + input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); const next = normalize(input.value); - if (next) post('navigate:' + next); + if (next) { + post('navigate:' + next); + input.blur(); + } + return; + } + if (ev.key === 'Escape') { + ev.preventDefault(); + input.value = currentUrl; + input.blur(); } }); +// Plain mouse wheel scrolls the tab strip horizontally. +tabStrip.addEventListener('wheel', (ev) => { + if (ev.deltaY === 0) return; + ev.preventDefault(); + tabStrip.scrollLeft += ev.deltaY; +}, { passive: false }); + +const setSiteIcon = (url) => { + let state = 'search'; + let hint = 'Search or enter address'; + try { + const parsed = new URL(url); + if (parsed.protocol === 'https:') { state = 'secure'; hint = 'Connection is secure (HTTPS)'; } + else if (parsed.protocol === 'http:') { state = 'insecure'; hint = 'Not secure (HTTP)'; } + else if (parsed.protocol === 'data:' || parsed.protocol === 'file:' || parsed.protocol === 'about:') { + state = 'internal'; hint = 'RustyWolf internal page'; + } + } catch (_) {} + siteIcon.dataset.state = state; + siteIcon.title = hint; +}; + +const hostOf = (url) => { + try { return new URL(url).hostname.replace(/^www\./, ''); } catch (_) { return ''; } +}; + +const hashHue = (text) => { + let hash = 0; + for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) % 360; + return hash; +}; + +const ICON_PIN = ''; +const ICON_CLOSE = ''; +const ICON_MOON = ''; + const shortTitle = (tab, i) => { - if (tab.title && tab.title.trim()) return tab.title.trim().slice(0, 22); - try { return new URL(tab.url).hostname || 'Tab ' + (i + 1); } - catch (_) { return 'Tab ' + (i + 1); } + if (tab.title && tab.title.trim()) return tab.title.trim(); + const host = hostOf(tab.url); + return host || 'Tab ' + (i + 1); +}; + +const makeTab = (tab, i, activeId) => { + const isActive = tab.id === activeId; + const el = document.createElement('div'); + el.className = 'tab' + (isActive ? ' active' : '') + (tab.loaded ? '' : ' sleeping'); + el.title = tab.url; + el.setAttribute('role', 'tab'); + el.setAttribute('aria-selected', isActive ? 'true' : 'false'); + el.tabIndex = 0; + el.addEventListener('click', () => post('switch_tab:' + tab.id)); + el.addEventListener('keydown', (ev) => { + if (ev.target !== el) return; + if (ev.key === 'Enter' || ev.key === ' ') { + ev.preventDefault(); + post('switch_tab:' + tab.id); + } + }); + el.addEventListener('auxclick', (ev) => { + if (ev.button === 1) { + ev.preventDefault(); + post('close_tab:' + tab.id); + } + }); + + const host = hostOf(tab.url); + const favicon = document.createElement('span'); + favicon.className = 'tab-favicon'; + if (host) { + const hue = hashHue(host); + favicon.textContent = host.charAt(0).toUpperCase(); + favicon.style.background = 'linear-gradient(135deg, hsl(' + hue + ', 62%, 50%), hsl(' + hue + ', 68%, 36%))'; + favicon.title = host; + } else if ((tab.url || '').startsWith('data:')) { + favicon.textContent = 'R'; + favicon.style.background = 'linear-gradient(135deg, hsl(218, 62%, 52%), hsl(218, 68%, 38%))'; + favicon.title = 'RustyWolf internal page'; + } else { + favicon.textContent = '?'; + favicon.style.background = 'linear-gradient(135deg, hsl(220, 10%, 38%), hsl(220, 12%, 28%))'; + favicon.title = 'New tab'; + } + el.appendChild(favicon); + + const title = document.createElement('span'); + title.className = 'tab-title'; + title.textContent = shortTitle(tab, i); + el.appendChild(title); + + if (!tab.loaded) { + const sleep = document.createElement('span'); + sleep.className = 'tab-sleep'; + sleep.title = 'Suspended to save memory - click the tab to reload it'; + sleep.innerHTML = ICON_MOON; + el.appendChild(sleep); + } + + const pin = document.createElement('button'); + pin.className = 'tab-pin' + (tab.kirEnabled ? ' on' : ''); + pin.title = tab.kirEnabled + ? 'Keeping in RAM (KIR): on - click to let it suspend when inactive' + : 'Keeping in RAM (KIR): off - click to keep this tab loaded in the background'; + pin.setAttribute('aria-label', 'Toggle keeping in RAM'); + pin.setAttribute('aria-pressed', tab.kirEnabled ? 'true' : 'false'); + pin.innerHTML = ICON_PIN; + pin.addEventListener('click', (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + post('toggle_kir:' + tab.id); + }); + el.appendChild(pin); + + const close = document.createElement('button'); + close.className = 'tab-close'; + close.title = 'Close tab (Ctrl+W)'; + close.setAttribute('aria-label', 'Close tab'); + close.innerHTML = ICON_CLOSE; + close.addEventListener('click', (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + post('close_tab:' + tab.id); + }); + el.appendChild(close); + + return el; }; window.__rustywolfSetTabs = (state) => { - const row = document.getElementById('tabs-row'); - row.innerHTML = ''; - state.tabs.forEach((tab, i) => { - const btn = document.createElement('button'); - btn.className = - 'tab-btn' + - (tab.id === state.activeId ? ' active' : '') + - (tab.loaded ? '' : ' unloaded'); - btn.title = tab.url; - btn.addEventListener('click', () => post('switch_tab:' + tab.id)); + tabStrip.innerHTML = ''; + state.tabs.forEach((tab, i) => tabStrip.appendChild(makeTab(tab, i, state.activeId))); - const title = document.createElement('span'); - title.className = 'tab-title'; - title.textContent = shortTitle(tab, i) + (tab.loaded ? '' : ' [S]'); - btn.appendChild(title); - - const kir = document.createElement('button'); - kir.className = 'kir-btn' + (tab.kirEnabled ? ' active' : ''); - kir.textContent = 'K'; - kir.title = tab.kirEnabled ? 'KIR On' : 'KIR Off'; - kir.addEventListener('click', (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - post('toggle_kir:' + tab.id); - }); - btn.appendChild(kir); - - const x = document.createElement('button'); - x.className = 'close-btn'; - x.textContent = '-'; x.title = 'Close tab'; - x.addEventListener('click', (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - post('close_tab:' + tab.id); - }); - btn.appendChild(x); - row.appendChild(btn); - }); - - const add = document.createElement('button'); - add.id = 'add-btn'; add.textContent = '+'; add.title = 'New tab'; - add.addEventListener('click', () => post('new_tab')); - row.appendChild(add); - - const active = state.tabs.find(t => t.id === state.activeId); - if (active) input.value = (active.url && active.url !== 'about:blank') ? active.url : ''; + const active = state.tabs.find((t) => t.id === state.activeId); + currentUrl = (active && active.url && active.url !== 'about:blank') ? active.url : ''; + setSiteIcon(currentUrl); + if (document.activeElement !== input) input.value = currentUrl; }; const shortPath = (value) => { @@ -340,18 +638,23 @@ window.__rustywolfSetDownloads = (state) => { const items = (state && Array.isArray(state.downloads)) ? state.downloads : []; const panelOpen = !!(state && state.panelOpen); row.innerHTML = ''; + button.classList.toggle('active', panelOpen); + button.setAttribute('aria-pressed', panelOpen ? 'true' : 'false'); if (!panelOpen) { row.style.display = 'none'; - button.classList.remove('active'); return; } - button.classList.add('active'); row.style.display = 'flex'; if (items.length === 0) { const empty = document.createElement('div'); empty.className = 'dl-empty'; - empty.textContent = 'No active downloads'; + const glyph = document.createElement('span'); + glyph.innerHTML = ''; + const label = document.createElement('span'); + label.textContent = 'No active downloads'; + empty.appendChild(glyph); + empty.appendChild(label); row.appendChild(empty); return; } @@ -424,13 +727,11 @@ window.__rustywolfSetDownloads = (state) => { window.__rustywolfSetAdblock = (enabled) => { const button = document.getElementById('adblock-btn'); if (!button) return; - if (enabled) { - button.classList.add('active'); - button.title = 'Adblock: On'; - } else { - button.classList.remove('active'); - button.title = 'Adblock: Off'; - } + button.classList.toggle('active', !!enabled); + button.setAttribute('aria-pressed', enabled ? 'true' : 'false'); + button.title = enabled + ? 'Ad blocking: on - click to turn off' + : 'Ad blocking: off - click to turn on'; }; window.__rustywolfSetPopupPrompt = (state) => { @@ -2000,16 +2301,22 @@ fn render_properties_html(state: &BrowserState) -> String { format!( "RustyWolf Properties\ - \ -

Browser Properties

Session history and download activity.

\ -

Browsing History

{}

{history_rows}
VisitedTitleURL
\ -

Download History

{}

{download_rows}
UpdatedStatusSaved ToSource URL
\ - ", + \ +

Browser Properties

Session browsing history and download activity.

\ +

Browsing History

{}

{history_rows}
VisitedTitleURL
\ +

Download History

{}

{download_rows}
UpdatedStatusSaved ToSource URL
\ +
", if history_is_truncated { format!("Showing latest {PROPERTIES_MAX_ROWS} entries.") } else {