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 sanic.log import logger
from stream_zip import ZIP_AUTO, stream_zip 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.api import bp
from cista.util.apphelpers import handle_sanic_exception from cista.util.apphelpers import handle_sanic_exception
@ -25,6 +25,7 @@ sanic.helpers._ENTITY_HEADERS = frozenset()
app = Sanic("cista", strict_slashes=True) app = Sanic("cista", strict_slashes=True)
app.blueprint(auth.bp) app.blueprint(auth.bp)
app.blueprint(preview.bp)
app.blueprint(bp) app.blueprint(bp)
app.exception(Exception)(handle_sanic_exception) 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 // G toggles Gallery
else if (!input && keyup && event.key === 'g') { 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 // Keys Backquote-1-2-3 to sort columns
else if ( else if (

View File

@ -23,7 +23,7 @@
/> />
</template> </template>
<SvgButton ref="searchButton" name="find" @click.prevent="toggleSearchInput" /> <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" /> <SvgButton name="cog" @click="settingsMenu" />
</nav> </nav>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <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> <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> <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> <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 video = () => ['mkv', 'mp4', 'webm', 'mov', 'avi'].includes(props.doc.ext)
const audio = () => ['mp3', 'flac', 'ogg', 'aac'].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 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> </script>
<style scoped> <style scoped>

View File

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

View File

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

View File

@ -8,7 +8,7 @@
<p v-else>Empty folder</p> <p v-else>Empty folder</p>
</div> </div>
<Gallery <Gallery
v-else-if="store.gallery" v-else-if="store.prefs.gallery"
ref="fileExplorer" ref="fileExplorer"
:key="`gallery-${Router.currentRoute.value.path}`" :key="`gallery-${Router.currentRoute.value.path}`"
:path="props.path" :path="props.path"
@ -21,9 +21,9 @@
:path="props.path" :path="props.path"
:documents="documents" :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> <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> </div>
</template> </template>

View File

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