Implement media preview thumbnails for Gallery
This commit is contained in:
parent
85ac12ad33
commit
696e3ab568
|
@ -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
40
cista/preview.py
Normal 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()
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() : ''
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user