migrated file browser to ts

This commit is contained in:
arabcoders 2025-07-26 20:39:06 +03:00
parent 5942d12f2e
commit c94a4e5c75
4 changed files with 85 additions and 60 deletions

View file

@ -192,9 +192,10 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import moment from 'moment' import moment from 'moment'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { FileItem, FileBrowserResponse } from '~/types/filebrowser'
const route = useRoute() const route = useRoute()
const toast = useNotification() const toast = useNotification()
@ -206,26 +207,32 @@ const bg_opacity = useStorage('random_bg_opacity', 0.95)
const sort_by = useStorage('sort_by', 'name') const sort_by = useStorage('sort_by', 'name')
const sort_order = useStorage('sort_order', 'asc') const sort_order = useStorage('sort_order', 'asc')
const isLoading = ref(false) const isLoading = ref<boolean>(false)
const initialLoad = ref(true) const initialLoad = ref<boolean>(true)
const items = ref([]) const items = ref<FileItem[]>([])
const path = ref(`/${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`) const path = ref<string>((() => {
const table_container = ref(false) const slug = route.params.slug
const search = ref('') if (Array.isArray(slug) && slug.length > 0) {
const show_filter = ref(false) return '/' + slug.join('/')
}
return '/'
})())
const table_container = ref<boolean>(false)
const search = ref<string>('')
const show_filter = ref<boolean>(false)
const filteredItems = computed(() => { const filteredItems = computed<FileItem[]>(() => {
if (!search.value) { if (!search.value) {
return sortedItems(items.value) return sortedItems(items.value)
} }
const searchLower = search.value.toLowerCase() const searchLower = search.value.toLowerCase()
return sortedItems(items.value.filter(item => { return sortedItems(items.value.filter((item: FileItem) => {
return item.name.toLowerCase().includes(searchLower) return item.name.toLowerCase().includes(searchLower)
})) }))
}) })
const sortedItems = items => { const sortedItems = (items: FileItem[]): FileItem[] => {
if (!items || items.length < 1) { if (!items || items.length < 1) {
return [] return []
@ -247,7 +254,7 @@ const sortedItems = items => {
return items.sort((a, b) => { return items.sort((a, b) => {
let aDate = new Date(a.mtime) let aDate = new Date(a.mtime)
let bDate = new Date(b.mtime) let bDate = new Date(b.mtime)
return 'asc' === sort_order.value ? aDate - bDate : bDate - aDate return 'asc' === sort_order.value ? aDate.getTime() - bDate.getTime() : bDate.getTime() - aDate.getTime()
}) })
} }
@ -260,8 +267,8 @@ const sortedItems = items => {
return items return items
} }
const model_item = ref() const model_item = ref<any>()
const closeModel = () => model_item.value = null const closeModel = (): void => { model_item.value = null }
watch(() => config.app.basic_mode, async () => { watch(() => config.app.basic_mode, async () => {
if (!config.app.basic_mode) { if (!config.app.basic_mode) {
@ -284,7 +291,7 @@ watch(() => socket.isConnected, async () => {
} }
}) })
const handleClick = item => { const handleClick = (item: FileItem): void => {
if ('video' === item.content_type) { if ('video' === item.content_type) {
model_item.value = { model_item.value = {
"type": 'video', "type": 'video',
@ -325,10 +332,10 @@ const handleClick = item => {
return return
} }
window.location = makeDownload(config, { "filename": item.path, "folder": "", "extras": {} }) window.location.href = makeDownload(config, { "filename": item.path, "folder": "", "extras": {} })
} }
const reloadContent = async (dir = '/', fromMounted = false) => { const reloadContent = async (dir: string = '/', fromMounted: boolean = false): Promise<void> => {
try { try {
isLoading.value = true isLoading.value = true
@ -346,11 +353,7 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
items.value = [] items.value = []
const data = await response.json() const data = await response.json() as FileBrowserResponse
if (data.length < 1) {
return
}
if (!data?.contents || data.contents.length > 0) { if (!data?.contents || data.contents.length > 0) {
items.value = data.contents items.value = data.contents
@ -368,7 +371,7 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
} }
useHead({ title: decodeURIComponent(title) }) useHead({ title: decodeURIComponent(title) })
} catch (e) { } catch (e: any) {
if (fromMounted) { if (fromMounted) {
return return
} }
@ -379,17 +382,17 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
} }
} }
const event_handler = e => { const event_handler = (e: Event): void => {
const evt = e as PopStateEvent
if (!e.state) { if (!evt.state) {
return return
} }
if (e.state.path === path.value || e.state.path === window.location.pathname) { if (evt.state.path === path.value || evt.state.path === window.location.pathname) {
return return
} }
reloadContent(e.state.path, true) reloadContent(evt.state.path, true)
} }
onMounted(async () => { onMounted(async () => {
@ -397,12 +400,12 @@ onMounted(async () => {
await reloadContent(path.value, true) await reloadContent(path.value, true)
} }
document.addEventListener('popstate', event_handler) document.addEventListener('popstate', event_handler as EventListener)
}) })
onBeforeUnmount(() => document.removeEventListener('popstate', event_handler)) onBeforeUnmount(() => document.removeEventListener('popstate', event_handler as EventListener))
const makeBreadCrumb = path => { const makeBreadCrumb = (path: string): { name: string, link: string, path: string }[] => {
const baseLink = '/' const baseLink = '/'
path = path.replace(/^\/+/, '').replace(/\/+$/, '') path = path.replace(/^\/+/, '').replace(/\/+$/, '')
@ -428,16 +431,15 @@ const makeBreadCrumb = path => {
return links return links
} }
watch(model_item, v => { watch(model_item, v => {
if (!bg_enable.value) { if (!bg_enable.value) {
return return
} }
document.querySelector('body').setAttribute("style", `opacity: ${v ? 1 : bg_opacity.value}`) document.querySelector('body')?.setAttribute("style", `opacity: ${v ? 1 : bg_opacity.value}`)
}) })
const setIcon = item => { const setIcon = (item: FileItem): string => {
if ('link' === item.type) { if ('link' === item.type) {
return 'fa-link' return 'fa-link'
} }
@ -460,7 +462,7 @@ const setIcon = item => {
return 'fa-file' return 'fa-file'
} }
const changeSort = by => { const changeSort = (by: string): void => {
if (!['name', 'size', 'date', 'type'].includes(by)) { if (!['name', 'size', 'date', 'type'].includes(by)) {
return return
} }
@ -472,17 +474,17 @@ const changeSort = by => {
sort_order.value = 'asc' === sort_order.value ? 'desc' : 'asc' sort_order.value = 'asc' === sort_order.value ? 'desc' : 'asc'
} }
const toggleFilter = () => { const toggleFilter = (): void => {
show_filter.value = !show_filter.value show_filter.value = !show_filter.value
if (!show_filter.value) { if (!show_filter.value) {
search.value = '' search.value = ''
return return
} }
awaitElement('#search', e => e.focus()) awaitElement('#search', (e: Element) => (e as HTMLElement).focus())
} }
const createDirectory = async (dir) => { const createDirectory = async (dir: string): Promise<void> => {
if (!config.app.browser_control_enabled) { if (!config.app.browser_control_enabled) {
return return
} }
@ -497,13 +499,13 @@ const createDirectory = async (dir) => {
return return
} }
await actionRequest({ path: dir }, 'directory', { new_dir: new_dir }, (item, action, data) => { await actionRequest({ path: dir } as FileItem, 'directory', { new_dir: new_dir }, (item, action, data) => {
reloadContent(path.value, true) reloadContent(path.value, true)
toast.success(`Successfully created '${new_dir}'.`) toast.success(`Successfully created '${new_dir}'.`)
}) })
} }
const handleAction = async (action, item) => { const handleAction = async (action: string, item: FileItem): Promise<void> => {
if (!config.app.browser_control_enabled) { if (!config.app.browser_control_enabled) {
return return
} }
@ -561,7 +563,12 @@ const handleAction = async (action, item) => {
} }
} }
const actionRequest = async (item, action, data, cb) => { const actionRequest = async (
item: FileItem,
action: string,
data: Record<string, any>,
cb: (item: FileItem, action: string, data: any) => void
): Promise<any> => {
if (!config.app.browser_control_enabled) { if (!config.app.browser_control_enabled) {
return return
} }
@ -591,10 +598,9 @@ const actionRequest = async (item, action, data, cb) => {
} }
return response return response
} catch (error) { } catch (error: any) {
console.error(error) console.error(error)
toast.error(`Failed to perform action: ${error.message}`) toast.error(`Failed to perform action: ${error.message}`)
} }
} }
</script> </script>

View file

@ -172,7 +172,7 @@ watch(() => config.app.file_logging, async v => {
const filteredItems = computed(() => { const filteredItems = computed(() => {
const raw = query.value.trim().toLowerCase() const raw = query.value.trim().toLowerCase()
const contextMatch = raw.match(/context:(\d+)/) const contextMatch = raw.match(/context:(\d+)/)
const context = contextMatch ? parseInt(contextMatch[1], 10) : 0 const context = contextMatch ? parseInt(String(contextMatch[1]), 10) : 0
const searchTerm = raw.replace(/context:\d+/, '').trim() const searchTerm = raw.replace(/context:\d+/, '').trim()
if (!searchTerm) { if (!searchTerm) {
@ -191,7 +191,7 @@ const filteredItems = computed(() => {
}) })
Array.from(matchedIndexes).sort((a: any, b: any) => a - b).forEach((index: any) => { Array.from(matchedIndexes).sort((a: any, b: any) => a - b).forEach((index: any) => {
result.push(logs.value[index]) result.push(logs.value[index] as log_line)
}) })
return result return result
@ -276,21 +276,20 @@ const scrollToBottom = (fast = false) => {
}) })
} }
const log_handler = (data: log_line) => {
logs.value.push(data)
nextTick(() => {
if (autoScroll.value && bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
}
})
}
onMounted(async () => { onMounted(async () => {
await fetchLogs() await fetchLogs()
socket.on('log_lines', log_handler)
socket.emit('subscribe', 'log_lines') socket.emit('subscribe', 'log_lines')
socket.on('log_lines', (data: any) => {
logs.value.push(data)
nextTick(() => {
if (autoScroll.value && bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
}
})
})
if (bg_enable.value) { if (bg_enable.value) {
document.querySelector('body')?.setAttribute("style", `opacity: 1.0`) document.querySelector('body')?.setAttribute("style", `opacity: 1.0`)
} }
@ -298,7 +297,7 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
socket.emit('unsubscribe', 'log_lines') socket.emit('unsubscribe', 'log_lines')
socket.off('log_lines') socket.off('log_lines', log_handler)
if (bg_enable.value) { if (bg_enable.value) {
document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`) document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`)
} }
@ -306,7 +305,7 @@ onBeforeUnmount(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
socket.emit('unsubscribe', 'log_lines') socket.emit('unsubscribe', 'log_lines')
socket.off('log_lines') socket.off('log_lines', log_handler)
if (scrollTimeout) clearTimeout(scrollTimeout) if (scrollTimeout) clearTimeout(scrollTimeout)
}) })

View file

@ -20,7 +20,7 @@ export const useSocketStore = defineStore('socket', () => {
event.forEach(e => socket.value?.on(e, (...args) => true === withEvent ? callback(e, ...args) : callback(...args))) event.forEach(e => socket.value?.on(e, (...args) => true === withEvent ? callback(e, ...args) : callback(...args)))
} }
const off = (event: string | string[], callback: (...args: any[]) => void): any => { const off = (event: string | string[], callback?: (...args: any[]) => void): any => {
if (!Array.isArray(event)) { if (!Array.isArray(event)) {
event = [event] event = [event]
} }

20
ui/app/types/filebrowser.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
type FileItem = {
type: 'file' | 'dir' | 'link'
content_type: 'image' | 'video' | 'text' | 'subtitle' | 'metadata' | 'dir' | string
name: string
path: string
size: number
mime: string
mtime: string
ctime: string
is_dir: boolean
is_file: boolean
is_symlink: boolean
}
type FileBrowserResponse = {
path: string
contents: FileItem[]
}
export type { FileItem, FileBrowserResponse }