improve: mobile UX

fix: some UX bugs
This commit is contained in:
alikia2x (寒寒) 2024-11-24 03:58:26 +08:00
parent f1ecabd523
commit e573c70497
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
14 changed files with 127 additions and 108 deletions

View File

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

View File

@ -35,7 +35,7 @@
{#if $fileItems.length > 0}
<AddIcon class="z-[1] relative text-3xl" />
{:else}
<ImportIcon class="z-[1] relative text-4xl" />
<ImportIcon class="z-[1] relative text-4xl text-blue-500" />
{/if}
</div>
</button>

View File

@ -1,15 +1,3 @@
<div class={$$props.class}>
<svg width="1em" xmlns="http://www.w3.org/2000/svg" height="1em" fill="none" viewBox="0 0 12 17.2"
><g style="mix-blend-mode: darken; fill: rgb(0, 0, 0);"
><path
d="M12.935 6.131v7.701c0 1.532-.636 2.168-2.169 2.168H2.168C.636 16 0 15.364 0 13.832V6.131c0-1.495.636-2.168 2.168-2.168h1.869v1.121H2.168c-.785 0-1.121.337-1.121 1.047v7.701c0 .822.336 1.159 1.121 1.159h8.598c.711 0 1.047-.337 1.047-1.159V6.131c0-.71-.336-1.047-1.047-1.047H8.897V3.963l1.889-.006c1.513.006 2.149.641 2.149 2.174Z"
style="fill: rgb(0, 117, 255); fill-opacity: 1;"
class="fills"
/><path
d="M6.505 11.607c.127 0 .205-.093.336-.205l3.028-2.991a.459.459 0 0 0 .112-.299c.019-.374-.223-.561-.523-.561-.206 0-.262.075-.299.113L7.178 9.645V.673C7.178.336 6.841 0 6.505 0c-.337 0-.673.336-.673.673v8.972L3.85 7.664a.404.404 0 0 0-.299-.113c-.266 0-.542.187-.523.561.008.169.056.243.112.299l3.028 2.991c.131.112.209.205.337.205Z"
style="fill: rgb(0, 117, 255); fill-opacity: 1;"
class="fills"
/></g
></svg
>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M21 14a1 1 0 0 0-1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-4a1 1 0 0 0-2 0v4a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-4a1 1 0 0 0-1-1m-9.71 1.71a1 1 0 0 0 .33.21a.94.94 0 0 0 .76 0a1 1 0 0 0 .33-.21l4-4a1 1 0 0 0-1.42-1.42L13 12.59V3a1 1 0 0 0-2 0v9.59l-2.29-2.3a1 1 0 1 0-1.42 1.42Z"/></svg>
</div>

View File

@ -4,6 +4,7 @@
import userAdjustingProgress from '../state/userAdjustingProgress';
import progressBarSlideValue from '../state/progressBarSlideValue';
import truncate from '../utils/truncate';
import timestamp from '@core/utils/getCurrentTimestamp';
export let name: string;
export let singer: string = '';
@ -19,7 +20,7 @@
export let hasLyrics: boolean;
export let showInteractiveBox: boolean;
export let setShowingInteractiveBox: Function;
export let showingInteractiveBoxUntil: Function;
let progressBar: HTMLDivElement;
let volumeBar: HTMLDivElement;
@ -29,12 +30,11 @@
let songInfoTopContent: HTMLSpanElement;
let userAdjustingVolume = false;
let lastTouchClientX = 0;
let mobileDeviceAdjustingProgress = false;
setTimeout(() => {
if (screen.width < 728) {
setShowingInteractiveBox(false);
}
}, 3000);
if (screen.width < 728) {
showingInteractiveBoxUntil(timestamp() + 3000);
}
const mql = window.matchMedia('(max-width: 1280px)');
@ -81,27 +81,44 @@
window.addEventListener("mousemove", (event) => {
if ($userAdjustingProgress) {
adjustDisplayProgress(event.offsetX / progressBar.getBoundingClientRect().width);
const x = event.clientX;
const rec = progressBar.getBoundingClientRect();
adjustDisplayProgress(truncate((x - rec.left) / rec.width,0,1));
}
});
window.addEventListener("mouseup", (event) => {
if ($userAdjustingProgress) {
const x = event.clientX;
const rec = progressBar.getBoundingClientRect();
userAdjustingProgress.set(false);
adjustProgress(event.offsetX / progressBar.getBoundingClientRect().width);
adjustProgress(truncate((x - rec.left) / rec.width,0,1));
}
});
window.addEventListener("touchmove", (event) => {
if ($userAdjustingProgress) {
adjustDisplayProgress((event.touches[0].clientX - progressBar.getBoundingClientRect().left) / progressBar.getBoundingClientRect().width);
lastTouchClientX = event.touches[0].clientX;
const x = event.touches[0].clientX;
const rec = progressBar.getBoundingClientRect();
adjustDisplayProgress(truncate((x - rec.left) / rec.width,0,1));
lastTouchClientX = x;
}
});
window.addEventListener("touchend", (event) => {
if ($userAdjustingProgress) {
adjustProgress((lastTouchClientX - progressBar.getBoundingClientRect().left) / progressBar.getBoundingClientRect().width);
const x = lastTouchClientX;
const rec = progressBar.getBoundingClientRect();
adjustProgress(truncate((x - rec.left) / rec.width,0,1));
userAdjustingProgress.set(false);
mobileDeviceAdjustingProgress = false;
}
});
userAdjustingProgress.subscribe(()=> {
showingInteractiveBoxUntil(timestamp() + 5000);
});
function handleClick() {
showingInteractiveBoxUntil(timestamp() + 5000);
}
</script>
{#if showInfoTop}
@ -112,11 +129,11 @@
{/if}
<div
class={'absolute select-none bottom-12 h-60 w-[86vw] left-[7vw] duration-500 z-10 ' +
class={'absolute select-none bottom-12 h-60 w-[86vw] left-[7vw] duration-500 z-10 transition-[opacity,transform] ' +
(hasLyrics
? '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]') + ' ' +
(showInteractiveBox ? 'opacity-100' : 'opacity-0')}
(showInteractiveBox ? 'opacity-100' : 'opacity-0 translate-y-48')}
style={`z-index: ${showInteractiveBox ? "0" : "50"}`}
>
@ -132,12 +149,12 @@
</div>
{/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>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class={"absolute w-full h-2/3 bottom-0" + (showInteractiveBox ? '' : '-translate-y-48')}
style={`z-index: ${showInteractiveBox ? "0" : "50"}`}
on:click={handleClick}
></div>
<div class="progress top-16">
<div class="time-indicator text-shadow-md time-current">
@ -148,7 +165,7 @@
aria-valuemin="0"
aria-valuenow={progress}
bind:this={progressBar}
class="progress-bar shadow-md"
class="progress-bar shadow-md {mobileDeviceAdjustingProgress && '!h-[0.7rem]'}"
on:keydown
on:keyup
on:click={(e) => {
@ -159,11 +176,12 @@
}}
on:touchstart={() => {
userAdjustingProgress.set(true);
mobileDeviceAdjustingProgress = true;
}}
role="slider"
tabindex="0"
>
<div class="bar" style={`width: ${(progress / (duration + 0.001)) * 100}%;`}></div>
<div class="bar {mobileDeviceAdjustingProgress && '!h-[0.7rem]'}" style={`width: ${(progress / (duration + 0.001)) * 100}%;`}></div>
</div>
<div class="time-indicator text-shadow-md time-total">{formatDuration(duration)}</div>
@ -370,7 +388,7 @@
transition: 0.3s;
}
.progress-bar:hover {
.progress-bar:active {
height: 0.7rem;
}
@ -384,7 +402,7 @@
transition: height 0.3s;
}
.progress-bar:hover .bar {
.progress-bar:active .bar {
height: 0.7rem;
}
@ -410,5 +428,11 @@
.control-btn {
transition: 0.1s
}
.progress-bar:hover {
height: 0.7rem;
}
.progress-bar:hover .bar {
height: 0.7rem;
}
}
</style>

View File

@ -134,6 +134,11 @@
};
export const syncSpringWithDelta = (deltaY: number) => {
const target = positionY + deltaY;
springY!.setPosition(target);
}
export const getInfo = () => {
return {
x: positionX,

View File

@ -116,6 +116,7 @@
scrolling = true;
currentLyricComponent.stop();
currentLyricComponent.setY(currentY - deltaY);
currentLyricComponent.syncSpringWithDelta(deltaY);
}
scrolling = true;
if (scrollingTimeout) clearTimeout(scrollingTimeout);
@ -250,23 +251,26 @@
{#if debugMode}
<span
class="text-white text-lg absolute z-50 px-2 py-0.5 m-2 rounded-3xl bg-white bg-opacity-20 backdrop-blur-lg right-0 font-mono">
progress: {progress.toFixed(2)}, nextUpdate: {nextUpdate}, scrolling: {scrolling}, current: {currentLyricIndex}, uap: {$userAdjustingProgress}
class="text-white text-lg absolute z-50 px-2 py-0.5 m-2 rounded-3xl bg-white bg-opacity-20 backdrop-blur-lg
right-0 font-mono">
progress: {progress.toFixed(2)}, nextUpdate: {nextUpdate}, scrolling: {scrolling}, current: {currentLyricIndex},
uap: {$userAdjustingProgress}
</span>
{/if}
{#if originalLyrics && originalLyrics.scripts}
<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 duration-500
${showInteractiveBox ? "h-[calc(100vh-21rem)]" : "h-[calc(100vh-7rem)]"}
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)"}`}
rgba(0, 0, 0, 0) 100%);`}
bind:this={lyricsContainer}
>
{#each lyricLines as lyric, i}
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} {progress} {currentLyricIndex} {scrolling}/>
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} {progress}
{currentLyricIndex} {scrolling}/>
{/each}
</div>
{/if}

View File

@ -4,8 +4,11 @@ export default function(key: string){
"audio/ogg": "OGG 容器",
"audio/flac": "FLAC 无损音频",
"audio/aac": "AAC 音频",
"audio/wav": "WAV 音频",
"ttml": "TTML歌词",
"lrc": "LRC 歌词"
}
if (!key) return "未知格式";
if (!(key in dict)) return key;
else return dict[key as keyof typeof dict];
}

View File

@ -0,0 +1,4 @@
export default function timestamp() {
const ts = new Date().getTime();
return ts;
}

View File

@ -2,12 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/icon.png" />
<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">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>AquaVox</title>
%sveltekit.head%
</head>

View File

@ -67,8 +67,15 @@
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"
>
<span class="font-bold">{musicList[id].name}</span> <br />
<span>{toHumanSize(musicList[id].size)}</span> ·
<a class="!no-underline" href={`/import/${id}/lyric`}>导入歌词</a>
<div class="flex pt-0.5 items-center">
<span class="w-[4.5rem]">{toHumanSize(musicList[id].size)}</span>
<div class="!no-underline import inline-block cursor-default z-50 px-2 py-0.5 rounded ml-2
hover:bg-gray-200 dark:hover:bg-zinc-500"
onclick={(e) => {location.href = `/import/${id}/lyric`;e.stopPropagation();}}>
导入歌词
</div>
</div>
{#if musicList[id].coverUrl}
<img
class="h-16 w-16 object-cover absolute rounded-lg right-2 top-2"
@ -95,7 +102,10 @@
</div>
<style lang="postcss">
a {
@apply text-red-500 hover:text-red-400 duration-150 underline;
.import {
@apply text-red-500 duration-150 underline;
}
.import:hover {
text-shadow: 0 0 3px rgba(239, 68, 68, 0.3);
}
</style>

View File

@ -14,6 +14,7 @@
import progressBarRaw from '@core/state/progressBarRaw';
import { parseTTML, parseLRC } from '@alikia/aqualyrics';
import NewLyrics from '@core/components/lyrics/newLyrics.svelte';
import timestamp from '@core/utils/getCurrentTimestamp';
const audioId = $page.params.id;
let audioPlayer: HTMLAudioElement | null = null;
@ -31,6 +32,7 @@
let hasLyrics: boolean;
const coverPath = writable('');
let mainInterval: ReturnType<typeof setInterval>;
let showInteractiveBoxUntil = Infinity;
let showInteractiveBox = true;
function setMediaSession() {
@ -197,8 +199,8 @@
}
}
function setShowingInteractiveBox(showing: boolean) {
showInteractiveBox = showing;
function showingInteractiveBoxUntil(until: number) {
showInteractiveBoxUntil = until;
}
$: {
@ -224,6 +226,10 @@
$: hasLyrics = !!originalLyrics;
setInterval(() => {
showInteractiveBox = timestamp() < showInteractiveBoxUntil || screen.width > 728;
}, 500);
readDB();
</script>
@ -231,41 +237,45 @@
<title>{name} - AquaVox</title>
</svelte:head>
<Background coverId={audioId} />
<Cover {coverPath} {hasLyrics} />
<InteractiveBox
{name}
{singer}
{duration}
{volume}
progress={currentProgress}
clickPlay={playAudio}
{paused}
{adjustProgress}
{adjustVolume}
{adjustDisplayProgress}
{hasLyrics}
{showInteractiveBox}
{setShowingInteractiveBox}
/>
<div class="select-none">
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer} {showInteractiveBox} />
<Background coverId={audioId} />
<Cover {coverPath} {hasLyrics} />
<InteractiveBox
{name}
{singer}
{duration}
{volume}
progress={currentProgress}
clickPlay={playAudio}
{paused}
{adjustProgress}
{adjustVolume}
{adjustDisplayProgress}
{hasLyrics}
{showInteractiveBox}
{showingInteractiveBoxUntil}
/>
<audio
bind:this={audioPlayer}
controls
style="display: none"
on:play={() => {
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer} {showInteractiveBox}/>
<audio
bind:this={audioPlayer}
controls
style="display: none"
on:play={() => {
if (audioPlayer === null) return;
paused = audioPlayer.paused;
}}
on:pause={() => {
on:pause={() => {
if (audioPlayer === null) return;
paused = audioPlayer.paused;
}}
on:ended={() => {
on:ended={() => {
paused = true;
if (audioPlayer == null) return;
audioPlayer.pause();
}}
></audio>
></audio>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,16 +0,0 @@
{
"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"
}
]
}

View File

@ -1,11 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="189.875" height="259.125">
<g>
<rect height="259.125" opacity="0" width="189.875" x="0" y="0"/>
<path d="M20 259L56 259C69.375 259 76 252.375 76 239L76 20C76 6.25 69.375 0 56 0L20 0C6.625 0 0 6.75 0 20L0 239C0 252.375 6.625 259 20 259ZM133.875 259L169.875 259C183.25 259 189.875 252.375 189.875 239L189.875 20C189.875 6.25 183.25 0 169.875 0L133.875 0C120.5 0 113.875 6.75 113.875 20L113.875 239C113.875 252.375 120.5 259 133.875 259Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>
<svg width="255" xmlns="http://www.w3.org/2000/svg" height="259" id="screenshot-d13e393b-6a61-8076-8005-51e8b692555d" viewBox="-0 0 255 259" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g xmlns:xlink="http://www.w3.org/1999/xlink" width="189.875" height="259.125" id="shape-d13e393b-6a61-8076-8005-51e8b692555d" data-testid="pause" style="fill: rgb(0, 0, 0);" ry="0" rx="0" version="1.1"><g id="shape-d13e393b-6a61-8076-8005-51e8b692d284" data-testid="base-background" style="display: none;"><g class="fills" id="fills-d13e393b-6a61-8076-8005-51e8b692d284"><rect width="255" height="259" x="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: none;" ry="0" fill="none" rx="0" y="0"/></g></g><g id="shape-d13e393b-6a61-8076-8005-51e8b6933311" data-testid="svg-g" rx="0" ry="0" style="fill: rgb(0, 0, 0);"><g id="shape-d13e393b-6a61-8076-8005-51e8b6933312" data-testid="svg-rect" style="opacity: 0;"><g class="fills" id="fills-d13e393b-6a61-8076-8005-51e8b6933312"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="215.19166666666672" height="259"/></g></g><g id="shape-d13e393b-6a61-8076-8005-51e8b693b101" data-testid="svg-path"><g class="fills" id="fills-d13e393b-6a61-8076-8005-51e8b693b101"><path d="M22.667,258.875L63.467,258.875C78.625,258.875,86.133,252.253,86.133,238.885L86.133,19.990C86.133,6.247,78.625,0.000,63.467,0.000L22.667,0.000C7.508,0.000,-0.000,6.747,-0.000,19.990L-0.000,238.885C-0.000,252.253,7.508,258.875,22.667,258.875ZL22.667,258.875ZM191.533,258.875L232.333,258.875C247.492,258.875,255.000,252.253,255.000,238.885L255.000,19.990C255.000,6.247,247.492,0.000,232.333,0.000L191.533,0.000C176.375,0.000,168.867,6.747,168.867,19.990L168.867,238.885C168.867,252.253,176.375,258.875,191.533,258.875ZL191.533,258.875Z" style="fill: rgb(255, 255, 255); fill-opacity: 0.85;"/></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 1.9 KiB