feat(frontend): add plyr to preview videos

This commit is contained in:
2026-06-04 00:18:56 +03:00
parent f8c56623a4
commit 0ef7728ac0
6 changed files with 109 additions and 27 deletions

View File

@@ -47,5 +47,8 @@
"vite": "^7.3.1", "vite": "^7.3.1",
"vitest": "^4.1.0", "vitest": "^4.1.0",
"vitest-browser-svelte": "^2.0.2" "vitest-browser-svelte": "^2.0.2"
},
"dependencies": {
"plyr": "^3.8.4"
} }
} }

View File

@@ -7,6 +7,10 @@ settings:
importers: importers:
.: .:
dependencies:
plyr:
specifier: ^3.8.4
version: 3.8.4
devDependencies: devDependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^2.0.3 specifier: ^2.0.3
@@ -811,6 +815,9 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
core-js@3.49.0:
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -820,6 +827,9 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true hasBin: true
custom-event-polyfill@1.0.7:
resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -1127,6 +1137,9 @@ packages:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
loadjs@4.3.0:
resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
locate-character@3.0.0: locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
@@ -1207,6 +1220,9 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
plyr@3.8.4:
resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==}
pngjs@7.0.0: pngjs@7.0.0:
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
engines: {node: '>=14.19.0'} engines: {node: '>=14.19.0'}
@@ -1321,6 +1337,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
rangetouch@2.0.1:
resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
readdirp@4.1.2: readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
@@ -1442,6 +1461,9 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-polyfill@1.1.14:
resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2163,6 +2185,8 @@ snapshots:
cookie@0.6.0: {} cookie@0.6.0: {}
core-js@3.49.0: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -2171,6 +2195,8 @@ snapshots:
cssesc@3.0.0: {} cssesc@3.0.0: {}
custom-event-polyfill@1.0.7: {}
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -2467,6 +2493,8 @@ snapshots:
lilconfig@2.1.0: {} lilconfig@2.1.0: {}
loadjs@4.3.0: {}
locate-character@3.0.0: {} locate-character@3.0.0: {}
locate-path@6.0.0: locate-path@6.0.0:
@@ -2530,6 +2558,14 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
plyr@3.8.4:
dependencies:
core-js: 3.49.0
custom-event-polyfill: 1.0.7
loadjs: 4.3.0
rangetouch: 2.0.1
url-polyfill: 1.1.14
pngjs@7.0.0: {} pngjs@7.0.0: {}
postcss-load-config@3.1.4(postcss@8.5.8): postcss-load-config@3.1.4(postcss@8.5.8):
@@ -2580,6 +2616,8 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
rangetouch@2.0.1: {}
readdirp@4.1.2: {} readdirp@4.1.2: {}
rollup@4.60.1: rollup@4.60.1:
@@ -2728,6 +2766,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
url-polyfill@1.1.14: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0): vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0):

View File

@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { FileRecord } from '$lib/types'; import type { FileRecord } from '$lib/types';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import VideoPreview from './VideoPreview.svelte';
let { file, onclose }: { file: FileRecord; onclose: () => void } = $props(); let { file, onclose }: { file: FileRecord; onclose: () => void } = $props();
const isVideo = $derived(file.fileType.includes('video'));
</script> </script>
<svelte:window <svelte:window
@@ -14,18 +16,24 @@
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
onclick={onclose} onclick={onclose}
transition:fade={{ duration: 150 }} transition:fade={{ duration: 150 }}
> >
<div <div
class="flex h-2/3 w-2/3 flex-col rounded-sm border border-sky-200/20 bg-[#0f1117] p-6" class="relative aspect-video h-[85vh] max-w-[95vw] overflow-hidden rounded-xl border border-sky-200/20 bg-[#0f1117]"
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
> >
<div class="mb-4 flex items-center justify-between"> {#if isVideo}
<span class="text-sm font-medium text-white">{file.name}</span> <VideoPreview src={`/api/files/${file.id}/download`} />
<div
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between bg-linear-to-b from-black/70 to-transparent px-4 py-3"
>
<span class="pointer-events-auto truncate pr-3 text-sm font-medium text-white"
>{file.name}</span
>
<button <button
class="cursor-pointer text-white/40 transition-colors hover:text-white" class="pointer-events-auto cursor-pointer text-white/70 transition-colors hover:text-white"
onclick={onclose} onclick={onclose}
aria-label="Close" aria-label="Close"
> >
@@ -44,8 +52,10 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="flex flex-1 items-center justify-center text-sm text-white/20"> {:else}
preview coming soon <div class="flex h-full w-full items-center justify-center text-sm text-white/40">
</div> No preview available
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import Plyr from 'plyr';
import 'plyr/dist/plyr.css';
let { src }: { src: string } = $props();
let videoElement: HTMLVideoElement;
$effect(() => {
const player = new Plyr(videoElement, {
ratio: '16:9',
controls: [
'play-large',
'play',
'progress',
'current-time',
'mute',
'volume',
'captions',
'settings',
'fullscreen'
]
});
return () => player.destroy();
});
</script>
<!-- svelte-ignore a11y_media_has_caption -->
<video bind:this={videoElement} {src} poster="/video-placeholder.png" playsinline controls></video>

View File

@@ -2,5 +2,6 @@ export type FileRecord = {
id: number; id: number;
name: string; name: string;
size: number; size: number;
fileType: string;
uploadedAt: string; uploadedAt: string;
}; };

View File

@@ -1,6 +1,4 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@import '@fontsource/fira-mono'; @import '@fontsource/fira-mono';
:root { :root {
@@ -13,6 +11,8 @@
--column-margin-top: 4rem; --column-margin-top: 4rem;
font-family: var(--font-body); font-family: var(--font-body);
color: var(--color-text); color: var(--color-text);
--plyr-color-main: var(--color-sky-400);
--plyr-video-background: #0f1117;
} }
body { body {