diff --git a/frontend-tools/chapters-editor/client/src/assets/audioPosterUrl.ts b/frontend-tools/chapters-editor/client/src/assets/audioPosterUrl.ts new file mode 100644 index 00000000..1c13a96b --- /dev/null +++ b/frontend-tools/chapters-editor/client/src/assets/audioPosterUrl.ts @@ -0,0 +1,6 @@ +// Import the audio poster image as a module +// Vite will handle this and provide the correct URL +import audioPosterJpg from '../../public/audio-poster.jpg'; + +export const AUDIO_POSTER_URL = audioPosterJpg; + diff --git a/frontend-tools/chapters-editor/client/src/components/IOSVideoPlayer.tsx b/frontend-tools/chapters-editor/client/src/components/IOSVideoPlayer.tsx index 17ced1d5..e5bd3f48 100644 --- a/frontend-tools/chapters-editor/client/src/components/IOSVideoPlayer.tsx +++ b/frontend-tools/chapters-editor/client/src/components/IOSVideoPlayer.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef } from 'react'; import { formatTime } from '@/lib/timeUtils'; +import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl'; import '../styles/IOSVideoPlayer.css'; interface IOSVideoPlayerProps { @@ -45,7 +46,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; - setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined)); + setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined)); }, [videoRef]); // Function to jump 15 seconds backward diff --git a/frontend-tools/chapters-editor/client/src/components/VideoPlayer.tsx b/frontend-tools/chapters-editor/client/src/components/VideoPlayer.tsx index e176b8d5..1fc874d2 100644 --- a/frontend-tools/chapters-editor/client/src/components/VideoPlayer.tsx +++ b/frontend-tools/chapters-editor/client/src/components/VideoPlayer.tsx @@ -1,5 +1,6 @@ import React, { useRef, useEffect, useState } from 'react'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; +import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl'; import logger from '../lib/logger'; import '../styles/VideoPlayer.css'; @@ -44,7 +45,7 @@ const VideoPlayer: React.FC = ({ // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; - const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined); + const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined); // Detect iOS device and Safari browser useEffect(() => { diff --git a/frontend-tools/chapters-editor/client/src/vite-env.d.ts b/frontend-tools/chapters-editor/client/src/vite-env.d.ts new file mode 100644 index 00000000..37a2d8ff --- /dev/null +++ b/frontend-tools/chapters-editor/client/src/vite-env.d.ts @@ -0,0 +1,32 @@ +/// + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + diff --git a/frontend-tools/chapters-editor/vite.chapters-editor.config.ts b/frontend-tools/chapters-editor/vite.chapters-editor.config.ts index 0839d497..06387395 100644 --- a/frontend-tools/chapters-editor/vite.chapters-editor.config.ts +++ b/frontend-tools/chapters-editor/vite.chapters-editor.config.ts @@ -2,33 +2,12 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { copyFileSync, mkdirSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Plugin to copy audio-poster.jpg to build output -const copyAudioPoster = () => { - return { - name: 'copy-audio-poster', - closeBundle() { - const outDir = path.resolve(__dirname, '../../../static/video_editor'); - const sourceFile = path.resolve(__dirname, 'client/public/audio-poster.jpg'); - const destFile = path.resolve(outDir, 'audio-poster.jpg'); - - try { - mkdirSync(outDir, { recursive: true }); - copyFileSync(sourceFile, destFile); - console.log('✓ Copied audio-poster.jpg to build output'); - } catch (error) { - console.error('Error copying audio-poster.jpg:', error); - } - }, - }; -}; - export default defineConfig({ - plugins: [react(), copyAudioPoster()], + plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, 'client', 'src'), @@ -53,8 +32,14 @@ export default defineConfig({ output: { assetFileNames: (assetInfo) => { if (assetInfo.name === 'style.css') return 'chapters-editor.css'; - return assetInfo.name; + // Keep original names for image assets + if (assetInfo.name && /\.(png|jpe?g|svg|gif|webp)$/i.test(assetInfo.name)) { + return assetInfo.name; + } + return assetInfo.name || 'asset-[hash][extname]'; }, + // Inline small assets, emit larger ones + inlineDynamicImports: true, globals: { react: 'React', 'react-dom': 'ReactDOM', @@ -65,5 +50,7 @@ export default defineConfig({ outDir: '../../../static/video_editor', emptyOutDir: true, external: ['react', 'react-dom'], + // Inline assets smaller than 100KB, emit larger ones + assetsInlineLimit: 102400, }, }); diff --git a/frontend-tools/video-editor/client/src/assets/audioPosterUrl.ts b/frontend-tools/video-editor/client/src/assets/audioPosterUrl.ts new file mode 100644 index 00000000..1c13a96b --- /dev/null +++ b/frontend-tools/video-editor/client/src/assets/audioPosterUrl.ts @@ -0,0 +1,6 @@ +// Import the audio poster image as a module +// Vite will handle this and provide the correct URL +import audioPosterJpg from '../../public/audio-poster.jpg'; + +export const AUDIO_POSTER_URL = audioPosterJpg; + diff --git a/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx b/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx index 423f0389..6c901c24 100644 --- a/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx +++ b/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef } from 'react'; import { formatTime } from '@/lib/timeUtils'; +import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl'; import '../styles/IOSVideoPlayer.css'; interface IOSVideoPlayerProps { @@ -45,7 +46,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; - setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined)); + setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined)); }, [videoRef]); // Function to jump 15 seconds backward diff --git a/frontend-tools/video-editor/client/src/components/VideoPlayer.tsx b/frontend-tools/video-editor/client/src/components/VideoPlayer.tsx index 792858ea..d49a7387 100644 --- a/frontend-tools/video-editor/client/src/components/VideoPlayer.tsx +++ b/frontend-tools/video-editor/client/src/components/VideoPlayer.tsx @@ -1,5 +1,6 @@ import React, { useRef, useEffect, useState } from 'react'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; +import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl'; import logger from '../lib/logger'; import '../styles/VideoPlayer.css'; @@ -44,7 +45,7 @@ const VideoPlayer: React.FC = ({ // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; - const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined); + const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined); // Detect iOS device useEffect(() => { diff --git a/frontend-tools/video-editor/client/src/vite-env.d.ts b/frontend-tools/video-editor/client/src/vite-env.d.ts new file mode 100644 index 00000000..37a2d8ff --- /dev/null +++ b/frontend-tools/video-editor/client/src/vite-env.d.ts @@ -0,0 +1,32 @@ +/// + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + diff --git a/frontend-tools/video-editor/vite.video-editor.config.ts b/frontend-tools/video-editor/vite.video-editor.config.ts index d537e97a..0706ba6c 100644 --- a/frontend-tools/video-editor/vite.video-editor.config.ts +++ b/frontend-tools/video-editor/vite.video-editor.config.ts @@ -32,11 +32,17 @@ export default defineConfig({ }, rollupOptions: { output: { - // Ensure CSS file has a predictable name + // Ensure CSS file has a predictable name and keep image assets assetFileNames: (assetInfo) => { if (assetInfo.name === 'style.css') return 'video-editor.css'; - return assetInfo.name; + // Keep original names for image assets + if (assetInfo.name && /\.(png|jpe?g|svg|gif|webp)$/i.test(assetInfo.name)) { + return assetInfo.name; + } + return assetInfo.name || 'asset-[hash][extname]'; }, + // Inline small assets, emit larger ones + inlineDynamicImports: true, // Add this to ensure the final bundle exposes React correctly globals: { 'react': 'React', @@ -47,6 +53,8 @@ export default defineConfig({ // Output to Django's static directory outDir: '../../../static/video_editor', emptyOutDir: true, - external: ['react', 'react-dom'] + external: ['react', 'react-dom'], + // Inline assets smaller than 100KB, emit larger ones + assetsInlineLimit: 102400, }, }); \ No newline at end of file