Gallery improvements, better layout and autoplay of next media file.
This commit is contained in:
parent
a9d713dbd0
commit
102a970174
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
return false
|
} else {
|
||||||
|
media.value.pause()
|
||||||
|
}
|
||||||
|
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%;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user