Gallery improvements, better layout and autoplay of next media file.

This commit is contained in:
Leo Vasanko 2023-11-20 01:51:34 -08:00
parent a9d713dbd0
commit 102a970174
5 changed files with 137 additions and 66 deletions

View File

@ -118,7 +118,6 @@ defineExpose({
// Wrapping either end, just land outside the list // Wrapping either end, just land outside the list
if (Math.abs(d) >= N || Math.sign(d) !== Math.sign(moveto - index)) moveto = N if (Math.abs(d) >= N || Math.sign(d) !== Math.sign(moveto - index)) moveto = N
} }
console.log("Gallery cursorMove", d, index, moveto, moveto - index)
store.cursor = docs[moveto]?.key ?? '' store.cursor = docs[moveto]?.key ?? ''
const tr = store.cursor ? document.getElementById(`file-${store.cursor}`) : '' const tr = store.cursor ? document.getElementById(`file-${store.cursor}`) : ''
if (select) { if (select) {

View File

@ -3,20 +3,17 @@
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key }" :class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key }"
@contextmenu.stop @contextmenu.stop
@focus.stop="store.cursor = doc.key" @focus.stop="store.cursor = doc.key"
@click="ev => { @click=onclick
if (m!.play()) ev.preventDefault()
store.cursor = doc.key
}"
> >
<figure> <figure>
<slot></slot> <slot></slot>
<MediaPreview ref=m :doc="doc" tabindex=-1 quality="sz=512" /> <MediaPreview ref=m :doc="doc" tabindex=-1 quality="sz=512" class="figcontent" />
<caption> <div class="titlespacer"></div>
<label> <figcaption>
<SelectBox :doc=doc /> <SelectBox :doc=doc />
<span :title="doc.name + '\n' + doc.modified + '\n' + doc.sizedisp">{{ doc.name }}</span> <span :title="doc.name + '\n' + doc.modified + '\n' + doc.sizedisp">{{ doc.name }}</span>
</label> <div class=namespacer></div>
</caption> </figcaption>
</figure> </figure>
</a> </a>
</template> </template>
@ -33,10 +30,15 @@ const props = defineProps<{
index: number index: number
}>() }>()
const m = ref<typeof MediaPreview | null>(null) const m = ref<typeof MediaPreview | null>(null)
const onclick = (ev: Event) => {
if (m.value!.play()) ev.preventDefault()
store.cursor = props.doc.key
}
</script> </script>
<style scoped> <style scoped>
.gallery figure { figure {
max-height: 15em; max-height: 15em;
position: relative; position: relative;
border-radius: .5em; border-radius: .5em;
@ -48,51 +50,48 @@ const m = ref<typeof MediaPreview | null>(null)
justify-content: end; justify-content: end;
overflow: hidden; overflow: hidden;
} }
.icon { figure > article {
justify-self: end; flex: 0 0 auto;
} }
figure caption { .titlespacer {
font-weight: 600; flex-shrink: 100000;
color: var(--text-color); width: 100%;
text-shadow: 0 0 .2em var(--primary-background), 0 0 .2em var(--primary-background); height: 2em;
} }
.cursor caption { figcaption {
background: var(--accent-color);
text-shadow: none;
}
caption {
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%; width: 100%;
} }
caption label { figcaption input[type='checkbox'] {
width: 100%; width: 1.5em;
display: flex; height: 1.5em;
align-items: center; margin: .25em;
}
label span {
flex: 1 1;
margin-right: 2em;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
label input[type='checkbox'] {
width: 2em;
height: 2em;
opacity: 0; opacity: 0;
flex-shrink: 0; flex-shrink: 0;
} }
label input[type='checkbox']:checked { figcaption input[type='checkbox']:checked, figcaption input[type='checkbox']:hover {
opacity: 1; opacity: 1;
} }
a { figcaption span {
text-decoration: none; padding: .5em;
color: #fff;
font-weight: 600;
text-shadow: 0 0 .2em #000, 0 0 .2em #000;
text-wrap: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.cursor figcaption span {
color: var(--accent-color);
}
figcaption .namespacer {
flex-shrink: 100000;
height: 2em;
width: 2em;
} }
</style> </style>

View File

@ -2,8 +2,10 @@
<img v-if=preview() :src="`${doc.previewurl}?${quality}&t=${doc.mtime}`" alt=""> <img v-if=preview() :src="`${doc.previewurl}?${quality}&t=${doc.mtime}`" alt="">
<img v-else-if=doc.img :src=doc.url alt=""> <img v-else-if=doc.img :src=doc.url alt="">
<span v-else-if=doc.dir class="folder icon"></span> <span v-else-if=doc.dir class="folder icon"></span>
<video ref=vid v-else-if=video() :src=doc.url :poster="`${doc.previewurl}?${quality}&t=${doc.mtime}`" controls preload=none @click.prevent>🎞</video> <video ref=vid v-else-if=video() :src=doc.url :poster=poster preload=none @play=onplay @pause=onpaused @ended=next @seeking=media!.play()></video>
<audio ref=aud v-else-if=audio() :src=doc.url controls preload=metadata @click.stop>🔈</audio> <div v-else-if=audio() class="audio icon">
<audio ref=aud :src=doc.url class=icon preload=none @play=onplay @pause=onpaused @ended=next @seeking=media!.play()></audio>
</div>
<span v-else-if=archive() class="archive icon"></span> <span v-else-if=archive() class="archive icon"></span>
<span v-else class="file icon" :class="`ext-${doc.ext}`"></span> <span v-else class="file icon" :class="`ext-${doc.ext}`"></span>
</template> </template>
@ -15,20 +17,83 @@ import type { Doc } from '@/repositories/Document'
const aud = ref<HTMLAudioElement | null>(null) const aud = ref<HTMLAudioElement | null>(null)
const vid = ref<HTMLVideoElement | null>(null) const vid = ref<HTMLVideoElement | null>(null)
const media = computed(() => aud.value || vid.value) const media = computed(() => aud.value || vid.value)
const poster = computed(() => `${props.doc.previewurl}?${props.quality}&t=${props.doc.mtime}`)
const props = defineProps<{ const props = defineProps<{
doc: Doc doc: Doc
quality: string quality: string
}>() }>()
const onplay = () => {
if (!media.value) return
media.value.controls = true
media.value.setAttribute('data-playing', '')
}
const onpaused = () => {
if (!media.value) return
media.value.controls = false
media.value.removeAttribute('data-playing')
}
let fscurrent: HTMLVideoElement | null = null
const next = () => {
if (!media.value) return
media.value.load() // Restore poster
const medias = Array.from(document.querySelectorAll('video, audio')) as (HTMLAudioElement | HTMLVideoElement)[]
if (medias.length === 0) return
let el: HTMLAudioElement | HTMLVideoElement | null = null
for (const i in medias) {
if (medias[i] === (fscurrent || media.value)) {
el = medias[+i + 1] || medias[0]
break
}
}
if (!el) return
if (el.tagName === "VIDEO" && document.fullscreenElement === media.value) {
// Fullscreen needs to use the current video element for the next video
// because we are not allowed to fullscreen the next one.
// FIXME: Write our own player to avoid this problem...
const elem = media.value as HTMLVideoElement
const playing = el as HTMLVideoElement
if (elem === playing) {
playing.play() // Only one video, just replay
return
}
if (!fscurrent) {
elem.addEventListener('fullscreenchange', ev => {
if (!fscurrent) return
// Restore the original video element and continue with the one that was playing
fscurrent.currentTime = elem.currentTime
fscurrent.click()
if (!elem.paused) fscurrent.play()
fscurrent = null
elem.src = props.doc.url
elem.poster = poster.value
onpaused()
}, {once: true})
}
fscurrent = playing
elem.src = playing.src
elem.poster = ''
elem.play()
} else {
document.exitFullscreen()
el.click()
}
}
defineExpose({ defineExpose({
play() { play() {
if (media.value) { if (!media.value) return false
if (media.value.paused) media.value.play() if (media.value.paused) {
else media.value.pause() media.value.play()
return true for (const el of Array.from(document.querySelectorAll('video, audio')) as (HTMLAudioElement | HTMLVideoElement)[]) {
if (el === media.value) continue
el.pause()
}
} else {
media.value.pause()
} }
return false return true
}, },
media,
}) })
@ -44,20 +109,12 @@ const preview = () => (
<style scoped> <style scoped>
img, embed, .icon, audio, video { img, embed, .icon, audio, video {
font-size: 10em; font-size: 8em;
overflow: hidden; overflow: hidden;
min-width: 50%; min-width: 50%;
max-width: 100%; max-width: 100%;
min-height: 50%;
max-height: 100%; max-height: 100%;
object-fit: cover; border-radius: calc(.5em / 8);
border-radius: .05em;
}
img {
justify-self: start;
}
audio, video {
padding-bottom: 2rem;
} }
.folder::before { .folder::before {
content: '📁'; content: '📁';
@ -80,6 +137,22 @@ audio, video {
.ext-torrent::before { .ext-torrent::before {
content: '🏴‍☠️'; content: '🏴‍☠️';
} }
.audio audio {
opacity: 0;
transition: opacity var(--transition-time) ease-in-out;
}
.audio:hover audio {
opacity: 1;
}
.audio.icon::before {
width: 100%;
content: '🔈';
}
.audio.icon:has(audio[data-playing])::before {
position: absolute;
content: '🔊';
bottom: 0;
}
.icon { .icon {
filter: brightness(0.9); filter: brightness(0.9);
} }
@ -87,7 +160,7 @@ figure.cursor .icon {
filter: brightness(1); filter: brightness(1);
} }
img::before, video::before { img::before, video::before {
/* broken image */ /* broken media */
background: #888; background: #888;
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@ -1,5 +1,5 @@
<template> <template>
<input type=checkbox tabindex=-1 :checked="store.selected.has(doc.key)" <input type=checkbox tabindex=-1 :checked="store.selected.has(doc.key)" @click.stop
@change="ev => { @change="ev => {
if ((ev.target as HTMLInputElement).checked) { if ((ev.target as HTMLInputElement).checked) {
store.selected.add(doc.key) store.selected.add(doc.key)

View File

@ -94,7 +94,7 @@ watchEffect(() => {
justify-content: center; justify-content: center;
height: 100%; height: 100%;
font-size: 2rem; font-size: 2rem;
text-shadow: 0 0 1rem #000, 0 0 2rem #000; text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
color: var(--accent-color); color: var(--accent-color);
} }
@keyframes rotate { @keyframes rotate {
@ -118,6 +118,6 @@ svg.cog {
margin: 0 auto; margin: 0 auto;
animation: rotate 10s linear infinite; animation: rotate 10s linear infinite;
filter: drop-shadow(0 0 1rem black); filter: drop-shadow(0 0 1rem black);
fill: var(--primary-color); fill: #888;
} }
</style> </style>