From a634f83a8ef95db1549ef1ad3a52d60273124860 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 7 Sep 2026 05:41:17 -0500 Subject: [PATCH] Shorts = portrait-only reel; Watch videos = landscape; badge name in gold - Videos now carry client-detected pixel dimensions (videoW/videoH) captured at campaign create (upload or direct link). serveVideo takes an orientation filter: Shorts reel (/shorts) serves portrait (height>width); the Watch videos tab serves landscape, including legacy/unknown-dimension videos so nothing is orphaned. - shorts.js requests the portrait feed and has a client guard that skips any landscape video that slips through. Create form shows the detected orientation. - Achievement badge share image: member name now renders in gold with a dark outline (was low-contrast dark ink) and sits centered on the ribbon. Co-Authored-By: Claude Opus 4.8 --- ads.js | Bin 58954 -> 59931 bytes public/assets/my.js | 47 +++++++++++++++++++++++++++++++++++----- public/assets/shorts.js | 11 ++++++++-- public/my.html | 10 ++++----- public/shorts.html | 2 +- server.js | 3 ++- v.jar | 5 +++++ 7 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 v.jar diff --git a/ads.js b/ads.js index a610cf9e6f376d2e25e5c2d8ec01de4612269e18..4fe759a9680cf99f24521bb2901f5999a98d0af6 100644 GIT binary patch delta 976 zcmaKrPixdb7{(9vAS;LB@!@c0pvF%M)6k5TS1D4!utqC-Mi6JZdkh;W5k2SZ=K#E2LZRR|)zn39m0 zFMC8$AIExi5LckDAW`)b?kOK_xXIDMj72t=p{{14JqH~;!85!f%CAR3B0by4{!}?= zLMB4A%BDW!V_y|rSLNORG`+X*?mA`HtJ@ndCpW6PdHSLDbcXX*<-DRv1(6{|l%~>z zHAi5;9|WP`R~l}NO5nTU}RIaNNFV=xi3sxj%gFZ!dcp1XrYPGoQ*cb2n= z0|+)IblT8t=85Hi5i2ePLP4hBGLK8?G)Uw@k%M~r_F`0}I#6W>(cX3t>e$xTVea*% z;T)Wu!Lrit@xz&5T28gvvU1w|P%kv=+qzaVa0(HgyIaT;?hWA%cHxft{j6F<^zU*G ztFSOEko&wYj1~-6oa}{_u^GH6*u%IKl}yrU=k|`3e%XGMzCXR0{AUM@`tx#pD+?{+gNaSAh97^qzX1;5LIMB) delta 63 zcmV-F0Kos7)C0=Q1F#b+li?vYlg1_yvvMo`YLgPW7n6RMLzA^-7?W?k7?T}!2$TPq VX_Io9Y?El%E|apFXR{@n_D-9O9nk;) diff --git a/public/assets/my.js b/public/assets/my.js index 8ab9fee..88e190f 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -80,12 +80,19 @@ const c = document.createElement('canvas'); c.width = img.width; c.height = img.height; const x = c.getContext('2d'); x.drawImage(img, 0, 0); - if (who) { // the blank ribbon sits at ~76% down; name it in dark ink on the mint banner + if (who) { // name it on the blank ribbon: gold with a dark outline so it reads on any badge color x.textAlign = 'center'; - x.fillStyle = '#04231b'; - x.font = 'bold ' + Math.round(c.width * 0.052) + 'px Sora, "Segoe UI", sans-serif'; - x.fillText(who, c.width / 2, c.height * 0.78); + x.textBaseline = 'middle'; + x.font = 'bold ' + Math.round(c.width * 0.055) + 'px Sora, "Segoe UI", sans-serif'; + const y = c.height * 0.82; + x.lineJoin = 'round'; + x.lineWidth = Math.max(3, Math.round(c.width * 0.008)); + x.strokeStyle = 'rgba(4,20,15,.9)'; + x.strokeText(who, c.width / 2, y); + x.fillStyle = '#ffd15c'; + x.fillText(who, c.width / 2, y); x.textAlign = 'left'; + x.textBaseline = 'alphabetic'; } try { c.toBlob(bl => { @@ -563,6 +570,13 @@ document.addEventListener('change', e => { if (e.target && e.target.id === 'cWatchSecs') videoHint(); }); $('cBudget').addEventListener('input', () => { if ($('cType').value === 'video') videoHint(); }); $('cVideoUploadBtn').addEventListener('click', () => $('cVideoFile').click()); + $('cVideoUrl').addEventListener('change', async () => { + const url = $('cVideoUrl').value.trim(); + $('cVideoInfo').textContent = url ? 'checking video…' : ''; + cVidDims = url ? await probeVideoDims(url) : null; + if (url && !cVidDims) $('cVideoInfo').textContent = 'could not read that video'; + else { $('cVideoInfo').textContent = ''; showVidOrient(); } + }); $('cVideoFile').addEventListener('change', async () => { const f = $('cVideoFile').files[0]; if (!f) return; @@ -574,6 +588,7 @@ $('cVideoInfo').textContent = f.name + ' uploaded'; $('cVideoPrev').hidden = false; $('cVideoPrev').innerHTML = ''; + cVidDims = await probeVideoDims(r.url); showVidOrient(); } catch (e) { $('cVideoInfo').textContent = 'Upload failed. Try again.'; } $('cVideoFile').value = ''; }); @@ -583,11 +598,13 @@ if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value; const t = $('cType').value; const isVideo = t === 'video', isFeat = t === 'featured'; + if (isVideo && !cVidDims && $('cVideoUrl').value) cVidDims = await probeVideoDims($('cVideoUrl').value); await api('/api/my/campaigns', { type: t, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, size: $('cSize').value, title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : t === 'visits' ? $('cVisitTitle').value : $('cTitle').value, body: t === 'solo' ? soloBody : $('cBody').value, videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value), + videoW: cVidDims ? cVidDims.w : null, videoH: cVidDims ? cVidDims.h : null, days: Number($('cFeatDays').value), startDay: featStartDay, count: Number($('cVisitCount').value), ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, @@ -672,9 +689,27 @@ // ── watch-to-earn videos: escape-proof player, server-clock reward ── const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false }; + // detected dimensions of the video being created — portrait goes to Shorts, landscape to Watch videos + let cVidDims = null; + function probeVideoDims(url) { + return new Promise(resolve => { + if (!url) return resolve(null); + const v = document.createElement('video'); v.preload = 'metadata'; v.muted = true; + let done = false; const fin = r => { if (!done) { done = true; resolve(r); } }; + v.onloadedmetadata = () => fin(v.videoWidth && v.videoHeight ? { w: v.videoWidth, h: v.videoHeight } : null); + v.onerror = () => fin(null); setTimeout(() => fin(null), 8000); v.src = url; + }); + } + function showVidOrient() { + const el = $('cVideoInfo'); if (!el) return; + if (!cVidDims) return; + const portrait = cVidDims.h > cVidDims.w; + el.textContent = (el.textContent ? el.textContent + ' · ' : '') + + (portrait ? 'portrait — shows in the Shorts reel' : 'landscape — shows in the Watch videos tab'); + } async function loadVideoStatus() { try { - const st = await (await fetch('/api/my/videos')).json(); + const st = await (await fetch('/api/my/videos?orientation=landscape')).json(); if (st.error) return; $('vidProgress').textContent = 'today: ' + (st.status.count || 0) + ' / ' + st.status.cap + ' videos watched'; if (st.status.left <= 0) { @@ -687,7 +722,7 @@ } async function loadVideoAd() { let r = null; - try { r = await (await fetch('/api/my/videos')).json(); } catch (e) {} + try { r = await (await fetch('/api/my/videos?orientation=landscape')).json(); } catch (e) {} if (!r || !r.ad) { $('vidBox').innerHTML = '' + (r && r.status && r.status.left <= 0 ? 'That is today\'s video set. Come back tomorrow.' diff --git a/public/assets/shorts.js b/public/assets/shorts.js index a749df7..ff69b7e 100644 --- a/public/assets/shorts.js +++ b/public/assets/shorts.js @@ -3,7 +3,7 @@ // then advance to the next. Reuses /api/my/videos + /api/my/videowatch. (function () { const $ = id => document.getElementById(id); - const st = { token: null, secs: 0, maxSeen: 0, credited: false, done: false }; + const st = { token: null, secs: 0, maxSeen: 0, credited: false, done: false, skips: 0 }; function j(url, body) { return fetch(url, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } @@ -13,7 +13,7 @@ st.token = null; st.maxSeen = 0; st.credited = false; st.done = false; $('shOver').hidden = true; $('shCta').hidden = true; $('shNext').hidden = true; let r = null; - try { r = await j('/api/my/videos'); } catch (e) {} + try { r = await j('/api/my/videos?orientation=portrait'); } catch (e) {} if (!r || r.error) { $('shVideo').hidden = true; $('shMsg').hidden = false; $('shMsg').textContent = 'Sign in on the dashboard to watch shorts and earn.'; return; } $('shStat').textContent = 'today: ' + (r.status.count || 0) + ' / ' + r.status.cap; if (!r.ad) { @@ -25,6 +25,13 @@ const v = $('shVideo'); $('shMsg').hidden = true; v.hidden = false; v.src = r.ad.videoUrl; v.currentTime = 0; + // safety net: this reel is portrait-only. If a landscape video slips through, skip to the next. + v.onloadedmetadata = () => { + if (v.videoWidth && v.videoHeight && v.videoWidth > v.videoHeight) { + if (st.skips++ < 4) { load(); return; } + v.hidden = true; $('shMsg').hidden = false; $('shMsg').textContent = 'No shorts in rotation right now. Check back soon.'; + } else { st.skips = 0; } + }; $('shTitle').textContent = r.ad.title || ''; $('shCta').href = r.ad.ctaUrl; $('shCta').textContent = r.ad.ctaLabel || 'Learn more'; $('shOver').hidden = false; $('shCta').hidden = false; diff --git a/public/my.html b/public/my.html index c92a38e..172ddcd 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -627,9 +627,9 @@ - - - - + + + + diff --git a/public/shorts.html b/public/shorts.html index 97b731c..34a83a7 100644 --- a/public/shorts.html +++ b/public/shorts.html @@ -43,6 +43,6 @@ - + diff --git a/server.js b/server.js index a36bf3c..a6a2467 100644 --- a/server.js +++ b/server.js @@ -867,7 +867,8 @@ const server = http.createServer(async (req, res) => { if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' }); const status = await ads.videoStatus(s.email); if (status.left <= 0) return json(res, 200, { ad: null, status }); - const ad = await ads.serveVideo({ excludeEmail: s.email }); // never your own video + const orientation = String(u.searchParams.get('orientation') || ''); // 'portrait' = Shorts reel, 'landscape' = Watch videos tab + const ad = await ads.serveVideo({ excludeEmail: s.email, orientation }); // never your own video if (!ad) return json(res, 200, { ad: null, status }); const token = crypto.randomBytes(16).toString('hex'); videoTokens.set(s.email, { token, ts: Date.now(), id: ad.id, secs: ad.watchSecs }); diff --git a/v.jar b/v.jar new file mode 100644 index 0000000..dcbafd8 --- /dev/null +++ b/v.jar @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_127.0.0.1 FALSE / FALSE 1791369645 iap.sid 2b2c027da3d3e566d594e406c3bd05a175a7845d7ffa702be92fd923ac18d432