improve: experience for mobile

This commit is contained in:
alikia2x (寒寒) 2024-11-23 07:20:07 +08:00
parent 2742ba43f2
commit f1ecabd523
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
9 changed files with 95 additions and 27 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "aquavox", "name": "aquavox",
"version": "2.9.2", "version": "2.9.3",
"private": false, "private": false,
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",

View File

@ -11,7 +11,8 @@
{#if hasLyrics} {#if hasLyrics}
<img <img
class="absolute shadow-md select-none z-10 object-cover rounded-lg md:rounded-2xl max-md:h-20 max-xl:h-32 max-xl:top-6 md:max-h-[calc(94vh-20rem)] xl:w-auto max-w-[90%] xl:max-w-[37vw] class="absolute shadow-md select-none z-10 object-cover rounded-lg md:rounded-2xl max-md:h-20 max-xl:h-32 max-xl:top-6
md:max-h-[calc(94vh-20rem)] max-md:w-20 xl:w-auto max-w-[90%] xl:max-w-[37vw]
md:bottom-[21rem] left-6 md:left-[calc(7vw-1rem)] lg:left-[calc(12vw-1rem)] xl:translate-x-[-50%] xl:left-[25vw]" md:bottom-[21rem] left-6 md:left-[calc(7vw-1rem)] lg:left-[calc(12vw-1rem)] xl:translate-x-[-50%] xl:left-[25vw]"
src={path} src={path}
width="1200" width="1200"
@ -19,7 +20,8 @@
/> />
{:else} {:else}
<img <img
class="absolute shadow-md select-none z-10 object-cover rounded-2xl max-h-[calc(94vh-18rem)] md:max-h-[calc(94vh-20rem)] xl:w-auto max-w-[90%] md:max-w-[75%] xl:max-w-[37vw] class="absolute shadow-md select-none z-10 object-cover rounded-2xl max-h-[calc(94vh-18rem)]
md:max-h-[calc(94vh-20rem)] xl:w-auto max-w-[90%] md:max-w-[75%] xl:max-w-[37vw]
bottom-72 md:bottom-80 left-1/2 translate-x-[-50%]" bottom-72 md:bottom-80 left-1/2 translate-x-[-50%]"
src={path} src={path}
alt="封面" alt="封面"

View File

@ -18,6 +18,9 @@
export let hasLyrics: boolean; export let hasLyrics: boolean;
export let showInteractiveBox: boolean;
export let setShowingInteractiveBox: Function;
let progressBar: HTMLDivElement; let progressBar: HTMLDivElement;
let volumeBar: HTMLDivElement; let volumeBar: HTMLDivElement;
let showInfoTop: boolean = false; let showInfoTop: boolean = false;
@ -25,6 +28,13 @@
let songInfoTopContainer: HTMLDivElement; let songInfoTopContainer: HTMLDivElement;
let songInfoTopContent: HTMLSpanElement; let songInfoTopContent: HTMLSpanElement;
let userAdjustingVolume = false; let userAdjustingVolume = false;
let lastTouchClientX = 0;
setTimeout(() => {
if (screen.width < 728) {
setShowingInteractiveBox(false);
}
}, 3000);
const mql = window.matchMedia('(max-width: 1280px)'); const mql = window.matchMedia('(max-width: 1280px)');
@ -68,6 +78,30 @@
$: { $: {
showInfoTop = mql.matches && hasLyrics; showInfoTop = mql.matches && hasLyrics;
} }
window.addEventListener("mousemove", (event) => {
if ($userAdjustingProgress) {
adjustDisplayProgress(event.offsetX / progressBar.getBoundingClientRect().width);
}
});
window.addEventListener("mouseup", (event) => {
if ($userAdjustingProgress) {
userAdjustingProgress.set(false);
adjustProgress(event.offsetX / progressBar.getBoundingClientRect().width);
}
});
window.addEventListener("touchmove", (event) => {
if ($userAdjustingProgress) {
adjustDisplayProgress((event.touches[0].clientX - progressBar.getBoundingClientRect().left) / progressBar.getBoundingClientRect().width);
lastTouchClientX = event.touches[0].clientX;
}
});
window.addEventListener("touchend", (event) => {
if ($userAdjustingProgress) {
adjustProgress((lastTouchClientX - progressBar.getBoundingClientRect().left) / progressBar.getBoundingClientRect().width);
userAdjustingProgress.set(false);
}
});
</script> </script>
{#if showInfoTop} {#if showInfoTop}
@ -78,11 +112,14 @@
{/if} {/if}
<div <div
class={'absolute select-none bottom-12 h-60 w-[86vw] left-[7vw] z-10 ' + class={'absolute select-none bottom-12 h-60 w-[86vw] left-[7vw] duration-500 z-10 ' +
(hasLyrics (hasLyrics
? 'lg:w-[76vw] lg:left-[12vw] xl:w-[37vw] xl:left-[7vw]' ? 'lg:w-[76vw] lg:left-[12vw] xl:w-[37vw] xl:left-[7vw]'
: 'lg:w-[76vw] lg:left-[12vw] xl:w-[37vw] xl:left-[31.5vw]')} : 'lg:w-[76vw] lg:left-[12vw] xl:w-[37vw] xl:left-[31.5vw]') + ' ' +
(showInteractiveBox ? 'opacity-100' : 'opacity-0')}
style={`z-index: ${showInteractiveBox ? "0" : "50"}`}
> >
{#if !showInfoTop} {#if !showInfoTop}
<div class="song-info"> <div class="song-info">
<div class="song-info-regular {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}> <div class="song-info-regular {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}>
@ -95,6 +132,13 @@
</div> </div>
{/if} {/if}
<div class="absolute w-full h-2/3 bottom-0" style={`z-index: ${showInteractiveBox ? "0" : "50"}`} on:click={() => {
setShowingInteractiveBox(true);
setTimeout(() => {
setShowingInteractiveBox(false);
}, 5000);
}}></div>
<div class="progress top-16"> <div class="progress top-16">
<div class="time-indicator text-shadow-md time-current"> <div class="time-indicator text-shadow-md time-current">
{formatDuration(progress)} {formatDuration(progress)}
@ -107,23 +151,14 @@
class="progress-bar shadow-md" class="progress-bar shadow-md"
on:keydown on:keydown
on:keyup on:keyup
on:click={(e) => {
progressBarOnClick(e);
}}
on:mousedown={() => { on:mousedown={() => {
userAdjustingProgress.set(true); userAdjustingProgress.set(true);
}} }}
on:mousemove={(e) => { on:touchstart={() => {
if ($userAdjustingProgress) { userAdjustingProgress.set(true);
adjustDisplayProgress(e.offsetX / progressBar.getBoundingClientRect().width);
}
}}
on:mouseup={(e) => {
const offsetX = e.offsetX;
progressBarOnClick(e);
// Q: why it needs delay?
// A: I do not know.
setTimeout(()=> {
userAdjustingProgress.set(false);
progressBarMouseUp(offsetX);
}, 50);
}} }}
role="slider" role="slider"
tabindex="0" tabindex="0"

View File

@ -16,10 +16,11 @@
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
// Props // Props
let { originalLyrics, progress, player } : { let { originalLyrics, progress, player, showInteractiveBox } : {
originalLyrics: LyricData, originalLyrics: LyricData,
progress: number, progress: number,
player: HTMLAudioElement | null player: HTMLAudioElement | null,
showInteractiveBox: boolean
} = $props(); } = $props();
// States // States
@ -195,7 +196,7 @@
lastEventLyricIndex = currentLyricIndex; lastEventLyricIndex = currentLyricIndex;
lastEventProgress = progress; lastEventProgress = progress;
if (!lyricChanged) return; if (!lyricChanged || scrolling) return;
if (!lyricIndexDeltaTooBig && deltaInRange) { if (!lyricIndexDeltaTooBig && deltaInRange) {
console.log("Event: regular move"); console.log("Event: regular move");
console.log(new Date().getTime(), lastSeekForward); console.log(new Date().getTime(), lastSeekForward);
@ -257,8 +258,11 @@
{#if originalLyrics && originalLyrics.scripts} {#if originalLyrics && originalLyrics.scripts}
<div <div
class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12 class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12
lg:px-[7.5rem] xl:left-[46vw] xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans lg:px-[7.5rem] xl:left-[46vw] xl:px-[3vw] xl:h-screen font-sans
text-left no-scrollbar z-[1] pt-16 overflow-hidden" text-left no-scrollbar z-[1] pt-16 overflow-hidden"
style={`mask: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 7%, rgba(0, 0, 0, 1) 95%,
rgba(0, 0, 0, 0) 100%);
height: ${showInteractiveBox ? "calc(100vh - 21rem)" : "calc(100vh - 7rem)"}`}
bind:this={lyricsContainer} bind:this={lyricsContainer}
> >
{#each lyricLines as lyric, i} {#each lyricLines as lyric, i}

View File

@ -2,8 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- <link rel="icon" href="%sveltekit.assets%/favicon.png" /> --> <link rel="icon" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="/icon.png">
<title>AquaVox</title> <title>AquaVox</title>
%sveltekit.head% %sveltekit.head%
</head> </head>

View File

@ -62,7 +62,7 @@
{#each idList as id} {#each idList as id}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="!no-underline !text-black dark:!text-white" onclick={() => goto(`/play/${id}`)}> <div class="!no-underline !text-black dark:!text-white" onclick={() => location.href = (`/play/${id}`)}>
<li <li
class="relative my-4 p-4 duration-150 bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600 rounded-lg" class="relative my-4 p-4 duration-150 bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600 rounded-lg"
> >

View File

@ -31,6 +31,7 @@
let hasLyrics: boolean; let hasLyrics: boolean;
const coverPath = writable(''); const coverPath = writable('');
let mainInterval: ReturnType<typeof setInterval>; let mainInterval: ReturnType<typeof setInterval>;
let showInteractiveBox = true;
function setMediaSession() { function setMediaSession() {
if ('mediaSession' in navigator === false) return; if ('mediaSession' in navigator === false) return;
@ -196,6 +197,10 @@
} }
} }
function setShowingInteractiveBox(showing: boolean) {
showInteractiveBox = showing;
}
$: { $: {
clearInterval(mainInterval); clearInterval(mainInterval);
mainInterval = setInterval(() => { mainInterval = setInterval(() => {
@ -240,9 +245,11 @@
{adjustVolume} {adjustVolume}
{adjustDisplayProgress} {adjustDisplayProgress}
{hasLyrics} {hasLyrics}
{showInteractiveBox}
{setShowingInteractiveBox}
/> />
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer}/> <NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer} {showInteractiveBox} />
<audio <audio
bind:this={audioPlayer} bind:this={audioPlayer}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -0,0 +1,16 @@
{
"orientation": "portrait",
"name": "AquaVox",
"short_name": "AquaVox",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"description": "A readable Hacker News app",
"icons": [
{
"src": "/icon.png",
"sizes": "192x192",
"type": "image/png"
}
]
}