From 5ba61db478c83ed54ecee0236c15f18c63035e56 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 18 Mar 2026 09:06:36 +0100 Subject: [PATCH] Add new paywalled features, polish, and favicon New subscriptions: Like ($0.25, does nothing), HD Audio (removes low-pass filter from synth), Recently Played (shows current song), Cancellation Processing ($4.99 to unsubscribe). Polish: welcome banner fades out smoothly, exit ad plays an annoying jingle via Tone.js, 64kbps Suffering Quality badge shown when HD Audio not purchased, favicon added. Moved og-card.svg source to tools/ (not needed in public/). Co-Authored-By: Claude Opus 4.6 (1M context) --- public/app.js | 44 ++++++++++++++++++++++++++++++---- public/favicon.ico | Bin 0 -> 5430 bytes public/index.html | 1 + public/style.css | 1 + tools/favicon.svg | 5 ++++ {public => tools}/og-card.svg | 0 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 public/favicon.ico create mode 100644 tools/favicon.svg rename {public => tools}/og-card.svg (100%) diff --git a/public/app.js b/public/app.js index 10ef8d1..906de5a 100644 --- a/public/app.js +++ b/public/app.js @@ -48,6 +48,10 @@ const SUBS = { exit: { name:"Silent Exit™", price:"$1.99/mo", micro:0.25, desc:"Close the app without an unskippable ad.", color:"#c0392b", dismiss:"I love ads actually" }, eq: { name:"EQ Preset Pack™", price:"$0.50/preset",micro:0.50, desc:"Shape your sound. Shape your spending.", color:"#d35400", dismiss:"Flat is a vibe" }, bluetooth: { name:"Wireless Freedom Pass™", price:"$2.99/mo", micro:0.15, desc:"Cut the cord. Not the cost.", color:"#2980b9", dismiss:"Wires are retro" }, + like: { name:"Appreciation License™", price:"$1.99/mo", micro:0.25, desc:"Express feelings. For a fee.", color:"#e74c3c", dismiss:"I feel nothing" }, + hd: { name:"HD Audio™", price:"$3.99/mo", micro:0.20, desc:"Upgrade from 64kbps Suffering Quality™.", color:"#9b59b6", dismiss:"Lo-fi is a genre" }, + history: { name:"Recently Played™", price:"$2.49/mo", micro:0.15, desc:"We know what you listened to. Pay to find out too.", color:"#1abc9c", dismiss:"I'll forget" }, + unsub: { name:"Cancellation Processing™",price:"$4.99/unsub", micro:4.99, desc:"Freedom from commitment. Commitment from your wallet.", color:"#c0392b", dismiss:"I'll keep paying" }, }; const LYRICS_DATA = [ @@ -88,13 +92,32 @@ const playChaChing = async () => { } catch(e) {} }; +// Annoying jingle for the exit ad +const playAdJingle = async () => { + try { + await Tone.start(); + const s = new Tone.Synth({ + oscillator: { type: "square" }, + envelope: { attack: 0.01, decay: 0.1, sustain: 0.3, release: 0.1 }, + volume: -15 + }).toDestination(); + const now = Tone.now(); + ["C5","E5","G5","C6","G5","E5","C5"].forEach((n, i) => + s.triggerAttackRelease(n, "16n", now + i * 0.12) + ); + setTimeout(() => s.dispose(), 2000); + } catch(e) {} +}; + // ============================================================ // COMPONENTS // ============================================================ function WelcomeBanner({ balance, onStart }) { + const [fading, setFading] = useState(false); + const dismiss = () => { setFading(true); setTimeout(onStart, 400); }; return ( -
+
🎵💸

Pay2Play!

@@ -102,7 +125,7 @@ function WelcomeBanner({ balance, onStart }) { Every feature costs money. Pause, skip, volume — all paywalled. Your complimentary balance: ${balance.toFixed(2)}

-
Part of donothireus.com
@@ -153,6 +176,7 @@ function ExitAd({ onDone }) { const [adIdx, setAdIdx] = useState(() => Math.floor(Math.random() * AD_COPY.length)); const onDoneR = useRef(onDone); onDoneR.current = onDone; + useEffect(() => { playAdJingle(); }, []); useEffect(() => { if(c<=0){onDoneR.current();return;} const t=setTimeout(()=>setC(x=>x-1),1000); return()=>clearTimeout(t); }, [c]); useEffect(() => { const t=setInterval(()=>setAdIdx(i=>(i+1)%AD_COPY.length),3500); return()=>clearInterval(t); }, []); return ( @@ -202,6 +226,8 @@ function PayPlay() { const [welcome, setWelcome] = useState(true); const [bright, setBright] = useState(false); const [balKey, setBalKey] = useState(0); + const [likes, setLikes] = useState(0); + const [hdAudio, setHdAudio] = useState(false); const synthR = useRef(null); const loopR = useRef(null); @@ -209,6 +235,7 @@ function PayPlay() { const visT = useRef(null); const hitLimit = useRef(false); const audioR = useRef(null); + const filterR = useRef(null); const milestoneR = useRef(new Set()); const song = SONGS[ci]; @@ -301,6 +328,7 @@ function PayPlay() { if (audioR.current) { audioR.current.pause(); audioR.current.currentTime = 0; audioR.current = null; } if (loopR.current) { loopR.current.stop(); loopR.current.dispose(); loopR.current = null; } if (synthR.current) { synthR.current.dispose(); synthR.current = null; } + if (filterR.current) { filterR.current.dispose(); filterR.current = null; } try { Tone.getTransport().stop(); } catch(e) {} clearInterval(progT.current); clearInterval(visT.current); setBars(Array(16).fill(2)); @@ -327,9 +355,11 @@ function PayPlay() { return; } - // Tone.js procedural synth + // Tone.js procedural synth (low-pass filtered unless HD Audio purchased) await initTone(); - const synth = new Tone.Synth({ oscillator: { type: s.wave }, envelope: { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.8 }, volume: -30 + vol * 0.3 }).toDestination(); + const filter = new Tone.Filter(hdAudio ? 20000 : 1200, "lowpass").toDestination(); + filterR.current = filter; + const synth = new Tone.Synth({ oscillator: { type: s.wave }, envelope: { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.8 }, volume: -30 + vol * 0.3 }).connect(filter); synthR.current = synth; const loop = new Tone.Loop(time => s.gen(synth, time), 2.5); loopR.current = loop; @@ -338,7 +368,7 @@ function PayPlay() { let p = from; progT.current = setInterval(() => { p += 1; setProg(p); if (p >= s.duration) { clearInterval(progT.current); stopAudio(); setOn(false); setProg(0); } }, 1000); visT.current = setInterval(() => setBars(b => b.map(() => 2 + Math.random() * 28)), 150); - }, [initTone, stopAudio, vol]); + }, [initTone, stopAudio, vol, hdAudio]); useEffect(() => () => stopAudio(), [stopAudio]); @@ -525,11 +555,15 @@ function PayPlay() { {vol}% {!has("volume") && 🔒 23-47%}
+ {!hdAudio && !song.url &&
🔊 64kbps Suffering Quality™ — Upgrade to HD Audio
} {/* Feature buttons (responsive grid) */}
{[ { l:"Lyrics", i:"📝", a:() => tryAct("lyrics", () => setLyr(x => !x)) }, + { l:"HD Audio", i:"🔊", a:() => tryAct("hd", () => { setHdAudio(true); if(filterR.current) filterR.current.frequency.rampTo(20000, 0.5); flash("HD Audio enabled. Hear the difference? (There isn't one.)", "#9b59b6"); }) }, + { l:"Like", i:"❤️", a:() => tryAct("like", () => { setLikes(l => l + 1); flash(`Liked! (${likes + 1} total). This does absolutely nothing.`, "#e74c3c"); }) }, + { l:"History", i:"📜", a:() => tryAct("history", () => flash(`Recently played: "${song.title}". That's it. You're still on it.`, "#1abc9c")) }, { l:"EQ", i:"🎛️", a:() => tryAct("eq", () => flash("EQ: Flat ($0.50). Bass Boost: another $0.50. Each.", "#f39c12")) }, { l:"Bluetooth", i:"📡", a:() => tryAct("bluetooth", () => flash("Bluetooth connected... for now.", "#3498db")) }, { l:"Dark Mode", i:"🌙", a:() => tryAct("dark", () => { setBright(b => !b); flash(!bright ? "Welcome to Light Mode™. Suffering." : "Dark Mode restored. You paid for the default.", !bright ? "#f39c12" : "#00ee44"); }) }, diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..402ce8961c34935e31db1b94792461dd9ac6e851 GIT binary patch literal 5430 zcmcgwONg9B5bk;G&dzI%kBIKPXLe`TT}@(=T{ip5?7BHby(r?kSGWW67S{r^-TZM(|`Z}{IdrKn(n{4s=n&#>gwuI zDy7ou$PtCUsh-U$)m2J0oBi*PKz{?YCD1SgN1u*PDbU^Dic-^a#g1c|E&}?{;?Nxvj4Y|_L;&E6aLUL13Y_d>^lBZPIkUeYuU|% zj8u;f{XiaD#vXI7-YsdlwfD2K{Pax7FJ5_jzpz$f_I^sq_SLj(UCs<`r_GN2uK%c? zeqMQIM()3m3+)%2C0E1)tgL`5(cfA7{a|U0>cVLIB&+{FeUpF62`prDkMC+k3AL$jM>=> zu<0ALTOQj+d~t|*>il{R7{s#8sOIw((7!u`&coRREMn4zd8KlbU^CWgRpgZg#KJOS z0gYqQ;$k$kHrO!m`Nhzj6L*~F*9W7S89TPFy>W4dwm& z4_?|zS?Y+uCjFnNxz{Gn4%#rr4}lHbGx{ybx>w(slGXR70$JBS^R4|roz|Ui=G?w>7V(RF&ZYallj2jB z@xYk$=`l2(n|Z`;RIIT+IR5gqYwvYy!>dk21(FY^-iWM)+UCbYIIKv^^2Yl_V zf;{j-E?C3qxBL6g*djjL@>4UB^2{B?@a0L~qs(DZ*giV(*FMbZTF~IHJQstHbr6HO z+SSJ#&OGYUec#=O^K$adY3cpk_q)MnEHH2QWDKpnjy;|aw5actJAXnNme#Kcj{^28QV;+ ziXX0S}W4vcPm;o*3{E=r|{xNzgN8v~0f{jlzviW63pXns` zOI-e;?s56o;G2Gu{Lh>|F8@+*2l*lC{li=vm(Puz>{-K%yMM?R+`rDd(AG8Sc#`{< zx*PYQ_Q72>uKq}3$E5X#(aAhP8{GG?nNQLO^;djOj(qEAKd}B}Ju+eaNtp!o=dDEb zH)4`?iMAsyKVXhL3ne`x4Dr?@!|MH+Y9~FRq~dn@CSmygnawUV+aq WnspQRK&i|t{EOg!{t@#4*#7~=f=9>z literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html index 912d4e3..3175880 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,7 @@ + diff --git a/public/style.css b/public/style.css index 362f3be..943dd3d 100644 --- a/public/style.css +++ b/public/style.css @@ -10,6 +10,7 @@ body { background: #0d0d1a; font-family: 'IBM Plex Mono', monospace; } @keyframes balFlash { 0% { color:#00ee44 } 20% { color:#e74c3c } 50% { color:#ff6b6b } 100% { color:#00ee44 } } @keyframes countPulse { 0%,100% { transform:scale(1) } 50% { transform:scale(1.1) } } @keyframes successPop { 0% { transform:scale(0.5);opacity:0 } 50% { transform:scale(1.2) } 100% { transform:scale(1);opacity:1 } } +@keyframes fadeOut { from { opacity:1 } to { opacity:0 } } /* Range input — larger thumb for touch targets */ input[type=range] { -webkit-appearance:none; appearance:none; height:6px; border-radius:3px; outline:none; } diff --git a/tools/favicon.svg b/tools/favicon.svg new file mode 100644 index 0000000..614d152 --- /dev/null +++ b/tools/favicon.svg @@ -0,0 +1,5 @@ + + + $ + + diff --git a/public/og-card.svg b/tools/og-card.svg similarity index 100% rename from public/og-card.svg rename to tools/og-card.svg