PageSourceSearch

https://kalameh.com/themes/custom/kalameh_webapp/js/mini-player.js?v=11.3.1

js kalameh.com collected 2026-10-02 11:07:06 UTC 10,961 bytes, 310 lines download raw bytes

1((Drupal) => {
2  const STORAGE_KEY = 'kalameh.miniPlayer.v1';
3  const I_PLAY = '<polygon points="6 4 20 12 6 20 6 4"/>';
4  const I_PAUSE = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
5  const RATES = [1, 1.25, 1.5, 2, 0.75];
6
7  const fmt = (sec) => {
8    if (!Number.isFinite(sec) || sec < 0) {
9      return '0:00';
10    }
11    const m = Math.floor(sec / 60);
12    const s = Math.floor(sec % 60);
13    return `${m}:${s.toString().padStart(2, '0')}`;
14  };
15
16  const rateLabel = (r) => (r === 1 ? '1×' : `${String(r).replace(/\.0$/, '')}×`);
17
18  const loadState = () => {
19    try {
20      const raw = sessionStorage.getItem(STORAGE_KEY);
21      return raw ? JSON.parse(raw) : null;
22    }
23    catch (e) {
24      return null;
25    }
26  };
27
28  const saveState = (partial) => {
29    try {
30      const prev = loadState() || {};
31      const next = { ...prev, ...partial, updatedAt: Date.now() };
32      if (!next.src) {
33        sessionStorage.removeItem(STORAGE_KEY);
34        return;
35      }
36      sessionStorage.setItem(STORAGE_KEY, JSON.stringify(next));
37    }
38    catch (e) {
39      /* private mode / quota */
40    }
41  };
42
43  const clearState = () => {
44    try {
45      sessionStorage.removeItem(STORAGE_KEY);
46    }
47    catch (e) {
48      /* ignore */
49    }
50  };
51
52  const ensure = () => {
53    let root = document.getElementById('kalMiniPlayer');
54    if (root) {
55      return root;
56    }
57    root = document.createElement('div');
58    root.id = 'kalMiniPlayer';
59    root.className = 'kal-mini-player';
60    root.setAttribute('aria-hidden', 'true');
61    root.innerHTML = `
62      <button type="button" class="mp-cv" data-mp="cover" aria-label="Cover"><img alt=""/></button>
63      <div class="mp-info"><p class="mp-t" data-mp="title">—</p><p class="mp-a" data-mp="sub"></p></div>
64      <div class="mp-controls">
65        <button type="button" data-mp="back10" aria-label="Back 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L3 9"/><text x="12" y="16" font-family="system-ui,sans-serif" font-size="8" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none">10</text></svg></button>
66        <button type="button" class="mp-play" data-mp="play" aria-label="Play/pause"><svg data-mp="play-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">${I_PLAY}</svg></button>
67        <button type="button" data-mp="fwd10" aria-label="Forward 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 3v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.13-9.36L21 9"/><text x="12" y="16" font-family="system-ui,sans-serif" font-size="8" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none">10</text></svg></button>
68        <button type="button" class="mp-speed" data-mp="speed" aria-label="Speed">1×</button>
69        <button type="button" data-mp="close" aria-label="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
70      </div>
71      <div class="mp-prog"><span class="mp-tm" data-mp="cur">0:00</span><div class="mp-bar" data-mp="bar"><div class="mp-fill" data-mp="fill"></div></div><span class="mp-tm" data-mp="dur">0:00</span></div>
72      <audio data-mp="audio" preload="metadata"></audio>
73    `;
74    document.body.appendChild(root);
75
76    const audio = root.querySelector('[data-mp="audio"]');
77    const playIcon = root.querySelector('[data-mp="play-icon"]');
78    let rateIdx = 0;
79    let persistTimer = null;
80
81    const renderPlay = () => {
82      playIcon.innerHTML = audio.paused ? I_PLAY : I_PAUSE;
83    };
84
85    const persistNow = () => {
86      if (!audio.src) {
87        return;
88      }
89      saveState({
90        src: audio.currentSrc || audio.src,
91        title: root.querySelector('[data-mp="title"]').textContent || '',
92        subtitle: root.querySelector('[data-mp="sub"]').textContent || '',
93        cover: root.querySelector('[data-mp="cover"] img')?.getAttribute('src') || '',
94        id: root.dataset.trackId || '',
95        shareUrl: root.dataset.shareUrl || '',
96        currentTime: audio.currentTime || 0,
97        playbackRate: audio.playbackRate || 1,
98        playing: !audio.paused,
99      });
100    };
101
102    const schedulePersist = () => {
103      clearTimeout(persistTimer);
104      persistTimer = setTimeout(persistNow, 400);
105    };
106
107    root.querySelector('[data-mp="play"]').addEventListener('click', () => {
108      if (audio.paused) {
109        audio.play().catch(() => {});
110      }
111      else {
112        audio.pause();
113      }
114    });
115    root.querySelector('[data-mp="back10"]').addEventListener('click', () => {
116      audio.currentTime = Math.max(0, audio.currentTime - 10);
117      persistNow();
118    });
119    root.querySelector('[data-mp="fwd10"]').addEventListener('click', () => {
120      audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 10);
121      persistNow();
122    });
123    root.querySelector('[data-mp="speed"]').addEventListener('click', (e) => {
124      rateIdx = (rateIdx + 1) % RATES.length;
125      audio.playbackRate = RATES[rateIdx];
126      e.currentTarget.textContent = rateLabel(RATES[rateIdx]);
127      persistNow();
128    });
129    root.querySelector('[data-mp="close"]').addEventListener('click', () => {
130      Drupal.kalamehMiniPlayer.close();
131    });
132    root.querySelector('[data-mp="bar"]').addEventListener('click', (e) => {
133      const rect = e.currentTarget.getBoundingClientRect();
134      const ratio = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
135      if (audio.duration) {
136        audio.currentTime = ratio * audio.duration;
137        persistNow();
138      }
139    });
140
141    audio.addEventListener('play', () => {
142      renderPlay();
143      root.classList.add('on');
144      root.setAttribute('aria-hidden', 'false');
145      document.body.classList.add('kal-mini-player-open');
146      persistNow();
147    });
148    audio.addEventListener('pause', () => {
149      renderPlay();
150      persistNow();
151    });
152    audio.addEventListener('timeupdate', () => {
153      root.querySelector('[data-mp="cur"]').textContent = fmt(audio.currentTime);
154      root.querySelector('[data-mp="dur"]').textContent = fmt(audio.duration || 0);
155      const pct = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
156      root.querySelector('[data-mp="fill"]').style.width = `${pct}%`;
157      schedulePersist();
158    });
159    audio.addEventListener('ended', () => {
160      persistNow();
161    });
162
163    // Persist right before leaving the page.
164    window.addEventListener('pagehide', persistNow);
165    document.addEventListener('visibilitychange', () => {
166      if (document.visibilityState === 'hidden') {
167        persistNow();
168      }
169    });
170
171    root._audio = audio;
172    root._setRateIdx = (i) => { rateIdx = i; };
173    root._rateIdx = () => rateIdx;
174    return root;
175  };
176
177  const applyTrackMeta = (root, track) => {
178    root.querySelector('[data-mp="title"]').textContent = track.title || '—';
179    root.querySelector('[data-mp="sub"]').textContent = track.subtitle || '';
180    const img = root.querySelector('[data-mp="cover"] img');
181    if (track.cover) {
182      img.src = track.cover;
183      img.hidden = false;
184    }
185    else {
186      img.removeAttribute('src');
187      img.hidden = true;
188    }
189    root.dataset.trackId = track.id || '';
190    root.dataset.shareUrl = track.shareUrl || '';
191  };
192
193  Drupal.kalamehMiniPlayer = {
194    play(track) {
195      const root = ensure();
196      const audio = root._audio;
197      applyTrackMeta(root, track);
198      const nextSrc = track.src;
199      const same = audio.src && (audio.currentSrc === nextSrc || audio.src.endsWith(nextSrc) || audio.src === nextSrc);
200      if (!same) {
201        audio.src = nextSrc;
202      }
203      root._setRateIdx(0);
204      audio.playbackRate = track.playbackRate || 1;
205      const ri = RATES.indexOf(audio.playbackRate);
206      root._setRateIdx(ri >= 0 ? ri : 0);
207      root.querySelector('[data-mp="speed"]').textContent = rateLabel(audio.playbackRate || 1);
208      if (typeof track.currentTime === 'number' && track.currentTime > 0) {
209        const seek = () => {
210          try {
211            audio.currentTime = track.currentTime;
212          }
213          catch (e) {
214            /* ignore */
215          }
216          audio.removeEventListener('loadedmetadata', seek);
217        };
218        if (audio.readyState >= 1) {
219          seek();
220        }
221        else {
222          audio.addEventListener('loadedmetadata', seek);
223        }
224      }
225      audio.play().catch(() => {});
226      saveState({
227        src: nextSrc,
228        title: track.title || '',
229        subtitle: track.subtitle || '',
230        cover: track.cover || '',
231        id: track.id || '',
232        shareUrl: track.shareUrl || '',
233        currentTime: track.currentTime || 0,
234        playbackRate: audio.playbackRate || 1,
235        playing: true,
236      });
237    },
238    close() {
239      const root = document.getElementById('kalMiniPlayer');
240      if (!root) {
241        return;
242      }
243      const audio = root._audio;
244      if (audio) {
245        audio.pause();
246        audio.removeAttribute('src');
247        audio.load();
248      }
249      root.classList.remove('on');
250      root.setAttribute('aria-hidden', 'true');
251      document.body.classList.remove('kal-mini-player-open');
252      clearState();
253    },
254    restore() {
255      const state = loadState();
256      if (!state?.src) {
257        return;
258      }
259      const root = ensure();
260      const audio = root._audio;
261      applyTrackMeta(root, state);
262      audio.src = state.src;
263      const rate = state.playbackRate || 1;
264      audio.playbackRate = rate;
265      const ri = RATES.indexOf(rate);
266      root._setRateIdx(ri >= 0 ? ri : 0);
267      root.querySelector('[data-mp="speed"]').textContent = rateLabel(rate);
268      root.classList.add('on');
269      root.setAttribute('aria-hidden', 'false');
270      document.body.classList.add('kal-mini-player-open');
271
272      const seek = () => {
273        try {
274          if (state.currentTime > 0) {
275            audio.currentTime = state.currentTime;
276          }
277        }
278        catch (e) {
279          /* ignore */
280        }
281      };
282      if (audio.readyState >= 1) {
283        seek();
284      }
285      else {
286        audio.addEventListener('loadedmetadata', seek, { once: true });
287      }
288
289      if (state.playing) {
290        audio.play().catch(() => {});
291      }
292      else {
293        audio.pause();
294        root.querySelector('[data-mp="play-icon"]').innerHTML = I_PLAY;
295      }
296    },
297  };
298
299  // Restore across full page navigations (MPA).
300  const boot = () => {
301    ensure();
302    Drupal.kalamehMiniPlayer.restore();
303  };
304  if (document.readyState === 'loading') {
305    document.addEventListener('DOMContentLoaded', boot);
306  }
307  else {
308    boot();
309  }
310})(Drupal);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.