Guest User

Untitled

a guest
Sep 18th, 2026
16
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 37.78 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>myNoise Explorer</title>
  7. <style>
  8. :root{
  9. --bg: #f2f4f0;
  10. --bg-elev: #ffffff;
  11. --bg-card: #ffffff;
  12. --ink: #22322b;
  13. --ink-soft: #5b6b62;
  14. --ink-faint: #8a9990;
  15. --accent: #4f7d6f;
  16. --accent-soft: #d9e7de;
  17. --accent-strong: #38604f;
  18. --heart: #c9694f;
  19. --heart-soft: #f4ddd4;
  20. --border: #dfe6e0;
  21. --shadow: 0 1px 2px rgba(30,50,40,0.05), 0 6px 20px rgba(30,50,40,0.06);
  22. --locked: #eef1ee;
  23. --radius: 16px;
  24. --radius-sm: 10px;
  25. font-synthesis: none;
  26. }
  27. @media (prefers-color-scheme: dark){
  28. :root{
  29. --bg: #131a17;
  30. --bg-elev: #182420;
  31. --bg-card: #182420;
  32. --ink: #e7efe9;
  33. --ink-soft: #a9bbb1;
  34. --ink-faint: #6f8279;
  35. --accent: #7fb39f;
  36. --accent-soft: #24352d;
  37. --accent-strong: #a4d2bf;
  38. --heart: #e0876a;
  39. --heart-soft: #3a2822;
  40. --border: #26332c;
  41. --shadow: 0 1px 2px rgba(0,0,0,0.25), 0 8px 24px rgba(0,0,0,0.35);
  42. --locked: #1b2622;
  43. }
  44. }
  45. *{ box-sizing: border-box; }
  46. html,body{ margin:0; padding:0; }
  47. body{
  48. background: var(--bg);
  49. color: var(--ink);
  50. font-family: "Iowan Old Style","Palatino Linotype","Georgia",ui-serif,serif;
  51. -webkit-font-smoothing: antialiased;
  52. min-height: 100vh;
  53. }
  54. .wrap{
  55. max-width: 960px;
  56. margin: 0 auto;
  57. padding: 32px 20px 80px;
  58. }
  59. header.top{
  60. display:flex;
  61. align-items: baseline;
  62. justify-content: space-between;
  63. gap: 16px;
  64. flex-wrap: wrap;
  65. margin-bottom: 24px;
  66. }
  67. h1{
  68. font-size: 1.9rem;
  69. font-weight: 600;
  70. letter-spacing: 0.2px;
  71. margin: 0;
  72. font-family: "Iowan Old Style","Palatino Linotype",Georgia,serif;
  73. }
  74. h1 .sub{
  75. display:block;
  76. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  77. font-size: 0.72rem;
  78. text-transform: uppercase;
  79. letter-spacing: 1.8px;
  80. color: var(--ink-faint);
  81. font-weight: 600;
  82. margin-bottom: 4px;
  83. }
  84. .progress-pill{
  85. display:flex;
  86. gap: 18px;
  87. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  88. background: var(--bg-elev);
  89. border: 1px solid var(--border);
  90. border-radius: 999px;
  91. padding: 10px 20px;
  92. box-shadow: var(--shadow);
  93. }
  94. .progress-pill .stat{
  95. display:flex;
  96. flex-direction: column;
  97. align-items: center;
  98. line-height: 1.15;
  99. }
  100. .progress-pill .stat b{
  101. font-size: 1.05rem;
  102. color: var(--accent-strong);
  103. }
  104. .progress-pill .stat span{
  105. font-size: 0.62rem;
  106. text-transform: uppercase;
  107. letter-spacing: 1px;
  108. color: var(--ink-faint);
  109. }
  110.  
  111. nav.tabs{
  112. display:flex;
  113. gap: 6px;
  114. margin-bottom: 24px;
  115. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  116. }
  117. nav.tabs button{
  118. font: inherit;
  119. font-size: 0.85rem;
  120. font-weight: 600;
  121. letter-spacing: 0.3px;
  122. background: transparent;
  123. border: 1px solid var(--border);
  124. color: var(--ink-soft);
  125. padding: 8px 16px;
  126. border-radius: 999px;
  127. cursor: pointer;
  128. transition: all .15s ease;
  129. }
  130. nav.tabs button:hover{ border-color: var(--accent); color: var(--accent-strong); }
  131. nav.tabs button.active{
  132. background: var(--accent);
  133. border-color: var(--accent);
  134. color: #fff;
  135. }
  136.  
  137. .panel{ display:none; }
  138. .panel.active{ display:block; }
  139.  
  140. .now-card{
  141. background: linear-gradient(160deg, var(--accent-soft), var(--bg-card) 70%);
  142. border: 1px solid var(--border);
  143. border-radius: var(--radius);
  144. padding: 28px 30px;
  145. box-shadow: var(--shadow);
  146. margin-bottom: 32px;
  147. position: relative;
  148. overflow: hidden;
  149. }
  150. .now-card .eyebrow{
  151. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  152. font-size: 0.68rem;
  153. text-transform: uppercase;
  154. letter-spacing: 2px;
  155. color: var(--accent-strong);
  156. font-weight: 700;
  157. margin-bottom: 10px;
  158. }
  159. .now-card h2{
  160. margin: 0 0 4px;
  161. font-size: 2rem;
  162. font-weight: 600;
  163. }
  164. .now-card .cat-name{
  165. color: var(--ink-soft);
  166. font-size: 0.98rem;
  167. margin-bottom: 22px;
  168. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  169. }
  170. .now-row{
  171. display:flex;
  172. align-items:center;
  173. justify-content: space-between;
  174. flex-wrap: wrap;
  175. gap: 18px;
  176. }
  177. .timer{
  178. font-family: "SF Mono", "Menlo", monospace;
  179. font-size: 2.6rem;
  180. font-weight: 500;
  181. color: var(--accent-strong);
  182. letter-spacing: 1px;
  183. font-variant-numeric: tabular-nums;
  184. }
  185. .timer.paused{ opacity: 0.5; }
  186. .now-actions{
  187. display:flex;
  188. align-items:center;
  189. gap: 10px;
  190. }
  191. .btn{
  192. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  193. font-weight: 600;
  194. font-size: 0.88rem;
  195. border-radius: var(--radius-sm);
  196. border: 1px solid transparent;
  197. padding: 11px 20px;
  198. cursor: pointer;
  199. display: inline-flex;
  200. align-items: center;
  201. gap: 8px;
  202. text-decoration: none;
  203. transition: transform .1s ease, opacity .15s ease;
  204. }
  205. .btn:active{ transform: scale(0.97); }
  206. .btn-primary{
  207. background: var(--accent);
  208. color: #fff;
  209. box-shadow: var(--shadow);
  210. }
  211. .btn-primary:hover{ background: var(--accent-strong); }
  212. .btn-ghost{
  213. background: var(--bg-elev);
  214. color: var(--ink);
  215. border-color: var(--border);
  216. }
  217. .btn-ghost:hover{ border-color: var(--accent); color: var(--accent-strong); }
  218. .btn-icon{
  219. width: 42px; height: 42px;
  220. padding: 0;
  221. justify-content: center;
  222. background: var(--bg-elev);
  223. border: 1px solid var(--border);
  224. color: var(--ink-faint);
  225. border-radius: 50%;
  226. font-size: 1.1rem;
  227. }
  228. .btn-icon.active, .btn-icon:hover{ color: var(--heart); border-color: var(--heart-soft); background: var(--heart-soft); }
  229. .btn-pause{ font-size: 1.05rem; }
  230.  
  231. .link-badge{
  232. margin-top: 16px;
  233. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  234. font-size: 0.78rem;
  235. color: var(--ink-faint);
  236. }
  237. .link-badge.linked{ color: var(--accent-strong); font-weight: 600; }
  238. .catchup-note{
  239. margin-top: 10px;
  240. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  241. font-size: 0.85rem;
  242. background: var(--heart-soft);
  243. color: var(--heart);
  244. border-radius: var(--radius-sm);
  245. padding: 10px 14px;
  246. display: none;
  247. }
  248. .catchup-note.show{ display:block; }
  249.  
  250. section.grid-section h3, .fav-section h3{
  251. font-size: 1.15rem;
  252. font-weight: 600;
  253. margin: 0 0 16px;
  254. display:flex;
  255. align-items:baseline;
  256. gap: 10px;
  257. }
  258. section.grid-section h3 span.count{
  259. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  260. font-size: 0.72rem;
  261. color: var(--ink-faint);
  262. font-weight: 500;
  263. }
  264.  
  265. .cat-grid{
  266. display: grid;
  267. grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  268. gap: 16px;
  269. }
  270. .cat-card{
  271. background: var(--bg-card);
  272. border: 1px solid var(--border);
  273. border-radius: var(--radius);
  274. padding: 18px 20px 14px;
  275. box-shadow: var(--shadow);
  276. }
  277. .cat-card-head{
  278. display:flex;
  279. justify-content: space-between;
  280. align-items: baseline;
  281. margin-bottom: 10px;
  282. }
  283. .cat-card-head h4{
  284. margin: 0;
  285. font-size: 1.08rem;
  286. font-weight: 600;
  287. }
  288. .cat-card-head .frac{
  289. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  290. font-size: 0.72rem;
  291. color: var(--ink-faint);
  292. background: var(--accent-soft);
  293. padding: 3px 9px;
  294. border-radius: 999px;
  295. white-space: nowrap;
  296. }
  297. .track-row{
  298. display:flex;
  299. align-items:center;
  300. justify-content: space-between;
  301. padding: 7px 0;
  302. border-top: 1px solid var(--border);
  303. gap: 8px;
  304. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  305. }
  306. .track-row:first-of-type{ border-top: none; }
  307. .track-row .t-name{
  308. font-size: 0.9rem;
  309. cursor: pointer;
  310. color: var(--ink);
  311. flex: 1;
  312. display:flex;
  313. align-items:center;
  314. gap: 7px;
  315. }
  316. .track-row .t-name:hover{ color: var(--accent-strong); }
  317. .track-row .t-name.is-current{ color: var(--accent-strong); font-weight: 700; }
  318. .track-row .played-dot{
  319. width: 7px; height: 7px; border-radius: 50%;
  320. background: var(--border);
  321. flex-shrink: 0;
  322. }
  323. .track-row .played-dot.played{ background: var(--accent); }
  324. .row-actions{ display:flex; align-items:center; gap: 4px; }
  325. .mini-heart{
  326. background: none;
  327. border: none;
  328. cursor: pointer;
  329. font-size: 0.95rem;
  330. color: var(--ink-faint);
  331. padding: 4px;
  332. line-height: 1;
  333. }
  334. .mini-heart.active{ color: var(--heart); }
  335. .mini-open{
  336. background: none;
  337. border: none;
  338. cursor: pointer;
  339. font-size: 0.8rem;
  340. color: var(--ink-faint);
  341. padding: 4px;
  342. text-decoration: none;
  343. }
  344. .mini-open:hover{ color: var(--accent-strong); }
  345. .locked-note{
  346. margin-top: 8px;
  347. padding-top: 8px;
  348. border-top: 1px solid var(--border);
  349. font-size: 0.78rem;
  350. color: var(--ink-faint);
  351. }
  352.  
  353. .fav-list{ display:flex; flex-direction:column; gap: 10px; }
  354. .fav-row{
  355. background: var(--bg-card);
  356. border: 1px solid var(--border);
  357. border-radius: var(--radius-sm);
  358. padding: 14px 18px;
  359. display:flex;
  360. align-items:center;
  361. justify-content: space-between;
  362. box-shadow: var(--shadow);
  363. }
  364. .fav-row .fav-info b{ display:block; font-size: 1rem; }
  365. .fav-row .fav-info span{
  366. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  367. font-size: 0.78rem;
  368. color: var(--ink-faint);
  369. }
  370. .fav-row .fav-actions{ display:flex; gap: 8px; align-items:center; }
  371. .empty-state{
  372. color: var(--ink-faint);
  373. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  374. font-size: 0.9rem;
  375. padding: 30px 0;
  376. text-align:center;
  377. }
  378. footer.foot{
  379. margin-top: 50px;
  380. text-align:center;
  381. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  382. font-size: 0.72rem;
  383. color: var(--ink-faint);
  384. }
  385. </style>
  386. </head>
  387. <body>
  388. <div class="wrap">
  389.  
  390. <header class="top">
  391. <h1><span class="sub">Ambient Sound Companion</span>myNoise Explorer</h1>
  392. <div class="progress-pill">
  393. <div class="stat"><b id="stat-played">0/100</b><span>Played</span></div>
  394. <div class="stat"><b id="stat-flagged">0</b><span>Flagged</span></div>
  395. </div>
  396. </header>
  397.  
  398. <nav class="tabs">
  399. <button class="tab-btn active" data-tab="now">Now Playing</button>
  400. <button class="tab-btn" data-tab="browse">Categories</button>
  401. <button class="tab-btn" data-tab="favorites">Favorites</button>
  402. </nav>
  403.  
  404. <div class="panel active" id="panel-now">
  405. <div class="now-card">
  406. <div class="eyebrow" id="now-eyebrow">Now Playing</div>
  407. <h2 id="now-name">—</h2>
  408. <div class="cat-name" id="now-cat">—</div>
  409. <div class="now-row">
  410. <div class="timer" id="now-timer">05:00</div>
  411. <div class="now-actions">
  412. <button class="btn btn-icon btn-pause" id="btn-pause" title="Pause / resume timer">⏸</button>
  413. <button class="btn btn-icon" id="btn-heart-now" title="Flag as favorite">♥</button>
  414. <button class="btn btn-ghost" id="btn-skip">Skip ⟶</button>
  415. <a class="btn btn-primary" id="btn-open" href="#" target="mynoise_companion">Open in myNoise ↗</a>
  416. </div>
  417. </div>
  418. <div class="link-badge" id="link-badge">Click Open once to link the tab, then it auto-advances forever</div>
  419. <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>
  420. </div>
  421.  
  422. <section class="grid-section">
  423. <h3>All Categories <span class="count">tap a track to jump to it</span></h3>
  424. <div class="cat-grid" id="cat-grid-now"></div>
  425. </section>
  426. </div>
  427.  
  428. <div class="panel" id="panel-browse">
  429. <section class="grid-section">
  430. <h3>Browse Categories</h3>
  431. <div class="cat-grid" id="cat-grid-browse"></div>
  432. </section>
  433. </div>
  434.  
  435. <div class="panel" id="panel-favorites">
  436. <section class="fav-section">
  437. <h3>Your Favorites</h3>
  438. <div class="fav-list" id="fav-list"></div>
  439. </section>
  440. </div>
  441.  
  442. <footer class="foot">myNoise Explorer · a personal companion for mynoise.net · progress saved on this device</footer>
  443. </div>
  444.  
  445. <script>
  446. (function(){
  447. "use strict";
  448.  
  449. const DATA = {
  450. "Natural Noises": {"more": 25, "tracks": [
  451. ["Distant Thunder", "https://mynoise.net/NoiseMachines/thunderNoiseGenerator.php"],
  452. ["Stormy Weather", "https://mynoise.net/NoiseMachines/stormSoundGenerator.php"],
  453. ["Rain on a Tent", "https://mynoise.net/NoiseMachines/campingRainNoiseGenerator.php"],
  454. ["Fireplace", "https://mynoise.net/NoiseMachines/fireNoiseGenerator.php"],
  455. ["Tin Roof Rain", "https://mynoise.net/NoiseMachines/tinRoofRainNoiseGenerator.php"]
  456. ]},
  457. "Synthetic Noises": {"more": 6, "tracks": [
  458. ["White Noise & Co", "https://mynoise.net/NoiseMachines/whiteNoiseGenerator.php"],
  459. ["Brownish White", "https://mynoise.net/NoiseMachines/notWhiteNoisePlayer.php"],
  460. ["Xenobiota", "https://mynoise.net/NoiseMachines/xenobiotaGenerativeNoise.php"],
  461. ["Sound Horizon", "https://mynoise.net/NoiseMachines/baryonAcousticOscillationsNoisePlayer.php"],
  462. ["Grey Noise", "https://mynoise.net/NoiseMachines/greyNoiseGenerator.php"]
  463. ]},
  464. "Experimental": {"more": 5, "tracks": [
  465. ["Binaural Beats", "https://mynoise.net/NoiseMachines/binauralBrainwaveGenerator.php"],
  466. ["40 Hertz", "https://mynoise.net/NoiseMachines/forty40HzBrainwaveGenerator.php"],
  467. ["Binaural Harmonics", "https://mynoise.net/NoiseMachines/pureBinauralBrainwaveGenerator.php"],
  468. ["Solfeggio Tones", "https://mynoise.net/NoiseMachines/solfeggioTonesGenerator.php"],
  469. ["Isochronic Tones", "https://mynoise.net/NoiseMachines/isochronicBrainwaveGenerator.php"]
  470. ]},
  471. "Landscapes": {"more": 48, "tracks": [
  472. ["Japanese Garden", "https://mynoise.net/NoiseMachines/japaneseGardenSoundscapeGenerator.php"],
  473. ["Irish Coast", "https://mynoise.net/NoiseMachines/windSeaRainNoiseGenerator.php"],
  474. ["Primeval Forest", "https://mynoise.net/NoiseMachines/primevalEuropeanForestSoundscapeGenerator.php"],
  475. ["Healing Water", "https://mynoise.net/NoiseMachines/healingWaterSoundscapeGenerator.php"],
  476. ["Icelandic Shores", "https://mynoise.net/NoiseMachines/oceanWavesSeaShoreNoiseGenerator.php"]
  477. ]},
  478. "Ambiences": {"more": 15, "tracks": [
  479. ["Autumn Walk", "https://mynoise.net/NoiseMachines/autumnWalkSoundscapeGenerator.php"],
  480. ["Cosy Rain", "https://mynoise.net/NoiseMachines/customRainOnWindow.php"],
  481. ["Nightscape", "https://mynoise.net/NoiseMachines/customNightscape.php"],
  482. ["Underwater", "https://mynoise.net/NoiseMachines/finWhaleUnderwaterSoundscapeGenerator.php"],
  483. ["Bad Weather", "https://mynoise.net/NoiseMachines/customStormy.php"]
  484. ]},
  485. "Fantasy": {"more": 10, "tracks": [
  486. ["Medieval Library", "https://mynoise.net/NoiseMachines/customMedievalLibrary.php"],
  487. ["Medieval Village", "https://mynoise.net/NoiseMachines/medievalVillageSoundscapeGenerator.php"],
  488. ["Sad Minstrel", "https://mynoise.net/NoiseMachines/medievalMinstrelSoundscapeGenerator.php"],
  489. ["Name of the Rose", "https://mynoise.net/NoiseMachines/customTheNameOfTheRose.php"],
  490. ["Dark Forest", "https://mynoise.net/NoiseMachines/enchantedForestSoundscapeGenerator.php"]
  491. ]},
  492. "Magic": {"more": 18, "tracks": [
  493. ["Dunes Of Arrakis", "https://mynoise.net/NoiseMachines/magicDuneArrakisGenerator.php"],
  494. ["Zen Garden", "https://mynoise.net/NoiseMachines/magicMeditationGenerator.php"],
  495. ["Mermaids Calling", "https://mynoise.net/NoiseMachines/magicSirenSongGenerator.php"],
  496. ["日本", "https://mynoise.net/NoiseMachines/magicJapanGenerator.php"],
  497. ["Rainy Days", "https://mynoise.net/NoiseMachines/magicRainyDaysGenerator.php"]
  498. ]},
  499. "Vocal": {"more": 13, "tracks": [
  500. ["Gregorian Chants", "https://mynoise.net/NoiseMachines/gregorianChoirGenerator.php"],
  501. ["Forest Voice", "https://mynoise.net/NoiseMachines/voiceInTheWoods.php"],
  502. ["Silk and Hopes", "https://mynoise.net/NoiseMachines/raphaelAlongTheSilkRoad.php"],
  503. ["Oak Song", "https://mynoise.net/NoiseMachines/riikkaVocalSoundscape.php"],
  504. ["Deep Chant", "https://mynoise.net/NoiseMachines/deepVoiceSoundscape.php"]
  505. ]},
  506. "Acoustic": {"more": 23, "tracks": [
  507. ["The Pilgrim", "https://mynoise.net/NoiseMachines/tongueDrumSoundscapeGenerator.php"],
  508. ["Duduk Song", "https://mynoise.net/NoiseMachines/dudukSoundGenerator.php"],
  509. ["88 Keys", "https://mynoise.net/NoiseMachines/acousticPianoSoundscapeGenerator.php"],
  510. ["Marimbas", "https://mynoise.net/NoiseMachines/benClarkMarimbaGenerator.php"],
  511. ["Silver Drift", "https://mynoise.net/NoiseMachines/benClarkVibraphoneSoundscapeGenerator.php"]
  512. ]},
  513. "Cinematic": {"more": 7, "tracks": [
  514. ["Home Again", "https://mynoise.net/NoiseMachines/meditationMusicGenerator.php"],
  515. ["Japanese Oase", "https://mynoise.net/NoiseMachines/japaneseSuikinkutsuSoundscapeGenerator.php"],
  516. ["Solomon's Dream", "https://mynoise.net/NoiseMachines/middleEasternMusicGenerator.php"],
  517. ["Cinematic East", "https://mynoise.net/NoiseMachines/cinematicEasternSoundscapeGenerator.php"],
  518. ["Dark Seas", "https://mynoise.net/NoiseMachines/customDarkWaters.php"]
  519. ]},
  520. "Electric": {"more": 14, "tracks": [
  521. ["Night Blue", "https://mynoise.net/NoiseMachines/blueJazzGenerator.php"],
  522. ["Walking Through", "https://mynoise.net/NoiseMachines/downtempoBackgroundMusicGenerator.php"],
  523. ["Mr. Rhodes", "https://mynoise.net/NoiseMachines/rhodesSoundscapeGenerator.php"],
  524. ["Shroom With a View", "https://mynoise.net/NoiseMachines/hanShroomMusicGenerator.php"],
  525. ["Tape Memories", "https://mynoise.net/NoiseMachines/oldTapeSoundscapeGenerator.php"]
  526. ]},
  527. "Synthesizers": {"more": 33, "tracks": [
  528. ["The Architect's Eclipse", "https://mynoise.net/NoiseMachines/ionStreamSpaceAmbientGenerativeMusic.php"],
  529. ["Take It Easy", "https://mynoise.net/NoiseMachines/paulNagleSequenceGenerator.php"],
  530. ["Psynesthesia", "https://mynoise.net/NoiseMachines/ambientPsytranceMusicGenerator.php"],
  531. ["Dissociative Trance", "https://mynoise.net/NoiseMachines/dissociativeTranceMusicGenerator.php"],
  532. ["Neon Voyager", "https://mynoise.net/NoiseMachines/retroSpaceElectroSoundscapeGenerator.php"]
  533. ]},
  534. "Ambient": {"more": 19, "tracks": [
  535. ["Floating", "https://mynoise.net/NoiseMachines/deepRelaxingAmbientSoundscapeGenerator.php"],
  536. ["Sleeping Dragon", "https://mynoise.net/NoiseMachines/sleepingDragonToneGenerator.php"],
  537. ["Osmosis", "https://mynoise.net/NoiseMachines/osmosisDroneGenerator.php"],
  538. ["Dreaming Nautilus", "https://mynoise.net/NoiseMachines/dreamingNautilusGenerativeSoundscape.php"],
  539. ["Anamnesis", "https://mynoise.net/NoiseMachines/anamnesisSoundscapeGenerator.php"]
  540. ]},
  541. "Traditional": {"more": 3, "tracks": [
  542. ["Tibetan Spirit", "https://mynoise.net/NoiseMachines/tibetSoundscapeGenerator.php"],
  543. ["Berber Tent", "https://mynoise.net/NoiseMachines/berberTentSoundscapeGenerator.php"],
  544. ["Buddhist Chants", "https://mynoise.net/NoiseMachines/buddhistTempleCeremonySoundscapeGenerator.php"],
  545. ["Finnish Sauna", "https://mynoise.net/NoiseMachines/finnishSaunaSoundscapeGenerator.php"],
  546. ["Japanese Heritage", "https://mynoise.net/NoiseMachines/japaneseShrineSoundscapeGenerator.php"]
  547. ]},
  548. "Spaces": {"more": 9, "tracks": [
  549. ["Cafe Restaurant", "https://mynoise.net/NoiseMachines/cafeRestaurantNoiseGenerator.php"],
  550. ["Calm Office", "https://mynoise.net/NoiseMachines/openOfficeNoiseGenerator.php"],
  551. ["Car Interior", "https://mynoise.net/NoiseMachines/carInteriorSoundscapeGenerator.php"],
  552. ["Vintage Office", "https://mynoise.net/NoiseMachines/vintageOfficeNoiseGenerator.php"],
  553. ["Church", "https://mynoise.net/NoiseMachines/churchAtmosphereSoundGenerator.php"]
  554. ]},
  555. "Industrial": {"more": 8, "tracks": [
  556. ["Data Center", "https://mynoise.net/NoiseMachines/dataCenterNoiseGenerator.php"],
  557. ["Fan Noise", "https://mynoise.net/NoiseMachines/fanNoiseGenerator.php"],
  558. ["Steel Mill", "https://mynoise.net/NoiseMachines/steelMillNoiseGenerator.php"],
  559. ["Vinyl Groove", "https://mynoise.net/NoiseMachines/dustyScratchedVinylNoiseGenerator.php"],
  560. ["Factory", "https://mynoise.net/NoiseMachines/industrialRevolutionNoiseGenerator.php"]
  561. ]},
  562. "Transports": {"more": 8, "tracks": [
  563. ["Railroads", "https://mynoise.net/NoiseMachines/railroadNoiseGenerator.php"],
  564. ["Spaceship", "https://mynoise.net/NoiseMachines/spaceshipNoiseGenerator.php"],
  565. ["Flying Fortress", "https://mynoise.net/NoiseMachines/propellerNoiseGenerator.php"],
  566. ["Sailboat", "https://mynoise.net/NoiseMachines/squeakingSailboatSoundscapeGenerator.php"],
  567. ["Aircraft Cabin", "https://mynoise.net/NoiseMachines/cabinNoiseGenerator.php"]
  568. ]},
  569. "Urban": {"more": 7, "tracks": [
  570. ["Wet Asphalt", "https://mynoise.net/NoiseMachines/urbanRainSoundGenerator.php"],
  571. ["City Rain", "https://mynoise.net/NoiseMachines/customRainInTheCity.php"],
  572. ["Sherbrooke Str", "https://mynoise.net/NoiseMachines/sherbrookeStreetAmbienceGenerator.php"],
  573. ["Port Town", "https://mynoise.net/NoiseMachines/harborSoundscapeGenerator.php"],
  574. ["Senegal Town", "https://mynoise.net/NoiseMachines/senegalUrbanLifeAmbienceGenerator.php"]
  575. ]},
  576. "Tonal Drones": {"more": 8, "tracks": [
  577. ["Singing Bowls", "https://mynoise.net/NoiseMachines/singingBowlsDroneGenerator.php"],
  578. ["Tibetan Choir", "https://mynoise.net/NoiseMachines/throatSingingDroneGenerator.php"],
  579. ["Time Still", "https://mynoise.net/NoiseMachines/timeStillMusicalDroneGenerator.php"],
  580. ["Orchestron", "https://mynoise.net/NoiseMachines/orchestronDroneGenerator.php"],
  581. ["Ice World", "https://mynoise.net/NoiseMachines/iceDroneGenerator.php"]
  582. ]},
  583. "Patterns": {"more": 9, "tracks": [
  584. ["Polyrhythms", "https://mynoise.net/NoiseMachines/polyrhythmBeatGenerator.php"],
  585. ["Tuned Cave", "https://mynoise.net/NoiseMachines/contemplationCaveGenerator.php"],
  586. ["Temple Bells", "https://mynoise.net/NoiseMachines/windchimesGenerator.php"],
  587. ["Cave Chime", "https://mynoise.net/NoiseMachines/stalactitesCaveChimeSoundscapeGenerator.php"],
  588. ["Clockwork", "https://mynoise.net/NoiseMachines/clockTickTockNoiseGenerator.php"]
  589. ]}
  590. };
  591.  
  592. const CATS = Object.keys(DATA);
  593. // Flat list of {id, cat, idx, name, url}
  594. const FLAT = [];
  595. CATS.forEach(cat => {
  596. DATA[cat].tracks.forEach((t, idx) => {
  597. FLAT.push({ id: cat + "::" + idx, cat: cat, idx: idx, name: t[0], url: t[1] });
  598. });
  599. });
  600. const TOTAL = FLAT.length;
  601. const byId = {};
  602. FLAT.forEach(t => byId[t.id] = t);
  603.  
  604. const STORAGE_KEY = "mynoise_explorer_state_v1";
  605. const TAB_NAME = "mynoise_companion";
  606. const TICK_SECONDS = 5 * 60;
  607.  
  608. function loadState(){
  609. let s = null;
  610. try{ s = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); }catch(e){ s = null; }
  611. if(!s || typeof s !== "object"){
  612. s = {};
  613. }
  614. if(!s.currentId || !byId[s.currentId]) s.currentId = FLAT[0].id;
  615. if(!Array.isArray(s.played)) s.played = [];
  616. if(!Array.isArray(s.favorites)) s.favorites = [];
  617. s.played = s.played.filter(id => byId[id]);
  618. s.favorites = s.favorites.filter(id => byId[id]);
  619. if(typeof s.paused !== "boolean") s.paused = false;
  620. if(s.paused){
  621. if(typeof s.pausedRemainingMs !== "number" || s.pausedRemainingMs < 0) s.pausedRemainingMs = TICK_SECONDS * 1000;
  622. delete s.endAt;
  623. } else {
  624. if(typeof s.endAt !== "number" || !isFinite(s.endAt)) s.endAt = Date.now() + TICK_SECONDS * 1000;
  625. delete s.pausedRemainingMs;
  626. }
  627. return s;
  628. }
  629.  
  630. // The countdown is driven by an absolute wall-clock deadline (state.endAt),
  631. // never by counting down "1 per tick". A setInterval(fn, 1000) firing every
  632. // second is only a *request* — a backgrounded/inactive browser tab gets
  633. // throttled by the browser and may only fire every several seconds, or
  634. // pause almost entirely. Decrementing a counter once per firing under that
  635. // throttling makes the on-screen clock silently fall behind real time
  636. // (exactly the "3:30 left, come back 5 min later, now 2:15 left" bug).
  637. // Recomputing remaining time from Date.now() vs. a fixed deadline is
  638. // immune to that: whenever the tick does fire, it shows the true time left.
  639. function currentRemainingMs(){
  640. if(state.paused){
  641. return state.pausedRemainingMs != null ? state.pausedRemainingMs : TICK_SECONDS * 1000;
  642. }
  643. if(typeof state.endAt !== "number") return TICK_SECONDS * 1000;
  644. return state.endAt - Date.now();
  645. }
  646.  
  647. function startCountdown(seconds){
  648. state.paused = false;
  649. state.endAt = Date.now() + seconds * 1000;
  650. delete state.pausedRemainingMs;
  651. }
  652.  
  653. let state = loadState();
  654. let noiseWin = null;
  655. let intervalHandle = null;
  656.  
  657. function save(){
  658. try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }catch(e){ /* ignore quota errors */ }
  659. }
  660.  
  661. function fmtTime(sec){
  662. const m = Math.floor(sec / 60);
  663. const s = sec % 60;
  664. return String(m).padStart(2,"0") + ":" + String(s).padStart(2,"0");
  665. }
  666.  
  667. function isFav(id){ return state.favorites.indexOf(id) !== -1; }
  668. function isPlayed(id){ return state.played.indexOf(id) !== -1; }
  669.  
  670. function toggleFav(id){
  671. const i = state.favorites.indexOf(id);
  672. if(i === -1) state.favorites.push(id); else state.favorites.splice(i,1);
  673. save();
  674. renderAll();
  675. }
  676.  
  677. function markPlayed(id){
  678. if(state.played.indexOf(id) === -1) state.played.push(id);
  679. }
  680.  
  681. function setCurrent(id, opts){
  682. opts = opts || {};
  683. state.currentId = id;
  684. startCountdown(TICK_SECONDS);
  685. hideCatchup();
  686. save();
  687. renderAll();
  688. if(opts.open){
  689. openCurrentTrack(true);
  690. }
  691. }
  692.  
  693. // Opening a NEW window requires a real user gesture (click). But once we
  694. // hold a JS reference to that window, we can change its .location later
  695. // from a timer callback with NO gesture and NO popup-blocker involvement —
  696. // that's only true window.open() calls that get blocked. So: the user
  697. // clicks "Open in myNoise" once to link the tab; every auto-advance after
  698. // that just re-points the same window object, which browsers always allow.
  699. function openCurrentTrack(fromUserGesture){
  700. const t = byId[state.currentId];
  701. if(fromUserGesture){
  702. try{
  703. const w = window.open(t.url, TAB_NAME);
  704. if(w){
  705. noiseWin = w;
  706. try{ w.focus(); }catch(e){}
  707. hideCatchup();
  708. updateLinkBadge();
  709. return true;
  710. }
  711. }catch(e){ /* fall through */ }
  712. hideCatchup();
  713. updateLinkBadge();
  714. return false;
  715. }
  716.  
  717. // Not a user gesture: try to reuse the already-linked window first.
  718. if(noiseWin && !noiseWin.closed){
  719. try{
  720. noiseWin.location = t.url;
  721. try{ noiseWin.focus(); }catch(e){}
  722. hideCatchup();
  723. updateLinkBadge();
  724. return true;
  725. }catch(e){ /* window may have navigated away/closed unexpectedly */ }
  726. }
  727.  
  728. // No linked window (never opened, or user closed it). A fresh
  729. // window.open() from here has no user gesture behind it, so most
  730. // browsers will silently block it — try anyway, then surface the note.
  731. try{
  732. const w = window.open(t.url, TAB_NAME);
  733. if(w){
  734. noiseWin = w;
  735. hideCatchup();
  736. updateLinkBadge();
  737. return true;
  738. }
  739. }catch(e){ /* ignore */ }
  740. showCatchup();
  741. updateLinkBadge();
  742. return false;
  743. }
  744.  
  745. function updateLinkBadge(){
  746. const linked = !!(noiseWin && !noiseWin.closed);
  747. const badge = document.getElementById("link-badge");
  748. if(!badge) return;
  749. badge.textContent = linked ? "🔗 Tab linked — auto-advances hands-free" : "Click Open once to link the tab, then it auto-advances forever";
  750. badge.classList.toggle("linked", linked);
  751. }
  752.  
  753. function showCatchup(){
  754. document.getElementById("catchup-note").classList.add("show");
  755. }
  756. function hideCatchup(){
  757. document.getElementById("catchup-note").classList.remove("show");
  758. }
  759.  
  760. function nextTrack(currentId){
  761. const t = byId[currentId];
  762. const pos = FLAT.findIndex(x => x.id === currentId);
  763. const nextPos = (pos + 1) % FLAT.length;
  764. return FLAT[nextPos].id;
  765. }
  766.  
  767. // Pure state mutation for one 5-minute rollover — no opening/rendering here,
  768. // so tick() can call this in a loop to catch up if several deadlines were
  769. // missed at once (e.g. the laptop slept, or the tab was backgrounded long
  770. // enough that timers were suspended entirely).
  771. function advanceState(){
  772. const prevId = state.currentId;
  773. markPlayed(prevId);
  774. state.currentId = nextTrack(prevId);
  775. // Chain off the previous deadline (not "now") so a string of missed
  776. // rollovers is caught up one real 5-minute increment at a time, instead
  777. // of every catch-up collapsing into a single skip.
  778. state.endAt = (typeof state.endAt === "number" ? state.endAt : Date.now()) + TICK_SECONDS * 1000;
  779. }
  780.  
  781. function tick(){
  782. updateLinkBadge();
  783. if(state.paused){
  784. updateTimerDisplay();
  785. return;
  786. }
  787. if(typeof state.endAt !== "number") state.endAt = Date.now() + TICK_SECONDS * 1000;
  788. let advanced = false;
  789. let guard = 0;
  790. while(state.endAt - Date.now() <= 0 && guard < 500){
  791. advanceState();
  792. advanced = true;
  793. guard++;
  794. }
  795. save();
  796. updateTimerDisplay();
  797. if(advanced){
  798. renderAll();
  799. // Attempt silent hop into the same named tab (no fresh user gesture
  800. // needed since the tab already exists under this name in most browsers).
  801. openCurrentTrack(false);
  802. }
  803. }
  804.  
  805. function updateTimerDisplay(){
  806. const el = document.getElementById("now-timer");
  807. const secs = Math.max(0, Math.round(currentRemainingMs() / 1000));
  808. el.textContent = fmtTime(secs);
  809. el.classList.toggle("paused", state.paused);
  810. document.getElementById("btn-pause").textContent = state.paused ? "▶" : "⏸";
  811. }
  812.  
  813. // ---------- Rendering ----------
  814.  
  815. function renderNowCard(){
  816. const t = byId[state.currentId];
  817. document.getElementById("now-name").textContent = t.name;
  818. document.getElementById("now-cat").textContent = t.cat;
  819. document.getElementById("btn-open").href = t.url;
  820. updateTimerDisplay();
  821. const heartBtn = document.getElementById("btn-heart-now");
  822. heartBtn.classList.toggle("active", isFav(t.id));
  823. }
  824.  
  825. function renderProgress(){
  826. document.getElementById("stat-played").textContent = state.played.length + "/" + TOTAL;
  827. document.getElementById("stat-flagged").textContent = String(state.favorites.length);
  828. }
  829.  
  830. function trackRowHTML(t){
  831. const played = isPlayed(t.id);
  832. const fav = isFav(t.id);
  833. const current = t.id === state.currentId;
  834. return (
  835. '<div class="track-row" data-id="' + t.id + '">' +
  836. '<span class="t-name' + (current ? ' is-current' : '') + '" data-jump="' + t.id + '">' +
  837. '<span class="played-dot' + (played ? ' played' : '') + '"></span>' +
  838. escapeHTML(t.name) +
  839. '</span>' +
  840. '<span class="row-actions">' +
  841. '<a class="mini-open" href="' + t.url + '" target="mynoise_companion" data-open="' + t.id + '" title="Open in myNoise">↗</a>' +
  842. '<button class="mini-heart' + (fav ? ' active' : '') + '" data-heart="' + t.id + '" title="Flag as favorite">' + (fav ? '♥' : '♡') + '</button>' +
  843. '</span>' +
  844. '</div>'
  845. );
  846. }
  847.  
  848. function categoryCardHTML(cat){
  849. const info = DATA[cat];
  850. const trackObjs = FLAT.filter(t => t.cat === cat);
  851. const playedCount = trackObjs.filter(t => isPlayed(t.id)).length;
  852. let rows = trackObjs.map(trackRowHTML).join("");
  853. let lockedNote = info.more > 0
  854. ? '<div class="locked-note">🔒 ' + info.more + ' more with myNoise Pro</div>'
  855. : '';
  856. return (
  857. '<div class="cat-card">' +
  858. '<div class="cat-card-head"><h4>' + escapeHTML(cat) + '</h4>' +
  859. '<span class="frac">' + playedCount + '/' + trackObjs.length + ' played</span></div>' +
  860. rows +
  861. lockedNote +
  862. '</div>'
  863. );
  864. }
  865.  
  866. function renderCatGrids(){
  867. const html = CATS.map(categoryCardHTML).join("");
  868. document.getElementById("cat-grid-now").innerHTML = html;
  869. document.getElementById("cat-grid-browse").innerHTML = html;
  870. }
  871.  
  872. function favRowHTML(t){
  873. return (
  874. '<div class="fav-row" data-id="' + t.id + '">' +
  875. '<div class="fav-info"><b>' + escapeHTML(t.name) + '</b><span>' + escapeHTML(t.cat) + '</span></div>' +
  876. '<div class="fav-actions">' +
  877. '<a class="btn btn-ghost" href="' + t.url + '" target="mynoise_companion" data-open="' + t.id + '">Open ↗</a>' +
  878. '<button class="btn btn-icon active" data-heart="' + t.id + '" title="Remove from favorites">♥</button>' +
  879. '</div>' +
  880. '</div>'
  881. );
  882. }
  883.  
  884. function renderFavorites(){
  885. const list = document.getElementById("fav-list");
  886. if(state.favorites.length === 0){
  887. list.innerHTML = '<div class="empty-state">No favorites yet — tap the heart on any track to flag it.</div>';
  888. return;
  889. }
  890. const items = state.favorites.map(id => byId[id]).filter(Boolean);
  891. list.innerHTML = items.map(favRowHTML).join("");
  892. }
  893.  
  894. function escapeHTML(s){
  895. return String(s).replace(/[&<>"']/g, c => ({
  896. "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
  897. }[c]));
  898. }
  899.  
  900. function renderAll(){
  901. renderNowCard();
  902. renderProgress();
  903. renderCatGrids();
  904. renderFavorites();
  905. }
  906.  
  907. // ---------- Event wiring ----------
  908.  
  909. function wireStaticEvents(){
  910. document.querySelectorAll(".tab-btn").forEach(btn => {
  911. btn.addEventListener("click", () => {
  912. document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
  913. document.querySelectorAll(".panel").forEach(p => p.classList.remove("active"));
  914. btn.classList.add("active");
  915. document.getElementById("panel-" + btn.dataset.tab).classList.add("active");
  916. });
  917. });
  918.  
  919. document.getElementById("btn-open").addEventListener("click", function(e){
  920. e.preventDefault();
  921. openCurrentTrack(true);
  922. });
  923.  
  924. document.getElementById("btn-skip").addEventListener("click", function(){
  925. // A manual skip should always reset to a fresh 5:00 from right now.
  926. // (advanceState() chains its new deadline off the OLD one, which is
  927. // correct for tick()'s catch-up loop — those deadlines are already in
  928. // the past — but wrong here: the old deadline is still in the future
  929. // when you skip early, so chaining off it just piles the unused
  930. // remainder on top, e.g. 6 quick skips = 30 minutes on the clock.)
  931. markPlayed(state.currentId);
  932. state.currentId = nextTrack(state.currentId);
  933. startCountdown(TICK_SECONDS);
  934. save();
  935. renderAll();
  936. openCurrentTrack(true);
  937. });
  938.  
  939. document.getElementById("btn-pause").addEventListener("click", function(){
  940. if(state.paused){
  941. // Resuming: rebuild an absolute deadline from the frozen remaining time.
  942. state.endAt = Date.now() + (state.pausedRemainingMs != null ? state.pausedRemainingMs : TICK_SECONDS * 1000);
  943. state.paused = false;
  944. delete state.pausedRemainingMs;
  945. } else {
  946. // Pausing: freeze the remaining time instead of leaving endAt to run out silently.
  947. state.pausedRemainingMs = Math.max(0, currentRemainingMs());
  948. state.paused = true;
  949. delete state.endAt;
  950. }
  951. save();
  952. updateTimerDisplay();
  953. });
  954.  
  955. document.getElementById("btn-heart-now").addEventListener("click", function(){
  956. toggleFav(state.currentId);
  957. });
  958.  
  959. // Event delegation for dynamically-rendered rows
  960. document.body.addEventListener("click", function(e){
  961. const heartBtn = e.target.closest("[data-heart]");
  962. if(heartBtn){
  963. toggleFav(heartBtn.getAttribute("data-heart"));
  964. return;
  965. }
  966. const jumpEl = e.target.closest("[data-jump]");
  967. if(jumpEl){
  968. setCurrent(jumpEl.getAttribute("data-jump"), { open: false });
  969. return;
  970. }
  971. const openEl = e.target.closest("[data-open]");
  972. if(openEl){
  973. // Handle this ourselves (instead of letting the anchor's own
  974. // target="mynoise_companion" fire) so we capture the window
  975. // reference in `noiseWin` — that reference is what lets later
  976. // auto-advances navigate the tab without needing another click.
  977. e.preventDefault();
  978. const id = openEl.getAttribute("data-open");
  979. state.currentId = id;
  980. startCountdown(TICK_SECONDS);
  981. save();
  982. renderAll();
  983. openCurrentTrack(true);
  984. return;
  985. }
  986. });
  987. }
  988.  
  989. function init(){
  990. wireStaticEvents();
  991. renderAll();
  992. updateLinkBadge();
  993. intervalHandle = setInterval(tick, 1000);
  994. window.addEventListener("beforeunload", save);
  995. // Backgrounded tabs get their timers throttled or fully suspended by the
  996. // browser, so the countdown can look "stuck" until you switch back. Force
  997. // an immediate recompute (and any due rollovers) the moment this tab is
  998. // visible again, rather than waiting up to a second for the next tick.
  999. document.addEventListener("visibilitychange", function(){
  1000. if(document.visibilityState === "visible") tick();
  1001. });
  1002. }
  1003.  
  1004. init();
  1005. })();
  1006. </script>
  1007. </body>
  1008. </html>
  1009.  
Advertisement
Add Comment
Please, Sign In to add comment