Compare commits

...

5 Commits

Author SHA1 Message Date
090a145211 add previous/next track button handling 2026-03-17 00:05:44 +01:00
1dc3f4505b fix seeking problems due to progress bar width inconsistency 2026-03-16 23:21:00 +01:00
7febb497e9 update homepage message 2026-03-16 20:38:35 +01:00
6c12cfd49b Merge branch 'offline-mode' 2026-03-16 20:33:03 +01:00
f777873432 work around opus bug 2026-02-20 16:21:22 +01:00
2 changed files with 63 additions and 16 deletions

View File

@@ -11,7 +11,12 @@
</div>
<div class="description-bubble">
<p>Hello, there, welcome to V1.5.</p>
<p>Hello, there, welcome to V1.6.</p>
<p>
2026-03-16 update: you can now cache streams offline for playback in low-connectivity environments (and to prevent buffering)!<br>
Simply click on the download icon in the stream list (and click again to remove them from cache). You can also filter by cached streams at the top. This all goes into your browser storage, and a stream is generally 100~250MB depending on its length.
</p>
<hr>
<p>
Still in construction. The design is responsive now, so phones should work well enough!
Needs touch events though.

View File

@@ -78,12 +78,26 @@
let volumeBar = $state();
let innerWidth = $state();
let innerHeight = $state();
let isSafari = $state(false);
onMount(async () => {
// work around coreaudio bug
// see https://git.webbieweb.org/apt-get/strimserve/issues/1
const ua = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(ua);
const isDesktopSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua);
isSafari = isIOS || isDesktopSafari;
// default volume
const volumeData = localStorage.getItem('volume');
volume = volumeData ? parseFloat(volumeData) : 0.67;
setVolume(volume);
// set actions for previous/next track media buttons
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', previousTrack);
navigator.mediaSession.setActionHandler('nexttrack', nextTrack);
}
});
// update browser metadata on track changes
@@ -93,8 +107,28 @@
}
});
function previousTrack() {
if (!ctx.current || ctx.songIndex == null) return;
const tracks = ctx.current.tracks;
const trackStart = tracks[ctx.songIndex][0];
// if more than 3s into the track, restart it; otherwise go to previous
if (audio.currentTime - trackStart > 3 || ctx.songIndex === 0) {
audio.currentTime = currentTime = trackStart;
} else {
audio.currentTime = currentTime = tracks[ctx.songIndex - 1][0];
}
}
function nextTrack() {
if (!ctx.current || ctx.songIndex == null) return;
const tracks = ctx.current.tracks;
if (ctx.songIndex < tracks.length - 1) {
audio.currentTime = currentTime = tracks[ctx.songIndex + 1][0];
}
}
function seek(event, bounds) {
let x = event.pageX - bounds.left;
let x = event.clientX - bounds.left;
return Math.min(Math.max(x / bounds.width, 0), 1);
}
@@ -136,11 +170,6 @@
}
}
function seekAudio(event) {
if (!songBar) return;
audio.currentTime = seek(event, songBar.getBoundingClientRect()) * duration;
}
function updateSeekVisual(event) {
if (!songBar) return;
pendingSeekTime = seek(event, songBar.getBoundingClientRect()) * duration;
@@ -154,7 +183,7 @@
muted = false;
}
function formatSeconds(totalSeconds) {
function formatSeconds(totalSeconds, forceHours = false) {
if (isNaN(totalSeconds)) return 'No Data';
totalSeconds = parseInt(totalSeconds, 10);
var hours = Math.floor(totalSeconds / 3600);
@@ -163,18 +192,22 @@
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.filter((v, i) => v !== '00' || i > 0 || forceHours)
.join(':');
}
function seekTooltip(event) {
if (!inlineTooltip) {
let tooltipBounds = tooltip.getBoundingClientRect();
tooltipX = Math.min(event.pageX + 10, innerWidth - tooltipBounds.width);
tooltipX = Math.min(event.clientX + 10, innerWidth - tooltipBounds.width);
tooltipY = Math.min(songBar.offsetTop - 30, innerHeight - tooltipBounds.height);
}
let bounds = songBar.getBoundingClientRect();
let seekValue = ((event.pageX - bounds.left) * duration) / bounds.width;
let seekValue = ((event.clientX - bounds.left) * duration) / bounds.width;
updateTooltipText(seekValue);
}
function updateTooltipText(seekValue: number) {
if (!ctx.current) return;
let trackArray = ctx.current.tracks[ctx.getSongAtTime(seekValue)];
seekTrack = trackArray[1] + ' - ' + trackArray[2];
@@ -220,6 +253,8 @@
onmouseup={() => {
if (seeking && pendingSeekTime != null) {
audio.currentTime = pendingSeekTime;
currentTime = pendingSeekTime;
updateTooltipText(pendingSeekTime);
pendingSeekTime = null;
}
seeking = volumeSeeking = false;
@@ -244,14 +279,13 @@
bind:this={songBar}
value={seeking && pendingSeekTime != null ? pendingSeekTime : (currentTime || 0)}
max={duration}
onmousedown={() => (seeking = true)}
onmousedown={(e) => { seeking = true; updateSeekVisual(e); }}
onmouseenter={() => (showTooltip = true)}
onmouseleave={() => (showTooltip = false)}
onclick={seekAudio}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="song-progress"
></progress>
<div class="control-times">{formatSeconds(currentTime)}/{formatSeconds(duration)}</div>
<div class="control-times">{formatSeconds(currentTime, duration >= 3600)}/{formatSeconds(duration)}</div>
<button
style="--icon-color:{iconColor}"
class="material-icons"
@@ -311,8 +345,13 @@
onended={bubble('ended')}
{preload}
>
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
{#if isSafari}
<source src="{src}.mp3" type="audio/mpeg" />
<source {src} type="audio/ogg;codecs=opus" />
{:else}
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
{/if}
</audio>
<style>
@@ -337,6 +376,9 @@
.control-times {
margin: auto;
margin-right: 5px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
min-width: max-content;
}
.tooltip {