Compare commits

..

No commits in common. "567bb18e91faa515984ee01bce3cfaeacc3d5ff9" and "4e5b5a3e5b024ccfffb73e98de3b2684ffa601b7" have entirely different histories.

29 changed files with 197 additions and 359 deletions

4
.gitignore vendored
View File

@ -31,7 +31,3 @@ static/video_editor/videos/sample-video-37s.mp4
static/video_editor/videos/sample-video-10m.mp4 static/video_editor/videos/sample-video-10m.mp4
static/video_editor/videos/sample-video-10s.mp4 static/video_editor/videos/sample-video-10s.mp4
frontend-tools/video-js/public/videos/sample-video-white.mp4 frontend-tools/video-js/public/videos/sample-video-white.mp4
frontend-tools/video-editor/client/public/videos/sample-video.mp3
frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
static/chapters_editor/videos/sample-video.mp3
static/video_editor/videos/sample-video.mp3

View File

@ -1,3 +1 @@
/templates/cms/* /templates/cms/*
/templates/*.html
*.scss

View File

@ -1 +1 @@
VERSION = "6.7.1.beta-9" VERSION = "6.7.1.beta-8"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

View File

@ -1,6 +0,0 @@
// 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;

View File

@ -1,6 +1,5 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils'; import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css'; import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
@ -12,7 +11,6 @@ interface IOSVideoPlayerProps {
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(''); const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -28,25 +26,15 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
let url = '';
if (videoRef.current && videoRef.current.querySelector('source')) { if (videoRef.current && videoRef.current.querySelector('source')) {
const source = videoRef.current.querySelector('source') as HTMLSourceElement; const source = videoRef.current.querySelector('source') as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
url = source.src; setVideoUrl(source.src);
} }
} else { } else {
// Fallback to sample video if needed // Fallback to sample video if needed
url = '/videos/sample-video.mp4'; setVideoUrl('/videos/sample-video.mp4');
} }
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// 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_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -139,7 +127,6 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
x-webkit-airplay="allow" x-webkit-airplay="allow"
preload="auto" preload="auto"
crossOrigin="anonymous" crossOrigin="anonymous"
poster={posterImage}
> >
<source src={videoUrl} type="video/mp4" /> <source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

View File

@ -3947,11 +3947,11 @@ const TimelineControls = ({
<button <button
onClick={() => setShowSaveChaptersModal(true)} onClick={() => setShowSaveChaptersModal(true)}
className="save-chapters-button" className="save-chapters-button"
data-tooltip={clipSegments.length === 0 data-tooltip={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? "Clear all chapters" ? "Clear all chapters"
: "Save chapters"} : "Save chapters"}
> >
{clipSegments.length === 0 {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters' ? 'Clear Chapters'
: 'Save Chapters'} : 'Save Chapters'}
</button> </button>
@ -3974,7 +3974,7 @@ const TimelineControls = ({
className="modal-button modal-button-primary" className="modal-button modal-button-primary"
onClick={handleSaveChaptersConfirm} onClick={handleSaveChaptersConfirm}
> >
{clipSegments.length === 0 {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters' ? 'Clear Chapters'
: 'Save Chapters'} : 'Save Chapters'}
</button> </button>
@ -3982,9 +3982,14 @@ const TimelineControls = ({
} }
> >
<p className="modal-message"> <p className="modal-message">
{clipSegments.length === 0 {(() => {
? "Are you sure you want to clear all chapters? This will remove all existing chapters from the database." const chaptersWithTitles = clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length;
: `Are you sure you want to save the chapters? This will save ${clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length} chapters to the database.`} if (chaptersWithTitles === 0) {
return "Are you sure you want to clear all chapters? This will remove all existing chapters from the database.";
} else {
return `Are you sure you want to save the chapters? This will save ${chaptersWithTitles} chapters to the database.`;
}
})()}
</p> </p>
</Modal> </Modal>

View File

@ -1,6 +1,5 @@
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState } from 'react';
import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import logger from '../lib/logger'; import logger from '../lib/logger';
import '../styles/VideoPlayer.css'; import '../styles/VideoPlayer.css';
@ -39,14 +38,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4'; (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
// Check if the media is an audio file
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// 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_URL : undefined);
// Detect iOS device and Safari browser // Detect iOS device and Safari browser
useEffect(() => { useEffect(() => {
const checkIOS = () => { const checkIOS = () => {
@ -363,7 +354,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
x-webkit-airplay="allow" x-webkit-airplay="allow"
controls={false} controls={false}
muted={isMuted} muted={isMuted}
poster={posterImage}
> >
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
{/* Safari fallback for audio files */} {/* Safari fallback for audio files */}

View File

@ -147,15 +147,8 @@ const useVideoChapters = () => {
initialSegments.push(segment); initialSegments.push(segment);
} }
} else { } else {
// Create a default segment that spans the entire video on first load // Start with empty state - no default segment
const initialSegment: Segment = { initialSegments = [];
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
initialSegments = [initialSegment];
} }
// Initialize history state with the segments // Initialize history state with the segments
@ -274,24 +267,17 @@ const useVideoChapters = () => {
// Check if we now have duration and initialize if needed // Check if we now have duration and initialize if needed
if (video.duration > 0 && clipSegments.length === 0) { if (video.duration > 0 && clipSegments.length === 0) {
logger.debug('Safari: Successfully initialized metadata, creating default segment'); logger.debug('Safari: Successfully initialized metadata with empty state');
const defaultSegment: Segment = {
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
setDuration(video.duration); setDuration(video.duration);
setTrimEnd(video.duration); setTrimEnd(video.duration);
setClipSegments([defaultSegment]); setClipSegments([]);
const initialState: EditorState = { const initialState: EditorState = {
trimStart: 0, trimStart: 0,
trimEnd: video.duration, trimEnd: video.duration,
splitPoints: [], splitPoints: [],
clipSegments: [defaultSegment], clipSegments: [],
}; };
setHistory([initialState]); setHistory([initialState]);

View File

@ -6,7 +6,6 @@ if (typeof window !== 'undefined') {
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: '', videoUrl: '',
mediaId: '', mediaId: '',
posterUrl: ''
}; };
window.lastSeekedPosition = 0; window.lastSeekedPosition = 0;
} }
@ -16,7 +15,6 @@ declare global {
MEDIA_DATA: { MEDIA_DATA: {
videoUrl: string; videoUrl: string;
mediaId: string; mediaId: string;
posterUrl?: string;
}; };
seekToFunction?: (time: number) => void; seekToFunction?: (time: number) => void;
lastSeekedPosition: number; lastSeekedPosition: number;

View File

@ -1,32 +0,0 @@
/// <reference types="vite/client" />
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;
}

View File

@ -32,14 +32,8 @@ export default defineConfig({
output: { output: {
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'chapters-editor.css'; if (assetInfo.name === 'style.css') return 'chapters-editor.css';
// Keep original names for image assets return assetInfo.name;
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: { globals: {
react: 'React', react: 'React',
'react-dom': 'ReactDOM', 'react-dom': 'ReactDOM',
@ -50,7 +44,5 @@ export default defineConfig({
outDir: '../../../static/video_editor', outDir: '../../../static/video_editor',
emptyOutDir: true, emptyOutDir: true,
external: ['react', 'react-dom'], external: ['react', 'react-dom'],
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
}, },
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

View File

@ -236,46 +236,6 @@ const App = () => {
}); });
}; };
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Don't handle keyboard shortcuts if user is typing in an input field
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
switch (event.code) {
case 'Space':
event.preventDefault(); // Prevent default spacebar behavior (scrolling, button activation)
handlePlay();
break;
case 'ArrowLeft':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.max(currentTime - 10, 0);
handleMobileSafeSeek(newTime);
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
}
break;
case 'ArrowRight':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.min(currentTime + 10, duration);
handleMobileSafeSeek(newTime);
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
}
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
return ( return (
<div className="bg-background min-h-screen"> <div className="bg-background min-h-screen">
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} /> <MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />

View File

@ -1,6 +0,0 @@
// 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;

View File

@ -1,6 +1,5 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils'; import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css'; import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
@ -12,7 +11,6 @@ interface IOSVideoPlayerProps {
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(''); const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -28,25 +26,15 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
let url = '';
if (videoRef.current && videoRef.current.querySelector('source')) { if (videoRef.current && videoRef.current.querySelector('source')) {
const source = videoRef.current.querySelector('source') as HTMLSourceElement; const source = videoRef.current.querySelector('source') as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
url = source.src; setVideoUrl(source.src);
} }
} else { } else {
// Fallback to sample video if needed // Fallback to sample video if needed
url = '/videos/sample-video.mp3'; setVideoUrl('/videos/sample-video.mp4');
} }
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// 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_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward
@ -139,7 +127,6 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
x-webkit-airplay="allow" x-webkit-airplay="allow"
preload="auto" preload="auto"
crossOrigin="anonymous" crossOrigin="anonymous"
poster={posterImage}
> >
<source src={videoUrl} type="video/mp4" /> <source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

View File

@ -3,7 +3,7 @@ import { formatTime, formatDetailedTime } from '../lib/timeUtils';
import { generateThumbnail, generateSolidColor } from '../lib/videoUtils'; import { generateThumbnail, generateSolidColor } from '../lib/videoUtils';
import { Segment } from './ClipSegments'; import { Segment } from './ClipSegments';
import Modal from './Modal'; import Modal from './Modal';
import { trimVideo, autoSaveVideo } from '../services/videoApi'; import { trimVideo, autoSaveVideo, fetchAutoSavedSegments } from '../services/videoApi';
import logger from '../lib/logger'; import logger from '../lib/logger';
import '../styles/TimelineControls.css'; import '../styles/TimelineControls.css';
import '../styles/TwoRowTooltip.css'; import '../styles/TwoRowTooltip.css';
@ -1078,10 +1078,44 @@ const TimelineControls = ({
// Get savedSegments directly from window.MEDIA_DATA // Get savedSegments directly from window.MEDIA_DATA
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.savedSegments) || null; let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.savedSegments) || null;
// If no saved segments, don't load anything - the useVideoTrimmer hook already creates the initial full-length segment // If no saved segments, use default segments
if (!savedData) { if (!savedData) {
logger.debug('No saved segments found in MEDIA_DATA, skipping load (initial segment already created by useVideoTrimmer)'); logger.debug('No saved segments found in MEDIA_DATA, using default segments');
return; savedData = {
segments: [
{
startTime: '00:00:01.130',
endTime: '00:00:05.442',
name: 'segment',
},
{
startTime: '00:00:06.152',
endTime: '00:00:10.518',
name: 'segment',
},
{
startTime: '00:00:11.518',
endTime: '00:00:15.121',
name: 'segment',
},
{
startTime: '00:00:16.757',
endTime: '00:00:20.769',
name: 'segment',
},
{
startTime: '00:00:21.158',
endTime: '00:00:25.870',
name: 'segment',
},
{
startTime: '00:00:26.430',
endTime: '00:00:29.798',
name: 'segment',
},
],
updated_at: '2025-06-24 14:59:14',
};
} }
logger.debug('Loading saved segments:', savedData); logger.debug('Loading saved segments:', savedData);
@ -1098,6 +1132,7 @@ const TimelineControls = ({
endTime: parseTimeString(seg.endTime), endTime: parseTimeString(seg.endTime),
thumbnail: '', thumbnail: '',
})); }));
console.log('convertedSegments', convertedSegments);
// Dispatch event to update segments // Dispatch event to update segments
const updateEvent = new CustomEvent('update-segments', { const updateEvent = new CustomEvent('update-segments', {

View File

@ -1,6 +1,5 @@
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState } from 'react';
import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import logger from '../lib/logger'; import logger from '../lib/logger';
import '../styles/VideoPlayer.css'; import '../styles/VideoPlayer.css';
@ -37,15 +36,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [tooltipTime, setTooltipTime] = useState(0); const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp3'; (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
// Check if the media is an audio file
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
// 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_URL : undefined);
// Detect iOS device // Detect iOS device
useEffect(() => { useEffect(() => {
@ -353,7 +344,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
x-webkit-airplay="allow" x-webkit-airplay="allow"
controls={false} controls={false}
muted={isMuted} muted={isMuted}
poster={posterImage}
> >
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>

View File

@ -5,8 +5,7 @@ import "./index.css";
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: "", videoUrl: "",
mediaId: "", mediaId: ""
posterUrl: ""
}; };
window.lastSeekedPosition = 0; window.lastSeekedPosition = 0;
} }
@ -16,7 +15,6 @@ declare global {
MEDIA_DATA: { MEDIA_DATA: {
videoUrl: string; videoUrl: string;
mediaId: string; mediaId: string;
posterUrl?: string;
}; };
seekToFunction?: (time: number) => void; seekToFunction?: (time: number) => void;
lastSeekedPosition: number; lastSeekedPosition: number;

View File

@ -1,32 +0,0 @@
/// <reference types="vite/client" />
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;
}

View File

@ -32,17 +32,11 @@ export default defineConfig({
}, },
rollupOptions: { rollupOptions: {
output: { output: {
// Ensure CSS file has a predictable name and keep image assets // Ensure CSS file has a predictable name
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'video-editor.css'; if (assetInfo.name === 'style.css') return 'video-editor.css';
// Keep original names for image assets return assetInfo.name;
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 // Add this to ensure the final bundle exposes React correctly
globals: { globals: {
'react': 'React', 'react': 'React',
@ -53,8 +47,6 @@ export default defineConfig({
// Output to Django's static directory // Output to Django's static directory
outDir: '../../../static/video_editor', outDir: '../../../static/video_editor',
emptyOutDir: true, emptyOutDir: true,
external: ['react', 'react-dom'], external: ['react', 'react-dom']
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
}, },
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,12 +13,13 @@
<script> <script>
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: "{{ media_file_path }}", videoUrl: "{{ media_file_path }}",
posterUrl: "{{ media_object.poster_url }}",
mediaId: "{{ media_object.friendly_token }}", mediaId: "{{ media_object.friendly_token }}",
redirectURL: "{{ media_object.get_absolute_url }}", redirectURL: "{{ media_object.get_absolute_url }}",
redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}", redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}",
chapters: {{ chapters|safe }}, chapters: {{ chapters|safe }},
}; };
console.log(window.MEDIA_DATA.chapters)
</script> </script>
{%endblock topimports %} {%endblock topimports %}

View File

@ -13,7 +13,6 @@
<script> <script>
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: "{{ media_file_path }}", videoUrl: "{{ media_file_path }}",
posterUrl: "{{ media_object.poster_url }}",
mediaId: "{{ media_object.friendly_token }}", mediaId: "{{ media_object.friendly_token }}",
redirectURL: "{{ media_object.get_absolute_url }}", redirectURL: "{{ media_object.get_absolute_url }}",
redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}" redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}"