Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>myNoise Explorer</title>
- <style>
- :root{
- --bg: #f2f4f0;
- --bg-elev: #ffffff;
- --bg-card: #ffffff;
- --ink: #22322b;
- --ink-soft: #5b6b62;
- --ink-faint: #8a9990;
- --accent: #4f7d6f;
- --accent-soft: #d9e7de;
- --accent-strong: #38604f;
- --heart: #c9694f;
- --heart-soft: #f4ddd4;
- --border: #dfe6e0;
- --shadow: 0 1px 2px rgba(30,50,40,0.05), 0 6px 20px rgba(30,50,40,0.06);
- --locked: #eef1ee;
- --radius: 16px;
- --radius-sm: 10px;
- font-synthesis: none;
- }
- @media (prefers-color-scheme: dark){
- :root{
- --bg: #131a17;
- --bg-elev: #182420;
- --bg-card: #182420;
- --ink: #e7efe9;
- --ink-soft: #a9bbb1;
- --ink-faint: #6f8279;
- --accent: #7fb39f;
- --accent-soft: #24352d;
- --accent-strong: #a4d2bf;
- --heart: #e0876a;
- --heart-soft: #3a2822;
- --border: #26332c;
- --shadow: 0 1px 2px rgba(0,0,0,0.25), 0 8px 24px rgba(0,0,0,0.35);
- --locked: #1b2622;
- }
- }
- *{ box-sizing: border-box; }
- html,body{ margin:0; padding:0; }
- body{
- background: var(--bg);
- color: var(--ink);
- font-family: "Iowan Old Style","Palatino Linotype","Georgia",ui-serif,serif;
- -webkit-font-smoothing: antialiased;
- min-height: 100vh;
- }
- .wrap{
- max-width: 960px;
- margin: 0 auto;
- padding: 32px 20px 80px;
- }
- header.top{
- display:flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 16px;
- flex-wrap: wrap;
- margin-bottom: 24px;
- }
- h1{
- font-size: 1.9rem;
- font-weight: 600;
- letter-spacing: 0.2px;
- margin: 0;
- font-family: "Iowan Old Style","Palatino Linotype",Georgia,serif;
- }
- h1 .sub{
- display:block;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.72rem;
- text-transform: uppercase;
- letter-spacing: 1.8px;
- color: var(--ink-faint);
- font-weight: 600;
- margin-bottom: 4px;
- }
- .progress-pill{
- display:flex;
- gap: 18px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- background: var(--bg-elev);
- border: 1px solid var(--border);
- border-radius: 999px;
- padding: 10px 20px;
- box-shadow: var(--shadow);
- }
- .progress-pill .stat{
- display:flex;
- flex-direction: column;
- align-items: center;
- line-height: 1.15;
- }
- .progress-pill .stat b{
- font-size: 1.05rem;
- color: var(--accent-strong);
- }
- .progress-pill .stat span{
- font-size: 0.62rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- color: var(--ink-faint);
- }
- nav.tabs{
- display:flex;
- gap: 6px;
- margin-bottom: 24px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- }
- nav.tabs button{
- font: inherit;
- font-size: 0.85rem;
- font-weight: 600;
- letter-spacing: 0.3px;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--ink-soft);
- padding: 8px 16px;
- border-radius: 999px;
- cursor: pointer;
- transition: all .15s ease;
- }
- nav.tabs button:hover{ border-color: var(--accent); color: var(--accent-strong); }
- nav.tabs button.active{
- background: var(--accent);
- border-color: var(--accent);
- color: #fff;
- }
- .panel{ display:none; }
- .panel.active{ display:block; }
- .now-card{
- background: linear-gradient(160deg, var(--accent-soft), var(--bg-card) 70%);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 28px 30px;
- box-shadow: var(--shadow);
- margin-bottom: 32px;
- position: relative;
- overflow: hidden;
- }
- .now-card .eyebrow{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.68rem;
- text-transform: uppercase;
- letter-spacing: 2px;
- color: var(--accent-strong);
- font-weight: 700;
- margin-bottom: 10px;
- }
- .now-card h2{
- margin: 0 0 4px;
- font-size: 2rem;
- font-weight: 600;
- }
- .now-card .cat-name{
- color: var(--ink-soft);
- font-size: 0.98rem;
- margin-bottom: 22px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- }
- .now-row{
- display:flex;
- align-items:center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 18px;
- }
- .timer{
- font-family: "SF Mono", "Menlo", monospace;
- font-size: 2.6rem;
- font-weight: 500;
- color: var(--accent-strong);
- letter-spacing: 1px;
- font-variant-numeric: tabular-nums;
- }
- .timer.paused{ opacity: 0.5; }
- .now-actions{
- display:flex;
- align-items:center;
- gap: 10px;
- }
- .btn{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-weight: 600;
- font-size: 0.88rem;
- border-radius: var(--radius-sm);
- border: 1px solid transparent;
- padding: 11px 20px;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: transform .1s ease, opacity .15s ease;
- }
- .btn:active{ transform: scale(0.97); }
- .btn-primary{
- background: var(--accent);
- color: #fff;
- box-shadow: var(--shadow);
- }
- .btn-primary:hover{ background: var(--accent-strong); }
- .btn-ghost{
- background: var(--bg-elev);
- color: var(--ink);
- border-color: var(--border);
- }
- .btn-ghost:hover{ border-color: var(--accent); color: var(--accent-strong); }
- .btn-icon{
- width: 42px; height: 42px;
- padding: 0;
- justify-content: center;
- background: var(--bg-elev);
- border: 1px solid var(--border);
- color: var(--ink-faint);
- border-radius: 50%;
- font-size: 1.1rem;
- }
- .btn-icon.active, .btn-icon:hover{ color: var(--heart); border-color: var(--heart-soft); background: var(--heart-soft); }
- .btn-pause{ font-size: 1.05rem; }
- .link-badge{
- margin-top: 16px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.78rem;
- color: var(--ink-faint);
- }
- .link-badge.linked{ color: var(--accent-strong); font-weight: 600; }
- .catchup-note{
- margin-top: 10px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.85rem;
- background: var(--heart-soft);
- color: var(--heart);
- border-radius: var(--radius-sm);
- padding: 10px 14px;
- display: none;
- }
- .catchup-note.show{ display:block; }
- section.grid-section h3, .fav-section h3{
- font-size: 1.15rem;
- font-weight: 600;
- margin: 0 0 16px;
- display:flex;
- align-items:baseline;
- gap: 10px;
- }
- section.grid-section h3 span.count{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.72rem;
- color: var(--ink-faint);
- font-weight: 500;
- }
- .cat-grid{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
- gap: 16px;
- }
- .cat-card{
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 18px 20px 14px;
- box-shadow: var(--shadow);
- }
- .cat-card-head{
- display:flex;
- justify-content: space-between;
- align-items: baseline;
- margin-bottom: 10px;
- }
- .cat-card-head h4{
- margin: 0;
- font-size: 1.08rem;
- font-weight: 600;
- }
- .cat-card-head .frac{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.72rem;
- color: var(--ink-faint);
- background: var(--accent-soft);
- padding: 3px 9px;
- border-radius: 999px;
- white-space: nowrap;
- }
- .track-row{
- display:flex;
- align-items:center;
- justify-content: space-between;
- padding: 7px 0;
- border-top: 1px solid var(--border);
- gap: 8px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- }
- .track-row:first-of-type{ border-top: none; }
- .track-row .t-name{
- font-size: 0.9rem;
- cursor: pointer;
- color: var(--ink);
- flex: 1;
- display:flex;
- align-items:center;
- gap: 7px;
- }
- .track-row .t-name:hover{ color: var(--accent-strong); }
- .track-row .t-name.is-current{ color: var(--accent-strong); font-weight: 700; }
- .track-row .played-dot{
- width: 7px; height: 7px; border-radius: 50%;
- background: var(--border);
- flex-shrink: 0;
- }
- .track-row .played-dot.played{ background: var(--accent); }
- .row-actions{ display:flex; align-items:center; gap: 4px; }
- .mini-heart{
- background: none;
- border: none;
- cursor: pointer;
- font-size: 0.95rem;
- color: var(--ink-faint);
- padding: 4px;
- line-height: 1;
- }
- .mini-heart.active{ color: var(--heart); }
- .mini-open{
- background: none;
- border: none;
- cursor: pointer;
- font-size: 0.8rem;
- color: var(--ink-faint);
- padding: 4px;
- text-decoration: none;
- }
- .mini-open:hover{ color: var(--accent-strong); }
- .locked-note{
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px solid var(--border);
- font-size: 0.78rem;
- color: var(--ink-faint);
- }
- .fav-list{ display:flex; flex-direction:column; gap: 10px; }
- .fav-row{
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 14px 18px;
- display:flex;
- align-items:center;
- justify-content: space-between;
- box-shadow: var(--shadow);
- }
- .fav-row .fav-info b{ display:block; font-size: 1rem; }
- .fav-row .fav-info span{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.78rem;
- color: var(--ink-faint);
- }
- .fav-row .fav-actions{ display:flex; gap: 8px; align-items:center; }
- .empty-state{
- color: var(--ink-faint);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.9rem;
- padding: 30px 0;
- text-align:center;
- }
- footer.foot{
- margin-top: 50px;
- text-align:center;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- font-size: 0.72rem;
- color: var(--ink-faint);
- }
- </style>
- </head>
- <body>
- <div class="wrap">
- <header class="top">
- <h1><span class="sub">Ambient Sound Companion</span>myNoise Explorer</h1>
- <div class="progress-pill">
- <div class="stat"><b id="stat-played">0/100</b><span>Played</span></div>
- <div class="stat"><b id="stat-flagged">0</b><span>Flagged</span></div>
- </div>
- </header>
- <nav class="tabs">
- <button class="tab-btn active" data-tab="now">Now Playing</button>
- <button class="tab-btn" data-tab="browse">Categories</button>
- <button class="tab-btn" data-tab="favorites">Favorites</button>
- </nav>
- <div class="panel active" id="panel-now">
- <div class="now-card">
- <div class="eyebrow" id="now-eyebrow">Now Playing</div>
- <h2 id="now-name">—</h2>
- <div class="cat-name" id="now-cat">—</div>
- <div class="now-row">
- <div class="timer" id="now-timer">05:00</div>
- <div class="now-actions">
- <button class="btn btn-icon btn-pause" id="btn-pause" title="Pause / resume timer">⏸</button>
- <button class="btn btn-icon" id="btn-heart-now" title="Flag as favorite">♥</button>
- <button class="btn btn-ghost" id="btn-skip">Skip ⟶</button>
- <a class="btn btn-primary" id="btn-open" href="#" target="mynoise_companion">Open in myNoise ↗</a>
- </div>
- </div>
- <div class="link-badge" id="link-badge">Click Open once to link the tab, then it auto-advances forever</div>
- <div class="catchup-note" id="catchup-note">Your browser blocked the automatic hop — click <strong>Open in myNoise</strong> to catch up with the current track.</div>
- </div>
- <section class="grid-section">
- <h3>All Categories <span class="count">tap a track to jump to it</span></h3>
- <div class="cat-grid" id="cat-grid-now"></div>
- </section>
- </div>
- <div class="panel" id="panel-browse">
- <section class="grid-section">
- <h3>Browse Categories</h3>
- <div class="cat-grid" id="cat-grid-browse"></div>
- </section>
- </div>
- <div class="panel" id="panel-favorites">
- <section class="fav-section">
- <h3>Your Favorites</h3>
- <div class="fav-list" id="fav-list"></div>
- </section>
- </div>
- <footer class="foot">myNoise Explorer · a personal companion for mynoise.net · progress saved on this device</footer>
- </div>
- <script>
- (function(){
- "use strict";
- const DATA = {
- "Natural Noises": {"more": 25, "tracks": [
- ["Distant Thunder", "https://mynoise.net/NoiseMachines/thunderNoiseGenerator.php"],
- ["Stormy Weather", "https://mynoise.net/NoiseMachines/stormSoundGenerator.php"],
- ["Rain on a Tent", "https://mynoise.net/NoiseMachines/campingRainNoiseGenerator.php"],
- ["Fireplace", "https://mynoise.net/NoiseMachines/fireNoiseGenerator.php"],
- ["Tin Roof Rain", "https://mynoise.net/NoiseMachines/tinRoofRainNoiseGenerator.php"]
- ]},
- "Synthetic Noises": {"more": 6, "tracks": [
- ["White Noise & Co", "https://mynoise.net/NoiseMachines/whiteNoiseGenerator.php"],
- ["Brownish White", "https://mynoise.net/NoiseMachines/notWhiteNoisePlayer.php"],
- ["Xenobiota", "https://mynoise.net/NoiseMachines/xenobiotaGenerativeNoise.php"],
- ["Sound Horizon", "https://mynoise.net/NoiseMachines/baryonAcousticOscillationsNoisePlayer.php"],
- ["Grey Noise", "https://mynoise.net/NoiseMachines/greyNoiseGenerator.php"]
- ]},
- "Experimental": {"more": 5, "tracks": [
- ["Binaural Beats", "https://mynoise.net/NoiseMachines/binauralBrainwaveGenerator.php"],
- ["40 Hertz", "https://mynoise.net/NoiseMachines/forty40HzBrainwaveGenerator.php"],
- ["Binaural Harmonics", "https://mynoise.net/NoiseMachines/pureBinauralBrainwaveGenerator.php"],
- ["Solfeggio Tones", "https://mynoise.net/NoiseMachines/solfeggioTonesGenerator.php"],
- ["Isochronic Tones", "https://mynoise.net/NoiseMachines/isochronicBrainwaveGenerator.php"]
- ]},
- "Landscapes": {"more": 48, "tracks": [
- ["Japanese Garden", "https://mynoise.net/NoiseMachines/japaneseGardenSoundscapeGenerator.php"],
- ["Irish Coast", "https://mynoise.net/NoiseMachines/windSeaRainNoiseGenerator.php"],
- ["Primeval Forest", "https://mynoise.net/NoiseMachines/primevalEuropeanForestSoundscapeGenerator.php"],
- ["Healing Water", "https://mynoise.net/NoiseMachines/healingWaterSoundscapeGenerator.php"],
- ["Icelandic Shores", "https://mynoise.net/NoiseMachines/oceanWavesSeaShoreNoiseGenerator.php"]
- ]},
- "Ambiences": {"more": 15, "tracks": [
- ["Autumn Walk", "https://mynoise.net/NoiseMachines/autumnWalkSoundscapeGenerator.php"],
- ["Cosy Rain", "https://mynoise.net/NoiseMachines/customRainOnWindow.php"],
- ["Nightscape", "https://mynoise.net/NoiseMachines/customNightscape.php"],
- ["Underwater", "https://mynoise.net/NoiseMachines/finWhaleUnderwaterSoundscapeGenerator.php"],
- ["Bad Weather", "https://mynoise.net/NoiseMachines/customStormy.php"]
- ]},
- "Fantasy": {"more": 10, "tracks": [
- ["Medieval Library", "https://mynoise.net/NoiseMachines/customMedievalLibrary.php"],
- ["Medieval Village", "https://mynoise.net/NoiseMachines/medievalVillageSoundscapeGenerator.php"],
- ["Sad Minstrel", "https://mynoise.net/NoiseMachines/medievalMinstrelSoundscapeGenerator.php"],
- ["Name of the Rose", "https://mynoise.net/NoiseMachines/customTheNameOfTheRose.php"],
- ["Dark Forest", "https://mynoise.net/NoiseMachines/enchantedForestSoundscapeGenerator.php"]
- ]},
- "Magic": {"more": 18, "tracks": [
- ["Dunes Of Arrakis", "https://mynoise.net/NoiseMachines/magicDuneArrakisGenerator.php"],
- ["Zen Garden", "https://mynoise.net/NoiseMachines/magicMeditationGenerator.php"],
- ["Mermaids Calling", "https://mynoise.net/NoiseMachines/magicSirenSongGenerator.php"],
- ["日本", "https://mynoise.net/NoiseMachines/magicJapanGenerator.php"],
- ["Rainy Days", "https://mynoise.net/NoiseMachines/magicRainyDaysGenerator.php"]
- ]},
- "Vocal": {"more": 13, "tracks": [
- ["Gregorian Chants", "https://mynoise.net/NoiseMachines/gregorianChoirGenerator.php"],
- ["Forest Voice", "https://mynoise.net/NoiseMachines/voiceInTheWoods.php"],
- ["Silk and Hopes", "https://mynoise.net/NoiseMachines/raphaelAlongTheSilkRoad.php"],
- ["Oak Song", "https://mynoise.net/NoiseMachines/riikkaVocalSoundscape.php"],
- ["Deep Chant", "https://mynoise.net/NoiseMachines/deepVoiceSoundscape.php"]
- ]},
- "Acoustic": {"more": 23, "tracks": [
- ["The Pilgrim", "https://mynoise.net/NoiseMachines/tongueDrumSoundscapeGenerator.php"],
- ["Duduk Song", "https://mynoise.net/NoiseMachines/dudukSoundGenerator.php"],
- ["88 Keys", "https://mynoise.net/NoiseMachines/acousticPianoSoundscapeGenerator.php"],
- ["Marimbas", "https://mynoise.net/NoiseMachines/benClarkMarimbaGenerator.php"],
- ["Silver Drift", "https://mynoise.net/NoiseMachines/benClarkVibraphoneSoundscapeGenerator.php"]
- ]},
- "Cinematic": {"more": 7, "tracks": [
- ["Home Again", "https://mynoise.net/NoiseMachines/meditationMusicGenerator.php"],
- ["Japanese Oase", "https://mynoise.net/NoiseMachines/japaneseSuikinkutsuSoundscapeGenerator.php"],
- ["Solomon's Dream", "https://mynoise.net/NoiseMachines/middleEasternMusicGenerator.php"],
- ["Cinematic East", "https://mynoise.net/NoiseMachines/cinematicEasternSoundscapeGenerator.php"],
- ["Dark Seas", "https://mynoise.net/NoiseMachines/customDarkWaters.php"]
- ]},
- "Electric": {"more": 14, "tracks": [
- ["Night Blue", "https://mynoise.net/NoiseMachines/blueJazzGenerator.php"],
- ["Walking Through", "https://mynoise.net/NoiseMachines/downtempoBackgroundMusicGenerator.php"],
- ["Mr. Rhodes", "https://mynoise.net/NoiseMachines/rhodesSoundscapeGenerator.php"],
- ["Shroom With a View", "https://mynoise.net/NoiseMachines/hanShroomMusicGenerator.php"],
- ["Tape Memories", "https://mynoise.net/NoiseMachines/oldTapeSoundscapeGenerator.php"]
- ]},
- "Synthesizers": {"more": 33, "tracks": [
- ["The Architect's Eclipse", "https://mynoise.net/NoiseMachines/ionStreamSpaceAmbientGenerativeMusic.php"],
- ["Take It Easy", "https://mynoise.net/NoiseMachines/paulNagleSequenceGenerator.php"],
- ["Psynesthesia", "https://mynoise.net/NoiseMachines/ambientPsytranceMusicGenerator.php"],
- ["Dissociative Trance", "https://mynoise.net/NoiseMachines/dissociativeTranceMusicGenerator.php"],
- ["Neon Voyager", "https://mynoise.net/NoiseMachines/retroSpaceElectroSoundscapeGenerator.php"]
- ]},
- "Ambient": {"more": 19, "tracks": [
- ["Floating", "https://mynoise.net/NoiseMachines/deepRelaxingAmbientSoundscapeGenerator.php"],
- ["Sleeping Dragon", "https://mynoise.net/NoiseMachines/sleepingDragonToneGenerator.php"],
- ["Osmosis", "https://mynoise.net/NoiseMachines/osmosisDroneGenerator.php"],
- ["Dreaming Nautilus", "https://mynoise.net/NoiseMachines/dreamingNautilusGenerativeSoundscape.php"],
- ["Anamnesis", "https://mynoise.net/NoiseMachines/anamnesisSoundscapeGenerator.php"]
- ]},
- "Traditional": {"more": 3, "tracks": [
- ["Tibetan Spirit", "https://mynoise.net/NoiseMachines/tibetSoundscapeGenerator.php"],
- ["Berber Tent", "https://mynoise.net/NoiseMachines/berberTentSoundscapeGenerator.php"],
- ["Buddhist Chants", "https://mynoise.net/NoiseMachines/buddhistTempleCeremonySoundscapeGenerator.php"],
- ["Finnish Sauna", "https://mynoise.net/NoiseMachines/finnishSaunaSoundscapeGenerator.php"],
- ["Japanese Heritage", "https://mynoise.net/NoiseMachines/japaneseShrineSoundscapeGenerator.php"]
- ]},
- "Spaces": {"more": 9, "tracks": [
- ["Cafe Restaurant", "https://mynoise.net/NoiseMachines/cafeRestaurantNoiseGenerator.php"],
- ["Calm Office", "https://mynoise.net/NoiseMachines/openOfficeNoiseGenerator.php"],
- ["Car Interior", "https://mynoise.net/NoiseMachines/carInteriorSoundscapeGenerator.php"],
- ["Vintage Office", "https://mynoise.net/NoiseMachines/vintageOfficeNoiseGenerator.php"],
- ["Church", "https://mynoise.net/NoiseMachines/churchAtmosphereSoundGenerator.php"]
- ]},
- "Industrial": {"more": 8, "tracks": [
- ["Data Center", "https://mynoise.net/NoiseMachines/dataCenterNoiseGenerator.php"],
- ["Fan Noise", "https://mynoise.net/NoiseMachines/fanNoiseGenerator.php"],
- ["Steel Mill", "https://mynoise.net/NoiseMachines/steelMillNoiseGenerator.php"],
- ["Vinyl Groove", "https://mynoise.net/NoiseMachines/dustyScratchedVinylNoiseGenerator.php"],
- ["Factory", "https://mynoise.net/NoiseMachines/industrialRevolutionNoiseGenerator.php"]
- ]},
- "Transports": {"more": 8, "tracks": [
- ["Railroads", "https://mynoise.net/NoiseMachines/railroadNoiseGenerator.php"],
- ["Spaceship", "https://mynoise.net/NoiseMachines/spaceshipNoiseGenerator.php"],
- ["Flying Fortress", "https://mynoise.net/NoiseMachines/propellerNoiseGenerator.php"],
- ["Sailboat", "https://mynoise.net/NoiseMachines/squeakingSailboatSoundscapeGenerator.php"],
- ["Aircraft Cabin", "https://mynoise.net/NoiseMachines/cabinNoiseGenerator.php"]
- ]},
- "Urban": {"more": 7, "tracks": [
- ["Wet Asphalt", "https://mynoise.net/NoiseMachines/urbanRainSoundGenerator.php"],
- ["City Rain", "https://mynoise.net/NoiseMachines/customRainInTheCity.php"],
- ["Sherbrooke Str", "https://mynoise.net/NoiseMachines/sherbrookeStreetAmbienceGenerator.php"],
- ["Port Town", "https://mynoise.net/NoiseMachines/harborSoundscapeGenerator.php"],
- ["Senegal Town", "https://mynoise.net/NoiseMachines/senegalUrbanLifeAmbienceGenerator.php"]
- ]},
- "Tonal Drones": {"more": 8, "tracks": [
- ["Singing Bowls", "https://mynoise.net/NoiseMachines/singingBowlsDroneGenerator.php"],
- ["Tibetan Choir", "https://mynoise.net/NoiseMachines/throatSingingDroneGenerator.php"],
- ["Time Still", "https://mynoise.net/NoiseMachines/timeStillMusicalDroneGenerator.php"],
- ["Orchestron", "https://mynoise.net/NoiseMachines/orchestronDroneGenerator.php"],
- ["Ice World", "https://mynoise.net/NoiseMachines/iceDroneGenerator.php"]
- ]},
- "Patterns": {"more": 9, "tracks": [
- ["Polyrhythms", "https://mynoise.net/NoiseMachines/polyrhythmBeatGenerator.php"],
- ["Tuned Cave", "https://mynoise.net/NoiseMachines/contemplationCaveGenerator.php"],
- ["Temple Bells", "https://mynoise.net/NoiseMachines/windchimesGenerator.php"],
- ["Cave Chime", "https://mynoise.net/NoiseMachines/stalactitesCaveChimeSoundscapeGenerator.php"],
- ["Clockwork", "https://mynoise.net/NoiseMachines/clockTickTockNoiseGenerator.php"]
- ]}
- };
- const CATS = Object.keys(DATA);
- // Flat list of {id, cat, idx, name, url}
- const FLAT = [];
- CATS.forEach(cat => {
- DATA[cat].tracks.forEach((t, idx) => {
- FLAT.push({ id: cat + "::" + idx, cat: cat, idx: idx, name: t[0], url: t[1] });
- });
- });
- const TOTAL = FLAT.length;
- const byId = {};
- FLAT.forEach(t => byId[t.id] = t);
- const STORAGE_KEY = "mynoise_explorer_state_v1";
- const TAB_NAME = "mynoise_companion";
- const TICK_SECONDS = 5 * 60;
- function loadState(){
- let s = null;
- try{ s = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); }catch(e){ s = null; }
- if(!s || typeof s !== "object"){
- s = {};
- }
- if(!s.currentId || !byId[s.currentId]) s.currentId = FLAT[0].id;
- if(!Array.isArray(s.played)) s.played = [];
- if(!Array.isArray(s.favorites)) s.favorites = [];
- s.played = s.played.filter(id => byId[id]);
- s.favorites = s.favorites.filter(id => byId[id]);
- if(typeof s.paused !== "boolean") s.paused = false;
- if(s.paused){
- if(typeof s.pausedRemainingMs !== "number" || s.pausedRemainingMs < 0) s.pausedRemainingMs = TICK_SECONDS * 1000;
- delete s.endAt;
- } else {
- if(typeof s.endAt !== "number" || !isFinite(s.endAt)) s.endAt = Date.now() + TICK_SECONDS * 1000;
- delete s.pausedRemainingMs;
- }
- return s;
- }
- // The countdown is driven by an absolute wall-clock deadline (state.endAt),
- // never by counting down "1 per tick". A setInterval(fn, 1000) firing every
- // second is only a *request* — a backgrounded/inactive browser tab gets
- // throttled by the browser and may only fire every several seconds, or
- // pause almost entirely. Decrementing a counter once per firing under that
- // throttling makes the on-screen clock silently fall behind real time
- // (exactly the "3:30 left, come back 5 min later, now 2:15 left" bug).
- // Recomputing remaining time from Date.now() vs. a fixed deadline is
- // immune to that: whenever the tick does fire, it shows the true time left.
- function currentRemainingMs(){
- if(state.paused){
- return state.pausedRemainingMs != null ? state.pausedRemainingMs : TICK_SECONDS * 1000;
- }
- if(typeof state.endAt !== "number") return TICK_SECONDS * 1000;
- return state.endAt - Date.now();
- }
- function startCountdown(seconds){
- state.paused = false;
- state.endAt = Date.now() + seconds * 1000;
- delete state.pausedRemainingMs;
- }
- let state = loadState();
- let noiseWin = null;
- let intervalHandle = null;
- function save(){
- try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }catch(e){ /* ignore quota errors */ }
- }
- function fmtTime(sec){
- const m = Math.floor(sec / 60);
- const s = sec % 60;
- return String(m).padStart(2,"0") + ":" + String(s).padStart(2,"0");
- }
- function isFav(id){ return state.favorites.indexOf(id) !== -1; }
- function isPlayed(id){ return state.played.indexOf(id) !== -1; }
- function toggleFav(id){
- const i = state.favorites.indexOf(id);
- if(i === -1) state.favorites.push(id); else state.favorites.splice(i,1);
- save();
- renderAll();
- }
- function markPlayed(id){
- if(state.played.indexOf(id) === -1) state.played.push(id);
- }
- function setCurrent(id, opts){
- opts = opts || {};
- state.currentId = id;
- startCountdown(TICK_SECONDS);
- hideCatchup();
- save();
- renderAll();
- if(opts.open){
- openCurrentTrack(true);
- }
- }
- // Opening a NEW window requires a real user gesture (click). But once we
- // hold a JS reference to that window, we can change its .location later
- // from a timer callback with NO gesture and NO popup-blocker involvement —
- // that's only true window.open() calls that get blocked. So: the user
- // clicks "Open in myNoise" once to link the tab; every auto-advance after
- // that just re-points the same window object, which browsers always allow.
- function openCurrentTrack(fromUserGesture){
- const t = byId[state.currentId];
- if(fromUserGesture){
- try{
- const w = window.open(t.url, TAB_NAME);
- if(w){
- noiseWin = w;
- try{ w.focus(); }catch(e){}
- hideCatchup();
- updateLinkBadge();
- return true;
- }
- }catch(e){ /* fall through */ }
- hideCatchup();
- updateLinkBadge();
- return false;
- }
- // Not a user gesture: try to reuse the already-linked window first.
- if(noiseWin && !noiseWin.closed){
- try{
- noiseWin.location = t.url;
- try{ noiseWin.focus(); }catch(e){}
- hideCatchup();
- updateLinkBadge();
- return true;
- }catch(e){ /* window may have navigated away/closed unexpectedly */ }
- }
- // No linked window (never opened, or user closed it). A fresh
- // window.open() from here has no user gesture behind it, so most
- // browsers will silently block it — try anyway, then surface the note.
- try{
- const w = window.open(t.url, TAB_NAME);
- if(w){
- noiseWin = w;
- hideCatchup();
- updateLinkBadge();
- return true;
- }
- }catch(e){ /* ignore */ }
- showCatchup();
- updateLinkBadge();
- return false;
- }
- function updateLinkBadge(){
- const linked = !!(noiseWin && !noiseWin.closed);
- const badge = document.getElementById("link-badge");
- if(!badge) return;
- badge.textContent = linked ? "🔗 Tab linked — auto-advances hands-free" : "Click Open once to link the tab, then it auto-advances forever";
- badge.classList.toggle("linked", linked);
- }
- function showCatchup(){
- document.getElementById("catchup-note").classList.add("show");
- }
- function hideCatchup(){
- document.getElementById("catchup-note").classList.remove("show");
- }
- function nextTrack(currentId){
- const t = byId[currentId];
- const pos = FLAT.findIndex(x => x.id === currentId);
- const nextPos = (pos + 1) % FLAT.length;
- return FLAT[nextPos].id;
- }
- // Pure state mutation for one 5-minute rollover — no opening/rendering here,
- // so tick() can call this in a loop to catch up if several deadlines were
- // missed at once (e.g. the laptop slept, or the tab was backgrounded long
- // enough that timers were suspended entirely).
- function advanceState(){
- const prevId = state.currentId;
- markPlayed(prevId);
- state.currentId = nextTrack(prevId);
- // Chain off the previous deadline (not "now") so a string of missed
- // rollovers is caught up one real 5-minute increment at a time, instead
- // of every catch-up collapsing into a single skip.
- state.endAt = (typeof state.endAt === "number" ? state.endAt : Date.now()) + TICK_SECONDS * 1000;
- }
- function tick(){
- updateLinkBadge();
- if(state.paused){
- updateTimerDisplay();
- return;
- }
- if(typeof state.endAt !== "number") state.endAt = Date.now() + TICK_SECONDS * 1000;
- let advanced = false;
- let guard = 0;
- while(state.endAt - Date.now() <= 0 && guard < 500){
- advanceState();
- advanced = true;
- guard++;
- }
- save();
- updateTimerDisplay();
- if(advanced){
- renderAll();
- // Attempt silent hop into the same named tab (no fresh user gesture
- // needed since the tab already exists under this name in most browsers).
- openCurrentTrack(false);
- }
- }
- function updateTimerDisplay(){
- const el = document.getElementById("now-timer");
- const secs = Math.max(0, Math.round(currentRemainingMs() / 1000));
- el.textContent = fmtTime(secs);
- el.classList.toggle("paused", state.paused);
- document.getElementById("btn-pause").textContent = state.paused ? "▶" : "⏸";
- }
- // ---------- Rendering ----------
- function renderNowCard(){
- const t = byId[state.currentId];
- document.getElementById("now-name").textContent = t.name;
- document.getElementById("now-cat").textContent = t.cat;
- document.getElementById("btn-open").href = t.url;
- updateTimerDisplay();
- const heartBtn = document.getElementById("btn-heart-now");
- heartBtn.classList.toggle("active", isFav(t.id));
- }
- function renderProgress(){
- document.getElementById("stat-played").textContent = state.played.length + "/" + TOTAL;
- document.getElementById("stat-flagged").textContent = String(state.favorites.length);
- }
- function trackRowHTML(t){
- const played = isPlayed(t.id);
- const fav = isFav(t.id);
- const current = t.id === state.currentId;
- return (
- '<div class="track-row" data-id="' + t.id + '">' +
- '<span class="t-name' + (current ? ' is-current' : '') + '" data-jump="' + t.id + '">' +
- '<span class="played-dot' + (played ? ' played' : '') + '"></span>' +
- escapeHTML(t.name) +
- '</span>' +
- '<span class="row-actions">' +
- '<a class="mini-open" href="' + t.url + '" target="mynoise_companion" data-open="' + t.id + '" title="Open in myNoise">↗</a>' +
- '<button class="mini-heart' + (fav ? ' active' : '') + '" data-heart="' + t.id + '" title="Flag as favorite">' + (fav ? '♥' : '♡') + '</button>' +
- '</span>' +
- '</div>'
- );
- }
- function categoryCardHTML(cat){
- const info = DATA[cat];
- const trackObjs = FLAT.filter(t => t.cat === cat);
- const playedCount = trackObjs.filter(t => isPlayed(t.id)).length;
- let rows = trackObjs.map(trackRowHTML).join("");
- let lockedNote = info.more > 0
- ? '<div class="locked-note">🔒 ' + info.more + ' more with myNoise Pro</div>'
- : '';
- return (
- '<div class="cat-card">' +
- '<div class="cat-card-head"><h4>' + escapeHTML(cat) + '</h4>' +
- '<span class="frac">' + playedCount + '/' + trackObjs.length + ' played</span></div>' +
- rows +
- lockedNote +
- '</div>'
- );
- }
- function renderCatGrids(){
- const html = CATS.map(categoryCardHTML).join("");
- document.getElementById("cat-grid-now").innerHTML = html;
- document.getElementById("cat-grid-browse").innerHTML = html;
- }
- function favRowHTML(t){
- return (
- '<div class="fav-row" data-id="' + t.id + '">' +
- '<div class="fav-info"><b>' + escapeHTML(t.name) + '</b><span>' + escapeHTML(t.cat) + '</span></div>' +
- '<div class="fav-actions">' +
- '<a class="btn btn-ghost" href="' + t.url + '" target="mynoise_companion" data-open="' + t.id + '">Open ↗</a>' +
- '<button class="btn btn-icon active" data-heart="' + t.id + '" title="Remove from favorites">♥</button>' +
- '</div>' +
- '</div>'
- );
- }
- function renderFavorites(){
- const list = document.getElementById("fav-list");
- if(state.favorites.length === 0){
- list.innerHTML = '<div class="empty-state">No favorites yet — tap the heart on any track to flag it.</div>';
- return;
- }
- const items = state.favorites.map(id => byId[id]).filter(Boolean);
- list.innerHTML = items.map(favRowHTML).join("");
- }
- function escapeHTML(s){
- return String(s).replace(/[&<>"']/g, c => ({
- "&":"&","<":"<",">":">",'"':""","'":"'"
- }[c]));
- }
- function renderAll(){
- renderNowCard();
- renderProgress();
- renderCatGrids();
- renderFavorites();
- }
- // ---------- Event wiring ----------
- function wireStaticEvents(){
- document.querySelectorAll(".tab-btn").forEach(btn => {
- btn.addEventListener("click", () => {
- document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
- document.querySelectorAll(".panel").forEach(p => p.classList.remove("active"));
- btn.classList.add("active");
- document.getElementById("panel-" + btn.dataset.tab).classList.add("active");
- });
- });
- document.getElementById("btn-open").addEventListener("click", function(e){
- e.preventDefault();
- openCurrentTrack(true);
- });
- document.getElementById("btn-skip").addEventListener("click", function(){
- // A manual skip should always reset to a fresh 5:00 from right now.
- // (advanceState() chains its new deadline off the OLD one, which is
- // correct for tick()'s catch-up loop — those deadlines are already in
- // the past — but wrong here: the old deadline is still in the future
- // when you skip early, so chaining off it just piles the unused
- // remainder on top, e.g. 6 quick skips = 30 minutes on the clock.)
- markPlayed(state.currentId);
- state.currentId = nextTrack(state.currentId);
- startCountdown(TICK_SECONDS);
- save();
- renderAll();
- openCurrentTrack(true);
- });
- document.getElementById("btn-pause").addEventListener("click", function(){
- if(state.paused){
- // Resuming: rebuild an absolute deadline from the frozen remaining time.
- state.endAt = Date.now() + (state.pausedRemainingMs != null ? state.pausedRemainingMs : TICK_SECONDS * 1000);
- state.paused = false;
- delete state.pausedRemainingMs;
- } else {
- // Pausing: freeze the remaining time instead of leaving endAt to run out silently.
- state.pausedRemainingMs = Math.max(0, currentRemainingMs());
- state.paused = true;
- delete state.endAt;
- }
- save();
- updateTimerDisplay();
- });
- document.getElementById("btn-heart-now").addEventListener("click", function(){
- toggleFav(state.currentId);
- });
- // Event delegation for dynamically-rendered rows
- document.body.addEventListener("click", function(e){
- const heartBtn = e.target.closest("[data-heart]");
- if(heartBtn){
- toggleFav(heartBtn.getAttribute("data-heart"));
- return;
- }
- const jumpEl = e.target.closest("[data-jump]");
- if(jumpEl){
- setCurrent(jumpEl.getAttribute("data-jump"), { open: false });
- return;
- }
- const openEl = e.target.closest("[data-open]");
- if(openEl){
- // Handle this ourselves (instead of letting the anchor's own
- // target="mynoise_companion" fire) so we capture the window
- // reference in `noiseWin` — that reference is what lets later
- // auto-advances navigate the tab without needing another click.
- e.preventDefault();
- const id = openEl.getAttribute("data-open");
- state.currentId = id;
- startCountdown(TICK_SECONDS);
- save();
- renderAll();
- openCurrentTrack(true);
- return;
- }
- });
- }
- function init(){
- wireStaticEvents();
- renderAll();
- updateLinkBadge();
- intervalHandle = setInterval(tick, 1000);
- window.addEventListener("beforeunload", save);
- // Backgrounded tabs get their timers throttled or fully suspended by the
- // browser, so the countdown can look "stuck" until you switch back. Force
- // an immediate recompute (and any due rollovers) the moment this tab is
- // visible again, rather than waiting up to a second for the next tick.
- document.addEventListener("visibilitychange", function(){
- if(document.visibilityState === "visible") tick();
- });
- }
- init();
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment