From cd13997800df1b342180f34ce42121f0892e5f08 Mon Sep 17 00:00:00 2001 From: traffic66 Date: Fri, 6 Nov 2026 00:37:58 +0900 Subject: [PATCH 1/2] Web UI: restore full logo size; enlarge sidebar sponsor bar Keep the compact sidebar layout, but stop shrinking the brand logo on short screens so it stays at the original 72px max height. Make the unregistered sponsor slot about 20% larger (image height, label and text). --- internal/web/static/app.css | 14 +++++++------- internal/web/static/sponsor.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 68d5783..b85b65e 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -117,22 +117,22 @@ nav.dash button[aria-current="page"] .ni{opacity:1} /* The sponsor slot of an unregistered installation: in the sidebar, above the language and theme, one small banner at a time at the sidebar's width (sponsor.js decides, and sets the height). */ -.sponsor{border-top:1px solid var(--side-line);padding:8px 12px 4px} +.sponsor{border-top:1px solid var(--side-line);padding:10px 12px 5px} .sponsor[hidden]{display:none} -.sponsor .sp-h{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:0 0 3px} -.sponsor .sp-text{margin:0 0 6px;border:1px dashed var(--line-2);border-radius:6px;padding:6px 8px;font-size:11.5px;line-height:1.35;color:var(--ink-3);overflow-wrap:anywhere} +.sponsor .sp-h{font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:0 0 4px} +.sponsor .sp-text{margin:0 0 8px;border:1px dashed var(--line-2);border-radius:6px;padding:8px 10px;font-size:13.5px;line-height:1.4;color:var(--ink-3);overflow-wrap:anywhere} .sponsor .sp-text[hidden],.spc[hidden]{display:none} .spc-strip{display:grid;border-radius:4px;overflow:hidden} .spc-cell{position:relative;overflow:hidden;background:var(--surface)} .spc-cell>a,.spc-cell>div{display:block;width:100%;height:100%} .spc-cell img{width:100%;height:100%;object-fit:cover;display:block} -.spc-dots{display:flex;gap:6px;justify-content:center;padding:4px 0 5px} -.spc-dot{width:6px;height:6px;padding:0;border-radius:50%;border:0;cursor:pointer;background:var(--line-2)} +.spc-dots{display:flex;gap:7px;justify-content:center;padding:5px 0 6px} +.spc-dot{width:7px;height:7px;padding:0;border-radius:50%;border:0;cursor:pointer;background:var(--line-2)} .spc-dot.on{background:var(--accent)} @media print{.sponsor{display:none}} /* shorter screens: tighten the sidebar so the menu still fits without scrolling */ -@media (max-height:900px) and (min-width:821px){.brand{padding:8px 14px 6px}.brand .logo{max-height:52px}nav.dash button{padding:5px 12px;font-size:14px}nav.dash hr{margin:3px 8px}aside>.pick{padding:6px 12px}.foot{padding:2px 12px 6px}#logout,#help{padding:4px}.sponsor{padding:5px 12px 2px}} -@media (max-height:780px) and (min-width:821px){.brand .logo{max-height:40px}nav.dash{padding:4px 8px}nav.dash button{padding:3.5px 12px;font-size:13.5px}nav.dash hr{margin:2px 8px}.sponsor .sp-h{padding:0 0 1px}} +@media (max-height:900px) and (min-width:821px){.brand{padding:8px 14px 6px}nav.dash button{padding:5px 12px;font-size:14px}nav.dash hr{margin:3px 8px}aside>.pick{padding:6px 12px}.foot{padding:2px 12px 6px}#logout,#help{padding:4px}.sponsor{padding:5px 12px 2px}} +@media (max-height:780px) and (min-width:821px){nav.dash{padding:4px 8px}nav.dash button{padding:3.5px 12px;font-size:13.5px}nav.dash hr{margin:2px 8px}.sponsor .sp-h{padding:0 0 1px}} aside>.pick{display:grid;grid-template-columns:1fr auto;gap:6px;padding:8px 12px;border-bottom:1px solid var(--side-line)} #theme{display:flex;align-items:center;gap:6px;border:1px solid var(--line-2);border-radius:6px;padding:4px 10px;background:var(--surface);color:var(--ink);cursor:pointer;font-size:13px;white-space:nowrap} #theme i{width:12px;height:12px;border-radius:50%;background:var(--accent);box-shadow:inset 0 0 0 2px var(--surface),0 0 0 1px var(--line-2)} diff --git a/internal/web/static/sponsor.js b/internal/web/static/sponsor.js index f8273a4..6d2a08c 100644 --- a/internal/web/static/sponsor.js +++ b/internal/web/static/sponsor.js @@ -131,7 +131,7 @@ var w = car.clientWidth || SMALL_W; strip.textContent = ''; var box = document.createElement('div'); box.className = 'spc-cell'; - box.style.height = Math.round(w * SMALL_H / SMALL_W) + 'px'; + box.style.height = Math.round(w * SMALL_H / SMALL_W * 1.2) + 'px'; box.appendChild(slideEl(s)); strip.appendChild(box); ds.forEach(function (d, i) { d.classList.toggle('on', i === cur); }); From b1eac1c17f2f7d3bc7bbff7c10917d8074502ba4 Mon Sep 17 00:00:00 2001 From: traffic66 Date: Fri, 6 Nov 2026 01:05:20 +0900 Subject: [PATCH 2/2] Web UI: square ads in sidebar, wide banners at top of main Classify sponsor images by natural aspect ratio (no new manifest field): ratio >= 3 goes to a slim centred strip above the page header; the rest fill the sidebar at the image's own proportions (max height 220px). Keep rotation, UTM tagging, hide-when-registered and the default text fallback. Revert the earlier 1.2x sidebar ad bump; logo stays at full size. --- CHANGELOG.md | 4 + internal/web/static/app.css | 26 +++-- internal/web/static/index.html | 1 + internal/web/static/sponsor.js | 173 ++++++++++++++++++++++----------- 4 files changed, 138 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91fbdbd..8203d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ The release notes on GitHub are taken from this file: the section whose heading is the version number. +## Unreleased + +- Web UI: square sponsor ads show in the sidebar at their own proportions (capped height); wide banners move to a slim strip at the top of the main area (not in the sidebar). The brand logo stays at its full size on short screens. + ## 2.5.7 - Web UI: the left sidebar now fits without scrolling, even at 1366×768: language and theme sit under the logo, Help sits beside Logout, and Ledger and Account moved into Settings as tabs. The ledger no longer shows device location (the API is unchanged). Old #v=racks and #v=ledger links open the Settings ledger tab. diff --git a/internal/web/static/app.css b/internal/web/static/app.css index b85b65e..aafb1fe 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -114,22 +114,28 @@ nav.dash button[aria-current="page"] .ni{opacity:1} .foot{padding:4px 12px 10px;border-top:1px solid var(--side-line)} .footrow{display:flex;align-items:center;gap:4px} .footrow .linkbtn{flex:1;justify-content:center} -/* The sponsor slot of an unregistered installation: in the sidebar, above - the language and theme, one small banner at a time at the sidebar's - width (sponsor.js decides, and sets the height). */ -.sponsor{border-top:1px solid var(--side-line);padding:10px 12px 5px} +/* Sidebar sponsor: square-ish ads (ratio < 3) at the image's own + proportions; height is set by sponsor.js, capped. Wide banners go to + .sponsor-top above the page header instead. */ +.sponsor{border-top:1px solid var(--side-line);padding:8px 12px 4px} .sponsor[hidden]{display:none} -.sponsor .sp-h{font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:0 0 4px} -.sponsor .sp-text{margin:0 0 8px;border:1px dashed var(--line-2);border-radius:6px;padding:8px 10px;font-size:13.5px;line-height:1.4;color:var(--ink-3);overflow-wrap:anywhere} +.sponsor .sp-h{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:0 0 3px} +.sponsor .sp-text{margin:0 0 6px;border:1px dashed var(--line-2);border-radius:6px;padding:6px 8px;font-size:11.5px;line-height:1.35;color:var(--ink-3);overflow-wrap:anywhere} .sponsor .sp-text[hidden],.spc[hidden]{display:none} .spc-strip{display:grid;border-radius:4px;overflow:hidden} .spc-cell{position:relative;overflow:hidden;background:var(--surface)} .spc-cell>a,.spc-cell>div{display:block;width:100%;height:100%} -.spc-cell img{width:100%;height:100%;object-fit:cover;display:block} -.spc-dots{display:flex;gap:7px;justify-content:center;padding:5px 0 6px} -.spc-dot{width:7px;height:7px;padding:0;border-radius:50%;border:0;cursor:pointer;background:var(--line-2)} +.spc-cell img{width:100%;height:100%;object-fit:contain;display:block} +.spc-dots{display:flex;gap:6px;justify-content:center;padding:4px 0 5px} +.spc-dot{width:6px;height:6px;padding:0;border-radius:50%;border:0;cursor:pointer;background:var(--line-2)} .spc-dot.on{background:var(--accent)} -@media print{.sponsor{display:none}} +/* Top strip: wide banners (ratio >= 3), slim, centred, max ~728x90. */ +.sponsor-top{display:flex;justify-content:center;padding:6px 16px;border-bottom:1px solid var(--line);background:var(--surface)} +.sponsor-top[hidden]{display:none} +.sponsor-top .spc{width:100%;max-width:728px} +.sponsor-top .spc-cell{background:transparent} +.sponsor-top .spc-dots{padding:3px 0 0} +@media print{.sponsor,.sponsor-top{display:none}} /* shorter screens: tighten the sidebar so the menu still fits without scrolling */ @media (max-height:900px) and (min-width:821px){.brand{padding:8px 14px 6px}nav.dash button{padding:5px 12px;font-size:14px}nav.dash hr{margin:3px 8px}aside>.pick{padding:6px 12px}.foot{padding:2px 12px 6px}#logout,#help{padding:4px}.sponsor{padding:5px 12px 2px}} @media (max-height:780px) and (min-width:821px){nav.dash{padding:4px 8px}nav.dash button{padding:3.5px 12px;font-size:13.5px}nav.dash hr{margin:2px 8px}.sponsor .sp-h{padding:0 0 1px}} diff --git a/internal/web/static/index.html b/internal/web/static/index.html index f6c06a2..6f030a2 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -42,6 +42,7 @@
+

diff --git a/internal/web/static/sponsor.js b/internal/web/static/sponsor.js index 6d2a08c..febebca 100644 --- a/internal/web/static/sponsor.js +++ b/internal/web/static/sponsor.js @@ -1,4 +1,4 @@ -// Remote sponsor banner for unregistered installations. +// Remote sponsor banners for unregistered installations. // // The slides are listed in a manifest in // https://github.com/githubflyideas/ads-sponsor (folder traffic66/), so they @@ -8,19 +8,24 @@ // shows a built-in text box instead; a broken image is never shown. // Registered installations never call this. // +// Classification is by each image's natural size (no manifest field): +// width/height >= WIDE_RATIO → slim banner at the top of the main area +// otherwise → square-ish ad in the sidebar +// // Usage: t66Sponsor.load() t66Sponsor.remove() (function () { 'use strict'; var URL_ = 'https://raw.githubusercontent.com/githubflyideas/ads-sponsor/main/traffic66/sponsors.json'; - // MAX is how many advertisers are fetched and rotated through. var TIMEOUT_MS = 3000, ROTATE_MS = 5500, MAX = 6; - // A small banner is 728x90 (1456x180 at 2x). The slot is in the - // sidebar, as wide as it is, so only small banners are shown there: an - // image LONG_RATIO times as wide as it is high or wider is a long banner, - // too thin at that width to read, and is left out. - var SMALL_W = 728, SMALL_H = 90, LONG_RATIO = 12; + // Images this many times as wide as they are high (or wider) are wide + // banners for the top strip; the rest are square-ish for the sidebar. + var WIDE_RATIO = 3; + // Sidebar: width fills the slot, height follows the image, capped. + var SIDE_MAX_H = 220; + // Top strip: classic 728x90 proportions, capped. + var TOP_MAX_W = 728, TOP_MAX_H = 90; var CACHE_KEY = 't66.sponsor', CACHE_MS = 30 * 60 * 1000; - var started = false, timerId = null, roId = null; + var started = false, timers = [], ros = []; function httpURL(v, base, httpsOnly) { if (typeof v !== 'string' || !v.trim()) return null; @@ -47,7 +52,7 @@ var img = new Image(); img.decoding = 'async'; img.referrerPolicy = 'no-referrer'; - img.onload = function () { img.naturalWidth > 0 ? ok(img) : bad(); }; + img.onload = function () { img.naturalWidth > 0 && img.naturalHeight > 0 ? ok(img) : bad(); }; img.onerror = bad; img.src = src; }); @@ -81,20 +86,6 @@ return m; }); } - // resolves with the slides whose images loaded, rejects when none did - function ready(m, base) { - var want = slidesOf(m, base); - if (!want.length) return Promise.reject(); - return settle(want.map(function (s) { return loadImg(s.src); })).then(function (imgs) { - var ok = []; - want.forEach(function (s, i) { - var im = imgs[i]; - if (im && im.naturalWidth < LONG_RATIO * im.naturalHeight) { s.img = im; ok.push(s); } - }); - if (!ok.length) throw 0; - return ok; - }); - } function slidesOf(m, base) { var list = m && Array.isArray(m.slides) ? m.slides : [], out = []; @@ -108,36 +99,53 @@ return out; } - // The slot shows one advertiser at a time and rotates between them, at - // the sidebar's width and the banner's own proportions. - function build(slot, slides) { - var car = slot.querySelector('.spc'); - car.hidden = false; + // Load images, drop broken ones, split into square (sidebar) and wide (top). + function ready(m, base) { + var want = slidesOf(m, base); + if (!want.length) return Promise.reject(); + return settle(want.map(function (s) { return loadImg(s.src); })).then(function (imgs) { + var square = [], wide = []; + want.forEach(function (s, i) { + var im = imgs[i]; + if (!im) return; + s.img = im; + if (im.naturalWidth >= WIDE_RATIO * im.naturalHeight) wide.push(s); + else square.push(s); + }); + if (!square.length && !wide.length) throw 0; + return {square: square, wide: wide}; + }); + } + + function slideEl(s) { + var a = document.createElement(s.link ? 'a' : 'div'); + if (s.link) { a.href = s.link; a.target = '_blank'; a.rel = 'noopener noreferrer sponsored'; } + if (s.title || s.alt) a.title = s.title || s.alt; + var img = s.img.cloneNode(false); + img.alt = s.alt; + a.appendChild(img); + return a; + } + + // One rotating carousel inside `car`. heightFn(carWidth, slide) → px height. + function carousel(car, slides, heightFn) { var strip = document.createElement('div'); strip.className = 'spc-strip'; var dots = document.createElement('div'); dots.className = 'spc-dots'; var ds = [], cur = 0, paused = false; - function slideEl(s) { - var a = document.createElement(s.link ? 'a' : 'div'); - if (s.link) { a.href = s.link; a.target = '_blank'; a.rel = 'noopener noreferrer sponsored'; } - if (s.title || s.alt) a.title = s.title || s.alt; - var img = s.img.cloneNode(false); - img.alt = s.alt; - a.appendChild(img); - return a; - } function draw() { var s = slides[cur]; - var w = car.clientWidth || SMALL_W; + var w = car.clientWidth || TOP_MAX_W; strip.textContent = ''; var box = document.createElement('div'); box.className = 'spc-cell'; - box.style.height = Math.round(w * SMALL_H / SMALL_W * 1.2) + 'px'; + box.style.height = Math.max(1, Math.round(heightFn(w, s))) + 'px'; box.appendChild(slideEl(s)); strip.appendChild(box); ds.forEach(function (d, i) { d.classList.toggle('on', i === cur); }); } function show(i) { cur = ((i % slides.length) + slides.length) % slides.length; draw(); } + car.hidden = false; car.textContent = ''; car.appendChild(strip); if (slides.length > 1) { @@ -151,35 +159,88 @@ car.appendChild(dots); ['mouseenter', 'focusin'].forEach(function (e) { car.addEventListener(e, function () { paused = true; }); }); ['mouseleave', 'focusout'].forEach(function (e) { car.addEventListener(e, function () { paused = false; }); }); - timerId = setInterval(function () { + timers.push(setInterval(function () { if (!paused && !document.hidden) show(cur + 1); - }, ROTATE_MS); + }, ROTATE_MS)); } - slot.hidden = false; show(0); - if (window.ResizeObserver) { roId = new ResizeObserver(draw); roId.observe(car); } - else window.addEventListener('resize', draw); + if (window.ResizeObserver) { + var ro = new ResizeObserver(draw); + ro.observe(car); + ros.push(ro); + } else { + window.addEventListener('resize', draw); + } + } + + function sideHeight(w, s) { + var ratio = s.img.naturalHeight / s.img.naturalWidth; + return Math.min(SIDE_MAX_H, w * ratio); + } + function topHeight(w, s) { + var ratio = s.img.naturalHeight / s.img.naturalWidth; + return Math.min(TOP_MAX_H, w * ratio); + } + + function showDefault(slot) { + if (!slot) return; + var t = slot.querySelector('.sp-text'); + if (t) t.hidden = false; + var c = slot.querySelector('.spc'); + if (c) { c.hidden = true; c.textContent = ''; } + slot.hidden = false; + } + + function buildSidebar(slot, slides) { + if (!slot) return; + if (!slides.length) { showDefault(slot); return; } + var t = slot.querySelector('.sp-text'); + if (t) t.hidden = true; + carousel(slot.querySelector('.spc'), slides, sideHeight); + slot.hidden = false; + } + + function buildTop(slot, slides) { + if (!slot) return; + if (!slides.length) { slot.hidden = true; slot.textContent = ''; return; } + slot.textContent = ''; + var wrap = document.createElement('div'); wrap.className = 'spc'; + slot.appendChild(wrap); + carousel(wrap, slides, topHeight); + slot.hidden = false; + } + + function clearTimers() { + timers.forEach(function (id) { clearInterval(id); }); + timers = []; + ros.forEach(function (ro) { try { ro.disconnect(); } catch (e) {} }); + ros = []; } window.t66Sponsor = { load: function () { - var slot = document.getElementById('sponsor'); - if (started || !slot || !window.URL || !window.Promise) return; + var side = document.getElementById('sponsor'); + var top = document.getElementById('sponsorTop'); + if (started || !side || !window.URL || !window.Promise) return; started = true; remote().then(function (m) { return ready(m, URL_); }) - .then(function (ok) { if (started) build(slot, ok); }, - function () { - if (!started) return; - slot.querySelector('.sp-text').hidden = false; - slot.hidden = false; - }); + .then(function (parts) { + if (!started) return; + buildSidebar(side, parts.square); + buildTop(top, parts.wide); + }, function () { + if (!started) return; + showDefault(side); + if (top) top.hidden = true; + }); }, remove: function () { started = false; - if (timerId) clearInterval(timerId); - if (roId) { roId.disconnect(); roId = null; } - var slot = document.getElementById('sponsor'); - if (slot) slot.remove(); + clearTimers(); + var side = document.getElementById('sponsor'); + if (side) side.remove(); + var top = document.getElementById('sponsorTop'); + if (top) top.remove(); } }; })();