2026-03-17 16:54:31 +01:00
const { useState , useEffect , useRef , useCallback } = React ;
// ============================================================
// SONG DEFINITIONS
//
2026-03-17 17:09:50 +01:00
// Procedural synth songs are the built-in fallback. If
// audio/songs.json exists (generated by tools/probe-songs.py),
// those MP3-backed songs replace the builtins at startup.
2026-03-17 16:54:31 +01:00
// ============================================================
2026-03-17 17:09:50 +01:00
const BUILTIN _SONGS = [
2026-03-17 16:54:31 +01:00
{ title : "Bureaucratic Sunrise" , artist : "The Paywalls" , duration : 90 , bpm : 120 ,
gen : ( s , t ) => { [ "C4" , "E4" , "G4" , "B4" , "C5" , "B4" , "G4" , "E4" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "8n" , t + i * 0.25 ) ) ; } ,
wave : "triangle" , color : "#e74c3c" } ,
{ title : "Terms of Sorrow" , artist : "Subscription Model" , duration : 105 , bpm : 72 ,
gen : ( s , t ) => { [ "A3" , "C4" , "E4" , "A4" , "G4" , "E4" , "D4" , "C4" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "4n" , t + i * 0.5 ) ) ; } ,
wave : "sine" , color : "#9b59b6" } ,
{ title : "Micro Transaction Blues" , artist : "Nickel & Dime" , duration : 75 , bpm : 140 ,
gen : ( s , t ) => { [ "E3" , "G3" , "A3" , "B3" , "D4" , "B3" , "A3" , "G3" , "E3" , "D3" , "E3" , "G3" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "16n" , t + i * 0.18 ) ) ; } ,
wave : "sawtooth" , color : "#f39c12" } ,
{ title : "404 Feeling Not Found" , artist : "Errorcode" , duration : 80 , bpm : 100 ,
gen : ( s , t ) => { [ "D4" , "F#4" , "A4" , "D5" , "C#5" , "A4" , "F#4" , "D4" , "C#4" , "D4" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "8n." , t + i * 0.35 ) ) ; } ,
wave : "square" , color : "#3498db" } ,
{ title : "EULA Lullaby" , artist : "Fine Print" , duration : 120 , bpm : 56 ,
gen : ( s , t ) => { [ "G3" , "B3" , "D4" , "G4" , "F#4" , "D4" , "B3" , "A3" , "G3" , "B3" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "2n" , t + i * 0.7 ) ) ; } ,
wave : "sine" , color : "#1abc9c" } ,
{ title : "Wallet on Fire" , artist : "DJ Overdraft" , duration : 68 , bpm : 155 ,
gen : ( s , t ) => { [ "C3" , "Eb3" , "G3" , "Bb3" , "C4" , "Eb4" , "G4" , "Eb4" , "C4" , "Bb3" , "G3" , "Eb3" , "C3" ] . forEach ( ( n , i ) => s . triggerAttackRelease ( n , "16n" , t + i * 0.15 ) ) ; } ,
wave : "sawtooth" , color : "#e67e22" } ,
] ;
2026-03-17 17:09:50 +01:00
// Mutable song list — replaced by audio/songs.json if available
let SONGS = BUILTIN _SONGS ;
2026-03-17 16:54:31 +01:00
const SUBS = {
pause : { name : "Pause Plus™" , price : "$2.99/mo" , micro : 0.01 , desc : "The freedom to stop. Whenever you want." , color : "#e74c3c" , dismiss : "I like surprises" } ,
resume : { name : "Resume Rights™" , price : "$2.49/mo" , micro : 0.02 , desc : "You paused. Now un-pause. Two different things." , color : "#e67e22" , dismiss : "Silence suits me" } ,
repeat : { name : "Loop Loyalty™" , price : "$5.99/mo" , micro : 0.03 , desc : "Hear it again. And again. And again." , color : "#9b59b6" , dismiss : "Once was enough" } ,
unrepeat : { name : "Premium Loop Loyalty™" , price : "$8.99/mo" , micro : 0.05 , desc : "Escape the loop. If you dare." , color : "#8e44ad" , dismiss : "I'll loop forever" } ,
skip : { name : "Skip Sprint™" , price : "$4.99/mo" , micro : 0.05 , desc : "Life's too short. Songs shouldn't be." , color : "#3498db" , dismiss : "I'll wait it out" } ,
prev : { name : "Nostalgia Pass™" , price : "$6.99/mo" , micro : 0.10 , desc : "Go back. Premium memories cost more." , color : "#1abc9c" , dismiss : "The past is gone" } ,
volume : { name : "Volume Freedom™" , price : "$3.99/mo" , micro : 0.08 , desc : "Break free from the 23-47% corridor." , color : "#f39c12" , dismiss : "35% is fine actually" } ,
seek : { name : "Precision Seek™" , price : "$2.99/mo" , micro : 0.04 , desc : "Go where you mean to go. Radical concept." , color : "#e74c3c" , dismiss : "I'll guess" } ,
shuffle : { name : "Smart Shuffle™" , price : "$6.99/mo" , micro : 0.06 , desc : "Actually random. Not weighted against you." , color : "#2ecc71" , dismiss : "Order is fine" } ,
queue : { name : "Queue Capacity Pack™" , price : "$1.49/3 slots" , micro : 0.50 , desc : "Your queue is full (1 song max). Expand it." , color : "#e74c3c" , dismiss : "One song is plenty" } ,
dark : { name : "Dark Mode™" , price : "$0.99/mo" , micro : 0.99 , desc : "Easy on the eyes. Hard on the wallet." , color : "#34495e" , dismiss : "I love bright screens" } ,
lyrics : { name : "Full Verse™" , price : "$3.49/mo" , micro : 0.07 , desc : "See ALL the words. Not just some of them." , color : "#e74c3c" , dismiss : "I'll guess the lyrics" } ,
continue : { name : "Continuity Plan™" , price : "$4.99/mo" , micro : 0.02 , desc : "Hear beyond 3 seconds. Revolutionary." , color : "#16a085" , dismiss : "3 seconds is enough" } ,
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" } ,
2026-03-18 09:06:36 +01:00
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" } ,
2026-03-17 16:54:31 +01:00
} ;
const LYRICS _DATA = [
[ [ "We" , "charge" , "for" , "every" , "click" ] , [ "Your" , "wallet" , "feels" , "the" , "pain" ] , [ "Subscribe" , "or" , "live" , "in" , "silence" ] , [ "The" , "music" , "never" , "stops" ] , [ "Unless" , "you" , "pay" , "to" , "pause" ] ] ,
[ [ "Somewhere" , "deep" , "inside" , "the" , "EULA" ] , [ "A" , "clause" , "awaits" , "your" , "soul" ] , [ "Terms" , "conditions" , "fine" , "print" , "sorrow" ] , [ "Accept" , "decline" , "accept" , "forever" ] , [ "Your" , "data" , "sings" , "for" , "us" ] ] ,
[ [ "Micro" , "transactions" , "falling" , "like" , "rain" ] , [ "A" , "penny" , "here" , "a" , "dollar" , "there" ] , [ "Your" , "balance" , "slowly" , "fading" ] , [ "But" , "music" , "keeps" , "on" , "playing" ] , [ "Three" , "seconds" , "at" , "a" , "time" ] ] ,
[ [ "Error" , "four" , "oh" , "four" , "feeling" ] , [ "Not" , "found" , "inside" , "this" , "app" ] , [ "Pay" , "twenty" , "five" , "cents" , "learn" ] , [ "What" , "went" , "wrong" , "this" , "time" ] , [ "Spoiler" , "it" , "was" , "you" ] ] ,
[ [ "Hush" , "little" , "user" , "don't" , "say" , "word" ] , [ "Papa's" , "gonna" , "buy" , "premium" , "tier" ] , [ "And" , "if" , "that" , "premium" , "runs" , "out" ] , [ "Papa's" , "gonna" , "charge" , "card" , "again" ] , [ "Sleep" , "tight" , "wallet" , "light" ] ] ,
[ [ "Fire" , "fire" , "wallet" , "burning" , "bright" ] , [ "Every" , "button" , "costs" , "a" , "fee" ] , [ "The" , "flames" , "of" , "monetization" ] , [ "Light" , "the" , "path" , "to" , "bankruptcy" ] , [ "Dance" , "dance" , "overdraft" , "dance" ] ] ,
] ;
const AD _COPY = [
"TIRED OF AFFORDABLE MUSIC? Try Pay2Play! Premium Ultra™ — now with 40% more paywalls!" ,
"Did you know? The average Pay2Play! user spends $847/year. Be above average." ,
"EXCLUSIVE: Unlock the ability to unlock abilities with our Meta-Unlock Bundle™!" ,
"Pay $19.99/mo to remove this ad. Or $29.99/mo to also remove the next one." ,
"Your listening data has been sold to 47 advertisers. Upgrade to Privacy Plus™ ($12.99/mo) to make it 46." ,
"Pay2Play! Kids™ — Teach your children the value of money. One paywall at a time." ,
"RATE US 5 STARS and receive absolutely nothing in return!" ,
"Pay2Play! was voted #1 Music Player by people who had no other choice." ,
] ;
const fmt = s => ` ${ Math . floor ( s / 60 ) } : ${ String ( Math . floor ( s % 60 ) ) . padStart ( 2 , "0" ) } ` ;
2026-03-18 09:16:20 +01:00
const track = ( event , props ) => { try { window . plausible ( event , { props } ) ; } catch ( e ) { } } ;
2026-03-17 16:54:31 +01:00
// Cha-ching sound effect via Tone.js
const playChaChing = async ( ) => {
try {
await Tone . start ( ) ;
const s = new Tone . Synth ( {
oscillator : { type : "triangle" } ,
envelope : { attack : 0.001 , decay : 0.12 , sustain : 0 , release : 0.08 } ,
volume : - 10
} ) . toDestination ( ) ;
const now = Tone . now ( ) ;
s . triggerAttackRelease ( "C7" , "32n" , now ) ;
s . triggerAttackRelease ( "E7" , "32n" , now + 0.08 ) ;
setTimeout ( ( ) => s . dispose ( ) , 500 ) ;
} catch ( e ) { }
} ;
2026-03-18 09:06:36 +01:00
// 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 ) { }
} ;
2026-03-17 16:54:31 +01:00
// ============================================================
// COMPONENTS
// ============================================================
function WelcomeBanner ( { balance , onStart } ) {
2026-03-18 09:06:36 +01:00
const [ fading , setFading ] = useState ( false ) ;
2026-03-18 09:31:28 +01:00
const dismiss = ( ) => { setFading ( true ) ; track ( "click: start_spending" ) ; setTimeout ( onStart , 400 ) ; } ;
2026-03-17 16:54:31 +01:00
return (
2026-03-18 09:06:36 +01:00
< div style = { { position : "fixed" , inset : 0 , background : "rgba(0,0,0,0.95)" , display : "flex" , alignItems : "center" , justifyContent : "center" , zIndex : 3000 , backdropFilter : "blur(8px)" , animation : fading ? "fadeOut 0.4s ease forwards" : "none" } } >
2026-03-17 16:54:31 +01:00
< div style = { { textAlign : "center" , maxWidth : 340 , padding : "40px 28px" } } >
< div style = { { fontSize : 48 , marginBottom : 16 } } > 🎵 💸 < / d i v >
< h1 style = { { fontSize : 36 , fontWeight : 900 , fontFamily : "'Anybody',system-ui,sans-serif" , color : "#fff" , marginBottom : 12 , letterSpacing : - 1 } } > Pay2Play ! < / h 1 >
< p style = { { fontSize : 13 , color : "#8899aa" , lineHeight : 1.7 , marginBottom : 24 } } >
Every feature costs money . Pause , skip , volume — all paywalled .
Your complimentary balance : < span style = { { color : "#00ee44" , fontWeight : 700 } } > $ { balance . toFixed ( 2 ) } < / s p a n >
< / p >
2026-03-18 09:06:36 +01:00
< button onClick = { dismiss } style = { { background : "linear-gradient(135deg,#00ee44,#00aa33)" , color : "#000" , border : "none" , borderRadius : 12 , padding : "16px 36px" , fontSize : 16 , fontWeight : 700 , cursor : "pointer" , boxShadow : "0 4px 20px rgba(0,238,68,0.3)" } } >
2026-03-17 16:54:31 +01:00
Start Spending
< / b u t t o n >
< div style = { { fontSize : 10 , color : "#556677" , marginTop : 20 } } > Part of donothireus . com < / d i v >
< / d i v >
< / d i v >
) ;
}
function Modal ( { sub , phase , successText , onSub , onMicro , onClose , onTopUp } ) {
if ( ! sub ) return null ;
const s = SUBS [ sub ] ;
return (
< div style = { { position : "fixed" , inset : 0 , background : "rgba(0,0,0,0.85)" , display : "flex" , alignItems : "center" , justifyContent : "center" , zIndex : 1000 , backdropFilter : "blur(4px)" } } >
< div style = { { background : "#1a1a2e" , border : ` 2px solid ${ s . color } ` , borderRadius : 16 , padding : "32px 28px" , maxWidth : 400 , width : "90%" , textAlign : "center" , boxShadow : ` 0 0 40px ${ s . color } 44 ` , animation : "modalIn 0.25s ease-out" } } >
{ phase === "success" ? (
< >
< div style = { { fontSize : 48 , animation : "successPop 0.4s ease-out" , marginBottom : 12 } } > ✅ < / d i v >
< div style = { { fontSize : 18 , fontWeight : 700 , color : "#fff" , fontFamily : "'Anybody',system-ui,sans-serif" , marginBottom : 8 } } > Congratulations ! < / d i v >
< div style = { { fontSize : 13 , color : "#8899aa" , fontStyle : "italic" } } > { successText } < / d i v >
< / >
) : phase === "broke" ? (
< >
< div style = { { fontSize : 48 , marginBottom : 12 } } > 💸 < / d i v >
< div style = { { fontSize : 18 , fontWeight : 700 , color : "#e74c3c" , fontFamily : "'Anybody',system-ui,sans-serif" , marginBottom : 8 } } > Insufficient Funds < / d i v >
< div style = { { fontSize : 13 , color : "#8899aa" , marginBottom : 24 } } > Even your wallet is on a free tier . < / d i v >
< button onClick = { onTopUp } style = { { width : "100%" , padding : "14px 16px" , background : "linear-gradient(135deg,#00ee44,#00aa33)" , color : "#000" , border : "none" , borderRadius : 10 , fontSize : 15 , fontWeight : 700 , cursor : "pointer" , marginBottom : 12 } } > Add $10 ( + $0 . 50 fee ) < / b u t t o n >
2026-03-18 09:31:28 +01:00
< button onClick = { ( ) => { track ( "click: accept_poverty" ) ; onClose ( ) ; } } style = { { background : "none" , border : "none" , color : "#667788" , fontSize : 11 , cursor : "pointer" , textDecoration : "underline" } } > Accept poverty < / b u t t o n >
2026-03-17 16:54:31 +01:00
< / >
) : (
< >
< div style = { { fontSize : 12 , fontWeight : 700 , letterSpacing : 3 , color : s . color , marginBottom : 8 } } > ⚡ FEATURE LOCKED ⚡ < / d i v >
< div style = { { fontSize : 22 , fontWeight : 800 , color : "#fff" , marginBottom : 6 , fontFamily : "'Anybody',system-ui,sans-serif" } } > { s . name } < / d i v >
< div style = { { fontSize : 14 , color : "#8899aa" , marginBottom : 24 , lineHeight : 1.5 , fontStyle : "italic" } } > { s . desc } < / d i v >
< button onClick = { onSub } style = { { width : "100%" , padding : "14px 16px" , background : ` linear-gradient(135deg, ${ s . color } , ${ s . color } cc) ` , color : "#fff" , border : "none" , borderRadius : 10 , fontSize : 15 , fontWeight : 700 , cursor : "pointer" , marginBottom : 10 } } > SUBSCRIBE — { s . price } < / b u t t o n >
< button onClick = { onMicro } style = { { width : "100%" , padding : "12px 16px" , background : "transparent" , color : s . color , border : ` 1px solid ${ s . color } 66 ` , borderRadius : 10 , fontSize : 13 , cursor : "pointer" , marginBottom : 16 , fontWeight : 600 } } > One - time use — $ { s . micro . toFixed ( 2 ) } < / b u t t o n >
2026-03-18 09:31:28 +01:00
< button onClick = { ( ) => { track ( "click: dismiss" , { feature : sub } ) ; onClose ( ) ; } } style = { { background : "none" , border : "none" , color : "#667788" , fontSize : 11 , cursor : "pointer" , textDecoration : "underline" } } > { s . dismiss } < / b u t t o n >
2026-03-17 16:54:31 +01:00
< / >
) }
< / d i v >
< / d i v >
) ;
}
function ExitAd ( { onDone } ) {
const [ c , setC ] = useState ( 15 ) ;
const [ adIdx , setAdIdx ] = useState ( ( ) => Math . floor ( Math . random ( ) * AD _COPY . length ) ) ;
const onDoneR = useRef ( onDone ) ;
onDoneR . current = onDone ;
2026-03-18 09:06:36 +01:00
useEffect ( ( ) => { playAdJingle ( ) ; } , [ ] ) ;
2026-03-17 16:54:31 +01:00
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 (
< div style = { { position : "fixed" , inset : 0 , background : "#000" , display : "flex" , flexDirection : "column" , alignItems : "center" , justifyContent : "center" , zIndex : 2000 , color : "#fff" } } >
< div style = { { fontSize : 11 , letterSpacing : 3 , color : "#e74c3c" , marginBottom : 16 } } > UNSKIPPABLE AD < / d i v >
< div style = { { fontSize : 56 , fontWeight : 900 , fontFamily : "'Anybody',system-ui,sans-serif" , animation : "countPulse 1s ease infinite" } } > { c } s < / d i v >
< div style = { { fontSize : 13 , color : "#8899aa" , marginTop : 24 , maxWidth : 320 , textAlign : "center" , lineHeight : 1.6 , minHeight : 80 , padding : "0 20px" } } > { AD _COPY [ adIdx ] } < / d i v >
< div style = { { fontSize : 11 , color : "#334455" , marginTop : 32 , fontStyle : "italic" } } > Subscribe to Silent Exit ™ ( $1 . 99 / mo ) to skip this < / d i v >
< / d i v >
) ;
}
function CBtn ( { onClick , color , size = 28 , children , label } ) {
return (
< button onClick = { onClick } aria - label = { label } style = { { background : "none" , border : "none" , fontSize : size , cursor : "pointer" , color , padding : 8 , minWidth : 44 , minHeight : 44 , display : "flex" , alignItems : "center" , justifyContent : "center" , filter : ` drop-shadow(0 0 4px ${ color } 44) ` } } >
{ children }
< / b u t t o n >
) ;
}
// ============================================================
// MAIN APP
// ============================================================
function PayPlay ( ) {
const [ ci , setCi ] = useState ( 0 ) ;
const [ on , setOn ] = useState ( false ) ;
const [ prog , setProg ] = useState ( 0 ) ;
const [ vol , setVol ] = useState ( 35 ) ;
const [ rep , setRep ] = useState ( false ) ;
const [ shuf , setShuf ] = useState ( false ) ;
const [ modal , setModal ] = useState ( null ) ;
const [ modalPhase , setModalPhase ] = useState ( "choose" ) ;
const [ successText , setSuccessText ] = useState ( "" ) ;
const [ bal , setBal ] = useState ( ( ) => + ( 0.50 + Math . random ( ) * 9.50 ) . toFixed ( 2 ) ) ;
const [ subs , setSubs _ ] = useState ( { } ) ;
const [ toastMsg , setToastMsg ] = useState ( "" ) ;
const [ toastColor , setToastColor ] = useState ( "#00ee44" ) ;
const [ toastVis , setToastVis ] = useState ( false ) ;
const [ lyr , setLyr ] = useState ( false ) ;
const [ showExit , setShowExit ] = useState ( false ) ;
const [ showAd , setShowAd ] = useState ( false ) ;
const [ spent , setSpent ] = useState ( 0 ) ;
const [ acts , setActs ] = useState ( 0 ) ;
const [ toneOk , setToneOk ] = useState ( false ) ;
const [ bars , setBars ] = useState ( Array ( 16 ) . fill ( 2 ) ) ;
const [ welcome , setWelcome ] = useState ( true ) ;
const [ bright , setBright ] = useState ( false ) ;
const [ balKey , setBalKey ] = useState ( 0 ) ;
2026-03-18 09:06:36 +01:00
const [ likes , setLikes ] = useState ( 0 ) ;
const [ hdAudio , setHdAudio ] = useState ( false ) ;
2026-03-17 16:54:31 +01:00
const synthR = useRef ( null ) ;
const loopR = useRef ( null ) ;
const progT = useRef ( null ) ;
const visT = useRef ( null ) ;
const hitLimit = useRef ( false ) ;
const audioR = useRef ( null ) ;
2026-03-18 09:06:36 +01:00
const filterR = useRef ( null ) ;
2026-03-17 16:54:31 +01:00
const milestoneR = useRef ( new Set ( ) ) ;
const song = SONGS [ ci ] ;
const has = k => subs [ k ] ;
// Theme-aware colors (toggle when Dark Mode joke triggers bright mode)
const bg = bright ? "#f5f5f0" : "#0d0d1a" ;
const card = bright ? "#e8e8f0" : "#1c1c32" ;
const text = bright ? "#111" : "#fff" ;
const muted = bright ? "#555" : "#7788aa" ;
const accent = bright ? "#00aa33" : "#00ee44" ;
// Sync body background with theme
useEffect ( ( ) => { document . body . style . background = bg ; } , [ bright ] ) ;
// Spending milestones
useEffect ( ( ) => {
const checks = [ [ 1 , "Over $1 spent. Getting started." ] , [ 5 , "$5 down. Commitment issues?" ] , [ 10 , "$10 gone. You could buy actual music." ] , [ 20 , "$20. Seek professional help." ] ] ;
checks . forEach ( ( [ threshold , msg ] ) => {
if ( spent >= threshold && ! milestoneR . current . has ( threshold ) ) {
milestoneR . current . add ( threshold ) ;
setTimeout ( ( ) => flash ( msg , "#f39c12" ) , 600 ) ;
}
} ) ;
} , [ spent ] ) ;
// Color-coded toasts: red for charges, green for unlocks, yellow for info
const flash = useCallback ( ( msg , color = "#00ee44" ) => {
setToastMsg ( msg ) ; setToastColor ( color ) ; setToastVis ( true ) ;
setTimeout ( ( ) => setToastVis ( false ) , 2500 ) ;
} , [ ] ) ;
// Charge with cha-ching sound + balance flash animation
const charge = useCallback ( ( a , l ) => {
setBal ( b => Math . max ( 0 , + ( b - a ) . toFixed ( 2 ) ) ) ;
setSpent ( t => + ( t + a ) . toFixed ( 2 ) ) ;
setActs ( x => x + 1 ) ;
setBalKey ( k => k + 1 ) ;
flash ( ` - $ ${ a . toFixed ( 2 ) } • ${ l } ` , "#e74c3c" ) ;
playChaChing ( ) ;
} , [ flash ] ) ;
const tryAct = ( k , fn ) => {
2026-03-18 09:25:14 +01:00
track ( "click: " + k , { subscribed : ! ! has ( k ) } ) ;
2026-03-17 16:54:31 +01:00
if ( has ( k ) ) { fn ( ) ; return ; }
setModal ( { key : k , action : fn } ) ;
setModalPhase ( "choose" ) ;
} ;
// Modal: subscribe (full price)
const onSubClick = ( ) => {
if ( ! modal ) return ;
const s = SUBS [ modal . key ] , p = parseFloat ( s . price . replace ( /[^0-9.]/g , "" ) ) ;
if ( bal < p ) { setModalPhase ( "broke" ) ; return ; }
charge ( p , s . name ) ;
setSubs _ ( prev => ( { ... prev , [ modal . key ] : true } ) ) ;
2026-03-18 09:16:20 +01:00
track ( "subscribe" , { feature : modal . key , price : p } ) ;
2026-03-17 16:54:31 +01:00
setSuccessText ( ` ${ s . name } is yours. Was it worth it? (It wasn't.) ` ) ;
setModalPhase ( "success" ) ;
setTimeout ( ( ) => { modal . action ( ) ; setModal ( null ) ; setModalPhase ( "choose" ) ; } , 1800 ) ;
// Humor: pause and resume are separate products
if ( modal . key === "pause" ) setTimeout ( ( ) => flash ( "Note: unpausing requires Resume Rights™" , "#f39c12" ) , 2500 ) ;
} ;
// Modal: one-time micro-transaction
const onMicroClick = ( ) => {
if ( ! modal ) return ;
const s = SUBS [ modal . key ] ;
if ( bal < s . micro ) { setModalPhase ( "broke" ) ; return ; }
charge ( s . micro , ` ${ s . name } (1x) ` ) ;
2026-03-18 09:16:20 +01:00
track ( "microtransaction" , { feature : modal . key , price : s . micro } ) ;
2026-03-17 16:54:31 +01:00
setSuccessText ( ` One-time access. That's $ ${ s . micro . toFixed ( 2 ) } you'll never see again. ` ) ;
setModalPhase ( "success" ) ;
setTimeout ( ( ) => { modal . action ( ) ; setModal ( null ) ; setModalPhase ( "choose" ) ; } , 1500 ) ;
} ;
// Modal: top-up from insufficient-funds screen (with fee)
const handleModalTopUp = ( ) => {
setBal ( b => + ( b + 9.50 ) . toFixed ( 2 ) ) ;
setSpent ( s => + ( s + 0.50 ) . toFixed ( 2 ) ) ;
setActs ( x => x + 1 ) ;
setBalKey ( k => k + 1 ) ;
flash ( "+$9.50 ($0.50 processing fee)" , "#f39c12" ) ;
playChaChing ( ) ;
2026-03-18 09:16:20 +01:00
track ( "topup" , { source : "modal" } ) ;
2026-03-17 16:54:31 +01:00
setModalPhase ( "choose" ) ;
} ;
// ---- Audio engine ----
const initTone = useCallback ( async ( ) => { if ( ! toneOk ) { await Tone . start ( ) ; setToneOk ( true ) ; } } , [ toneOk ] ) ;
const stopAudio = useCallback ( ( ) => {
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 ; }
2026-03-18 09:06:36 +01:00
if ( filterR . current ) { filterR . current . dispose ( ) ; filterR . current = null ; }
2026-03-17 16:54:31 +01:00
try { Tone . getTransport ( ) . stop ( ) ; } catch ( e ) { }
clearInterval ( progT . current ) ; clearInterval ( visT . current ) ;
setBars ( Array ( 16 ) . fill ( 2 ) ) ;
} , [ ] ) ;
const startAudio = useCallback ( async ( idx , from = 0 ) => {
stopAudio ( ) ; hitLimit . current = false ;
const s = SONGS [ idx ] ;
// HTML5 Audio for self-hosted MP3s
if ( s . url ) {
const audio = new Audio ( s . url ) ;
audio . currentTime = from ;
audio . volume = vol / 100 ;
audioR . current = audio ;
audio . play ( ) . catch ( ( ) => { } ) ;
let p = from ;
progT . current = setInterval ( ( ) => {
p = Math . floor ( audio . currentTime ) ;
setProg ( p ) ;
if ( p >= s . duration || audio . ended ) { clearInterval ( progT . current ) ; stopAudio ( ) ; setOn ( false ) ; setProg ( 0 ) ; }
} , 1000 ) ;
visT . current = setInterval ( ( ) => setBars ( b => b . map ( ( ) => 2 + Math . random ( ) * 28 ) ) , 150 ) ;
return ;
}
2026-03-18 09:06:36 +01:00
// Tone.js procedural synth (low-pass filtered unless HD Audio purchased)
2026-03-17 16:54:31 +01:00
await initTone ( ) ;
2026-03-18 09:06:36 +01:00
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 ) ;
2026-03-17 16:54:31 +01:00
synthR . current = synth ;
const loop = new Tone . Loop ( time => s . gen ( synth , time ) , 2.5 ) ;
loopR . current = loop ;
Tone . getTransport ( ) . bpm . value = s . bpm ;
Tone . getTransport ( ) . start ( ) ; loop . start ( 0 ) ;
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 ) ;
2026-03-18 09:06:36 +01:00
} , [ initTone , stopAudio , vol , hdAudio ] ) ;
2026-03-17 16:54:31 +01:00
useEffect ( ( ) => ( ) => stopAudio ( ) , [ stopAudio ] ) ;
// 3-second free limit with audio degradation at 2s
useEffect ( ( ) => {
if ( ! on || has ( "continue" ) ) return ;
// Muffle audio 1 second before cutoff
if ( prog === 2 ) {
if ( synthR . current ) synthR . current . volume . rampTo ( - 40 , 0.8 ) ;
if ( audioR . current ) audioR . current . volume = 0.1 ;
}
// Hard cutoff at 3 seconds
if ( prog >= 3 && ! hitLimit . current ) {
hitLimit . current = true ; stopAudio ( ) ; setOn ( false ) ;
setModal ( { key : "continue" , action : ( ) => { hitLimit . current = false ; setOn ( true ) ; startAudio ( ci , prog ) ; } } ) ;
setModalPhase ( "choose" ) ;
}
} , [ prog , on , subs ] ) ;
// ---- User actions ----
const handlePlay = async ( ) => {
2026-03-18 09:31:28 +01:00
track ( on ? "click: pause" : "click: play" , { song : song . title } ) ;
2026-03-17 16:54:31 +01:00
if ( on ) { tryAct ( "pause" , ( ) => { stopAudio ( ) ; setOn ( false ) ; } ) ; }
else {
if ( prog > 0 && ! has ( "continue" ) ) {
tryAct ( "resume" , ( ) => { setOn ( true ) ; startAudio ( ci , prog ) ; } ) ;
} else {
if ( ! SONGS [ ci ] . url ) await initTone ( ) ;
hitLimit . current = false ; setOn ( true ) ; startAudio ( ci , prog ) ;
}
}
} ;
const changeSong = idx => { stopAudio ( ) ; setCi ( idx ) ; setProg ( 0 ) ; setOn ( false ) ; hitLimit . current = false ; setBars ( Array ( 16 ) . fill ( 2 ) ) ; } ;
const handleSeek = e => {
const r = e . currentTarget . getBoundingClientRect ( ) , pct = ( e . clientX - r . left ) / r . width , tgt = Math . floor ( pct * song . duration ) ;
2026-03-18 09:31:28 +01:00
track ( "click: seek" , { subscribed : ! ! has ( "seek" ) } ) ;
2026-03-17 16:54:31 +01:00
if ( has ( "seek" ) ) { setProg ( tgt ) ; if ( on ) { stopAudio ( ) ; startAudio ( ci , tgt ) ; } }
else { setProg ( Math . max ( 0 , Math . min ( song . duration , tgt + Math . floor ( Math . random ( ) * 40 ) - 15 ) ) ) ; flash ( "Seek landed somewhere in the general vicinity." , "#f39c12" ) ; }
} ;
const handleSeekKey = e => {
let tgt = prog ;
if ( e . key === "ArrowRight" ) tgt = Math . min ( song . duration , prog + 5 ) ;
else if ( e . key === "ArrowLeft" ) tgt = Math . max ( 0 , prog - 5 ) ;
else return ;
e . preventDefault ( ) ;
if ( has ( "seek" ) ) { setProg ( tgt ) ; if ( on ) { stopAudio ( ) ; startAudio ( ci , tgt ) ; } }
else { setProg ( Math . max ( 0 , Math . min ( song . duration , tgt + Math . floor ( Math . random ( ) * 40 ) - 15 ) ) ) ; flash ( "Seek precision is approximate. Very approximate." , "#f39c12" ) ; }
} ;
const handleVol = e => {
const v = parseInt ( e . target . value ) ;
if ( ! has ( "volume" ) && ( v < 23 || v > 47 ) ) { tryAct ( "volume" , ( ) => { setVol ( v ) ; if ( synthR . current ) synthR . current . volume . value = - 30 + v * 0.3 ; if ( audioR . current ) audioR . current . volume = v / 100 ; } ) ; return ; }
setVol ( v ) ; if ( synthR . current ) synthR . current . volume . value = - 30 + v * 0.3 ; if ( audioR . current ) audioR . current . volume = v / 100 ;
} ;
const shareReceipt = ( ) => {
const lines = [
"Pay2Play! Receipt" , "" ,
` Total spent: $ ${ spent . toFixed ( 2 ) } ` ,
` Actions: ${ acts } ( ${ acts > 0 ? ` $ ${ ( spent / acts ) . toFixed ( 2 ) } /action ` : "free" } ) ` ,
` Subscriptions: ${ Object . keys ( subs ) . length } ` ,
Object . keys ( subs ) . length > 0 ? Object . keys ( subs ) . map ( k => SUBS [ k ] . name ) . join ( ", " ) : "None (cheapskate)" ,
2026-03-18 08:55:07 +01:00
"" , ` Try it: ${ window . location . href } `
2026-03-17 16:54:31 +01:00
] ;
if ( navigator . clipboard ) {
2026-03-18 09:16:20 +01:00
navigator . clipboard . writeText ( lines . join ( "\n" ) ) . then ( ( ) => { flash ( "Receipt copied! Share your shame." , accent ) ; track ( "share_receipt" , { spent : spent . toFixed ( 2 ) } ) ; } ) ;
2026-03-17 16:54:31 +01:00
}
} ;
// Volume slider gradient: shows free zone (23-47%) when not subscribed
const volBg = has ( "volume" )
? ` linear-gradient(to right, ${ song . color } ${ vol } %, ${ card } ${ vol } %) `
: ` linear-gradient(to right, ${ card } 0%, ${ card } 23%, ${ song . color } 23%, ${ song . color } ${ vol } %, ${ song . color } 33 ${ vol } %, ${ song . color } 33 47%, ${ card } 47%) ` ;
const curLyrics = LYRICS _DATA [ ci ] || LYRICS _DATA [ 0 ] ;
const toastTextColor = [ "#e74c3c" , "#f39c12" ] . includes ( toastColor ) ? "#fff" : "#000" ;
// ============================================================
// RENDER
// ============================================================
return (
< >
{ welcome && < WelcomeBanner balance = { bal } onStart = { ( ) => setWelcome ( false ) } / > }
{ showAd && < ExitAd onDone = { ( ) => { setShowAd ( false ) ; setShowExit ( true ) ; } } / > }
{ /* Exit / receipt screen */ }
{ showExit && (
< div style = { { position : "fixed" , inset : 0 , background : bg , display : "flex" , alignItems : "center" , justifyContent : "center" , zIndex : 2000 , flexDirection : "column" } } >
< div style = { { fontSize : 42 , marginBottom : 16 } } > 👋 < / d i v >
< div style = { { color : text , fontSize : 20 , fontWeight : 700 , fontFamily : "'Anybody',system-ui,sans-serif" } } > Thanks for listening ! < / d i v >
< div style = { { color : muted , fontSize : 14 , marginTop : 8 } } > Total spent : < span style = { { color : "#e74c3c" , fontWeight : 700 } } > $ { spent . toFixed ( 2 ) } < / s p a n > < / d i v >
< div style = { { color : muted , fontSize : 12 , marginTop : 4 } } > { acts } actions • $ { acts > 0 ? ( spent / acts ) . toFixed ( 2 ) : "0.00" } / action < / d i v >
< div style = { { color : muted , fontSize : 12 , marginTop : 4 } } > { Object . keys ( subs ) . length } subscription { Object . keys ( subs ) . length !== 1 ? "s" : "" } < / d i v >
< div style = { { display : "flex" , gap : 12 , marginTop : 24 } } >
< button onClick = { shareReceipt } style = { { padding : "10px 24px" , background : accent , color : "#000" , border : "none" , borderRadius : 8 , cursor : "pointer" , fontSize : 12 , fontWeight : 700 } } > Share Receipt < / b u t t o n >
2026-03-18 09:31:28 +01:00
< button onClick = { ( ) => { track ( "click: return_to_suffering" ) ; setShowExit ( false ) ; } } style = { { padding : "10px 24px" , background : "transparent" , color : muted , border : ` 1px solid ${ muted } 44 ` , borderRadius : 8 , cursor : "pointer" , fontSize : 12 } } > Return to suffering < / b u t t o n >
2026-03-17 16:54:31 +01:00
< / d i v >
< / d i v >
) }
< Modal sub = { modal ? . key } phase = { modalPhase } successText = { successText } onSub = { onSubClick } onMicro = { onMicroClick } onClose = { ( ) => { setModal ( null ) ; setModalPhase ( "choose" ) ; } } onTopUp = { handleModalTopUp } / >
{ /* Toast notification */ }
< div style = { { position : "fixed" , bottom : 90 , left : "50%" , transform : ` translateX(-50%) translateY( ${ toastVis ? 0 : 20 } px) ` , background : toastColor , color : toastTextColor , padding : "10px 20px" , borderRadius : 8 , fontWeight : 700 , fontSize : 13 , opacity : toastVis ? 1 : 0 , transition : "opacity 0.3s ease, transform 0.3s ease" , zIndex : 999 , boxShadow : ` 0 4px 20px ${ toastColor } 44 ` , pointerEvents : "none" } } > { toastMsg } < / d i v >
{ /* ---- Main player ---- */ }
< div style = { { background : bg , minHeight : "100vh" , color : text , fontFamily : "'IBM Plex Mono',monospace" , display : "flex" , flexDirection : "column" , alignItems : "center" , paddingBottom : 24 , transition : "background 0.5s ease, color 0.3s ease" } } >
{ /* Sticky header with balance */ }
< header style = { { width : "100%" , maxWidth : 420 , display : "flex" , justifyContent : "space-between" , alignItems : "center" , padding : "12px 20px" , marginBottom : 16 , position : "sticky" , top : 0 , zIndex : 100 , background : bg , transition : "background 0.5s ease" } } >
< div >
< div style = { { fontSize : 11 , letterSpacing : 3 , color : muted } } > PAY2PLAY ! < / d i v >
< div style = { { fontSize : 9 , color : muted , opacity : 0.6 } } > The Worst Music Player < / d i v >
< / d i v >
< div style = { { display : "flex" , gap : 10 , alignItems : "center" } } >
< div key = { balKey } style = { { background : card , padding : "6px 14px" , borderRadius : 20 , fontSize : 12 , fontWeight : 700 , color : accent , animation : balKey > 0 ? "balFlash 0.6s ease" : "none" , transition : "background 0.3s ease" } } > 💰 $ { bal . toFixed ( 2 ) } < / d i v >
2026-03-18 09:16:20 +01:00
< button onClick = { ( ) => { setBal ( b => + ( b + 9.50 ) . toFixed ( 2 ) ) ; setSpent ( s => + ( s + 0.50 ) . toFixed ( 2 ) ) ; setActs ( x => x + 1 ) ; setBalKey ( k => k + 1 ) ; flash ( "+$9.50 ($0.50 processing fee)" , "#f39c12" ) ; playChaChing ( ) ; track ( "topup" , { source : "header" } ) ; } } style = { { background : accent , color : "#000" , border : "none" , borderRadius : 20 , padding : "6px 14px" , fontSize : 11 , fontWeight : 700 , cursor : "pointer" , boxShadow : ` 0 2px 10px ${ accent } 33 ` } } > + $10 < / b u t t o n >
2026-03-17 16:54:31 +01:00
< / d i v >
< / h e a d e r >
{ /* Active subscriptions marquee (speed scales with count) */ }
{ Object . keys ( subs ) . length > 0 && (
< div style = { { width : "100%" , maxWidth : 420 , overflow : "hidden" , padding : "4px 0" , marginBottom : 16 } } >
< div style = { { display : "flex" , gap : 12 , animation : ` marquee ${ Math . max ( 10 , Object . keys ( subs ) . length * 4 + 8 ) } s linear infinite ` , whiteSpace : "nowrap" } } >
{ Object . keys ( subs ) . map ( k => < span key = { k } style = { { fontSize : 10 , color : SUBS [ k ] . color , background : ` ${ SUBS [ k ] . color } 15 ` , padding : "2px 8px" , borderRadius : 4 } } > ✓ { SUBS [ k ] . name } < / s p a n > ) }
< / d i v >
< / d i v >
) }
{ /* Visualizer (responsive width) */ }
< div style = { { width : "min(280px, calc(100vw - 80px))" , aspectRatio : "1" , borderRadius : 16 , background : ` linear-gradient(135deg, ${ card } , ${ bright ? "#d0d0e0" : "#1a1a3e" } ) ` , display : "flex" , alignItems : "flex-end" , justifyContent : "center" , marginBottom : 24 , position : "relative" , overflow : "hidden" , boxShadow : bright ? "0 10px 40px rgba(0,0,0,0.1)" : "0 20px 60px rgba(0,0,0,0.4)" , gap : 3 , padding : "0 20px 30px" , transition : "background 0.5s ease" } } >
{ bars . map ( ( h , i ) => < div key = { i } style = { { width : 12 , height : ` ${ h } % ` , borderRadius : 3 , background : ` linear-gradient(to top, ${ song . color } 88, ${ song . color } ) ` , transition : "height 0.15s ease" , opacity : on ? 0.9 : 0.15 } } / > ) }
{ /* Vinyl disc — freezes in place on pause */ }
< div style = { { position : "absolute" , top : "50%" , left : "50%" , transform : "translate(-50%,-50%)" , width : 80 , height : 80 , borderRadius : "50%" , border : ` 2px solid ${ muted } 33 ` , display : "flex" , alignItems : "center" , justifyContent : "center" , animation : "spin 3s linear infinite" , animationPlayState : on ? "running" : "paused" , background : bright ? "rgba(240,240,255,0.7)" : "rgba(13,13,26,0.7)" } } >
< div style = { { width : 20 , height : 20 , borderRadius : "50%" , background : bg , border : ` 1px solid ${ muted } 22 ` } } / >
< / d i v >
< div style = { { position : "absolute" , bottom : 8 , right : 12 , fontSize : 10 , color : bright ? "#00000033" : "#ffffff33" , fontWeight : 700 , letterSpacing : 2 } } > FREE TIER < / d i v >
{ /* Free-listening countdown badge */ }
{ on && ! has ( "continue" ) && prog < 3 && (
< div style = { { position : "absolute" , top : 12 , left : 12 , background : "rgba(231,76,60,0.9)" , color : "#fff" , padding : "4px 10px" , borderRadius : 6 , fontSize : 11 , fontWeight : 700 , letterSpacing : 1 , animation : "countPulse 0.8s ease infinite" } } >
FREE { 3 - prog } s
< / d i v >
) }
{ ! toneOk && ! SONGS [ 0 ] . url && < div style = { { position : "absolute" , top : "50%" , left : "50%" , transform : "translate(-50%,-50%)" , fontSize : 10 , color : muted , textAlign : "center" , background : bright ? "rgba(240,240,255,0.9)" : "rgba(13,13,26,0.9)" , padding : "8px 16px" , borderRadius : 8 , zIndex : 2 } } > Click play to start audio < / d i v > }
< / d i v >
{ /* Song info */ }
< div style = { { textAlign : "center" , marginBottom : 24 , maxWidth : 420 , padding : "0 20px" } } >
< h2 style = { { fontSize : 20 , fontWeight : 900 , fontFamily : "'Anybody',system-ui,sans-serif" , marginBottom : 4 , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } > { song . title } < / h 2 >
< div style = { { fontSize : 13 , color : muted } } > { song . artist } < / d i v >
< / d i v >
{ /* Seek bar — larger touch target, keyboard accessible */ }
< div style = { { width : "100%" , maxWidth : 380 , padding : "0 20px" , marginBottom : 24 } } >
< div onClick = { handleSeek } onKeyDown = { handleSeekKey } tabIndex = { 0 } role = "slider" aria - label = "Song progress" aria - valuenow = { prog } aria - valuemin = { 0 } aria - valuemax = { song . duration } style = { { width : "100%" , padding : "10px 0" , cursor : "pointer" } } >
< div style = { { width : "100%" , height : 6 , background : card , borderRadius : 3 , position : "relative" , transition : "background 0.3s ease" } } >
< div style = { { width : ` ${ Math . min ( 100 , ( prog / song . duration ) * 100 ) } % ` , height : "100%" , background : song . color , borderRadius : 3 , transition : "width 0.5s linear" } } / >
< / d i v >
< / d i v >
< div style = { { display : "flex" , justifyContent : "space-between" , fontSize : 11 , color : muted , marginTop : 4 } } >
< span > { fmt ( prog ) } < / s p a n >
< span >
{ ! has ( "seek" ) && < span style = { { color : "#aa4444" , marginRight : 8 , fontSize : 9 } } > 🔒 approx . < / s p a n > }
{ fmt ( song . duration ) }
< / s p a n >
< / d i v >
< / d i v >
{ /* Transport controls */ }
< div style = { { display : "flex" , alignItems : "center" , gap : 20 , marginBottom : 24 } } >
< CBtn onClick = { ( ) => tryAct ( "shuffle" , ( ) => setShuf ( s => ! s ) ) } color = { shuf ? accent : muted } label = "Shuffle" > 🔀 < / C B t n >
< CBtn onClick = { ( ) => tryAct ( "prev" , ( ) => changeSong ( ( ci - 1 + SONGS . length ) % SONGS . length ) ) } color = { muted } size = { 36 } label = "Previous" > ⏮ < / C B t n >
< button onClick = { handlePlay } aria - label = { on ? "Pause" : "Play" } style = { { width : 64 , height : 64 , borderRadius : "50%" , background : song . color , color : "#fff" , border : "none" , fontSize : 28 , cursor : "pointer" , display : "flex" , alignItems : "center" , justifyContent : "center" , boxShadow : ` 0 4px 20px ${ song . color } 55 ` } } > { on ? "⏸" : "▶" } < / b u t t o n >
< CBtn onClick = { ( ) => tryAct ( "skip" , ( ) => changeSong ( ( ci + 1 ) % SONGS . length ) ) } color = { muted } size = { 36 } label = "Next" > ⏭ < / C B t n >
< CBtn onClick = { ( ) => rep ? tryAct ( "unrepeat" , ( ) => setRep ( false ) ) : tryAct ( "repeat" , ( ) => setRep ( true ) ) } color = { rep ? accent : muted } label = "Repeat" > 🔁 < / C B t n >
< / d i v >
{ /* Volume slider with free-zone indicator */ }
< div style = { { width : "100%" , maxWidth : 380 , padding : "0 20px" , display : "flex" , alignItems : "center" , gap : 12 , marginBottom : 24 } } >
< span style = { { fontSize : 14 } } > 🔈 < / s p a n >
< input type = "range" min = "0" max = "100" value = { vol } onChange = { handleVol } style = { { flex : 1 , background : volBg } } / >
< span style = { { fontSize : 11 , color : muted , minWidth : 32 } } > { vol } % < / s p a n >
{ ! has ( "volume" ) && < span style = { { fontSize : 9 , color : "#aa4444" } } > 🔒 23 - 47 % < / s p a n > }
< / d i v >
2026-03-18 09:06:36 +01:00
{ ! hdAudio && ! song . url && < div style = { { textAlign : "center" , fontSize : 9 , color : "#aa4444" , marginTop : - 16 , marginBottom : 8 } } > 🔊 64 kbps Suffering Quality ™ — Upgrade to HD Audio < / d i v > }
2026-03-17 16:54:31 +01:00
{ /* Feature buttons (responsive grid) */ }
< div style = { { display : "grid" , gridTemplateColumns : "repeat(auto-fit,minmax(100px,1fr))" , gap : 8 , maxWidth : 420 , padding : "0 20px" , marginBottom : 16 , width : "100%" } } >
{ [
{ l : "Lyrics" , i : "📝" , a : ( ) => tryAct ( "lyrics" , ( ) => setLyr ( x => ! x ) ) } ,
2026-03-18 09:06:36 +01:00
{ 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" ) ) } ,
2026-03-17 16:54:31 +01:00
{ 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" ) ; } ) } ,
{ l : "Queue +" , i : "📋" , a : ( ) => tryAct ( "queue" , ( ) => flash ( "Queue expanded to 4 slots! (3 are decorative)" , "#00ee44" ) ) } ,
] . map ( f => < button key = { f . l } onClick = { f . a } style = { { background : card , border : ` 1px solid ${ muted } 22 ` , color : muted , padding : "8px 12px" , borderRadius : 8 , fontSize : 11 , cursor : "pointer" , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : 4 , transition : "background 0.3s ease, color 0.3s ease" } } > { f . i } { f . l } < / b u t t o n > ) }
< / d i v >
{ /* Lyrics panel — animated slide */ }
< div style = { { width : "100%" , maxWidth : 380 , overflow : "hidden" , maxHeight : lyr ? 500 : 0 , opacity : lyr ? 1 : 0 , transition : "max-height 0.35s ease, opacity 0.25s ease, margin 0.35s ease" , marginBottom : lyr ? 16 : 0 } } >
< div style = { { padding : "16px 20px" , background : card , borderRadius : 12 , textAlign : "center" , transition : "background 0.3s ease" } } >
< div style = { { fontSize : 10 , color : muted , marginBottom : 12 , letterSpacing : 2 } } > LYRICS { ! has ( "lyrics" ) && "— REDACTED VERSION" } < / d i v >
{ curLyrics . map ( ( line , i ) => (
< div key = { i } style = { { fontSize : 13 , color : has ( "lyrics" ) ? text : muted , marginBottom : 4 , lineHeight : 1.6 } } >
{ has ( "lyrics" ) ? line . join ( " " ) : line . map ( ( w , j ) => < span key = { j } > { j % 3 === 0 ? w : < span style = { { color : "#e74c3c" } } > █ █ █ █ █ < / s p a n > } { " " } < / s p a n > ) }
< / d i v >
) ) }
< / d i v >
< / d i v >
{ /* Up Next — card styled, with price on lock icons */ }
< section style = { { width : "100%" , maxWidth : 380 , marginBottom : 16 } } >
< h3 style = { { fontSize : 10 , color : muted , letterSpacing : 2 , padding : "0 20px" , marginBottom : 8 , fontWeight : 400 } } > UP NEXT < / h 3 >
< div style = { { background : card , borderRadius : 12 , overflow : "hidden" , transition : "background 0.3s ease" } } >
{ SONGS . map ( ( s , i ) => i !== ci && (
< div key = { i } className = "q-item" onClick = { ( ) => tryAct ( "skip" , ( ) => changeSong ( i ) ) } style = { { display : "flex" , alignItems : "center" , gap : 12 , padding : "10px 20px" , cursor : "pointer" , borderBottom : ` 1px solid ${ bright ? "#ddd" : muted + "11" } ` } } >
< div style = { { width : 8 , height : 8 , borderRadius : "50%" , background : s . color , flexShrink : 0 } } / >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 12 , fontWeight : 600 , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } > { s . title } < / d i v >
< div style = { { fontSize : 10 , color : muted } } > { s . artist } < / d i v >
< / d i v >
< div style = { { fontSize : 10 , color : muted , flexShrink : 0 } } > { fmt ( s . duration ) } < / d i v >
< div style = { { fontSize : 10 , color : "#aa4444" , flexShrink : 0 } } > 🔒 $ { SUBS . skip . micro . toFixed ( 2 ) } < / d i v >
< / d i v >
) ) }
< / d i v >
< / s e c t i o n >
{ /* Session Economics */ }
< section style = { { width : "100%" , maxWidth : 380 , padding : "12px 20px" , background : card , borderRadius : 12 , marginBottom : 16 , transition : "background 0.3s ease" } } >
< h3 style = { { fontSize : 10 , color : muted , letterSpacing : 2 , marginBottom : 8 , fontWeight : 400 } } > SESSION ECONOMICS < / h 3 >
{ [ [ "Spent" , ` $ ${ spent . toFixed ( 2 ) } ` , "#e74c3c" ] , [ "Subscriptions" , Object . keys ( subs ) . length , "#f39c12" ] , [ "Actions" , acts , text ] , [ "Cost/action" , acts > 0 ? ` $ ${ ( spent / acts ) . toFixed ( 2 ) } ` : "—" , "#e74c3c" ] ] . map ( ( [ l , v , c ] ) => (
< div key = { l } style = { { display : "flex" , justifyContent : "space-between" , fontSize : 12 , marginTop : 4 } } > < span style = { { color : muted } } > { l } < / s p a n > < s p a n s t y l e = { { c o l o r : c , f o n t W e i g h t : 7 0 0 } } > { v } < / s p a n > < / d i v >
) ) }
< / s e c t i o n >
2026-03-18 09:16:20 +01:00
< button onClick = { ( ) => { track ( "close_app" , { has _exit : ! ! has ( "exit" ) } ) ; has ( "exit" ) ? setShowExit ( true ) : setShowAd ( true ) ; } } style = { { background : "transparent" , border : "1px solid #e74c3c33" , color : "#e74c3c" , padding : "10px 28px" , borderRadius : 8 , fontSize : 11 , cursor : "pointer" , marginBottom : 16 } } > ✕ Close App < / b u t t o n >
2026-03-17 16:54:31 +01:00
< footer style = { { fontSize : 10 , color : muted , textAlign : "center" , maxWidth : 380 , padding : "0 20px" , lineHeight : 1.8 , opacity : 0.7 } } >
Pay2Play ! v6 . 6.6 • Music procedurally generated — even the songs are cheaply made •
Refunds available for $4 . 99 processing fee • © 2026 Suffering Inc .
< / f o o t e r >
< / d i v >
< / >
) ;
}
2026-03-17 17:09:50 +01:00
// Load real songs from audio/songs.json if available, then render
fetch ( "audio/songs.json" )
. then ( r => { if ( r . ok ) return r . json ( ) ; throw new Error ( ) ; } )
. then ( songs => { if ( Array . isArray ( songs ) && songs . length > 0 ) SONGS = songs ; } )
. catch ( ( ) => { } ) // No songs.json — use procedural builtins
. finally ( ( ) => ReactDOM . createRoot ( document . getElementById ( "root" ) ) . render ( < PayPlay / > ) ) ;