Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
090a145211 | ||
|
|
1dc3f4505b | ||
|
|
7febb497e9 | ||
|
|
6c12cfd49b | ||
|
|
f777873432 |
@@ -11,7 +11,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="description-bubble">
|
<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>
|
<p>
|
||||||
Still in construction. The design is responsive now, so phones should work well enough!
|
Still in construction. The design is responsive now, so phones should work well enough!
|
||||||
Needs touch events though.
|
Needs touch events though.
|
||||||
|
|||||||
@@ -78,12 +78,26 @@
|
|||||||
let volumeBar = $state();
|
let volumeBar = $state();
|
||||||
let innerWidth = $state();
|
let innerWidth = $state();
|
||||||
let innerHeight = $state();
|
let innerHeight = $state();
|
||||||
|
let isSafari = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
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
|
// default volume
|
||||||
const volumeData = localStorage.getItem('volume');
|
const volumeData = localStorage.getItem('volume');
|
||||||
volume = volumeData ? parseFloat(volumeData) : 0.67;
|
volume = volumeData ? parseFloat(volumeData) : 0.67;
|
||||||
setVolume(volume);
|
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
|
// 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) {
|
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);
|
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) {
|
function updateSeekVisual(event) {
|
||||||
if (!songBar) return;
|
if (!songBar) return;
|
||||||
pendingSeekTime = seek(event, songBar.getBoundingClientRect()) * duration;
|
pendingSeekTime = seek(event, songBar.getBoundingClientRect()) * duration;
|
||||||
@@ -154,7 +183,7 @@
|
|||||||
muted = false;
|
muted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSeconds(totalSeconds) {
|
function formatSeconds(totalSeconds, forceHours = false) {
|
||||||
if (isNaN(totalSeconds)) return 'No Data';
|
if (isNaN(totalSeconds)) return 'No Data';
|
||||||
totalSeconds = parseInt(totalSeconds, 10);
|
totalSeconds = parseInt(totalSeconds, 10);
|
||||||
var hours = Math.floor(totalSeconds / 3600);
|
var hours = Math.floor(totalSeconds / 3600);
|
||||||
@@ -163,18 +192,22 @@
|
|||||||
|
|
||||||
return [hours, minutes, seconds]
|
return [hours, minutes, seconds]
|
||||||
.map((v) => (v < 10 ? '0' + v : v))
|
.map((v) => (v < 10 ? '0' + v : v))
|
||||||
.filter((v, i) => v !== '00' || i > 0)
|
.filter((v, i) => v !== '00' || i > 0 || forceHours)
|
||||||
.join(':');
|
.join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
function seekTooltip(event) {
|
function seekTooltip(event) {
|
||||||
if (!inlineTooltip) {
|
if (!inlineTooltip) {
|
||||||
let tooltipBounds = tooltip.getBoundingClientRect();
|
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);
|
tooltipY = Math.min(songBar.offsetTop - 30, innerHeight - tooltipBounds.height);
|
||||||
}
|
}
|
||||||
let bounds = songBar.getBoundingClientRect();
|
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;
|
if (!ctx.current) return;
|
||||||
let trackArray = ctx.current.tracks[ctx.getSongAtTime(seekValue)];
|
let trackArray = ctx.current.tracks[ctx.getSongAtTime(seekValue)];
|
||||||
seekTrack = trackArray[1] + ' - ' + trackArray[2];
|
seekTrack = trackArray[1] + ' - ' + trackArray[2];
|
||||||
@@ -220,6 +253,8 @@
|
|||||||
onmouseup={() => {
|
onmouseup={() => {
|
||||||
if (seeking && pendingSeekTime != null) {
|
if (seeking && pendingSeekTime != null) {
|
||||||
audio.currentTime = pendingSeekTime;
|
audio.currentTime = pendingSeekTime;
|
||||||
|
currentTime = pendingSeekTime;
|
||||||
|
updateTooltipText(pendingSeekTime);
|
||||||
pendingSeekTime = null;
|
pendingSeekTime = null;
|
||||||
}
|
}
|
||||||
seeking = volumeSeeking = false;
|
seeking = volumeSeeking = false;
|
||||||
@@ -244,14 +279,13 @@
|
|||||||
bind:this={songBar}
|
bind:this={songBar}
|
||||||
value={seeking && pendingSeekTime != null ? pendingSeekTime : (currentTime || 0)}
|
value={seeking && pendingSeekTime != null ? pendingSeekTime : (currentTime || 0)}
|
||||||
max={duration}
|
max={duration}
|
||||||
onmousedown={() => (seeking = true)}
|
onmousedown={(e) => { seeking = true; updateSeekVisual(e); }}
|
||||||
onmouseenter={() => (showTooltip = true)}
|
onmouseenter={() => (showTooltip = true)}
|
||||||
onmouseleave={() => (showTooltip = false)}
|
onmouseleave={() => (showTooltip = false)}
|
||||||
onclick={seekAudio}
|
|
||||||
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
|
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
|
||||||
class="song-progress"
|
class="song-progress"
|
||||||
></progress>
|
></progress>
|
||||||
<div class="control-times">{formatSeconds(currentTime)}/{formatSeconds(duration)}</div>
|
<div class="control-times">{formatSeconds(currentTime, duration >= 3600)}/{formatSeconds(duration)}</div>
|
||||||
<button
|
<button
|
||||||
style="--icon-color:{iconColor}"
|
style="--icon-color:{iconColor}"
|
||||||
class="material-icons"
|
class="material-icons"
|
||||||
@@ -311,8 +345,13 @@
|
|||||||
onended={bubble('ended')}
|
onended={bubble('ended')}
|
||||||
{preload}
|
{preload}
|
||||||
>
|
>
|
||||||
<source {src} type="audio/ogg;codecs=opus" />
|
{#if isSafari}
|
||||||
<source src="{src}.mp3" type="audio/mpeg" />
|
<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>
|
</audio>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -337,6 +376,9 @@
|
|||||||
.control-times {
|
.control-times {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
|||||||
Reference in New Issue
Block a user