Implement media preview thumbnails for Gallery

This commit is contained in:
Leo Vasanko 2023-11-18 11:38:25 -08:00
parent 85ac12ad33
commit 696e3ab568
9 changed files with 57 additions and 9 deletions

View File

@ -16,7 +16,7 @@ from sanic.exceptions import Forbidden, NotFound
from sanic.log import logger
from stream_zip import ZIP_AUTO, stream_zip
from cista import auth, config, session, watching
from cista import auth, config, preview, session, watching
from cista.api import bp
from cista.util.apphelpers import handle_sanic_exception
@ -25,6 +25,7 @@ sanic.helpers._ENTITY_HEADERS = frozenset()
app = Sanic("cista", strict_slashes=True)
app.blueprint(auth.bp)
app.blueprint(preview.bp)
app.blueprint(bp)
app.exception(Exception)(handle_sanic_exception)

40
cista/preview.py Normal file
View File

@ -0,0 +1,40 @@
import asyncio
import io
from pathlib import PurePosixPath
from urllib.parse import unquote
from PIL import Image
from sanic import Blueprint, raw
from sanic.exceptions import Forbidden, NotFound
from cista import config
from cista.util.filename import sanitize
bp = Blueprint("preview", url_prefix="/preview")
@bp.get("/<path:path>")
async def preview(req, path):
"""Preview a file"""
width = int(req.query_string) if req.query_string else 768
rel = PurePosixPath(sanitize(unquote(path)))
path = config.config.path / rel
if not path.is_file():
raise NotFound("File not found")
size = path.lstat().st_size
if size > 20 * 10**6:
raise Forbidden("File too large")
img = await asyncio.get_event_loop().run_in_executor(
req.app.ctx.threadexec, process_image, path, width
)
return raw(img, content_type="image/webp")
def process_image(path, maxsize):
img = Image.open(path)
w, h = img.size
img.thumbnail((min(w, maxsize), min(h, maxsize)))
# Save as webp
imgdata = io.BytesIO()
img.save(imgdata, format="webp", quality=70, method=6)
return imgdata.getvalue()

View File

@ -93,7 +93,7 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
}
// G toggles Gallery
else if (!input && keyup && event.key === 'g') {
store.gallery = !store.gallery
store.prefs.gallery = !store.prefs.gallery
}
// Keys Backquote-1-2-3 to sort columns
else if (

View File

@ -23,7 +23,7 @@
/>
</template>
<SvgButton ref="searchButton" name="find" @click.prevent="toggleSearchInput" />
<SvgButton name="eye" @click="store.gallery = !store.gallery" />
<SvgButton name="eye" @click="store.prefs.gallery = !store.prefs.gallery" />
<SvgButton name="cog" @click="settingsMenu" />
</nav>
</template>

View File

@ -1,5 +1,5 @@
<template>
<img v-if=doc.img :src=doc.url alt="">
<img v-if=doc.img :src="preview() ? doc.previewurl : doc.url" alt="">
<span v-else-if=doc.dir class="folder icon"></span>
<video ref=vid v-else-if=video() :src=doc.url controls preload=none @click.prevent>📄</video>
<audio ref=aud v-else-if=audio() :src=doc.url controls preload=metadata @click.stop>📄</audio>
@ -32,8 +32,11 @@ defineExpose({
const video = () => ['mkv', 'mp4', 'webm', 'mov', 'avi'].includes(props.doc.ext)
const audio = () => ['mp3', 'flac', 'ogg', 'aac'].includes(props.doc.ext)
const embed = () => ['txt', 'py', 'html', 'css', 'js', 'json', 'xml', 'csv', 'tsv'].includes(props.doc.ext)
const archive = () => ['zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar'].includes(props.doc.ext)
const preview = () => (
props.doc.size > 500000 &&
['png', 'bmp', 'ico', 'webp', 'avif', 'jpg', 'jpeg'].includes(props.doc.ext)
)
</script>
<style scoped>

View File

@ -40,6 +40,9 @@ export class Doc {
const ext = this.name.split('.').pop()?.toLowerCase()
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'].includes(ext || '')
}
get previewurl(): string {
return this.url.replace(/^\/files/, '/preview')
}
get ext(): string {
const ext = this.name.split('.').pop()
return ext ? ext.toLowerCase() : ''

View File

@ -23,10 +23,10 @@ export const useMainStore = defineStore({
fileExplorer: null as any,
error: '' as string,
connected: false,
gallery: false,
cursor: '' as string,
server: {} as Record<string, any>,
prefs: {
gallery: false,
sortListing: '' as SortOrder,
sortFiltered: '' as SortOrder,
},

View File

@ -8,7 +8,7 @@
<p v-else>Empty folder</p>
</div>
<Gallery
v-else-if="store.gallery"
v-else-if="store.prefs.gallery"
ref="fileExplorer"
:key="`gallery-${Router.currentRoute.value.path}`"
:path="props.path"
@ -21,9 +21,9 @@
:path="props.path"
:documents="documents"
/>
<div v-if="!store.gallery && documents.some(doc => doc.img)" class="suggest-gallery">
<div v-if="!store.prefs.gallery && documents.some(doc => doc.img)" class="suggest-gallery">
<p>Media files found. Would you like a gallery view?</p>
<SvgButton name="eye" taborder=0 @click="() => { store.gallery = true }">Gallery</SvgButton>
<SvgButton name="eye" taborder=0 @click="() => { store.prefs.gallery = true }">Gallery</SvgButton>
</div>
</template>

View File

@ -45,6 +45,7 @@ export default defineConfig({
"/login": dev_backend,
"/logout": dev_backend,
"/zip": dev_backend,
"/preview": dev_backend,
}
},
build: {