Compare commits
13 Commits
v0.2.0
...
19a5c4ad8a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19a5c4ad8a | ||
|
|
3d3b078e60 | ||
|
|
d4e91ea9a6 | ||
|
|
dc4bb494f3 | ||
|
|
9b58b887b4 | ||
|
|
07848907f3 | ||
|
|
7a08f7cbe2 | ||
|
|
dd37238510 | ||
|
|
c8d5f335b1 | ||
|
|
bb80b3ee54 | ||
|
|
06d860c601 | ||
|
|
c321de13fd | ||
|
|
278e8303c4 |
13
cista/api.py
13
cista/api.py
@@ -37,16 +37,23 @@ async def upload(req, ws):
|
||||
)
|
||||
req = msgspec.json.decode(text, type=FileRange)
|
||||
pos = req.start
|
||||
data = None
|
||||
while pos < req.end and (data := await ws.recv()) and isinstance(data, bytes):
|
||||
while True:
|
||||
data = await ws.recv()
|
||||
if not isinstance(data, bytes):
|
||||
break
|
||||
if len(data) > req.end - pos:
|
||||
raise ValueError(
|
||||
f"Expected up to {req.end - pos} bytes, got {len(data)} bytes"
|
||||
)
|
||||
sentsize = await alink(("upload", req.name, pos, data, req.size))
|
||||
pos += typing.cast(int, sentsize)
|
||||
if pos >= req.end:
|
||||
break
|
||||
if pos != req.end:
|
||||
d = f"{len(data)} bytes" if isinstance(data, bytes) else data
|
||||
raise ValueError(f"Expected {req.end - pos} more bytes, got {d}")
|
||||
# Report success
|
||||
res = StatusMsg(status="ack", req=req)
|
||||
print("ack", res)
|
||||
await asend(ws, res)
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ class File:
|
||||
self.open_rw()
|
||||
assert self.fd is not None
|
||||
if file_size is not None:
|
||||
assert pos + len(buffer) <= file_size
|
||||
os.ftruncate(self.fd, file_size)
|
||||
os.lseek(self.fd, pos, os.SEEK_SET)
|
||||
os.write(self.fd, buffer)
|
||||
if buffer:
|
||||
os.lseek(self.fd, pos, os.SEEK_SET)
|
||||
os.write(self.fd, buffer)
|
||||
|
||||
def __getitem__(self, slice):
|
||||
if self.fd is None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
@@ -15,7 +15,6 @@ def run(*, dev=False):
|
||||
# Silence Sanic's warning about running in production rather than debug
|
||||
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1"
|
||||
confdir = config.conffile.parent
|
||||
wwwroot = PurePath(__file__).parent / "wwwroot"
|
||||
if opts.get("ssl"):
|
||||
# Run plain HTTP redirect/acme server on port 80
|
||||
server80.app.prepare(port=80, motd=False)
|
||||
@@ -27,7 +26,7 @@ def run(*, dev=False):
|
||||
motd=False,
|
||||
dev=dev,
|
||||
auto_reload=dev,
|
||||
reload_dir={confdir, wwwroot},
|
||||
reload_dir={confdir},
|
||||
access_log=True,
|
||||
) # type: ignore
|
||||
if dev:
|
||||
|
||||
@@ -44,8 +44,6 @@ watchEffect(() => {
|
||||
onMounted(loadSession)
|
||||
onMounted(watchConnect)
|
||||
onUnmounted(watchDisconnect)
|
||||
// Update human-readable x seconds ago messages from mtimes
|
||||
setInterval(documentStore.updateModified, 1000)
|
||||
const headerMain = ref<typeof HeaderMain | null>(null)
|
||||
let vert = 0
|
||||
let timer: any = null
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<td class="name">
|
||||
<FileRenameInput :doc="editing" :rename="mkdir" :exit="() => {editing = null}" />
|
||||
</td>
|
||||
<FileModified :doc=editing />
|
||||
<FileModified :doc=editing :key=nowkey />
|
||||
<FileSize :doc=editing />
|
||||
<td class="menu"></td>
|
||||
</tr>
|
||||
@@ -61,7 +61,7 @@
|
||||
<button v-if="cursor == doc" class="rename-button" @click="() => (editing = doc)">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<FileModified :doc=doc />
|
||||
<FileModified :doc=doc :key=nowkey />
|
||||
<FileSize :doc=doc />
|
||||
<td class="menu">
|
||||
<button tabindex="-1" @click.stop="contextMenu($event, doc)">⋮</button>
|
||||
@@ -79,28 +79,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { ref, computed, watchEffect, shallowRef, onMounted, onUnmounted } from 'vue'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import type { Document } from '@/repositories/Document'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import FileRenameInput from './FileRenameInput.vue'
|
||||
import { connect, controlUrl } from '@/repositories/WS'
|
||||
import { collator, formatSize, formatUnixDate } from '@/utils'
|
||||
import { collator, formatSize } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
path: Array<string>
|
||||
documents: Document[]
|
||||
documents: Doc[]
|
||||
}>()
|
||||
const documentStore = useDocumentStore()
|
||||
const router = useRouter()
|
||||
const url_for = (doc: Document) => {
|
||||
const url_for = (doc: Doc) => {
|
||||
const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||
return doc.dir ? `#/${p}/` : `/files/${p}`
|
||||
}
|
||||
const cursor = ref<Document | null>(null)
|
||||
const cursor = shallowRef<Doc | null>(null)
|
||||
// File rename
|
||||
const editing = ref<Document | null>(null)
|
||||
const rename = (doc: Document, newName: string) => {
|
||||
const editing = shallowRef<Doc | null>(null)
|
||||
const rename = (doc: Doc, newName: string) => {
|
||||
const oldName = doc.name
|
||||
const control = connect(controlUrl, {
|
||||
message(ev: MessageEvent) {
|
||||
@@ -124,7 +124,7 @@ const rename = (doc: Document, newName: string) => {
|
||||
}
|
||||
doc.name = newName // We should get an update from watch but this is quicker
|
||||
}
|
||||
const sortedDocuments = computed(() => sorted(props.documents as Document[]))
|
||||
const sortedDocuments = computed(() => sorted(props.documents))
|
||||
const showFolderBreadcrumb = (i: number) => {
|
||||
const docs = sortedDocuments.value
|
||||
const docloc = docs[i].loc
|
||||
@@ -132,19 +132,15 @@ const showFolderBreadcrumb = (i: number) => {
|
||||
}
|
||||
defineExpose({
|
||||
newFolder() {
|
||||
const now = Date.now() / 1000
|
||||
editing.value = {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
editing.value = new Doc({
|
||||
loc: loc.value,
|
||||
key: 'new',
|
||||
name: 'New Folder',
|
||||
dir: true,
|
||||
mtime: now,
|
||||
size: 0,
|
||||
sizedisp: formatSize(0),
|
||||
modified: formatUnixDate(now),
|
||||
haystack: '',
|
||||
}
|
||||
console.log("New")
|
||||
})
|
||||
},
|
||||
toggleSelectAll() {
|
||||
console.log('Select')
|
||||
@@ -229,7 +225,14 @@ watchEffect(() => {
|
||||
focusBreadcrumb()
|
||||
}
|
||||
})
|
||||
const mkdir = (doc: Document, name: string) => {
|
||||
let nowkey = ref(0)
|
||||
let modifiedTimer: any = null
|
||||
const updateModified = () => {
|
||||
nowkey.value = Math.floor(Date.now() / 1000)
|
||||
}
|
||||
onMounted(() => { updateModified(); modifiedTimer = setInterval(updateModified, 1000) })
|
||||
onUnmounted(() => { clearInterval(modifiedTimer) })
|
||||
const mkdir = (doc: Doc, name: string) => {
|
||||
const control = connect(controlUrl, {
|
||||
open() {
|
||||
control.send(
|
||||
@@ -250,7 +253,9 @@ const mkdir = (doc: Document, name: string) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
doc.name = name // We should get an update from watch but this is quicker
|
||||
// We should get an update from watch but this is quicker
|
||||
doc.name = name
|
||||
doc.key = crypto.randomUUID()
|
||||
}
|
||||
|
||||
// Column sort
|
||||
@@ -259,11 +264,11 @@ const toggleSort = (name: string) => {
|
||||
}
|
||||
const sort = ref<string>('')
|
||||
const sortCompare = {
|
||||
name: (a: Document, b: Document) => collator.compare(a.name, b.name),
|
||||
modified: (a: Document, b: Document) => b.mtime - a.mtime,
|
||||
size: (a: Document, b: Document) => b.size - a.size
|
||||
name: (a: Doc, b: Doc) => collator.compare(a.name, b.name),
|
||||
modified: (a: Doc, b: Doc) => b.mtime - a.mtime,
|
||||
size: (a: Doc, b: Doc) => b.size - a.size
|
||||
}
|
||||
const sorted = (documents: Document[]) => {
|
||||
const sorted = (documents: Doc[]) => {
|
||||
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
||||
const sorted = [...documents]
|
||||
if (cmp) sorted.sort(cmp)
|
||||
@@ -273,7 +278,7 @@ const selectionIndeterminate = computed({
|
||||
get: () => {
|
||||
return (
|
||||
props.documents.length > 0 &&
|
||||
props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) &&
|
||||
props.documents.some((doc: Doc) => documentStore.selected.has(doc.key)) &&
|
||||
!allSelected.value
|
||||
)
|
||||
},
|
||||
@@ -284,7 +289,7 @@ const allSelected = computed({
|
||||
get: () => {
|
||||
return (
|
||||
props.documents.length > 0 &&
|
||||
props.documents.every((doc: Document) => documentStore.selected.has(doc.key))
|
||||
props.documents.every((doc: Doc) => documentStore.selected.has(doc.key))
|
||||
)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
@@ -301,7 +306,7 @@ const allSelected = computed({
|
||||
|
||||
const loc = computed(() => props.path.join('/'))
|
||||
|
||||
const contextMenu = (ev: Event, doc: Document) => {
|
||||
const contextMenu = (ev: Event, doc: Doc) => {
|
||||
cursor.value = doc
|
||||
console.log('Context menu', ev, doc)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Document } from '@/repositories/Document'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const datetime = computed(() =>
|
||||
@@ -17,6 +17,6 @@ const tooltip = computed(() =>
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
doc: Document
|
||||
doc: Doc
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Document } from '@/repositories/Document'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
@@ -28,8 +28,8 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
doc: Document
|
||||
rename: (doc: Document, newName: string) => void
|
||||
doc: Doc
|
||||
rename: (doc: Doc, newName: string) => void
|
||||
exit: () => void
|
||||
}>()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Document } from '@/repositories/Document'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
@@ -12,7 +12,7 @@ const sizeClass = computed(() => {
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
doc: Document
|
||||
doc: Doc
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,13 +16,51 @@ type CloudFile = {
|
||||
cloudName: string
|
||||
cloudPos: number
|
||||
}
|
||||
|
||||
function pasteHandler(event: ClipboardEvent) {
|
||||
const items = Array.from(event.clipboardData?.items ?? [])
|
||||
const infiles = [] as File[]
|
||||
const dirs = [] as FileSystemDirectoryEntry[]
|
||||
for (const item of items) {
|
||||
if (item.kind !== 'file') continue
|
||||
const entry = item.webkitGetAsEntry()
|
||||
if (entry?.isFile) {
|
||||
const file = item.getAsFile()
|
||||
if (file) infiles.push(file)
|
||||
} else if (entry?.isDirectory) {
|
||||
dirs.push(entry as FileSystemDirectoryEntry)
|
||||
}
|
||||
}
|
||||
if (infiles.length || dirs.length) {
|
||||
event.preventDefault()
|
||||
uploadFiles(infiles)
|
||||
for (const entry of dirs) pasteDirectory(entry, `${props.path!.join('/')}/${entry.name}`)
|
||||
}
|
||||
}
|
||||
const pasteDirectory = async (entry: FileSystemDirectoryEntry, loc: string) => {
|
||||
const reader = entry.createReader()
|
||||
const entries = await new Promise<any[]>(resolve => reader.readEntries(resolve))
|
||||
const cloudfiles = [] as CloudFile[]
|
||||
for (const entry of entries) {
|
||||
const cloudName = `${loc}/${entry.name}`
|
||||
if (entry.isFile) {
|
||||
const file = await new Promise(resolve => entry.file(resolve)) as File
|
||||
cloudfiles.push({file, cloudName, cloudPos: 0})
|
||||
} else if (entry.isDirectory) {
|
||||
await pasteDirectory(entry, cloudName)
|
||||
}
|
||||
}
|
||||
if (cloudfiles.length) uploadCloudFiles(cloudfiles)
|
||||
}
|
||||
function uploadHandler(event: Event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// @ts-ignore
|
||||
const infiles = Array.from(event.dataTransfer?.files || event.target.files) as File[]
|
||||
if (!infiles.length) return
|
||||
const input = event.target as HTMLInputElement | null
|
||||
const infiles = Array.from((input ?? (event as DragEvent).dataTransfer)?.files ?? []) as File[]
|
||||
if (input) input.value = ''
|
||||
if (infiles.length) uploadFiles(infiles)
|
||||
}
|
||||
|
||||
const uploadFiles = (infiles: File[]) => {
|
||||
const loc = props.path!.join('/')
|
||||
let files = []
|
||||
for (const file of infiles) {
|
||||
@@ -32,6 +70,9 @@ function uploadHandler(event: Event) {
|
||||
cloudPos: 0,
|
||||
})
|
||||
}
|
||||
uploadCloudFiles(files)
|
||||
}
|
||||
const uploadCloudFiles = (files: CloudFile[]) => {
|
||||
const dotfiles = files.filter(f => f.cloudName.includes('/.'))
|
||||
if (dotfiles.length) {
|
||||
documentStore.error = "Won't upload dotfiles"
|
||||
@@ -76,7 +117,7 @@ const speed = computed(() => {
|
||||
if (tsince > 1 / s) return 1 / tsince // Next block is late or not coming, decay
|
||||
return s // "Current speed"
|
||||
})
|
||||
const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 100 ? 1 : 0) + '\u202FMB/s': 'stalled')
|
||||
const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 10 ? 1 : 0) + '\u202FMB/s': 'stalled')
|
||||
setInterval(() => {
|
||||
if (Date.now() - uprogress.tlast > 3000) {
|
||||
// Reset
|
||||
@@ -165,10 +206,6 @@ const worker = async () => {
|
||||
const ws = await WSCreate()
|
||||
while (upqueue.length) {
|
||||
const f = upqueue[0]
|
||||
if (f.cloudPos === f.file.size) {
|
||||
upqueue.shift()
|
||||
continue
|
||||
}
|
||||
const start = f.cloudPos
|
||||
const end = Math.min(f.file.size, start + (1<<20))
|
||||
const control = { name: f.cloudName, size: f.file.size, start, end }
|
||||
@@ -179,6 +216,7 @@ const worker = async () => {
|
||||
ws.sendMsg(control)
|
||||
// @ts-ignore
|
||||
await ws.sendData(data)
|
||||
if (f.cloudPos === f.file.size) upqueue.shift()
|
||||
}
|
||||
if (upqueue.length) startWorker()
|
||||
uprogress.status = "idle"
|
||||
@@ -196,8 +234,10 @@ onMounted(() => {
|
||||
// Need to prevent both to prevent browser from opening the file
|
||||
addEventListener('dragover', uploadHandler)
|
||||
addEventListener('drop', uploadHandler)
|
||||
addEventListener('paste', pasteHandler)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
removeEventListener('paste', pasteHandler)
|
||||
removeEventListener('dragover', uploadHandler)
|
||||
removeEventListener('drop', uploadHandler)
|
||||
})
|
||||
@@ -219,7 +259,7 @@ onUnmounted(() => {
|
||||
{{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="position" v-if="uprogress.filesize > 1e7">
|
||||
<span class="position" v-if="uprogress.total > 1e7">
|
||||
{{ (uprogress.uploaded / 1e6).toFixed(0) + '\u202F/\u202F' + (uprogress.total / 1e6).toFixed(0) + '\u202FMB' }}
|
||||
</span>
|
||||
<span class="speed">{{ speeddisp }}</span>
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
import { formatSize, formatUnixDate, haystackFormat } from "@/utils"
|
||||
|
||||
export type FUID = string
|
||||
|
||||
export type Document = {
|
||||
export type DocProps = {
|
||||
loc: string
|
||||
name: string
|
||||
key: FUID
|
||||
size: number
|
||||
sizedisp: string
|
||||
mtime: number
|
||||
modified: string
|
||||
haystack: string
|
||||
dir: boolean
|
||||
}
|
||||
|
||||
export class Doc {
|
||||
private _name: string = ""
|
||||
public loc: string = ""
|
||||
public key: FUID = ""
|
||||
public size: number = 0
|
||||
public mtime: number = 0
|
||||
public haystack: string = ""
|
||||
public dir: boolean = false
|
||||
|
||||
constructor(props: Partial<DocProps> = {}) { Object.assign(this, props) }
|
||||
get name() { return this._name }
|
||||
set name(name: string) {
|
||||
this._name = name
|
||||
this.haystack = haystackFormat(name)
|
||||
}
|
||||
get sizedisp(): string { return formatSize(this.size) }
|
||||
get modified(): string { return formatUnixDate(this.mtime) }
|
||||
}
|
||||
export type errorEvent = {
|
||||
error: {
|
||||
code: number
|
||||
@@ -36,7 +53,7 @@ export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry>
|
||||
// Helper structure for selections
|
||||
export interface SelectedItems {
|
||||
keys: FUID[]
|
||||
docs: Record<FUID, Document>
|
||||
recursive: Array<[string, string, Document]>
|
||||
docs: Record<FUID, Doc>
|
||||
recursive: Array<[string, string, Doc]>
|
||||
missing: Set<FUID>
|
||||
}
|
||||
|
||||
@@ -6,22 +6,26 @@ export const uploadUrl = '/api/upload'
|
||||
export const watchUrl = '/api/watch'
|
||||
|
||||
let tree = [] as FileEntry[]
|
||||
let reconnectDuration = 500
|
||||
let reconnDelay = 500
|
||||
let wsWatch = null as WebSocket | null
|
||||
|
||||
export const loadSession = () => {
|
||||
const s = localStorage['cista-files']
|
||||
if (!s) return false
|
||||
const store = useDocumentStore()
|
||||
try {
|
||||
tree = JSON.parse(sessionStorage["cista-files"])
|
||||
tree = JSON.parse(s)
|
||||
store.updateRoot(tree)
|
||||
console.log(`Loaded session with ${tree.length} items cached`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.log("Loading session failed", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const saveSession = () => {
|
||||
sessionStorage["cista-files"] = JSON.stringify(tree)
|
||||
localStorage["cista-files"] = JSON.stringify(tree)
|
||||
}
|
||||
|
||||
export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => {
|
||||
@@ -59,7 +63,7 @@ export const watchConnect = () => {
|
||||
console.log('Connected to backend', msg)
|
||||
store.server = msg.server
|
||||
store.connected = true
|
||||
reconnectDuration = 500
|
||||
reconnDelay = 500
|
||||
store.error = ''
|
||||
if (msg.user) store.login(msg.user.username, msg.user.privileged)
|
||||
else if (store.isUserLogged) store.logout()
|
||||
@@ -83,10 +87,10 @@ const watchReconnect = (event: MessageEvent) => {
|
||||
store.connected = false
|
||||
store.error = 'Reconnecting...'
|
||||
}
|
||||
reconnectDuration = Math.min(5000, reconnectDuration + 500)
|
||||
reconnDelay = Math.min(5000, reconnDelay + 500)
|
||||
// The server closes the websocket after errors, so we need to reopen it
|
||||
if (watchTimeout !== null) clearTimeout(watchTimeout)
|
||||
watchTimeout = setTimeout(watchConnect, reconnectDuration)
|
||||
watchTimeout = setTimeout(watchConnect, reconnDelay)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document'
|
||||
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
||||
import type { FileEntry, FUID, SelectedItems } from '@/repositories/Document'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { defineStore } from 'pinia'
|
||||
import { collator } from '@/utils'
|
||||
import { logoutUser } from '@/repositories/User'
|
||||
import { watchConnect } from '@/repositories/WS'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
|
||||
type DirectoryData = {
|
||||
[filename: string]: FileData
|
||||
}
|
||||
type User = {
|
||||
username: string
|
||||
privileged: boolean
|
||||
@@ -19,7 +16,7 @@ type User = {
|
||||
export const useDocumentStore = defineStore({
|
||||
id: 'documents',
|
||||
state: () => ({
|
||||
document: [] as Document[],
|
||||
document: shallowRef<Doc[]>([]),
|
||||
selected: new Set<FUID>(),
|
||||
fileExplorer: null as any,
|
||||
error: '' as string,
|
||||
@@ -38,23 +35,17 @@ export const useDocumentStore = defineStore({
|
||||
let loc = [] as string[]
|
||||
for (const [level, name, key, mtime, size, isfile] of root) {
|
||||
loc = loc.slice(0, level - 1)
|
||||
docs.push({
|
||||
docs.push(new Doc({
|
||||
name,
|
||||
loc: level ? loc.join('/') : '/',
|
||||
key,
|
||||
size,
|
||||
sizedisp: formatSize(size),
|
||||
mtime,
|
||||
modified: formatUnixDate(mtime),
|
||||
haystack: haystackFormat(name),
|
||||
dir: !isfile,
|
||||
})
|
||||
}))
|
||||
loc.push(name)
|
||||
}
|
||||
this.document = docs as Document[]
|
||||
},
|
||||
updateModified() {
|
||||
for (const doc of this.document) doc.modified = formatUnixDate(doc.mtime)
|
||||
this.document = docs
|
||||
},
|
||||
login(username: string, privileged: boolean) {
|
||||
this.user.username = username
|
||||
@@ -70,6 +61,7 @@ export const useDocumentStore = defineStore({
|
||||
console.log("Logout")
|
||||
await logoutUser()
|
||||
this.$reset()
|
||||
localStorage.clear()
|
||||
history.go() // Reload page
|
||||
}
|
||||
},
|
||||
@@ -77,12 +69,12 @@ export const useDocumentStore = defineStore({
|
||||
isUserLogged(): boolean {
|
||||
return this.user.isLoggedIn
|
||||
},
|
||||
recentDocuments(): Document[] {
|
||||
recentDocuments(): Doc[] {
|
||||
const ret = [...this.document]
|
||||
ret.sort((a, b) => b.mtime - a.mtime)
|
||||
return ret
|
||||
},
|
||||
largeDocuments(): Document[] {
|
||||
largeDocuments(): Doc[] {
|
||||
const ret = [...this.document]
|
||||
ret.sort((a, b) => b.size - a.size)
|
||||
return ret
|
||||
@@ -107,7 +99,7 @@ export const useDocumentStore = defineStore({
|
||||
for (const key of selected) if (!found.has(key)) ret.missing.add(key)
|
||||
// Build a flat list including contents recursively
|
||||
const relnames = new Set<string>()
|
||||
function add(rel: string, full: string, doc: Document) {
|
||||
function add(rel: string, full: string, doc: Doc) {
|
||||
if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`)
|
||||
relnames.add(rel)
|
||||
ret.recursive.push([rel, full, doc])
|
||||
|
||||
@@ -86,7 +86,7 @@ export function haystackFormat(str: string) {
|
||||
// Preformat search string for faster search
|
||||
export function needleFormat(query: string) {
|
||||
const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||
return {based, words: based.split(/\W+/)}
|
||||
return {based, words: based.split(/\s+/)}
|
||||
}
|
||||
|
||||
// Test if haystack includes needle
|
||||
|
||||
Reference in New Issue
Block a user