Merge pull request #466 from arabcoders/dev

[FEAT] add grid display style for files
This commit is contained in:
Abdulmohsen 2025-10-24 20:06:28 +03:00 committed by GitHub
commit 70c42252a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 410 additions and 195 deletions

View file

@ -4,7 +4,7 @@
<button class="button is-fullwidth is-justify-content-space-between" aria-haspopup="true" type="button"
aria-controls="dropdown-menu" @click="toggle" :class="button_classes">
<span class="icon" v-if="icons"><i :class="icons" /></span>
<span>{{ label }}</span>
<span :class="{ 'is-sr-only': hideLabel }">{{ label }}</span>
<div class="is-pulled-right">
<span class="icon"><i class="fas fa-angle-down" aria-hidden="true" /></span>
</div>
@ -15,6 +15,15 @@
<div v-if="isOpen" class="dropdown is-active dropdown-portal" ref="menu" :style="menuStyle">
<div class="dropdown-menu" role="menu">
<div class="dropdown-content" @click="handle_slot_click">
<template v-if="hideLabel">
<div class="dropdown-label">
<span class="icon-text">
<span class="icon" v-if="icons"><i :class="icons" /></span>
<span>{{ label }}</span>
</span>
</div>
<div class="dropdown-divider"></div>
</template>
<slot />
</div>
</div>
@ -27,7 +36,7 @@
import { ref, onMounted, onBeforeUnmount, nextTick, watchEffect, useTemplateRef } from 'vue'
const emitter = defineEmits(['open_state'])
defineProps({
const props = defineProps({
label: {
type: String,
default: 'Select'
@ -39,6 +48,10 @@ defineProps({
button_classes: {
type: String,
default: ''
},
hide_label_on_mobile: {
type: Boolean,
default: false
}
})
@ -47,6 +60,9 @@ const dropUp = ref(false)
const dropdown = useTemplateRef<HTMLDivElement>('dropdown')
const menu = useTemplateRef<HTMLDivElement>('menu')
const menuStyle = ref<Record<string, string>>({})
const isMobile = useMediaQuery({ maxWidth: 1024 })
const hideLabel = computed(() => isMobile.value && props.hide_label_on_mobile)
const updatePosition = () => {
if (!dropdown.value || !isOpen.value) {
@ -162,8 +178,10 @@ onBeforeUnmount(() => {
}
.dropdown.dropdown-portal .dropdown-menu {
display: block !important; /* Override Bulma's display: none */
position: static; /* Don't use absolute positioning inside fixed container */
display: block !important;
/* Override Bulma's display: none */
position: static;
/* Don't use absolute positioning inside fixed container */
max-height: 300px;
overflow-y: auto;
padding-top: 4px;
@ -176,4 +194,10 @@ onBeforeUnmount(() => {
padding-top: var(--bulma-dropdown-content-padding-top, 0.5rem);
padding-bottom: var(--bulma-dropdown-content-padding-bottom, 0.5rem);
}
.dropdown-label {
font-weight: 600;
padding: 0.5rem 0.75rem;
color: var(--bulma-dropdown-label-color, var(--bulma-text-color, #4a4a4a));
}
</style>

View file

@ -43,13 +43,14 @@
<template>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link is-arrowless">
<a class="navbar-link">
<span class="icon"><i class="fas fa-bell" /></span>
<span class="tag ml-2" v-if="store.unreadCount > 0">
<span class="tag ml-2">
<span class="is-underlined">{{ store.unreadCount }}</span>
<span>&nbsp;/&nbsp;</span>
<span class="is-underlined">{{ store.notifications.length }}</span>
</span>
</a>
<div class="navbar-dropdown is-right" style="width: 400px;">
<template v-if="store.notifications.length > 0">
<div class="px-3 py-2 is-flex is-justify-content-space-between is-align-items-center">
@ -112,11 +113,6 @@
</div>
</div>
</div>
</div>
<div class="navbar-item">
</div>
</template>

View file

@ -5,9 +5,11 @@
<form class="download-form__body" autocomplete="off" @submit.prevent="addDownload">
<label class="label" for="download-url">
What you would like to download?
<span class="icon is-pointer" :class="connectionStatusColor" @click="$emit('show_settings')"
v-tooltip="'WebUI Settings'">
<i class="fas fa-cogs" /></span>
<span class="is-pulled-right">
<span class="icon is-pointer" :class="connectionStatusColor" @click="$emit('show_settings')"
v-tooltip="'WebUI Settings'">
<i class="fas fa-cogs" /></span>
</span>
</label>
<div class="field has-addons">
<div class="control">

View file

@ -196,6 +196,11 @@ const captureFirstFramePoster = async (el: HTMLVideoElement): Promise<void> => {
thumbnail.value = dataUrl
havePoster.value = true
applyMediaSessionMetadata()
if (detachDecodeGuard) {
detachDecodeGuard()
detachDecodeGuard = null
}
}
onMounted(async () => {
@ -387,6 +392,7 @@ function attachDecodeFailGuard(el: HTMLVideoElement, onFail: () => void): () =>
let timer: number | null = null
let armed = false
let done = false
let playAttempted = false
const clearAll = () => {
if (null !== timer) {
@ -400,6 +406,7 @@ function attachDecodeFailGuard(el: HTMLVideoElement, onFail: () => void): () =>
el.removeEventListener('loadeddata', onLoadedData)
el.removeEventListener('error', onError)
el.removeEventListener('emptied', onError)
el.removeEventListener('play', onPlay)
}
const ok = () => {
@ -429,23 +436,30 @@ function attachDecodeFailGuard(el: HTMLVideoElement, onFail: () => void): () =>
return 0
}
const onPlay = () => playAttempted = true
const armCheck = () => {
if (armed || done) return
armed = true
if ('requestVideoFrameCallback' in el) {
rvfcId = (el as any).requestVideoFrameCallback(() => ok())
timer = window.setTimeout(() => fail(), 1000)
// Extend timeout for network delays and buffering
timer = window.setTimeout(() => fail(), 3000)
return
}
// Give more time for network playback and buffering
timer = window.setTimeout(() => {
if (0 === (el as any).videoWidth || 0 === (el as any).videoHeight || 0 === decodedFrames()) {
// Only fail if we actually tried to play and have no frames decoded after 3 seconds
if (playAttempted && 0 === (el as any).videoWidth && 0 === (el as any).videoHeight && 0 === decodedFrames()) {
fail()
} else {
} else if (0 < (el as any).videoWidth || 0 < (el as any).videoHeight || 0 < decodedFrames()) {
// Video dimensions or frames are available, stream is working
ok()
}
}, 800)
// If nothing is available yet but video hasn't been played, keep waiting
}, 3000)
}
const onLoadedData = () => armCheck()
@ -454,6 +468,7 @@ function attachDecodeFailGuard(el: HTMLVideoElement, onFail: () => void): () =>
el.addEventListener('loadeddata', onLoadedData, { once: true })
el.addEventListener('error', onError)
el.addEventListener('emptied', onError)
el.addEventListener('play', onPlay)
if (4 <= el.readyState || 2 <= el.readyState) {
queueMicrotask(armCheck)
@ -466,11 +481,22 @@ const src_error = async () => {
if (hls) {
return
}
// If we already successfully captured a frame, that means the streamed video codec is working.
if (havePoster.value) {
return
}
await nextTick()
if (destroyed.value) {
return
}
// Check if video is actually paused.
if (video.value && video.value.paused) {
return
}
console.warn('Source failed to load, attempting HLS fallback...')
attach_hls(makeDownload(config, props.item, 'm3u8'))
}

View file

@ -8,8 +8,8 @@
<template v-if="show_settings">
<Modal @close="closeSettings()"
:content-class="isMobile ? 'modal-content-max is-overflow-scroll ' : 'modal-content-max'">
<div class="columns is-multiline">
<div class="column is-12 mt-2">
<div class="columns is-multiline" style="width:100%">
<div class="column is-12">
<div class="card">
<header class="card-header">
<p class="card-header-title">WebUI Settings</p>
@ -41,8 +41,9 @@
<NuxtLink class="navbar-item is-text-overflow" to="/" @click.prevent="(e: MouseEvent) => changeRoute(e)"
v-tooltip="socket.isConnected ? 'Connected' : 'Connecting'">
<span class="is-text-overflow">
<span class="icon" v-if="'connecting' === socket.connectionStatus">
<i class="fas fa-arrows-rotate fa-spin" />
<span class="icon">
<i v-if="'connecting' === socket.connectionStatus" class="fas fa-arrows-rotate fa-spin" />
<i v-else class="fas fa-home" />
</span>
<span class="has-text-bold" :class="connectionStatusColor">
YTPTube
@ -60,12 +61,6 @@
<div class="navbar-menu is-unselectable" :class="{ 'is-active': showMenu }">
<div class="navbar-start">
<NuxtLink class="navbar-item" to="/" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-home" /></span>
<span>Home</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/browser" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-folder-tree" /></span>
<span>Files</span>
@ -76,15 +71,23 @@
<span>Presets</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/tasks" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-tasks" /></span>
<span>Tasks</span>
</NuxtLink>
<div class="navbar-item has-dropdown">
<a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)">
<span class="icon"><i class="fas fa-tasks" /></span>
<span>Tasks</span>
</a>
<div class="navbar-dropdown">
<NuxtLink class="navbar-item" to="/tasks" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-tasks" /></span>
<span>List</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/task_definitions" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-diagram-project" /></span>
<span>Task Definitions</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/task_definitions" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-diagram-project" /></span>
<span>Definitions</span>
</NuxtLink>
</div>
</div>
<NuxtLink class="navbar-item" to="/notifications" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon-text">
@ -100,17 +103,51 @@
</div>
<div class="navbar-end">
<div class="navbar-item has-dropdown" v-if="config.app.file_logging || config.app.console_enabled">
<a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)">
<span class="icon"><i class="fas fa-tools" /></span>
<span>Other</span>
</a>
<div class="navbar-dropdown">
<NuxtLink class="navbar-item" to="/logs" @click.prevent="(e: MouseEvent) => changeRoute(e)"
v-if="config.app.file_logging">
<span class="icon"><i class="fa-solid fa-file-lines" /></span>
<span>Logs</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/console" @click.prevent="(e: MouseEvent) => changeRoute(e)"
v-if="config.app.console_enabled">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Console</span>
</NuxtLink>
</div>
</div>
<div class="navbar-item" v-if="true === config.app.is_native">
<button class="button is-dark" @click="shutdownApp">
<span class="icon"><i class="fas fa-power-off" /></span>
<span v-if="isMobile">Shutdown</span>
</button>
</div>
<NuxtLink class="navbar-item" to="/settings" @click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Settings</span>
</NuxtLink>
<NotifyDropdown />
<div class="navbar-item">
<button class="button is-dark" @click="reloadPage">
<span class="icon"><i class="fas fa-refresh" /></span>
<span v-if="isMobile">Reload</span>
</button>
</div>
<div class="navbar-item">
<NuxtLink class="button is-dark " :class="{ 'has-tooltip-bottom mr-4': !isMobile }" to="/settings"
@click.prevent="(e: MouseEvent) => changeRoute(e)">
<span class="icon"><i class="fas fa-cog" /></span>
<span v-if="isMobile">WebUI Settings</span>
</NuxtLink>
</div>
</div>
</div>
</nav>
@ -444,6 +481,20 @@ const shutdownApp = async () => {
}
}
const openMenu = (e: MouseEvent) => {
const elm = (e.target as HTMLElement)?.closest('div.has-dropdown') as HTMLElement | null
document.querySelectorAll<HTMLElement>('div.has-dropdown').forEach(el => {
if (el !== elm) {
el.classList.remove('is-active')
}
})
elm?.classList.toggle('is-active')
}
const reloadPage = () => window.location.reload()
const connectionStatusColor = computed(() => {
switch (socket.connectionStatus) {
case 'connected':

View file

@ -19,14 +19,14 @@
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<div class="control has-icons-left" v-if="show_filter">
<div class="control has-icons-left" v-if="show_filter && items && items.length > 0">
<input type="search" v-model.lazy="search" class="input" id="search" placeholder="Filter">
<span class="icon is-left">
<i class="fas fa-filter"></i>
</span>
</div>
<div class="control">
<div class="control" v-if="items && items.length > 0">
<button class="button is-danger is-light" @click="toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
<span v-if="!isMobile">Filter</span>
@ -40,11 +40,63 @@
<span v-if="!isMobile">New Folder</span>
</button>
</p>
<p class="control">
<button class="button" @click="() => display_style = display_style === 'list' ? 'grid' : 'list'">
<span class="icon">
<i class="fa-solid"
:class="{ 'fa-table': display_style !== 'list', 'fa-table-list': display_style === 'list' }" /></span>
<span v-if="!isMobile">
{{ display_style === 'list' ? 'List' : 'Grid' }}
</span>
</button>
</p>
<p class="control" v-if="items && items.length > 0">
<Dropdown label="Sort&nbsp;&nbsp;" icons="fa-solid fa-sort" :hide_label_on_mobile="true">
<NuxtLink class="dropdown-item" :class="{ 'is-active': 'type' === sort_by }"
@click="changeSort('type')">
<span class="icon"><i class="fa-solid fa-hashtag" /></span>
<span>Type</span>
<span class="icon is-pulled-right" v-if="'type' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</NuxtLink>
<NuxtLink class="dropdown-item" :class="{ 'is-active': 'name' === sort_by }"
@click="changeSort('name')">
<span class="icon"><i class="fa-solid fa-arrow-down-a-z" /></span>
<span>Name</span>
<span class="icon is-pulled-right" v-if="'name' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</NuxtLink>
<NuxtLink class="dropdown-item" :class="{ 'is-active': 'size' === sort_by }"
@click="changeSort('size')">
<span class="icon"><i class="fa-solid fa-weight" /></span>
<span>Size</span>
<span class="icon is-pulled-right" v-if="'size' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</NuxtLink>
<NuxtLink class="dropdown-item" :class="{ 'is-active': 'date' === sort_by }"
@click="changeSort('date')">
<span class="icon"><i class="fa-solid fa-calendar" /></span>
<span>Date</span>
<span class="icon is-pulled-right" v-if="'date' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</NuxtLink>
</Dropdown>
</p>
<p class="control">
<button class="button is-info" @click="reloadContent(path, true)" :class="{ 'is-loading': isLoading }"
:disabled="!socket.isConnected || isLoading">
<span class="icon"><i class="fas fa-refresh" /></span>
<span v-if="!isMobile">Reload</span>
</button>
</p>
</div>
@ -52,7 +104,19 @@
</div>
</div>
<div class="columns is-multiline" v-if="config.app.browser_control_enabled">
<div class="columns is-mobile is-multiline" v-if="config.app.browser_control_enabled && items && items.length > 0">
<div class="column is-12-mobile">
<button type="button" class="button is-fullwidth is-ghost is-inverted"
@click="masterSelectAll = !masterSelectAll" :disabled="isLoading || items.length < 1">
<span class="icon-text is-block">
<span class="icon">
<i :class="!masterSelectAll ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" />
</span>
<span v-if="!masterSelectAll">Select All</span>
<span v-else>Unselect All</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-danger" @click="deleteSelected"
:disabled="selectedElms.length < 1 || isLoading || items.length < 1">
@ -74,126 +138,210 @@
</div>
<div class="columns is-multiline">
<div class="column is-12" v-if="items && items.length > 0">
<div :class="{ 'table-container': table_container }">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 1300px; table-layout: fixed;">
<thead>
<tr class="has-text-centered is-unselectable">
<th width="5%" v-if="config.app.browser_control_enabled">
<input type="checkbox" v-model="masterSelectAll" />
</th>
<th width="6%" @click="changeSort('type')">
#
<span class="icon" v-if="'type' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="65%" @click="changeSort('name')">
Name
<span class="icon" v-if="'name' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
<template v-if="'list' === display_style">
<div class="column is-12" v-if="filteredItems && filteredItems.length > 0">
<div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 1300px; table-layout: fixed;">
<thead>
<tr class="has-text-centered is-unselectable">
<th colspan="2" width="10%">
#
<span class="icon" v-if="'type' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="65%">
Name
<span class="icon" v-if="'name' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="10%" @click="changeSort('size')">
Size
<span class="icon" v-if="'size' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="10%">
Size
<span class="icon" v-if="'size' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="15%" @click="changeSort('date')">
Date
<span class="icon" v-if="'date' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="15%" v-if="config.app.browser_control_enabled">
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredItems" :key="item.path">
<td class="has-text-centered is-vcentered" v-if="config.app.browser_control_enabled">
<input type="checkbox" v-model="selectedElms" :value="item.path" />
</td>
<td class="has-text-centered is-vcentered user-hint" v-tooltip="item.name">
<span class="icon"><i class="fas fa-2x fa-solid" :class="setIcon(item)" /></span>
</td>
<td class="is-text-overflow is-vcentered">
<div class="field is-grouped">
<div class="control is-text-overflow is-expanded">
<a :href="uri(`/browser/${item.path}`)" v-if="'dir' === item.content_type"
@click.prevent="handleClick(item)">
{{ item.name }}
</a>
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
@click.prevent="handleClick(item)" v-else>
{{ item.name }}
</a>
</div>
<div class="control" v-if="'file' === item.type">
<span class="icon">
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
:download="item.name.split('/').reverse()[0]">
<i class="fas fa-download" />
</th>
<th width="15%">
Date
<span class="icon" v-if="'date' === sort_by">
<i class="fas"
:class="{ 'fa-sort-up': 'desc' === sort_order, 'fa-sort-down': 'asc' === sort_order }" />
</span>
</th>
<th width="15%" v-if="config.app.browser_control_enabled">
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredItems" :key="item.path">
<td class="has-text-centered is-vcentered" v-if="config.app.browser_control_enabled">
<input type="checkbox" v-model="selectedElms" :value="item.path" />
</td>
<td class="has-text-centered is-vcentered user-hint" v-tooltip="item.name">
<span class="icon"><i class="fas fa-2x fa-solid" :class="setIcon(item)" /></span>
</td>
<td class="is-text-overflow is-vcentered">
<div class="field is-grouped">
<div class="control is-text-overflow is-expanded">
<a :href="uri(`/browser/${item.path}`)" v-if="'dir' === item.content_type"
@click.prevent="handleClick(item)">
{{ item.name }}
</a>
</span>
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
@click.prevent="handleClick(item)" v-else>
{{ item.name }}
</a>
</div>
<div class="control" v-if="'file' === item.type">
<span class="icon">
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
:download="item.name.split('/').reverse()[0]">
<i class="fas fa-download" />
</a>
</span>
</div>
</div>
</td>
<td class="has-text-centered is-text-overflow is-unselectable is-vcentered">
{{ 'file' === item.type ? formatBytes(item.size) : ucFirst(item.type) }}
</td>
<td class="has-text-centered is-text-overflow is-unselectable is-vcentered">
<span :data-datetime="item.mtime" v-tooltip="moment(item.mtime).format('YYYY-MM-DD H:mm:ss Z')"
class="has-tooltip">
{{ moment(item.mtime).fromNow() }}
</span>
</td>
<td class="is-vcentered" v-if="config.app.browser_control_enabled">
<Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="'file' === item.type">
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
:download="item.name.split('/').reverse()[0]" class="dropdown-item">
<span class="icon"><i class="fa-solid fa-download" /></span>
<span>Download</span>
</a>
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item" @click="handleAction('rename', item)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Rename</span>
</NuxtLink>
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="handleAction('delete', item)">
<span class="icon has-text-danger"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</NuxtLink>
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="handleAction('move', item)">
<span class="icon"><i class="fa-solid fa-arrows-alt" /></span>
<span>Move</span>
</NuxtLink>
</Dropdown>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template v-else>
<div class="column is-6" v-for="item in filteredItems" :key="item.path">
<div class="card is-flex is-full-height is-flex-direction-column">
<header class="card-header">
<div class="card-header-title is-text-overflow is-block">
<a :href="uri(`/browser/${item.path}`)" v-if="'dir' === item.content_type"
@click.prevent="handleClick(item)" v-tooltip="item.name">
<span class="icon"> <i class="fas fa-solid" :class="setIcon(item)" /></span>
{{ item.name }}
</a>
<a :href="makeDownload({}, { filename: item.path, folder: '' })" @click.prevent="handleClick(item)"
v-tooltip="item.name" v-else>
<span class="icon"> <i class="fas fa-solid" :class="setIcon(item)" /></span>
{{ item.name }}
</a>
</div>
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control" v-if="'file' === item.type">
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
:download="item.name.split('/').reverse()[0]" class="has-text-link" v-tooltip="`Download File`">
<span class="icon"><i class="fa-solid fa-download" /></span>
</a>
</div>
</td>
<td class="has-text-centered is-text-overflow is-unselectable is-vcentered">
{{ 'file' === item.type ? formatBytes(item.size) : ucFirst(item.type) }}
</td>
<td class="has-text-centered is-text-overflow is-unselectable is-vcentered">
<span :data-datetime="item.mtime" v-tooltip="moment(item.mtime).format('MMMM Do YYYY, h:mm:ss a')">
<div class="control" v-if="config.app.browser_control_enabled">
<label class="checkbox is-block">
<input type="checkbox" v-model="selectedElms" :value="item.path">
</label>
</div>
</div>
</div>
</header>
<div class="card-footer mt-auto">
<div class="card-footer-item" v-if="config.app.browser_control_enabled">
<a class="has-text-danger" @click="handleAction('delete', item)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</a>
</div>
<div class="card-footer-item has-text-centered">
<p class="is-text-overflow">
<span class="icon"><i class="fa-solid fa-calendar" /></span>
<span v-tooltip="moment(item.mtime).format('YYYY-MM-DD H:mm:ss Z')" class="has-tooltip">
{{ moment(item.mtime).fromNow() }}
</span>
</td>
<td class="is-vcentered" v-if="config.app.browser_control_enabled">
<Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="'file' === item.type">
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
:download="item.name.split('/').reverse()[0]" class="dropdown-item">
<span class="icon"><i class="fa-solid fa-download" /></span>
<span>Download</span>
</a>
<hr class="dropdown-divider" />
</template>
</p>
</div>
<div class="card-footer-item" v-if="config.app.browser_control_enabled">
<Dropdown icons="fa-solid fa-cogs" label="Actions">
<NuxtLink class="dropdown-item" @click="handleAction('rename', item)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Rename</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="handleAction('rename', item)">
<span class="icon"><i class="fa-solid fa-edit" /></span>
<span>Rename</span>
</NuxtLink>
<hr class="dropdown-divider" />
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="handleAction('delete', item)">
<span class="icon has-text-danger"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="handleAction('delete', item)">
<span class="icon has-text-danger"><i class="fa-solid fa-trash" /></span>
<span>Delete</span>
</NuxtLink>
<hr class="dropdown-divider" />
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="handleAction('move', item)">
<span class="icon"><i class="fa-solid fa-arrows-alt" /></span>
<span>Move</span>
</NuxtLink>
</Dropdown>
</td>
</tr>
</tbody>
</table>
<NuxtLink class="dropdown-item" @click="handleAction('move', item)">
<span class="icon"><i class="fa-solid fa-arrows-alt" /></span>
<span>Move</span>
</NuxtLink>
</Dropdown>
</div>
</div>
</div>
</div>
</template>
<div class="column is-12" v-if="search && filteredItems.length < 1">
<Message message_class="has-background-warning-90 has-text-dark" title="No results" icon="fas fa-filter"
:useClose="true" @close="() => search = ''" v-if="search">
<p class="is-block">
No results found for '<span class="is-underlined is-bold">{{ search }}</span>'.
</p>
</Message>
</div>
<div class="column is-12" v-else>
<div class="column is-12" v-if="!items || items.length < 1">
<Message title="Loading content" class="has-background-info-90 has-text-dark" icon="fas fa-refresh fa-spin"
v-if="isLoading">
Loading file browser contents...
@ -238,6 +386,7 @@ const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.95)
const sort_by = useStorage('sort_by', 'name')
const sort_order = useStorage('sort_order', 'asc')
const display_style = useStorage<string>('browser_display_style', 'list')
const selectedElms = ref<string[]>([])
const masterSelectAll = ref(false)
@ -252,7 +401,6 @@ const path = ref<string>((() => {
}
return '/'
})())
const table_container = ref<boolean>(true)
const search = ref<string>('')
const show_filter = ref<boolean>(false)
@ -332,7 +480,7 @@ watch(() => socket.isConnected, async () => {
})
const handleClick = (item: FileItem): void => {
if (true === ['video','audio'].includes(item.content_type)) {
if (true === ['video', 'audio'].includes(item.content_type)) {
model_item.value = {
"type": 'video',
"filename": item.path,

View file

@ -2,13 +2,15 @@
<main class="m-6">
<div>
<span class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-cog" /></span>
<p class="card-header-title">Settings</p>
<template v-if="!simpleMode">
<span class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-cog" /></span>
<p class="card-header-title">WebUI Settings</p>
</span>
</span>
</span>
<span class="field is-horizontal" />
<span class="field is-horizontal" />
</template>
<div class="field is-horizontal">
<div class="field-label is-normal">
@ -82,7 +84,6 @@
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label"></label>
@ -91,7 +92,7 @@
<div class="field">
<div class="control">
<template v-if="bg_enable">
<button @click="$emit('reload_bg')" class="button is-primary" :disabled="isLoading">
<button @click="$emit('reload_bg')" class="has-text-link" :disabled="isLoading">
<span class="icon-text">
<span class="icon"><i class="fa"
:class="{ 'fa-spin fa-spinner': isLoading, 'fa-file-image': !isLoading }" /></span>
@ -258,39 +259,6 @@
</div>
</div>
</div>
<template v-if="!simpleMode">
<span class="field is-horizontal" />
<span class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-cog" /></span>
<p class="card-header-title">Application Settings</p>
</span>
</span>
<span class="field is-horizontal" />
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label"></label>
</div>
<div class="field-body">
<div class="field is-grouped">
<div class="control" v-if="config.app.file_logging">
<NuxtLink class="button is-primary" to="/logs">
<span class="icon"><i class="fa-solid fa-file-lines" /></span>
<span>Open Logs</span>
</NuxtLink>
</div>
<div class="control" v-if="config.app.console_enabled">
<NuxtLink class="button is-primary" to="/console">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Open Console</span>
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
</div>
</main>
</template>