Merge pull request #407 from stkevintan/file-browser-initial

This commit is contained in:
jxxghp
2025-12-06 03:50:51 +08:00
committed by GitHub
5 changed files with 139 additions and 81 deletions

View File

@@ -5,6 +5,11 @@ import FileNavigator from './filebrowser/FileNavigator.vue'
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
import { storageIconDict } from '@/api/constants'
// LocalStorage keys
const SORT_KEY = 'fileBrowser.sort'
const SHOW_TREE_KEY = 'fileBrowser.showDirTree'
const NAV_WIDTH_KEY = 'fileBrowser.navigatorWidth'
// 输入参数
const props = defineProps({
storages: Array as PropType<StorageConf[]>,
@@ -119,22 +124,33 @@ const fileIcons = {
// 加载次数
const loading = ref(0)
// 当前存储
const activeStorage = ref('local')
// 刷新
const refreshPending = ref(false)
// 排序
const sort = ref('name')
// 排序 - 从localStorage恢复
const sort = ref(localStorage.getItem(SORT_KEY) || 'name')
// 是否显示目录树
const showDirTree = ref(false)
// 是否显示目录树 - 从localStorage恢复
const showDirTree = ref(localStorage.getItem(SHOW_TREE_KEY) === 'true')
// 拖动分隔条相关
const navigatorWidth = ref(280) // 初始宽度
// 拖动分隔条相关 - 从localStorage恢复宽度
const navigatorWidth = ref(parseInt(localStorage.getItem(NAV_WIDTH_KEY) || '280'))
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartWidth = ref(0)
watch(sort, (val) => {
localStorage.setItem(SORT_KEY, val)
})
watch(showDirTree, (val) => {
localStorage.setItem(SHOW_TREE_KEY, String(val))
})
watch(navigatorWidth, (val) => {
localStorage.setItem(NAV_WIDTH_KEY, String(val))
})
// 计算属性
const storagesArray = computed(() => {
return props.storages?.map(item => ({
@@ -144,15 +160,15 @@ const storagesArray = computed(() => {
}))
})
// 方法
function loadingChanged(loading: number) {
if (loading) loading++
else if (loading > 0) loading--
function loadingChanged(isLoading: number) {
if (isLoading) loading.value++
else if (loading.value > 0) loading.value--
}
// 存储切换
async function storageChanged(storage: string) {
activeStorage.value = storage
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
}
@@ -235,12 +251,12 @@ function stopDrag() {
<template>
<div class="mx-auto" :loading="loading > 0">
<div v-if="activeStorage && item">
<div v-if="item">
<FileToolbar
:sort="sort"
:item="item"
:itemstack="itemstack"
:storages="storagesArray"
:storage="activeStorage"
:endpoints="endpoints"
:axios="axios"
@storagechanged="storageChanged"
@@ -251,7 +267,7 @@ function stopDrag() {
<div class="flex">
<FileNavigator
v-if="showDirTree"
:storage="activeStorage"
:storage="item.storage"
:currentPath="item.path"
:items="fileListItems"
:endpoints="endpoints"
@@ -266,7 +282,6 @@ function stopDrag() {
</div>
<FileList
:item="item"
:storage="activeStorage"
:icons="fileIcons"
:endpoints="endpoints"
:axios="axios"

View File

@@ -26,7 +26,6 @@ const { appMode } = usePWA()
// 输入参数
const inProps = defineProps({
icons: Object,
storage: String,
endpoints: Object as PropType<EndPoints>,
axios: {
type: Function,
@@ -183,6 +182,8 @@ function changeSelectMode() {
// 调API加载文件夹内的内容
async function list_files() {
loading.value = true
const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/');
const prevURI = takeURISnapshot();
emit('loading', true)
// 参数
@@ -195,7 +196,12 @@ async function list_files() {
}
// 加载数据
items.value = (await inProps.axios.request(config)) ?? []
const data = (await inProps.axios.request(config)) ?? []
// 如果当前路径已经变化,则放弃此次加载结果
if (prevURI !== takeURISnapshot()) {
return;
}
items.value = data
emit('loading', false)
loading.value = false
@@ -446,9 +452,9 @@ watch(
},
)
// 监听item变化或者storage变化
// 监听item变化
watch(
[() => inProps.item, () => inProps.storage],
[() => inProps.item],
async () => {
// 清空列表
items.value = []
@@ -550,7 +556,7 @@ async function scrape(item: FileItem, confirm: boolean = true) {
progressDialog.value = true
progressText.value = t('file.scraping', { path: item.path })
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.storage}`, item)
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.item.storage}`, item)
// 关闭进度条
progressDialog.value = false
@@ -808,7 +814,7 @@ onMounted(() => {
v-if="transferPopper"
v-model="transferPopper"
:items="transferItems"
:target_storage="inProps.storage"
:target_storage="inProps.item.storage"
@done="transferDone"
@close="transferPopper = false"
/>

View File

@@ -42,7 +42,7 @@ const availableHeight = computed(() => {
const props = defineProps({
storage: {
type: String,
default: 'local',
required: true,
},
currentPath: {
type: String,
@@ -223,7 +223,7 @@ watch(
watch(
() => props.items,
newItems => {
if (newItems && newItems.length > 0) {
if (newItems) {
// 过滤出目录项
const dirs = newItems.filter(item => item.type === 'dir')
@@ -283,9 +283,6 @@ onMounted(async () => {
await loadRootDirectories()
})
onActivated(() => {
updateHeight()
})
</script>
<template>
@@ -309,7 +306,6 @@ onActivated(() => {
<span>{{ t('file.rootDirectory') }}</span>
</div>
</div>
<!-- 加载根目录 -->
<div v-if="loading['/']" class="tree-loading">
<VProgressCircular indeterminate size="24" color="primary" class="ma-2" />

View File

@@ -13,7 +13,6 @@ const display = useDisplay()
// 输入参数
const inProps = defineProps({
storages: Array as PropType<any[]>,
storage: String,
item: {
type: Object as PropType<FileItem>,
required: true,
@@ -27,6 +26,10 @@ const inProps = defineProps({
type: Function,
required: true,
},
sort: {
type: String,
default: 'name',
},
})
// 对外事件
@@ -38,15 +41,10 @@ const newFolderPopper = ref(false)
// 新建文件名称
const newFolderName = ref('')
// 排序方式
const sort = ref('name')
// 调整排序方式
function changeSort() {
if (sort.value === 'name') sort.value = 'time'
else sort.value = 'name'
emit('sortchanged', sort.value)
const newSort = inProps.sort === 'name' ? 'time' : 'name'
emit('sortchanged', newSort)
}
// 计算PATH面包屑
@@ -67,12 +65,12 @@ const pathSegments = computed(() => {
// 当前存储
const storageObject = computed(() => {
return inProps.storages?.find(item => item.value === inProps.storage)
return inProps.storages?.find(item => item.value === inProps.item.storage)
})
// 切换存储
function changeStorage(code: string) {
if (inProps.storage !== code) {
if (inProps.item.storage!== code) {
emit('storagechanged', code)
}
}
@@ -113,7 +111,7 @@ async function mkdir() {
// 计算排序图标
const sortIcon = computed(() => {
if (sort.value === 'time') return 'mdi-sort-clock-ascending-outline'
if (inProps.sort === 'time') return 'mdi-sort-clock-ascending-outline'
else return 'mdi-sort-alphabetical-ascending'
})
</script>

View File

@@ -32,40 +32,13 @@ const endpoints = {
// 所有存储
const storages = ref<StorageConf[]>([])
// 查询存储
async function loadStorages() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Storages')
storages.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
const storageTypes = computed(() => storages.value.map(s => s.type))
// 当前文件项
const operItem = ref<FileItem>({
storage: 'local',
type: 'dir',
name: '/',
path: '/',
fileid: 'root',
})
const operItem = ref<FileItem | undefined>(undefined)
// fileid的堆栈
const itemstack = ref<FileItem[]>([
{
storage: 'local',
type: 'dir',
name: '/',
path: '/',
fileid: 'root',
},
])
// 下载目录列表
const downloadDirectories = ref<TransferDirectoryConf[]>([])
const itemstack = ref<FileItem[]>([])
// 计算公共路径
function findCommonPath(paths: string[]): string {
@@ -101,29 +74,97 @@ function findCommonPath(paths: string[]): string {
return commonPath
}
const STORAGE_KEY = 'fileBrowserView.activeStorage'
interface BrowserInitialParams {
storage: string;
path: string;
name: string;
}
// determine which entry to select initially
function determineBrowserInitialParams(downloadDirectories: TransferDirectoryConf[]): BrowserInitialParams {
const isAvailable = (storage: string) => storageTypes.value.includes(storage);
const buckets = downloadDirectories.reduce<Map<string, string[]>>((dict, item) => {
// filter out directories whose storage is not available
if (!isAvailable(item.storage)) {
return dict
}
if (item.download_path == undefined) {
return dict
}
if (!dict.has(item.storage)) {
dict.set(item.storage, [item.download_path])
} else {
dict.get(item.storage)!.push(item.download_path)
}
return dict
}, new Map());
const cachedStorage = localStorage.getItem(STORAGE_KEY) || '';
// if no download directories are configured, fall back to cached storage or first available storage
if (buckets.size === 0) {
return {
storage: isAvailable(cachedStorage)
? cachedStorage
: (storageTypes.value[0] || 'local'),
path: '/',
name: '/',
}
}
let selectedEntry: [string, string[]];
if (cachedStorage && buckets.has(cachedStorage)) {
selectedEntry = [cachedStorage, buckets.get(cachedStorage)!];
} else {
// if no storage selected previously, use the most populous one
selectedEntry = Array.from(buckets.entries()).reduce((prev, curr) => {
return curr[1].length > prev[1].length ? curr : prev;
});
}
const path = findCommonPath(selectedEntry[1]);
return {
storage: selectedEntry[0],
path,
name: path.split('/').filter(Boolean).pop() ?? '',
}
}
// 查询下载目录
async function loadDownloadDirectories() {
try {
// fetch available storages
const storageResult: { [key: string]: any } = await api.get('system/setting/Storages')
storages.value = storageResult.data?.value ?? []
const result: { [key: string]: any } = await api.get('system/setting/Directories')
if (result.success && result.data?.value) {
downloadDirectories.value = result.data.value
const path = findCommonPath(downloadDirectories.value.map(item => item.download_path) as string[])
const name = path.split('/').filter(Boolean).pop() ?? ''
const { storage, path, name } = determineBrowserInitialParams(result.data.value);
// operItem初始化
operItem.value = {
storage: 'local',
type: 'dir',
storage,
name: name,
path: path,
}
// itemstack初始化
itemstack.value = [
{
storage: storage,
type: 'dir',
name: '/',
path: '/',
fileid: 'root',
}
];
// 将初始数据拆分到堆栈中
const paths = path.split('/').filter(Boolean)
paths.map((name, index) => {
const path = '/' + paths.slice(0, index + 1).join('/') + '/'
itemstack.value.push({
storage: 'local',
storage,
type: 'dir',
name: name,
path: path,
name,
path,
})
})
}
@@ -134,6 +175,11 @@ async function loadDownloadDirectories() {
// 目录变化
function pathChanged(item: FileItem) {
// save storage to localStorage
if (item.storage !== operItem.value?.storage) {
localStorage.setItem(STORAGE_KEY, item.storage)
}
operItem.value = item
if (item.path == '/') {
itemstack.value = [
@@ -156,16 +202,13 @@ function pathChanged(item: FileItem) {
}
// 加载初始目录
onBeforeMount(loadDownloadDirectories)
onMounted(() => {
loadStorages()
})
onMounted(loadDownloadDirectories)
</script>
<template>
<div class="file-browser-view">
<FileBrowser
v-if="operItem"
:storages="storages"
:tree="false"
:itemstack="itemstack"