added scub.mp4 and more patches

This commit is contained in:
2026-06-05 12:20:45 +01:00
parent 557c3166bb
commit 8acc692b08
6 changed files with 388 additions and 14 deletions
+1
View File
@@ -3,6 +3,7 @@ ENV CI=true
RUN pnpm runtime set node 22 -g RUN pnpm runtime set node 22 -g
RUN apt update RUN apt update
RUN apt install git -y RUN apt install git -y
RUN apt install ffmpeg -y
WORKDIR /app WORKDIR /app
COPY pnpm-lock.yaml /app COPY pnpm-lock.yaml /app
+334 -11
View File
@@ -1,9 +1,9 @@
diff --git a/src/animated-cache.js b/src/animated-cache.js diff --git a/src/animated-cache.js b/src/animated-cache.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..7826a8d85a2ad9d6ae1a780d0fdbceb2ebd8cb52 index 0000000000000000000000000000000000000000..3ab3e8466866d99c20959638523c00cd6d4ca819
--- /dev/null --- /dev/null
+++ b/src/animated-cache.js +++ b/src/animated-cache.js
@@ -0,0 +1,205 @@ @@ -0,0 +1,240 @@
+import { createHash } from 'node:crypto'; +import { createHash } from 'node:crypto';
+import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
+import path from 'node:path'; +import path from 'node:path';
@@ -54,6 +54,41 @@ index 0000000000000000000000000000000000000000..7826a8d85a2ad9d6ae1a780d0fdbceb2
+ } catch { /* non-fatal */ } + } catch { /* non-fatal */ }
+} +}
+ +
+// ─── MP4 / video cache ───────────────────────────────────────────────────────
+
+/**
+ * @typedef {{ width: number, height: number }} VideoMeta
+ * @returns {{ webm_buf: Buffer, meta: VideoMeta } | null}
+ */
+export function read_video_cache(filepath) {
+ try {
+ const dir = path.join(CACHE_DIR, 'video', file_hash(filepath));
+ const webm_path = path.join(dir, 'output.webm');
+ const meta_path = path.join(dir, 'meta.json');
+ if (!existsSync(webm_path) || !existsSync(meta_path)) return null;
+ return {
+ webm_buf: readFileSync(webm_path),
+ meta: JSON.parse(readFileSync(meta_path, 'utf8'))
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * @param {string} filepath
+ * @param {Buffer} webm_buf
+ * @param {VideoMeta} meta
+ */
+export function write_video_cache(filepath, webm_buf, meta) {
+ try {
+ const dir = path.join(CACHE_DIR, 'video', file_hash(filepath));
+ mkdirSync(dir, { recursive: true });
+ writeFileSync(path.join(dir, 'output.webm'), webm_buf);
+ writeFileSync(path.join(dir, 'meta.json'), JSON.stringify(meta));
+ } catch { /* non-fatal */ }
+}
+
+// ─── General imagetools cache ───────────────────────────────────────────────── +// ─── General imagetools cache ─────────────────────────────────────────────────
+// Each cache entry stores the emitted asset buffers plus the module code template +// Each cache entry stores the emitted asset buffers plus the module code template
+// with __VITE_ASSET__ refs replaced by stable placeholder keys ($REF[key]). +// with __VITE_ASSET__ refs replaced by stable placeholder keys ($REF[key]).
@@ -210,16 +245,17 @@ index 0000000000000000000000000000000000000000..7826a8d85a2ad9d6ae1a780d0fdbceb2
+ return plugin; + return plugin;
+} +}
diff --git a/src/index.js b/src/index.js diff --git a/src/index.js b/src/index.js
index 013b9405436dd61d02d947a49c56ae1d2cad9f57..fc314fa1a2ed9f2d4776cc0f201ef2ec35f1c091 100644 index 013b9405436dd61d02d947a49c56ae1d2cad9f57..2f9205521c0bd01646f00cd051f158360322d4de 100644
--- a/src/index.js --- a/src/index.js
+++ b/src/index.js +++ b/src/index.js
@@ -1,17 +1,94 @@ @@ -1,17 +1,131 @@
import process from 'node:process'; import process from 'node:process';
+import path from 'node:path'; +import path from 'node:path';
+import sharp from 'sharp'; +import sharp from 'sharp';
import { imagetools } from 'vite-imagetools'; import { imagetools } from 'vite-imagetools';
import { image_plugin } from './vite-plugin.js'; import { image_plugin } from './vite-plugin.js';
+import { read_animated_cache, write_animated_cache, with_img_cache } from './animated-cache.js'; +import { read_animated_cache, write_animated_cache, read_video_cache, write_video_cache, with_img_cache } from './animated-cache.js';
+import { video_dimensions, mp4_to_webm } from './video-convert.js';
/** /**
* @returns {import('vite').Plugin[]} * @returns {import('vite').Plugin[]}
@@ -258,6 +294,42 @@ index 013b9405436dd61d02d947a49c56ae1d2cad9f57..fc314fa1a2ed9f2d4776cc0f201ef2ec
+ +
+ const filepath = q >= 0 ? id.slice(0, q) : id; + const filepath = q >= 0 ? id.slice(0, q) : id;
+ const ext = path.extname(filepath).toLowerCase(); + const ext = path.extname(filepath).toLowerCase();
+
+ // ── MP4 → WebM + MP4 (for <enhanced:video>) ──────────────────────
+ if (ext === '.mp4') {
+ const stem = path.basename(filepath, ext);
+
+ if (vite_config.command === 'serve') {
+ const rel = '/' + path.relative(vite_config.root, filepath).replace(/\\/g, '/');
+ const { width, height } = await video_dimensions(filepath);
+ return (
+ `export default { sources: { webm: ${JSON.stringify(rel)} },` +
+ ` video: { src: ${JSON.stringify(rel)}, w: ${width}, h: ${height} } }`
+ );
+ }
+
+ let cached_v = read_video_cache(filepath);
+ let webm_buf, vid_w, vid_h;
+ if (cached_v) {
+ ({ webm_buf, meta: { width: vid_w, height: vid_h } } = cached_v);
+ } else {
+ const dims = await video_dimensions(filepath);
+ vid_w = dims.width;
+ vid_h = dims.height;
+ webm_buf = await mp4_to_webm(filepath);
+ write_video_cache(filepath, webm_buf, { width: vid_w, height: vid_h });
+ }
+
+ const webm_ref = this.emitFile({ type: 'asset', name: `${stem}.webm`, source: webm_buf });
+
+ return [
+ `export default {`,
+ ` sources: { webm: "__VITE_ASSET__${webm_ref}__" },`,
+ ` video: { src: "__VITE_ASSET__${webm_ref}__", w: ${vid_w}, h: ${vid_h} }`,
+ `}`
+ ].join('\n');
+ }
+
+ if (ext !== '.gif' && ext !== '.webp') return null; + if (ext !== '.gif' && ext !== '.webp') return null;
+ +
+ const sharp_meta = await sharp(filepath, { animated: true }).metadata(); + const sharp_meta = await sharp(filepath, { animated: true }).metadata();
@@ -310,24 +382,94 @@ index 013b9405436dd61d02d947a49c56ae1d2cad9f57..fc314fa1a2ed9f2d4776cc0f201ef2ec
/** /**
* @param {import('sharp').Metadata} meta * @param {import('sharp').Metadata} meta
* @returns {string} * @returns {string}
diff --git a/src/video-convert.js b/src/video-convert.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f2ce0d75e71eee14dea3efc82cb7a7851a9f7d8
--- /dev/null
+++ b/src/video-convert.js
@@ -0,0 +1,61 @@
+import process from 'node:process';
+import { execFile } from 'node:child_process';
+import { readFileSync, unlinkSync } from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+/**
+ * @param {string} cmd
+ * @param {string[]} args
+ * @param {number} [timeout]
+ * @returns {Promise<string>}
+ */
+function run(cmd, args, timeout = 120000) {
+ return new Promise((resolve, reject) => {
+ execFile(cmd, args, { timeout }, (err, stdout, stderr) => {
+ if (err) reject(new Error(`${cmd} failed: ${(stderr || '').trim().split('\n').pop()}`));
+ else resolve(stdout.trim());
+ });
+ });
+}
+
+/** @param {string} ext */
+function tmp_path(ext) {
+ return path.join(os.tmpdir(), `enhanced_img_${process.hrtime.bigint()}.${ext}`);
+}
+
+/**
+ * Returns pixel dimensions of the first video stream via ffprobe.
+ * @param {string} filepath
+ * @returns {Promise<{ width: number, height: number }>}
+ */
+export async function video_dimensions(filepath) {
+ const out = await run('ffprobe', [
+ '-v', 'error',
+ '-select_streams', 'v:0',
+ '-show_entries', 'stream=width,height',
+ '-of', 'csv=p=0',
+ filepath
+ ], 10000);
+ const [w, h] = out.split(',').map(Number);
+ return { width: w || 0, height: h || 0 };
+}
+
+/**
+ * Converts an MP4 to a VP9+Opus WebM buffer (constant-quality, audio preserved).
+ * @param {string} filepath
+ * @returns {Promise<Buffer>}
+ */
+export async function mp4_to_webm(filepath) {
+ const out = tmp_path('webm');
+ await run('ffmpeg', [
+ '-i', filepath,
+ '-c:v', 'libvpx-vp9',
+ '-b:v', '0', '-crf', '33',
+ '-c:a', 'libopus', '-b:a', '128k',
+ '-y', out
+ ]);
+ const buf = readFileSync(out);
+ try { unlinkSync(out); } catch { /* ignore */ }
+ return buf;
+}
diff --git a/src/vite-plugin.js b/src/vite-plugin.js diff --git a/src/vite-plugin.js b/src/vite-plugin.js
index c8ccb26b3f17cb69eb1e725bab1cde02c88c36cf..3a52193c249edb0ccf60e2733fb360c21cce73b7 100644 index c8ccb26b3f17cb69eb1e725bab1cde02c88c36cf..08392ceb8d2788917c1557ce81161982d97ea60e 100644
--- a/src/vite-plugin.js --- a/src/vite-plugin.js
+++ b/src/vite-plugin.js +++ b/src/vite-plugin.js
@@ -1,6 +1,7 @@ @@ -1,6 +1,8 @@
/** @import { AST } from 'svelte/compiler' */ /** @import { AST } from 'svelte/compiler' */
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
+import { read_animated_cache, write_animated_cache, read_img_cache, write_img_cache } from './animated-cache.js'; +import { read_animated_cache, write_animated_cache, read_video_cache, write_video_cache, read_img_cache, write_img_cache } from './animated-cache.js';
+import { video_dimensions, mp4_to_webm } from './video-convert.js';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import sharp from 'sharp'; import sharp from 'sharp';
import { parse } from 'svelte-parse-markup'; import { parse } from 'svelte-parse-markup';
@@ -8,6 +9,57 @@ import { walk } from 'zimmerframe'; @@ -8,6 +10,97 @@ import { walk } from 'zimmerframe';
// TODO: expose this in vite-imagetools rather than duplicating it // TODO: expose this in vite-imagetools rather than duplicating it
const OPTIMIZABLE = /^[^?]+\.(avif|heif|gif|jpeg|jpg|png|tiff|webp)(\?.*)?$/; const OPTIMIZABLE = /^[^?]+\.(avif|heif|gif|jpeg|jpg|png|tiff|webp)(\?.*)?$/;
+// Formats that may carry animation — checked before handing off to imagetools. +// Formats that may carry animation — checked before handing off to imagetools.
+const ANIMATED_EXT = /\.(gif|webp)$/i; +const ANIMATED_EXT = /\.(gif|webp)$/i;
+// Video MIME types (use video/* instead of image/* in <source> elements).
+const VIDEO_FORMATS = new Set(['webm', 'mp4']);
+ +
+/** +/**
+ * Process an animated GIF/WebP into optimised animated WebP assets. + * Process an animated GIF/WebP into optimised animated WebP assets.
@@ -376,11 +518,58 @@ index c8ccb26b3f17cb69eb1e725bab1cde02c88c36cf..3a52193c249edb0ccf60e2733fb360c2
+ sources: { webp: `__VITE_ASSET__${webp_ref}__` }, + sources: { webp: `__VITE_ASSET__${webp_ref}__` },
+ img: { src: `__VITE_ASSET__${webp_ref}__`, w: width, h: height } + img: { src: `__VITE_ASSET__${webp_ref}__`, w: width, h: height }
+ }); + });
+}
+
+/**
+ * Process an MP4 source into optimised WebM + MP4 assets.
+ * In dev mode returns the original file so the browser can play it directly.
+ * @param {string} filepath
+ * @param {import('vite').Rollup.PluginContext} plugin_context
+ * @param {import('vite').ResolvedConfig} vite_config
+ * @returns {Promise<{ sources: Record<string,string>, video: { src: string, w: number, h: number } }>}
+ */
+async function process_video(filepath, plugin_context, vite_config) {
+ const ext = path.extname(filepath);
+ const stem = path.basename(filepath, ext);
+
+ if (vite_config.command === 'serve') {
+ const rel = '/' + path.relative(vite_config.root, filepath).replace(/\\/g, '/');
+ const { width, height } = await video_dimensions(filepath);
+ return { sources: { webm: rel }, video: { src: rel, w: width, h: height } };
+ }
+
+ let cached_v = read_video_cache(filepath);
+ let webm_buf, width, height;
+ if (cached_v) {
+ ({ webm_buf, meta: { width, height } } = cached_v);
+ } else {
+ const dims = await video_dimensions(filepath);
+ width = dims.width;
+ height = dims.height;
+ webm_buf = await mp4_to_webm(filepath);
+ write_video_cache(filepath, webm_buf, { width, height });
+ }
+
+ const webm_ref = plugin_context.emitFile({ type: 'asset', name: `${stem}.webm`, source: webm_buf });
+
+ return {
+ sources: { webm: `__VITE_ASSET__${webm_ref}__` },
+ video: { src: `__VITE_ASSET__${webm_ref}__`, w: width, h: height }
+ };
+} +}
/** /**
* Creates the Svelte image plugin. * Creates the Svelte image plugin.
@@ -110,8 +162,21 @@ export function image_plugin(imagetools_plugin) { @@ -38,7 +131,7 @@ export function image_plugin(imagetools_plugin) {
transform: {
order: 'pre', // puts it before vite-plugin-svelte:compile
filter: {
- code: /<enhanced:img/ // code filter must match in addition to the id filter set in configResolved hook above
+ code: /<enhanced:(img|video)/ // code filter must match in addition to the id filter set in configResolved hook above
},
async handler(content, filename) {
@@ -110,8 +203,21 @@ export function image_plugin(imagetools_plugin) {
); );
} }
@@ -403,7 +592,81 @@ index c8ccb26b3f17cb69eb1e725bab1cde02c88c36cf..3a52193c249edb0ccf60e2733fb360c2
s.update(node.start, node.end, img_to_picture(content, node, image)); s.update(node.start, node.end, img_to_picture(content, node, image));
} else { } else {
const metadata = await sharp(resolved_id).metadata(); const metadata = await sharp(resolved_id).metadata();
@@ -194,18 +259,70 @@ export function image_plugin(imagetools_plugin) { @@ -132,20 +238,58 @@ export function image_plugin(imagetools_plugin) {
}
/**
- * @type {Array<ReturnType<typeof update_element>>}
+ * Handles a static or dynamic <enhanced:video src=...> element.
+ * @param {import('svelte/compiler').AST.RegularElement} node
+ * @param {AST.Text | AST.ExpressionTag} src_attribute
+ */
+ async function update_video_element(node, src_attribute) {
+ if (src_attribute.type === 'ExpressionTag') {
+ const start =
+ 'end' in src_attribute.expression
+ ? src_attribute.expression.end
+ : src_attribute.expression.range?.[0];
+ const end =
+ 'start' in src_attribute.expression
+ ? src_attribute.expression.start
+ : src_attribute.expression.range?.[1];
+ if (typeof start !== 'number' || typeof end !== 'number') {
+ throw new Error('ExpressionTag has no range');
+ }
+ const src_var_name = content.substring(start, end).trim();
+ s.update(node.start, node.end, dynamic_video_to_video(content, node, src_var_name));
+ return;
+ }
+
+ const original_url = src_attribute.raw.trim();
+ const resolved_id = (await plugin_context.resolve(original_url, filename))?.id;
+ if (!resolved_id) {
+ throw new Error(`Could not locate ${original_url} for <enhanced:video>`);
+ }
+ const filepath = resolved_id.includes('?')
+ ? resolved_id.slice(0, resolved_id.indexOf('?'))
+ : resolved_id;
+ const video = await process_video(filepath, plugin_context, vite_config);
+ s.update(node.start, node.end, video_to_video(content, node, video));
+ }
+
+ /**
+ * @type {Array<ReturnType<typeof update_element | typeof update_video_element>>}
*/
const pending_ast_updates = [];
walk(/** @type {import('svelte/compiler').AST.TemplateNode} */ (ast), null, {
RegularElement(node, { next }) {
if ('name' in node && node.name === 'enhanced:img') {
- // Compare node tag match
const src = get_attr_value(node, 'src');
-
if (!src || typeof src === 'boolean') return;
-
pending_ast_updates.push(update_element(node, src));
+ return;
+ }
+ if ('name' in node && node.name === 'enhanced:video') {
+ const src = get_attr_value(node, 'src');
+ if (!src || typeof src === 'boolean') return;
+ pending_ast_updates.push(update_video_element(node, src));
return;
}
@@ -172,7 +316,9 @@ export function image_plugin(imagetools_plugin) {
if (ast.css) {
const css = content.substring(ast.css.start, ast.css.end);
- const modified = css.replaceAll('enhanced\\:img', 'img');
+ const modified = css
+ .replaceAll('enhanced\\:img', 'img')
+ .replaceAll('enhanced\\:video', 'video');
if (modified !== css) {
s.update(ast.css.start, ast.css.end, modified);
}
@@ -194,18 +340,70 @@ export function image_plugin(imagetools_plugin) {
* @param {import('vite').Plugin} imagetools_plugin * @param {import('vite').Plugin} imagetools_plugin
* @returns {Promise<import('vite-imagetools').Picture>} * @returns {Promise<import('vite-imagetools').Picture>}
*/ */
@@ -478,3 +741,63 @@ index c8ccb26b3f17cb69eb1e725bab1cde02c88c36cf..3a52193c249edb0ccf60e2733fb360c2
} }
/** /**
@@ -345,6 +543,59 @@ function to_value(src) {
return src.startsWith('__VITE_ASSET__') ? `{"${src}"}` : `"${src}"`;
}
+/**
+ * Generates a static <video> element from a processed video object.
+ * Emits WebM first (better compression) then MP4 (Safari fallback).
+ * @param {string} content
+ * @param {import('svelte/compiler').AST.RegularElement} node
+ * @param {{ sources: Record<string,string>, video: { src: string, w: number, h: number } }} video
+ */
+function video_to_video(content, node, video) {
+ const skip = new Set(['src', 'width', 'height']);
+ const has = (/** @type {string} */ name) =>
+ node.attributes.some((a) => 'name' in a && /** @type {any} */ (a).name === name);
+ const pass_through = node.attributes
+ .filter((a) => 'name' in a && !skip.has(/** @type {any} */ (a).name))
+ .map((a) => content.substring(a.start, a.end))
+ .join(' ');
+ const extra = pass_through ? ' ' + pass_through : '';
+ const preload = has('preload') ? '' : ' preload="metadata"';
+ const controls = has('controls') ? '' : ' controls';
+ let res = `<video width=${video.video.w} height=${video.video.h}${preload}${controls}${extra}>`;
+ for (const [format, src] of Object.entries(video.sources)) {
+ res += `<source src=${to_value(src)} type="video/${format}" />`;
+ }
+ return res + '</video>';
+}
+
+/**
+ * Generates a dynamic {#if} block for <enhanced:video src={expr} />.
+ * @param {string} content
+ * @param {import('svelte/compiler').AST.RegularElement} node
+ * @param {string} src_var_name
+ */
+function dynamic_video_to_video(content, node, src_var_name) {
+ const skip = new Set(['src', 'width', 'height']);
+ const has = (/** @type {string} */ name) =>
+ node.attributes.some((a) => 'name' in a && /** @type {any} */ (a).name === name);
+ const pass_through = node.attributes
+ .filter((a) => 'name' in a && !skip.has(/** @type {any} */ (a).name))
+ .map((a) => content.substring(a.start, a.end))
+ .join(' ');
+ const extra = pass_through ? ' ' + pass_through : '';
+ const preload = has('preload') ? '' : ' preload="metadata"';
+ const controls = has('controls') ? '' : ' controls';
+ return `{#if typeof ${src_var_name} === 'string'}
+ <video src={${src_var_name}}${preload}${controls}${extra}></video>
+{:else}
+ <video width={${src_var_name}.video.w} height={${src_var_name}.video.h}${preload}${controls}${extra}>
+ {#each Object.entries(${src_var_name}.sources) as [format, src]}
+ <source {src} type={'video/' + format} />
+ {/each}
+ </video>
+{/if}`;
+}
+
/**
* For images like `<img src={manually_imported} />`
* @param {string} content
+3 -3
View File
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies: patchedDependencies:
'@sveltejs/enhanced-img@0.10.4': 19d07a31be7c61768a462a8948488d3a9d90c545750bfa19660dcde7e6eec259 '@sveltejs/enhanced-img@0.10.4': e313100e058c1ce98f02a1eeb196881f22fcb6af5d992973eaae763037b27509
importers: importers:
@@ -16,7 +16,7 @@ importers:
version: 3.0.10(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))) version: 3.0.10(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))
'@sveltejs/enhanced-img': '@sveltejs/enhanced-img':
specifier: ^0.10.4 specifier: ^0.10.4
version: 0.10.4(patch_hash=19d07a31be7c61768a462a8948488d3a9d90c545750bfa19660dcde7e6eec259)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(rollup@4.60.2)(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)) version: 0.10.4(patch_hash=e313100e058c1ce98f02a1eeb196881f22fcb6af5d992973eaae763037b27509)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(rollup@4.60.2)(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.57.1 specifier: ^2.57.1
version: 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)) version: 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))
@@ -1587,7 +1587,7 @@ snapshots:
dependencies: dependencies:
'@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)) '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))
'@sveltejs/enhanced-img@0.10.4(patch_hash=19d07a31be7c61768a462a8948488d3a9d90c545750bfa19660dcde7e6eec259)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(rollup@4.60.2)(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))': '@sveltejs/enhanced-img@0.10.4(patch_hash=e313100e058c1ce98f02a1eeb196881f22fcb6af5d992973eaae763037b27509)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)))(rollup@4.60.2)(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.9.1)(sass-embedded@1.100.0)(sass@1.100.0))
magic-string: 0.30.21 magic-string: 0.30.21
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
const videos = import.meta.glob("./*.mp4", {
eager: true,
import: "default",
query: { enhanced: true },
});
console.log(videos);
const scub = videos["./scub.mp4"];
</script>
<svelte:head>
<meta property="og:type" content="video.other" />
<meta property="og:video" content="https://doloro.co.uk{scub.video.src}" />
<meta property="og:video:type" content="video/mp4" />
<meta property="og:video:width" content="1920" />
<meta property="og:video:height" content="1080" />
<meta property="og:title" content="Scublings" />
<!-- Optional but helps Discord show a thumbnail -->
<meta
property="og:image"
content="https://yourdomain.com/scub-thumbnail.jpg"
/>
</svelte:head>
<div class="video-container">
<enhanced:video src={scub}> </enhanced:video>
</div>
<style lang="scss">
.video-container {
display: flex;
height: auto;
aspect-ratio: 16/9;
flex-direction: column;
align-items: center;
border-width: 3px;
border-color: var(--primary);
border-style: ridge;
video {
width: 100%;
height: 100%;
}
}
</style>
Binary file not shown.
+6
View File
@@ -24,3 +24,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.SnackProductButtons {
display: flex;
flex-direction: row;
}