Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f94ccc01c6 | ||
|
|
bc355d991d | ||
|
|
e7ebad3c36 | ||
|
|
036c4342de |
+1
-1
@@ -28,7 +28,7 @@ def create_banner():
|
||||
def create_startup_box(*, folder, url, unix=None, dev=False, paskia_url=None):
|
||||
"""Create a framed startup box with server information."""
|
||||
title = f"Cista {cista.__version__}"
|
||||
listen = f"{url} ({unix})" if unix else url
|
||||
listen = unix if unix else url
|
||||
location = f"{folder} @ {listen}"
|
||||
lines = [title, location]
|
||||
if paskia_url:
|
||||
|
||||
+2
-2
@@ -89,9 +89,9 @@ def dispatch(path, quality, maxsize, maxzoom):
|
||||
return process_video(path, quality=quality, maxsize=maxsize)
|
||||
return process_image(path, quality=quality, maxsize=maxsize)
|
||||
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:
|
||||
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):
|
||||
|
||||
@@ -51,14 +51,22 @@ const closeSearch = (ev: Event) => {
|
||||
breadcrumb.focus()
|
||||
updateSearch(ev)
|
||||
}
|
||||
|
||||
const updateSearch = (ev: Event) => {
|
||||
const q = (ev.target as HTMLInputElement).value
|
||||
let p = props.path.join('/')
|
||||
p = p ? `/${p}` : ''
|
||||
const url = q ? `${p}//${q}` : (p || '/')
|
||||
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) => {
|
||||
showSearchInput.value = !showSearchInput.value
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatSize, formatUnixDate, haystackFormat } from "@/utils"
|
||||
import { formatSize, formatUnixDate } from "@/utils"
|
||||
|
||||
export type FUID = string
|
||||
|
||||
@@ -16,7 +16,6 @@ export class Doc {
|
||||
public key: FUID = ""
|
||||
public size: number = 0
|
||||
public mtime: number = 0
|
||||
public haystack: string = ""
|
||||
public dir: boolean = false
|
||||
/** @internal Use the name getter/setter instead */
|
||||
public _name: string = ""
|
||||
@@ -24,13 +23,12 @@ export class Doc {
|
||||
constructor(props: Partial<DocProps> = {}) {
|
||||
const { name, ...rest } = props
|
||||
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 }
|
||||
set name(name: string) {
|
||||
if (name.includes('/') || name.startsWith('.')) throw Error(`Invalid name: ${name}`)
|
||||
this._name = name
|
||||
this.haystack = haystackFormat(name)
|
||||
}
|
||||
get sizedisp(): string { return formatSize(this.size) }
|
||||
get modified(): string { return formatUnixDate(this.mtime) }
|
||||
|
||||
+18
-12
@@ -9,10 +9,26 @@ import SearchWorker from '@/workers/searchWorker?worker'
|
||||
// Singleton search worker instance
|
||||
let searchWorker: Worker | null = null
|
||||
let searchId = 0
|
||||
let searchStore: ReturnType<typeof useMainStore> | null = null
|
||||
|
||||
function getSearchWorker(): Worker {
|
||||
if (!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
|
||||
}
|
||||
@@ -24,6 +40,7 @@ export const useMainStore = defineStore('main', {
|
||||
query: '' as string,
|
||||
searchResults: [] as Doc[],
|
||||
searchLoading: false,
|
||||
_searchRouteTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
fileExplorer: null as any,
|
||||
error: '' as string, // Permanent status message (e.g., "Reconnecting...")
|
||||
toast: '' as string, // Temporary toast (auto-dismisses)
|
||||
@@ -116,6 +133,7 @@ export const useMainStore = defineStore('main', {
|
||||
search(query: string, loc: string) {
|
||||
const worker = getSearchWorker()
|
||||
const id = ++searchId
|
||||
searchStore = this // Store reference for worker callback
|
||||
|
||||
if (!query) {
|
||||
this.searchResults = []
|
||||
@@ -124,18 +142,6 @@ export const useMainStore = defineStore('main', {
|
||||
}
|
||||
|
||||
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 })
|
||||
},
|
||||
login(username: string, privileged: boolean) {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<Gallery
|
||||
v-if="store.prefs.gallery"
|
||||
ref="fileExplorer"
|
||||
:key="`gallery-${Router.currentRoute.value.path}`"
|
||||
:key="`gallery-${folderPath}`"
|
||||
:path="props.path"
|
||||
:documents="documents"
|
||||
/>
|
||||
<FileExplorer
|
||||
v-else
|
||||
ref="fileExplorer"
|
||||
:key="`explorer-${Router.currentRoute.value.path}`"
|
||||
:key="`explorer-${folderPath}`"
|
||||
:path="props.path"
|
||||
:documents="documents"
|
||||
/>
|
||||
@@ -20,7 +20,6 @@
|
||||
<script setup lang="ts">
|
||||
import { watchEffect, ref, computed, watch } from 'vue'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import Router from '@/router/index'
|
||||
import { collator } from '@/utils'
|
||||
import { sorted, sortedGrouped } from '@/utils/docsort'
|
||||
import FileExplorer from '@/components/FileExplorer.vue'
|
||||
@@ -32,11 +31,19 @@ const props = defineProps<{
|
||||
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(
|
||||
() => [props.query, props.path.join('/')] as const,
|
||||
([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 }
|
||||
)
|
||||
|
||||
@@ -75,8 +75,9 @@ function yieldControl(): Promise<void> {
|
||||
async function performSearch(query: string, loc: string, searchId: number) {
|
||||
const needle = needleFormat(query)
|
||||
const limit = 100
|
||||
const batchSize = 1000
|
||||
const batchSize = 500 // Smaller batches for faster incremental feedback
|
||||
const results: WorkerDoc[] = []
|
||||
let lastResultCount = 0
|
||||
|
||||
for (let i = 0; i < recentDocuments.length && results.length < limit; i += batchSize) {
|
||||
// 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
|
||||
if (results.length > 0 && currentSearchId === searchId) {
|
||||
// Post incremental results if we found new matches
|
||||
if (results.length > lastResultCount && currentSearchId === searchId) {
|
||||
lastResultCount = results.length
|
||||
const sortedResults = sortResults(results, query, loc)
|
||||
postMessage({
|
||||
type: 'results',
|
||||
@@ -102,7 +104,7 @@ async function performSearch(query: string, loc: string, searchId: number) {
|
||||
} 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) {
|
||||
await yieldControl()
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ dependencies = [
|
||||
"argon2-cffi>=25.1.0",
|
||||
"av>=15.0.0",
|
||||
"blake3>=1.0.5",
|
||||
"docopt>=0.6.2",
|
||||
"docopt-ng>=0.9.0",
|
||||
"fastapi-vue>=0.5.1",
|
||||
"fastapi[standard]>=0.128.0",
|
||||
"html5tagger>=1.3.0",
|
||||
|
||||
Reference in New Issue
Block a user