Files
markbase/markbase-tauri/src/src/views/FilePreview.vue
Warren 86984295bf Fix WebDAV PUT timeout: disable versioning for user WebDAV
Root cause: save_index() serializes entire 31KB db to JSON and writes to disk for every PUT operation (synchronous blocking call).

Fix: Disabled versioning for user WebDAV by changing line 2547 from Some(versioning.clone()) to None.

Performance improvement:
- Before: 2+ minutes timeout
- After: 10-27 milliseconds
- Speedup: 12000x faster

Tested: 31B, 100KB, 1MB files all upload successfully in <30ms
2026-06-30 04:56:37 +08:00

347 lines
8.0 KiB
Vue

<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { invoke } from '@tauri-apps/api/tauri'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
file: {
type: Object,
default: null
},
userId: {
type: String,
default: 'demo'
}
})
const emit = defineEmits(['update:visible'])
const loading = ref(false)
const fileUrl = ref('')
const fileContent = ref('')
const fileMetadata = ref(null)
const fileType = computed(() => {
if (!props.file) return 'unknown'
if (props.file.node_type === 'folder') return 'folder'
const ext = props.file.name.split('.').pop()?.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) return 'image'
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(ext)) return 'video'
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) return 'audio'
if (['pdf'].includes(ext)) return 'pdf'
if (['txt', 'md', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'conf', 'log', 'csv'].includes(ext)) return 'text'
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) return 'office'
return 'file'
})
const canPreview = computed(() => {
return ['image', 'video', 'audio', 'pdf', 'text'].includes(fileType.value)
})
const loadFileContent = async () => {
if (!props.file || !canPreview.value) return
loading.value = true
try {
const filePath = await invoke('download_file', {
userId: props.userId,
fileUuid: props.file.id
})
fileUrl.value = filePath
if (fileType.value === 'text') {
const content = await invoke('read_file_content', {
filePath: filePath
})
fileContent.value = content
}
const metadata = await invoke('get_file_metadata', {
userId: props.userId,
fileUuid: props.file.id
})
fileMetadata.value = metadata
} catch (error) {
ElMessage.error(`Failed to load file: ${error}`)
} finally {
loading.value = false
}
}
const downloadFile = async () => {
if (!props.file) return
try {
const filePath = await invoke('download_file', {
userId: props.userId,
fileUuid: props.file.id
})
await invoke('open_file', { filePath })
ElMessage.success('File opened successfully')
} catch (error) {
ElMessage.error(`Failed to open file: ${error}`)
}
}
const closePreview = () => {
emit('update:visible', false)
fileUrl.value = ''
fileContent.value = ''
fileMetadata.value = null
}
watch(() => props.visible, (newVal) => {
if (newVal && props.file) {
loadFileContent()
} else {
closePreview()
}
})
</script>
<template>
<el-dialog
:model-value="visible"
@update:model-value="emit('update:visible', $event)"
:title="file?.name || 'File Preview'"
fullscreen
@close="closePreview"
:destroy-on-close="true"
>
<div class="preview-container" v-loading="loading">
<!-- ImagePreview -->
<div v-if="fileType === 'image'" class="image-preview">
<img :src="fileUrl" :alt="file?.name" class="preview-image" />
</div>
<!-- VideoPreview -->
<div v-else-if="fileType === 'video'" class="video-preview">
<video :src="fileUrl" controls class="preview-video">
Your browser does not support the video tag.
</video>
</div>
<!-- AudioPreview -->
<div v-else-if="fileType === 'audio'" class="audio-preview">
<audio :src="fileUrl" controls class="preview-audio">
Your browser does not support the audio tag.
</audio>
<div class="audio-info">
<h3>{{ file?.name }}</h3>
<p v-if="fileMetadata">
<strong>Duration:</strong> {{ fileMetadata.duration || 'Unknown' }}
</p>
</div>
</div>
<!-- PdfPreview -->
<div v-else-if="fileType === 'pdf'" class="pdf-preview">
<iframe :src="fileUrl" class="preview-pdf">
This browser does not support PDFs. Please download the PDF to view it.
</iframe>
</div>
<!-- TextPreview -->
<div v-else-if="fileType === 'text'" class="text-preview">
<pre class="preview-text">{{ fileContent }}</pre>
</div>
<!-- OfficePreview (unsupported, fallback to download) -->
<div v-else-if="fileType === 'office'" class="office-preview">
<div class="office-message">
<h3>Office Document Preview</h3>
<p>Office documents cannot be previewed in browser.</p>
<el-button type="primary" @click="downloadFile">Download & Open</el-button>
</div>
</div>
<!-- UnsupportedPreview -->
<div v-else-if="fileType === 'folder'" class="folder-preview">
<div class="folder-message">
<h3>Folder Preview</h3>
<p>{{ file?.name }} is a folder.</p>
</div>
</div>
<!-- UnknownPreview -->
<div v-else class="unknown-preview">
<div class="unknown-message">
<h3>Unsupported File Type</h3>
<p>This file type cannot be previewed.</p>
<el-button type="primary" @click="downloadFile">Download & Open</el-button>
</div>
</div>
<!-- Metadata Panel -->
<div v-if="fileMetadata" class="metadata-panel">
<h3>File Metadata</h3>
<div class="metadata-item">
<strong>Name:</strong> {{ file?.name }}
</div>
<div class="metadata-item">
<strong>Type:</strong> {{ fileType }}
</div>
<div class="metadata-item">
<strong>Size:</strong> {{ fileMetadata.size }}
</div>
<div class="metadata-item">
<strong>Modified:</strong> {{ fileMetadata.modified }}
</div>
<div v-if="fileMetadata.permissions" class="metadata-item">
<strong>Permissions:</strong> {{ fileMetadata.permissions }}
</div>
</div>
</div>
<!-- Action Buttons -->
<template #footer>
<el-button @click="closePreview">Close</el-button>
<el-button type="primary" @click="downloadFile">Download</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.preview-container {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.image-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.video-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
}
.preview-video {
max-width: 100%;
max-height: 100%;
}
.audio-preview {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
.preview-audio {
width: 80%;
max-width: 500px;
}
.audio-info {
margin-top: 20px;
text-align: center;
}
.audio-info h3 {
margin-bottom: 10px;
}
.pdf-preview {
flex: 1;
overflow: hidden;
}
.preview-pdf {
width: 100%;
height: 100%;
border: none;
}
.text-preview {
flex: 1;
overflow: auto;
padding: 20px;
}
.preview-text {
background-color: #f5f7fa;
padding: 20px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
}
.office-preview,
.folder-preview,
.unknown-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.office-message,
.folder-message,
.unknown-message {
text-align: center;
padding: 40px;
background-color: #f5f7fa;
border-radius: 8px;
}
.office-message h3,
.folder-message h3,
.unknown-message h3 {
margin-bottom: 16px;
}
.office-message p,
.folder-message p,
.unknown-message p {
margin-bottom: 24px;
color: #606266;
}
.metadata-panel {
width: 300px;
border-left: 1px solid #e0e0e0;
padding: 20px;
overflow-y: auto;
background-color: #f5f7fa;
}
.metadata-panel h3 {
margin-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 8px;
}
.metadata-item {
margin-bottom: 12px;
}
.metadata-item strong {
color: #606266;
}
</style>