feat: add tooltip for progress bar seeking
This commit is contained in:
5 files changed
+113
-5
No files matched your search
@@ -471,22 +471,68 @@ body {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
transition: var(--transition-smooth);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.progress-time-tooltip {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
transform: translateX(-50%);
|
||||
padding: 3px 7px;
|
||||
border-radius: 5px;
|
||||
background: var(--bg-card, #1f1b2e);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.track-progress-slider {
|
||||
position: absolute;
|
||||
inset: -7px 0;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.track-progress-slider::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.track-progress-slider::-webkit-slider-thumb {
|
||||
width: 0;
|
||||
height: 20px;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.track-progress-slider::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.track-progress-slider::-moz-range-thumb {
|
||||
width: 0;
|
||||
height: 20px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.track-progress-slider:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ const current = computed(() => props.status?.current ?? null)
|
||||
const playing = computed(() => props.status?.playing ?? false)
|
||||
const loopMode = computed(() => loopModeOf(props.status))
|
||||
const seeking = ref(false)
|
||||
const seekSubmitting = ref(false)
|
||||
const seekPosition = ref(0)
|
||||
const hoverPosition = ref<number | null>(null)
|
||||
const hoverOffset = ref(0)
|
||||
const displayedElapsed = computed(() => seeking.value ? seekPosition.value : elapsed.value)
|
||||
|
||||
/** Podczas przeciągania suwaka pokazujemy wartość lokalną, nie tę z serwera. */
|
||||
@@ -100,12 +103,30 @@ function onSeekInput(event: Event) {
|
||||
seekPosition.value = Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
function onSeekHover(event: MouseEvent) {
|
||||
if (duration.value <= 0) {
|
||||
hoverPosition.value = null
|
||||
return
|
||||
}
|
||||
const element = event.target as HTMLElement
|
||||
const container = event.currentTarget as HTMLElement
|
||||
const slider = element.tagName === 'INPUT' ? element : container
|
||||
const sliderRect = slider.getBoundingClientRect()
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const { left, width } = sliderRect
|
||||
const ratio = Math.min(Math.max((event.clientX - left) / width, 0), 1)
|
||||
hoverPosition.value = ratio * duration.value
|
||||
hoverOffset.value = event.clientX - containerRect.left
|
||||
}
|
||||
|
||||
async function commitSeek() {
|
||||
if (!current.value || duration.value <= 0) return
|
||||
if (!current.value || duration.value <= 0 || seekSubmitting.value) return
|
||||
const position = seekPosition.value
|
||||
seekSubmitting.value = true
|
||||
try {
|
||||
await run(() => api.seek(position), 'Przewinięto utwór', 'Błąd przewijania utworu')
|
||||
} finally {
|
||||
seekSubmitting.value = false
|
||||
seeking.value = false
|
||||
}
|
||||
}
|
||||
@@ -182,7 +203,11 @@ async function cycleLoop() {
|
||||
class="track-progress-container"
|
||||
>
|
||||
<span class="time-text">{{ formatTime(displayedElapsed) }}</span>
|
||||
<div class="progress-bar-bg">
|
||||
<div
|
||||
class="progress-bar-bg"
|
||||
@mousemove="onSeekHover"
|
||||
@mouseleave="hoverPosition = null"
|
||||
>
|
||||
<div
|
||||
class="progress-bar-fill"
|
||||
:style="{ width: `${seeking ? (duration > 0 ? (seekPosition / duration) * 100 : 0) : percent}%` }"
|
||||
@@ -194,11 +219,18 @@ async function cycleLoop() {
|
||||
:max="duration"
|
||||
step="0.1"
|
||||
:value="seeking ? seekPosition : elapsed"
|
||||
:disabled="duration <= 0"
|
||||
:disabled="duration <= 0 || seekSubmitting"
|
||||
aria-label="Pozycja utworu"
|
||||
@input="onSeekInput"
|
||||
@change="commitSeek"
|
||||
>
|
||||
<span
|
||||
v-if="hoverPosition !== null"
|
||||
class="progress-time-tooltip"
|
||||
:style="{ left: `${hoverOffset}px` }"
|
||||
>
|
||||
{{ formatTime(hoverPosition) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="time-text">{{ duration > 0 ? formatTime(duration) : 'Live' }}</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user