Compare commits

...
4 Commits
8 changed files with 52 additions and 31 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ def create_banner():
def create_startup_box(*, folder, url, unix=None, dev=False, paskia_url=None): def create_startup_box(*, folder, url, unix=None, dev=False, paskia_url=None):
"""Create a framed startup box with server information.""" """Create a framed startup box with server information."""
title = f"Cista {cista.__version__}" title = f"Cista {cista.__version__}"
listen = f"{url} ({unix})" if unix else url listen = unix if unix else url
location = f"{folder} @ {listen}" location = f"{folder} @ {listen}"
lines = [title, location] lines = [title, location]
if paskia_url: if paskia_url:
+2 -2
View File
@@ -89,9 +89,9 @@ def dispatch(path, quality, maxsize, maxzoom):
return process_video(path, quality=quality, maxsize=maxsize) return process_video(path, quality=quality, maxsize=maxsize)
return process_image(path, quality=quality, maxsize=maxsize) return process_image(path, quality=quality, maxsize=maxsize)
except ValueError as e: except ValueError as e:
logger.warning(f"Cannot generate preview for {path.name}: {e}") logger.warning(f"Cannot generate preview for {path}: {e}")
except Exception as e: except Exception as e:
logger.exception(f"Error generating preview for {path.name}: {e}") logger.exception(f"Error generating preview for {path}: {e}")
def process_image(path, *, maxsize, quality): def process_image(path, *, maxsize, quality):
+10 -2
View File
@@ -51,14 +51,22 @@ const closeSearch = (ev: Event) => {
breadcrumb.focus() breadcrumb.focus()
updateSearch(ev) updateSearch(ev)
} }
const updateSearch = (ev: Event) => { const updateSearch = (ev: Event) => {
const q = (ev.target as HTMLInputElement).value const q = (ev.target as HTMLInputElement).value
let p = props.path.join('/') let p = props.path.join('/')
p = p ? `/${p}` : '' p = p ? `/${p}` : ''
const url = q ? `${p}//${q}` : (p || '/') const url = q ? `${p}//${q}` : (p || '/')
const u = url.replaceAll('?', '%3F').replaceAll('#', '%23') const u = url.replaceAll('?', '%3F').replaceAll('#', '%23')
if (!props.query && q) router.push(u)
else router.replace(u) // Start search immediately via store (worker handles it async)
store.search(q, props.path.join('/'))
// Update route in next frame to keep typing responsive
requestAnimationFrame(() => {
if (!props.query && q) router.push(u)
else router.replace(u)
})
} }
const toggleSearchInput = (ev: Event) => { const toggleSearchInput = (ev: Event) => {
showSearchInput.value = !showSearchInput.value showSearchInput.value = !showSearchInput.value
+2 -4
View File
@@ -1,4 +1,4 @@
import { formatSize, formatUnixDate, haystackFormat } from "@/utils" import { formatSize, formatUnixDate } from "@/utils"
export type FUID = string export type FUID = string
@@ -16,7 +16,6 @@ export class Doc {
public key: FUID = "" public key: FUID = ""
public size: number = 0 public size: number = 0
public mtime: number = 0 public mtime: number = 0
public haystack: string = ""
public dir: boolean = false public dir: boolean = false
/** @internal Use the name getter/setter instead */ /** @internal Use the name getter/setter instead */
public _name: string = "" public _name: string = ""
@@ -24,13 +23,12 @@ export class Doc {
constructor(props: Partial<DocProps> = {}) { constructor(props: Partial<DocProps> = {}) {
const { name, ...rest } = props const { name, ...rest } = props
Object.assign(this, rest) Object.assign(this, rest)
if (name) this.name = name // Use setter for validation if (name) this._name = name // Skip validation/haystack for bulk loading
} }
get name() { return this._name } get name() { return this._name }
set name(name: string) { set name(name: string) {
if (name.includes('/') || name.startsWith('.')) throw Error(`Invalid name: ${name}`) if (name.includes('/') || name.startsWith('.')) throw Error(`Invalid name: ${name}`)
this._name = name this._name = name
this.haystack = haystackFormat(name)
} }
get sizedisp(): string { return formatSize(this.size) } get sizedisp(): string { return formatSize(this.size) }
get modified(): string { return formatUnixDate(this.mtime) } get modified(): string { return formatUnixDate(this.mtime) }
+18 -12
View File
@@ -9,10 +9,26 @@ import SearchWorker from '@/workers/searchWorker?worker'
// Singleton search worker instance // Singleton search worker instance
let searchWorker: Worker | null = null let searchWorker: Worker | null = null
let searchId = 0 let searchId = 0
let searchStore: ReturnType<typeof useMainStore> | null = null
function getSearchWorker(): Worker { function getSearchWorker(): Worker {
if (!searchWorker) { if (!searchWorker) {
searchWorker = new SearchWorker() searchWorker = new SearchWorker()
// Set up message handler once
searchWorker.onmessage = (e) => {
if (!searchStore || e.data.id !== searchId) return // Stale result
// Convert plain data back to Doc instances (constructor is now lightweight)
const docs = []
for (const d of e.data.docs) {
docs.push(new Doc(d))
}
searchStore.searchResults = docs
if (e.data.done) {
searchStore.searchLoading = false
}
}
} }
return searchWorker return searchWorker
} }
@@ -24,6 +40,7 @@ export const useMainStore = defineStore('main', {
query: '' as string, query: '' as string,
searchResults: [] as Doc[], searchResults: [] as Doc[],
searchLoading: false, searchLoading: false,
_searchRouteTimer: null as ReturnType<typeof setTimeout> | null,
fileExplorer: null as any, fileExplorer: null as any,
error: '' as string, // Permanent status message (e.g., "Reconnecting...") error: '' as string, // Permanent status message (e.g., "Reconnecting...")
toast: '' as string, // Temporary toast (auto-dismisses) toast: '' as string, // Temporary toast (auto-dismisses)
@@ -116,6 +133,7 @@ export const useMainStore = defineStore('main', {
search(query: string, loc: string) { search(query: string, loc: string) {
const worker = getSearchWorker() const worker = getSearchWorker()
const id = ++searchId const id = ++searchId
searchStore = this // Store reference for worker callback
if (!query) { if (!query) {
this.searchResults = [] this.searchResults = []
@@ -124,18 +142,6 @@ export const useMainStore = defineStore('main', {
} }
this.searchLoading = true this.searchLoading = true
worker.onmessage = (e) => {
if (e.data.id !== searchId) return // Stale result
// Convert plain data back to Doc instances
this.searchResults = e.data.docs.map((d: any) => new Doc(d))
if (e.data.done) {
this.searchLoading = false
}
}
worker.postMessage({ type: 'search', query, loc, id }) worker.postMessage({ type: 'search', query, loc, id })
}, },
login(username: string, privileged: boolean) { login(username: string, privileged: boolean) {
+12 -5
View File
@@ -2,14 +2,14 @@
<Gallery <Gallery
v-if="store.prefs.gallery" v-if="store.prefs.gallery"
ref="fileExplorer" ref="fileExplorer"
:key="`gallery-${Router.currentRoute.value.path}`" :key="`gallery-${folderPath}`"
:path="props.path" :path="props.path"
:documents="documents" :documents="documents"
/> />
<FileExplorer <FileExplorer
v-else v-else
ref="fileExplorer" ref="fileExplorer"
:key="`explorer-${Router.currentRoute.value.path}`" :key="`explorer-${folderPath}`"
:path="props.path" :path="props.path"
:documents="documents" :documents="documents"
/> />
@@ -20,7 +20,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { watchEffect, ref, computed, watch } from 'vue' import { watchEffect, ref, computed, watch } from 'vue'
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
import Router from '@/router/index'
import { collator } from '@/utils' import { collator } from '@/utils'
import { sorted, sortedGrouped } from '@/utils/docsort' import { sorted, sortedGrouped } from '@/utils/docsort'
import FileExplorer from '@/components/FileExplorer.vue' import FileExplorer from '@/components/FileExplorer.vue'
@@ -32,11 +31,19 @@ const props = defineProps<{
query: string query: string
}>() }>()
// Trigger search when query changes // Folder path for component keys - only recreate component when folder changes, not search
const folderPath = computed(() => props.path.join('/'))
// Trigger search when query changes (from route, e.g., page load or back button)
// Note: Direct typing triggers search immediately via HeaderMain, this is for route-based changes
watch( watch(
() => [props.query, props.path.join('/')] as const, () => [props.query, props.path.join('/')] as const,
([query, loc]) => { ([query, loc]) => {
store.search(query, loc) // Only trigger if results don't match current query (avoid duplicate searches)
if (query && store.searchResults.length === 0) {
store.search(query, loc)
} else if (!query) {
store.search('', loc) // Clear search results
}
}, },
{ immediate: true } { immediate: true }
) )
+6 -4
View File
@@ -75,8 +75,9 @@ function yieldControl(): Promise<void> {
async function performSearch(query: string, loc: string, searchId: number) { async function performSearch(query: string, loc: string, searchId: number) {
const needle = needleFormat(query) const needle = needleFormat(query)
const limit = 100 const limit = 100
const batchSize = 1000 const batchSize = 500 // Smaller batches for faster incremental feedback
const results: WorkerDoc[] = [] const results: WorkerDoc[] = []
let lastResultCount = 0
for (let i = 0; i < recentDocuments.length && results.length < limit; i += batchSize) { for (let i = 0; i < recentDocuments.length && results.length < limit; i += batchSize) {
// Check if search was superseded // Check if search was superseded
@@ -91,8 +92,9 @@ async function performSearch(query: string, loc: string, searchId: number) {
} }
} }
// Post incremental results if we found any in this batch // Post incremental results if we found new matches
if (results.length > 0 && currentSearchId === searchId) { if (results.length > lastResultCount && currentSearchId === searchId) {
lastResultCount = results.length
const sortedResults = sortResults(results, query, loc) const sortedResults = sortResults(results, query, loc)
postMessage({ postMessage({
type: 'results', type: 'results',
@@ -102,7 +104,7 @@ async function performSearch(query: string, loc: string, searchId: number) {
} as ResultMessage) } as ResultMessage)
} }
// Yield control between batches // Yield control between batches to allow new search requests to interrupt
if (i + batchSize < recentDocuments.length && results.length < limit) { if (i + batchSize < recentDocuments.length && results.length < limit) {
await yieldControl() await yieldControl()
} }
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
"argon2-cffi>=25.1.0", "argon2-cffi>=25.1.0",
"av>=15.0.0", "av>=15.0.0",
"blake3>=1.0.5", "blake3>=1.0.5",
"docopt>=0.6.2", "docopt-ng>=0.9.0",
"fastapi-vue>=0.5.1", "fastapi-vue>=0.5.1",
"fastapi[standard]>=0.128.0", "fastapi[standard]>=0.128.0",
"html5tagger>=1.3.0", "html5tagger>=1.3.0",