1
0
Fork 0
muzika-gromche/Frontend/src/components/library/Card.vue

196 lines
5.1 KiB
Vue
Raw Normal View History

WIP: Add frontend web app player & editor in Vue 3 + Vite TODO: - implement viewing & editing. - Add links to deployment, and CHANGELOG. style.css package.json vite config .vscode eslint use --cache .vscode add vite-css-modules editorconfig tsconfig and updated vue-tsc (fixes most of the type checking bugs) fix last type errors audiowaveform gitignore ESLint ESLint: ignore autogenerated JSON lint:fix tsconfig and vite config migrate icon generating script to TS eslint src/lib/ eslint stores eslint src/*.ts eslint audio pnpm update update icon eslint ahh import new tracks json instructions on jq codenames codenames.json fix styles broken by import order eslint audio app error screen footer copyright year global header loading screen transition search field preview track info inspector control controls controls range controls impl controls index eslint no-console off AudioTrack view inspector cards and sliders more controls master volume slider playhead library page player page timeline markers timeline markers header tick timestamp timeline clip index clip empty clip lyrics clip palette clip fadeout clip default import order timeline timeline panel timeline track header timeline trackview clip view clip audio audio waveform scrollsync easy lints eslint store eslint no mutating props off pnpm catalog off add unhead dep use head eslint inspector eslint easy minor stuff eslint audiowaveform easy fix eslint use :key with v-for fix audio waveforms inspector makes more sense season remove debug inspector Merge codenames into main json bump pnpm pnpm in particular enabled monospace Game Over Move JSON to assets to avoid caching issues
2025-11-27 14:57:28 +00:00
<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue'
// Any card with a subtle outline and hover effect
const {
hoverEnabled = true,
playheadEnabled = true,
selected = false,
} = defineProps<{
hoverEnabled?: boolean
playheadEnabled?: boolean
selected?: boolean
}>()
const emit = defineEmits<{
(e: 'select'): void
(e: 'activate'): void
(e: 'playhead', pos: number): void
}>()
// Timeline / playhead position on hover in range 0..1,
// or NaN when not hovered or hover is not enabled.
const playheadPosition01 = shallowRef<number>(Number.NaN)
const playheadEl = useTemplateRef('playheadEl')
const card = useTemplateRef('card')
// Simply tracks pointer enter/leave
const isHovered = shallowRef(false)
// flag to detect a recent touch tap so the following click doesn't also emit 'select'
const lastTapWasTouch = shallowRef(false)
// show playhead on touch while pressing
const isTouchActive = shallowRef(false)
// Once dragging starts, pointer should no longer cause click on release/up
const isTouchDragging = shallowRef(false)
// Playhead is active on mouse hover or touch down, but not after touch up which leaves dangling :hover on mobile.
defineExpose({
playheadPosition01,
})
// Returns false if playhead wasn't updated
function updatePlayhead(event: PointerEvent): boolean {
const target = event.currentTarget as HTMLElement | null
if (!hoverEnabled || !playheadEnabled || !target) {
return false
}
const rect = target.getBoundingClientRect()
const x = event.clientX - rect.left
const pos = rect.width > 0 ? Math.max(0, Math.min(1, x / rect.width)) : 0
playheadPosition01.value = pos
if (playheadEl.value) {
// position the 1px playhead using percentage so it adapts to responsive widths
playheadEl.value.style.left = `${pos * 100}%`
}
// emit normalized position for parent components to react (e.g. preview color)
emit('playhead', pos)
return true
}
function onPointerEnter(_event: PointerEvent) {
if (hoverEnabled) {
isHovered.value = true
}
}
function onPointerLeave(_event: PointerEvent) {
if (hoverEnabled) {
isHovered.value = false
}
isTouchActive.value = false
isTouchDragging.value = false
playheadPosition01.value = Number.NaN
if (playheadEl.value) {
playheadEl.value.style.left = ''
}
emit('playhead', Number.NaN)
}
function onPointerDown(event: PointerEvent) {
if (event.pointerType !== 'touch') {
return
}
if (updatePlayhead(event)) {
isTouchActive.value = true
}
if (card.value) {
card.value.setPointerCapture(event.pointerId)
}
}
function onPointerUp(event: PointerEvent) {
// Treat a single touch tap as activation
if (event.pointerType === 'touch') {
if (!isTouchDragging.value) {
emit('activate')
}
lastTapWasTouch.value = true
// keep the flag true briefly so the subsequent click handler can suppress 'select'
setTimeout(() => {
lastTapWasTouch.value = false
}, 50)
// clear touch-active playhead state
isTouchActive.value = false
isTouchDragging.value = false
onPointerLeave(event)
}
}
function onPointerMove(event: PointerEvent) {
updatePlayhead(event)
if (event.pointerType === 'touch') {
isTouchDragging.value = true
}
}
function onClick(event: MouseEvent) {
// If this click follows a touch tap, suppress the 'select' event
if (lastTapWasTouch.value) {
event.preventDefault()
lastTapWasTouch.value = false
return
}
emit('select')
}
function onDblClick(_event: MouseEvent) {
emit('activate')
}
</script>
<template>
<div
ref="card" class="card card-border tw:min-w-10 tw:min-h-10 tw:grid" :class="{
'hover-enabled': hoverEnabled,
'playhead-enabled': hoverEnabled && playheadEnabled,
'playhead-active': isHovered || isTouchActive,
selected,
}" @pointerenter="onPointerEnter" @pointerleave="onPointerLeave" @pointerdown="onPointerDown"
@pointerup="onPointerUp" @pointermove="onPointerMove" @click="onClick" @dblclick="onDblClick"
@focusin="emit('select')"
>
<!-- content container -->
<div class="tw:row-span-full tw:col-span-full tw:w-full tw:h-full">
<slot />
</div>
<!-- playhead container -->
<div class="playhead-container tw:pointer-events-none tw:row-span-full tw:col-span-full">
<div ref="playheadEl" class="playhead" />
</div>
</div>
</template>
<style scoped>
.card {
color: var(--inactive-text-color);
background-color: var(--card-background-color);
overflow: hidden;
touch-action: pan-y;
}
.card.hover-enabled:hover,
.card.selected {
color: var(--active-text-color);
}
.card.hover-enabled:hover {
outline: 2px solid var(--card-outline-color);
}
.card.selected,
.card.selected:hover {
outline: 2px solid var(--card-outline-selected-color);
}
.playhead-container {
display: none;
position: relative;
}
.card.playhead-enabled.playhead-active .playhead-container {
display: unset;
}
.playhead {
width: 1px;
height: 100%;
/* center the 1px line on the pointer */
transform: translateX(-50%);
background-color: var(--timeline-playhead-color);
position: absolute;
top: 0;
}
</style>