feat: add Shiki syntax highlighting, video, and audio previews in FileNav
- Add Shiki-powered syntax highlighting for code files with dual light/dark themes (github-light/github-dark), line numbers, and source/preview toggle - Add native <video> player for mp4, webm, mov, ogv, avi, mkv - Add native <audio> player for mp3, wav, ogg, flac, m4a, aac, opus - New utility: src/lib/utils/codeHighlight.ts with extension-to-lang mapping using Shiki's bundled language registry
This commit is contained in:
@@ -91,6 +91,8 @@
|
|||||||
let selectedFile: string | null = null;
|
let selectedFile: string | null = null;
|
||||||
let fileContent: string | null = null;
|
let fileContent: string | null = null;
|
||||||
let fileImageUrl: string | null = null;
|
let fileImageUrl: string | null = null;
|
||||||
|
let fileVideoUrl: string | null = null;
|
||||||
|
let fileAudioUrl: string | null = null;
|
||||||
let filePdfData: ArrayBuffer | null = null;
|
let filePdfData: ArrayBuffer | null = null;
|
||||||
let fileLoading = false;
|
let fileLoading = false;
|
||||||
let filePreviewRef: FilePreview;
|
let filePreviewRef: FilePreview;
|
||||||
@@ -181,7 +183,11 @@
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
||||||
|
const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'ogv', 'avi', 'mkv']);
|
||||||
|
const AUDIO_EXTS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac', 'wma', 'opus']);
|
||||||
const isImage = (path: string) => IMAGE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
const isImage = (path: string) => IMAGE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||||
|
const isVideo = (path: string) => VIDEO_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||||
|
const isAudio = (path: string) => AUDIO_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||||
const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf';
|
const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf';
|
||||||
const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||||
|
|
||||||
@@ -206,6 +212,14 @@
|
|||||||
URL.revokeObjectURL(fileImageUrl);
|
URL.revokeObjectURL(fileImageUrl);
|
||||||
fileImageUrl = null;
|
fileImageUrl = null;
|
||||||
}
|
}
|
||||||
|
if (fileVideoUrl) {
|
||||||
|
URL.revokeObjectURL(fileVideoUrl);
|
||||||
|
fileVideoUrl = null;
|
||||||
|
}
|
||||||
|
if (fileAudioUrl) {
|
||||||
|
URL.revokeObjectURL(fileAudioUrl);
|
||||||
|
fileAudioUrl = null;
|
||||||
|
}
|
||||||
filePdfData = null;
|
filePdfData = null;
|
||||||
fileOfficeHtml = null;
|
fileOfficeHtml = null;
|
||||||
fileOfficeSlides = null;
|
fileOfficeSlides = null;
|
||||||
@@ -262,6 +276,12 @@
|
|||||||
if (isImage(filePath)) {
|
if (isImage(filePath)) {
|
||||||
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||||
if (result) fileImageUrl = URL.createObjectURL(result.blob);
|
if (result) fileImageUrl = URL.createObjectURL(result.blob);
|
||||||
|
} else if (isVideo(filePath)) {
|
||||||
|
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||||
|
if (result) fileVideoUrl = URL.createObjectURL(result.blob);
|
||||||
|
} else if (isAudio(filePath)) {
|
||||||
|
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||||
|
if (result) fileAudioUrl = URL.createObjectURL(result.blob);
|
||||||
} else if (isPdf(filePath)) {
|
} else if (isPdf(filePath)) {
|
||||||
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||||
if (result) filePdfData = await result.blob.arrayBuffer();
|
if (result) filePdfData = await result.blob.arrayBuffer();
|
||||||
@@ -524,6 +544,8 @@
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
||||||
|
if (fileVideoUrl) URL.revokeObjectURL(fileVideoUrl);
|
||||||
|
if (fileAudioUrl) URL.revokeObjectURL(fileAudioUrl);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -755,6 +777,8 @@
|
|||||||
{selectedFile}
|
{selectedFile}
|
||||||
{fileLoading}
|
{fileLoading}
|
||||||
{fileImageUrl}
|
{fileImageUrl}
|
||||||
|
{fileVideoUrl}
|
||||||
|
{fileAudioUrl}
|
||||||
{filePdfData}
|
{filePdfData}
|
||||||
{fileContent}
|
{fileContent}
|
||||||
{fileOfficeHtml}
|
{fileOfficeHtml}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
export let selectedFile: string | null = null;
|
export let selectedFile: string | null = null;
|
||||||
export let fileLoading = false;
|
export let fileLoading = false;
|
||||||
export let fileImageUrl: string | null = null;
|
export let fileImageUrl: string | null = null;
|
||||||
|
export let fileVideoUrl: string | null = null;
|
||||||
|
export let fileAudioUrl: string | null = null;
|
||||||
export let filePdfData: ArrayBuffer | null = null;
|
export let filePdfData: ArrayBuffer | null = null;
|
||||||
export let fileContent: string | null = null;
|
export let fileContent: string | null = null;
|
||||||
|
|
||||||
@@ -197,6 +199,27 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if fileVideoUrl !== null}
|
||||||
|
<div class="w-full h-full flex items-center justify-center bg-black">
|
||||||
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
|
<video
|
||||||
|
src={fileVideoUrl}
|
||||||
|
controls
|
||||||
|
class="max-w-full max-h-full"
|
||||||
|
>
|
||||||
|
{$i18n.t('Your browser does not support the video tag.')}
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
{:else if fileAudioUrl !== null}
|
||||||
|
<div class="w-full h-full flex items-center justify-center p-6">
|
||||||
|
<audio
|
||||||
|
src={fileAudioUrl}
|
||||||
|
controls
|
||||||
|
class="w-full max-w-md"
|
||||||
|
>
|
||||||
|
{$i18n.t('Your browser does not support the audio tag.')}
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
{:else if filePdfData !== null}
|
{:else if filePdfData !== null}
|
||||||
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
|
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
|
||||||
{:else if fileOfficeHtml !== null}
|
{:else if fileOfficeHtml !== null}
|
||||||
|
|||||||
Reference in New Issue
Block a user