Skip to main content

max / makenotwork

4.3 KB · 99 lines History Blame Raw
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>{{ title }} — Makenot.work</title>
7 <style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 body {
10 font-family: Lato, -apple-system, sans-serif;
11 background: #ede8e1; color: #3d3530;
12 display: flex; align-items: center;
13 height: 100vh; padding: 10px 12px;
14 }
15 .player { display: flex; align-items: center; gap: 12px; width: 100%; }
16 .cover { width: 80px; height: 80px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }
17 .placeholder { background: #f5f0eb; }
18 .right { flex: 1; min-width: 0; }
19 .top-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 4px; }
20 .title { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
21 .price { font-size: 12px; font-family: 'IBM Plex Mono', monospace; margin-left: 8px; white-space: nowrap; }
22 .creator { font-size: 11px; opacity: 0.6; margin-bottom: 8px; }
23 .controls { display: flex; align-items: center; gap: 8px; }
24 .play-btn {
25 width: 32px; height: 32px; border-radius: 50%;
26 background: #6c5ce7; color: #fff; border: none;
27 font-size: 14px; cursor: pointer; display: flex;
28 align-items: center; justify-content: center; flex-shrink: 0;
29 }
30 .play-btn:hover { background: #5a4bd6; }
31 .progress-bar {
32 flex: 1; height: 4px; background: rgba(61,53,48,0.15);
33 border-radius: 2px; cursor: pointer; position: relative;
34 }
35 .progress-fill { height: 100%; background: #6c5ce7; border-radius: 2px; width: 0%; transition: width 0.1s; }
36 .time { font-size: 10px; font-family: 'IBM Plex Mono', monospace; opacity: 0.6; white-space: nowrap; }
37 .bottom-row { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
38 .preview-label { font-size: 10px; opacity: 0.5; }
39 .buy-btn {
40 background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
41 padding: 6px 12px; font-size: 11px; font-weight: 600;
42 cursor: pointer; text-decoration: none; white-space: nowrap;
43 }
44 .buy-btn:hover { background: #5a4bd6; }
45 </style>
46 </head>
47 <body>
48 <div class="player" data-preview-url="{{ preview_url }}">
49 {% if let Some(url) = cover_image_url %}<img class="cover" src="{{ url }}" alt="">{% else %}<div class="cover placeholder"></div>{% endif %}
50 <div class="right">
51 <div class="top-row">
52 <div class="title">{{ title }}</div>
53 <div class="price">{{ price_display }}</div>
54 </div>
55 <div class="creator">by {{ creator_display_name }}</div>
56 <div class="controls">
57 <button class="play-btn" id="play" onclick="togglePlay()">&#9654;</button>
58 <div class="progress-bar" id="progress-bar" onclick="seek(event)">
59 <div class="progress-fill" id="progress"></div>
60 </div>
61 <span class="time" id="time">0:00</span>
62 </div>
63 <div class="bottom-row">
64 <span class="preview-label">Preview</span>
65 <a class="buy-btn" href="{{ purchase_url }}" target="_blank" rel="noopener">{{ button_text }}</a>
66 </div>
67 </div>
68 </div>
69 <script>
70 const audio = new Audio();
71 let loaded = false;
72 // Read the preview URL from the data- attribute rather than interpolating it
73 // into this JS string. Askama autoescapes for HTML, which is the correct
74 // escaper for an attribute value but NOT for a JS string literal — keeping the
75 // URL in the attribute keeps escaping correct even once preview URLs derive
76 // from user-influenced filenames.
77 const previewUrl = document.querySelector('.player').dataset.previewUrl;
78 function togglePlay() {
79 if (!loaded) { audio.src = previewUrl; loaded = true; }
80 if (audio.paused) { audio.play(); document.getElementById('play').innerHTML = '&#9646;&#9646;'; }
81 else { audio.pause(); document.getElementById('play').innerHTML = '&#9654;'; }
82 }
83 audio.ontimeupdate = () => {
84 const pct = (audio.currentTime / audio.duration) * 100;
85 document.getElementById('progress').style.width = pct + '%';
86 const m = Math.floor(audio.currentTime / 60);
87 const s = Math.floor(audio.currentTime % 60);
88 document.getElementById('time').textContent = m + ':' + (s < 10 ? '0' : '') + s;
89 };
90 audio.onended = () => { document.getElementById('play').innerHTML = '&#9654;'; };
91 function seek(e) {
92 if (!audio.duration) return;
93 const rect = e.currentTarget.getBoundingClientRect();
94 audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration;
95 }
96 </script>
97 </body>
98 </html>
99