Optimizing the search faster; don't trigger full re render by URL changes.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user