mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
enhance the file browser
This commit is contained in:
@@ -5,6 +5,11 @@ import FileNavigator from './filebrowser/FileNavigator.vue'
|
|||||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||||
import { storageIconDict } from '@/api/constants'
|
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({
|
const props = defineProps({
|
||||||
storages: Array as PropType<StorageConf[]>,
|
storages: Array as PropType<StorageConf[]>,
|
||||||
@@ -119,22 +124,33 @@ const fileIcons = {
|
|||||||
|
|
||||||
// 加载次数
|
// 加载次数
|
||||||
const loading = ref(0)
|
const loading = ref(0)
|
||||||
// 当前存储
|
|
||||||
const activeStorage = ref('local')
|
|
||||||
// 刷新
|
// 刷新
|
||||||
const refreshPending = ref(false)
|
const refreshPending = ref(false)
|
||||||
// 排序
|
// 排序 - 从localStorage恢复
|
||||||
const sort = ref('name')
|
const sort = ref(localStorage.getItem(SORT_KEY) || 'name')
|
||||||
|
|
||||||
// 是否显示目录树
|
// 是否显示目录树 - 从localStorage恢复
|
||||||
const showDirTree = ref(false)
|
const showDirTree = ref(localStorage.getItem(SHOW_TREE_KEY) === 'true')
|
||||||
|
|
||||||
// 拖动分隔条相关
|
// 拖动分隔条相关 - 从localStorage恢复宽度
|
||||||
const navigatorWidth = ref(280) // 初始宽度
|
const navigatorWidth = ref(parseInt(localStorage.getItem(NAV_WIDTH_KEY) || '280'))
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStartX = ref(0)
|
const dragStartX = ref(0)
|
||||||
const dragStartWidth = 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(() => {
|
const storagesArray = computed(() => {
|
||||||
return props.storages?.map(item => ({
|
return props.storages?.map(item => ({
|
||||||
@@ -144,15 +160,15 @@ const storagesArray = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
function loadingChanged(loading: number) {
|
function loadingChanged(isLoading: number) {
|
||||||
if (loading) loading++
|
if (isLoading) loading.value++
|
||||||
else if (loading > 0) loading--
|
else if (loading.value > 0) loading.value--
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储切换
|
// 存储切换
|
||||||
async function storageChanged(storage: string) {
|
async function storageChanged(storage: string) {
|
||||||
activeStorage.value = storage
|
|
||||||
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
|
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,12 +251,11 @@ function stopDrag() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto" :loading="loading > 0">
|
<div class="mx-auto" :loading="loading > 0">
|
||||||
<div v-if="activeStorage && item">
|
<div v-if="item">
|
||||||
<FileToolbar
|
<FileToolbar
|
||||||
:item="item"
|
:item="item"
|
||||||
:itemstack="itemstack"
|
:itemstack="itemstack"
|
||||||
:storages="storagesArray"
|
:storages="storagesArray"
|
||||||
:storage="activeStorage"
|
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
:axios="axios"
|
:axios="axios"
|
||||||
@storagechanged="storageChanged"
|
@storagechanged="storageChanged"
|
||||||
@@ -251,7 +266,7 @@ function stopDrag() {
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<FileNavigator
|
<FileNavigator
|
||||||
v-if="showDirTree"
|
v-if="showDirTree"
|
||||||
:storage="activeStorage"
|
:storage="item.storage"
|
||||||
:currentPath="item.path"
|
:currentPath="item.path"
|
||||||
:items="fileListItems"
|
:items="fileListItems"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
@@ -266,7 +281,6 @@ function stopDrag() {
|
|||||||
</div>
|
</div>
|
||||||
<FileList
|
<FileList
|
||||||
:item="item"
|
:item="item"
|
||||||
:storage="activeStorage"
|
|
||||||
:icons="fileIcons"
|
:icons="fileIcons"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
:axios="axios"
|
:axios="axios"
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const { appMode } = usePWA()
|
|||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
icons: Object,
|
icons: Object,
|
||||||
storage: String,
|
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
type: Function,
|
type: Function,
|
||||||
@@ -183,6 +182,8 @@ function changeSelectMode() {
|
|||||||
// 调API加载文件夹内的内容
|
// 调API加载文件夹内的内容
|
||||||
async function list_files() {
|
async function list_files() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/');
|
||||||
|
const prevURI = takeURISnapshot();
|
||||||
emit('loading', true)
|
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)
|
emit('loading', false)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
@@ -446,9 +452,9 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听item变化或者storage变化
|
// 监听item变化
|
||||||
watch(
|
watch(
|
||||||
[() => inProps.item, () => inProps.storage],
|
[() => inProps.item],
|
||||||
async () => {
|
async () => {
|
||||||
// 清空列表
|
// 清空列表
|
||||||
items.value = []
|
items.value = []
|
||||||
@@ -550,7 +556,7 @@ async function scrape(item: FileItem, confirm: boolean = true) {
|
|||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
progressText.value = t('file.scraping', { path: item.path })
|
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
|
progressDialog.value = false
|
||||||
@@ -808,7 +814,7 @@ onMounted(() => {
|
|||||||
v-if="transferPopper"
|
v-if="transferPopper"
|
||||||
v-model="transferPopper"
|
v-model="transferPopper"
|
||||||
:items="transferItems"
|
:items="transferItems"
|
||||||
:target_storage="inProps.storage"
|
:target_storage="inProps.item.storage"
|
||||||
@done="transferDone"
|
@done="transferDone"
|
||||||
@close="transferPopper = false"
|
@close="transferPopper = false"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const availableHeight = computed(() => {
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
storage: {
|
storage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'local',
|
required: true,
|
||||||
},
|
},
|
||||||
currentPath: {
|
currentPath: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -223,7 +223,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => props.items,
|
() => props.items,
|
||||||
newItems => {
|
newItems => {
|
||||||
if (newItems && newItems.length > 0) {
|
if (newItems) {
|
||||||
// 过滤出目录项
|
// 过滤出目录项
|
||||||
const dirs = newItems.filter(item => item.type === 'dir')
|
const dirs = newItems.filter(item => item.type === 'dir')
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
updateHeight()
|
// updateHeight()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -309,7 +309,6 @@ onActivated(() => {
|
|||||||
<span>{{ t('file.rootDirectory') }}</span>
|
<span>{{ t('file.rootDirectory') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载根目录 -->
|
<!-- 加载根目录 -->
|
||||||
<div v-if="loading['/']" class="tree-loading">
|
<div v-if="loading['/']" class="tree-loading">
|
||||||
<VProgressCircular indeterminate size="24" color="primary" class="ma-2" />
|
<VProgressCircular indeterminate size="24" color="primary" class="ma-2" />
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const display = useDisplay()
|
|||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
storages: Array as PropType<any[]>,
|
storages: Array as PropType<any[]>,
|
||||||
storage: String,
|
|
||||||
item: {
|
item: {
|
||||||
type: Object as PropType<FileItem>,
|
type: Object as PropType<FileItem>,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -67,12 +66,12 @@ const pathSegments = computed(() => {
|
|||||||
|
|
||||||
// 当前存储
|
// 当前存储
|
||||||
const storageObject = 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) {
|
function changeStorage(code: string) {
|
||||||
if (inProps.storage !== code) {
|
if (inProps.item.storage!== code) {
|
||||||
emit('storagechanged', code)
|
emit('storagechanged', code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,40 +32,13 @@ const endpoints = {
|
|||||||
|
|
||||||
// 所有存储
|
// 所有存储
|
||||||
const storages = ref<StorageConf[]>([])
|
const storages = ref<StorageConf[]>([])
|
||||||
|
const storageTypes = computed(() => storages.value.map(s => s.type))
|
||||||
// 查询存储
|
|
||||||
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 operItem = ref<FileItem>({
|
const operItem = ref<FileItem | undefined>(undefined)
|
||||||
storage: 'local',
|
|
||||||
type: 'dir',
|
|
||||||
name: '/',
|
|
||||||
path: '/',
|
|
||||||
fileid: 'root',
|
|
||||||
})
|
|
||||||
|
|
||||||
// fileid的堆栈
|
// fileid的堆栈
|
||||||
const itemstack = ref<FileItem[]>([
|
const itemstack = ref<FileItem[]>([])
|
||||||
{
|
|
||||||
storage: 'local',
|
|
||||||
type: 'dir',
|
|
||||||
name: '/',
|
|
||||||
path: '/',
|
|
||||||
fileid: 'root',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 下载目录列表
|
|
||||||
const downloadDirectories = ref<TransferDirectoryConf[]>([])
|
|
||||||
|
|
||||||
// 计算公共路径
|
// 计算公共路径
|
||||||
function findCommonPath(paths: string[]): string {
|
function findCommonPath(paths: string[]): string {
|
||||||
@@ -101,29 +74,97 @@ function findCommonPath(paths: string[]): string {
|
|||||||
return commonPath
|
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() {
|
async function loadDownloadDirectories() {
|
||||||
try {
|
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')
|
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
||||||
if (result.success && result.data?.value) {
|
if (result.success && result.data?.value) {
|
||||||
downloadDirectories.value = result.data.value
|
const { storage, path, name } = determineBrowserInitialParams(result.data.value);
|
||||||
const path = findCommonPath(downloadDirectories.value.map(item => item.download_path) as string[])
|
// operItem初始化
|
||||||
const name = path.split('/').filter(Boolean).pop() ?? ''
|
|
||||||
operItem.value = {
|
operItem.value = {
|
||||||
storage: 'local',
|
|
||||||
type: 'dir',
|
type: 'dir',
|
||||||
|
storage,
|
||||||
name: name,
|
name: name,
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}
|
||||||
|
// itemstack初始化
|
||||||
|
itemstack.value = [
|
||||||
|
{
|
||||||
|
storage: storage,
|
||||||
|
type: 'dir',
|
||||||
|
name: '/',
|
||||||
|
path: '/',
|
||||||
|
fileid: 'root',
|
||||||
|
}
|
||||||
|
];
|
||||||
// 将初始数据拆分到堆栈中
|
// 将初始数据拆分到堆栈中
|
||||||
const paths = path.split('/').filter(Boolean)
|
const paths = path.split('/').filter(Boolean)
|
||||||
paths.map((name, index) => {
|
paths.map((name, index) => {
|
||||||
const path = '/' + paths.slice(0, index + 1).join('/') + '/'
|
const path = '/' + paths.slice(0, index + 1).join('/') + '/'
|
||||||
itemstack.value.push({
|
itemstack.value.push({
|
||||||
storage: 'local',
|
storage,
|
||||||
type: 'dir',
|
type: 'dir',
|
||||||
name: name,
|
name,
|
||||||
path: path,
|
path,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -134,6 +175,11 @@ async function loadDownloadDirectories() {
|
|||||||
|
|
||||||
// 目录变化
|
// 目录变化
|
||||||
function pathChanged(item: FileItem) {
|
function pathChanged(item: FileItem) {
|
||||||
|
// save storage to localStorage
|
||||||
|
if (item.storage !== operItem.value?.storage) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, item.storage)
|
||||||
|
}
|
||||||
|
|
||||||
operItem.value = item
|
operItem.value = item
|
||||||
if (item.path == '/') {
|
if (item.path == '/') {
|
||||||
itemstack.value = [
|
itemstack.value = [
|
||||||
@@ -156,16 +202,13 @@ function pathChanged(item: FileItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载初始目录
|
// 加载初始目录
|
||||||
onBeforeMount(loadDownloadDirectories)
|
onMounted(loadDownloadDirectories)
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadStorages()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="file-browser-view">
|
<div class="file-browser-view">
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
|
v-if="operItem"
|
||||||
:storages="storages"
|
:storages="storages"
|
||||||
:tree="false"
|
:tree="false"
|
||||||
:itemstack="itemstack"
|
:itemstack="itemstack"
|
||||||
|
|||||||
Reference in New Issue
Block a user