MOVED frontend out of Vaadin/Spring

This commit is contained in:
2025-05-28 12:17:45 +02:00
parent c11346ec3b
commit 59fb65d377
18 changed files with 194 additions and 216 deletions

View File

@@ -1,23 +1,31 @@
import { VideoMetadata } from "Frontend/utils/Endpoints";
type prop = { type prop = {
setWidth: Function; setMetadata: Function;
setHeight: Function;
setFps: Function;
setFileSize: Function;
} }
export default function ClipConfig({setWidth, setHeight, setFps, setFileSize}: prop) { export default function ClipConfig({setMetadata}: prop) {
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => { const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
var vals = e.target.value.split(","); var vals = e.target.value.split(",");
setWidth(parseInt(vals[0])) setMetadata((prevState: VideoMetadata) => ({
setHeight(parseInt(vals[1])) ...prevState,
width: parseInt(vals[0]),
height: parseInt(vals[1])
}))
} }
const updateFps = (e: React.ChangeEvent<HTMLSelectElement>) => { const updateFps = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFps(parseInt(e.target.value)) setMetadata((prevState: VideoMetadata) => ({
...prevState,
fps: parseInt(e.target.value)
}))
} }
const updateFileSize = (e: React.ChangeEvent<HTMLInputElement>) => { const updateFileSize = (e: React.ChangeEvent<HTMLInputElement>) => {
setFileSize(parseInt(e.target.value)) setMetadata((prevState: VideoMetadata) => ({
...prevState,
fileSize: parseInt(e.target.value) * 1000
}))
} }
return ( return (

View File

@@ -2,20 +2,20 @@ import RangeSlider from 'react-range-slider-input';
import 'react-range-slider-input/dist/style.css'; import 'react-range-slider-input/dist/style.css';
import {useRef} from "react"; import {useRef} from "react";
import clsx from 'clsx'; import clsx from 'clsx';
import VideoMetadata from "Frontend/generated/com/ddf/vodsystem/entities/VideoMetadata"; import { VideoMetadata } from "../utils/Endpoints"
type Props = { type Props = {
videoRef: HTMLVideoElement | null; videoRef: HTMLVideoElement | null;
videoMetadata: VideoMetadata; videoMetadata: VideoMetadata;
setSliderValue: Function; setSliderValue: Function;
setClipRangeValue: Function; setMetadata: Function;
className?: string; className?: string;
}; };
export default function ClipRangeSlider({videoRef, export default function ClipRangeSlider({videoRef,
videoMetadata, videoMetadata,
setSliderValue, setSliderValue,
setClipRangeValue, setMetadata,
className}: Props) { className}: Props) {
const previousRangeSliderInput = useRef<[number, number]>([0, 0]); const previousRangeSliderInput = useRef<[number, number]>([0, 0]);
@@ -30,7 +30,12 @@ export default function ClipRangeSlider({videoRef,
setSliderValue(val[1]); setSliderValue(val[1]);
} }
setClipRangeValue(val); setMetadata((prevState: VideoMetadata) => ({
...prevState,
startPoint: val[0],
endPoint: val[1]
}
))
previousRangeSliderInput.current = val; previousRangeSliderInput.current = val;
}; };

View File

@@ -0,0 +1,9 @@
export default function Sidebar() {
return (
<div>
<ul>
<li>Create Clip</li>
</ul>
</div>
)
}

View File

@@ -0,0 +1,14 @@
// layout/MainLayout.jsx
import Sidebar from 'Frontend/components/Sidebar';
import { Outlet } from 'react-router-dom';
const MainLayout = () => (
<div className="flex">
<Sidebar />
<div className="flex-1 p-4">
<Outlet /> {/* This renders the nested route content */}
</div>
</div>
);
export default MainLayout;

View File

@@ -1,90 +1,42 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Playbar from "./../../components/Playbar"; import Playbar from "./../components/Playbar";
import PlaybackSlider from "./../../components/PlaybackSlider"; import PlaybackSlider from "./../components/PlaybackSlider";
import ClipRangeSlider from "./../../components/ClipRangeSlider"; import ClipRangeSlider from "./../components/ClipRangeSlider";
import ClipConfig from "./../../components/ClipConfig"; import ClipConfig from "./../components/ClipConfig";
import * as editService from "../../generated/EditService"; import { VideoMetadata, editFile, getMetadata } from "../utils/Endpoints"
import * as metadataService from "../../generated/MetadataService"
import VideoMetadata from "Frontend/generated/com/ddf/vodsystem/entities/VideoMetadata";
function exportFile(uuid: string, const ClipEdit = () => {
startPoint: number,
endPoint: number,
width: number,
height: number,
fps: number,
fileSize: number,
setProgress: Function,
setDownloadable: Function) {
setDownloadable(false);
const metadata: VideoMetadata = {
startPoint: startPoint,
endPoint: endPoint,
width: width,
height: height,
fps: fps,
fileSize: fileSize*1000
}
editService.edit(uuid, metadata)
.then(r => {
editService.process(uuid);
});
// get progress updates
const interval = setInterval(async () => {
try {
const result = await editService.getProgress(uuid);
setProgress(result);
if (result >= 1) {
clearInterval(interval);
setDownloadable(true);
console.log('Progress complete');
} else {
setDownloadable(false);
}
} catch (err) {
console.error('Failed to fetch progress', err);
clearInterval(interval);
}
}, 200); // 0.5 seconds
}
export default function VideoId() {
const { id } = useParams(); const { id } = useParams();
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
const videoUrl = `api/v1/download/input/${id}` const videoUrl = `api/v1/download/input/${id}`
const [metadata, setMetadata] = useState<VideoMetadata | null>(null); const [metadata, setMetadata] = useState<VideoMetadata | null>(null);
const [playbackValue, setPlaybackValue] = useState(0); const [playbackValue, setPlaybackValue] = useState(0);
const [clipRangeValue, setClipRangeValue] = useState([0, 1]);
const [width, setWidth] = useState(1280); const [outputMetadata, setOutputMetadata] = useState<VideoMetadata>({
const [height, setHeight] = useState(720); // default values
const [fps, setFps] = useState(30); startPoint: 0,
const [fileSize, setFileSize] = useState(10); endPoint: 5,
width: 1280,
height: 720,
fps: 30,
fileSize: 10000
});
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [downloadable, setDownloadable] = useState(false); const [downloadable, setDownloadable] = useState(false);
useEffect(() => {
if (!id) return;
metadataService.getInputFileMetadata(id)
.then((data) => setMetadata(data ?? null)) // 👈 Normalize undefined to null
.catch((err) => console.error("Metadata fetch failed:", err));
}, [id]);
const sendData = () => { const sendData = () => {
if (!id) return if (!id) return;
exportFile(id,clipRangeValue[0], clipRangeValue[1], width, height, fps, fileSize, setProgress, setDownloadable); editFile(id, outputMetadata);
} }
const handleDownload = async (filename: string | undefined) => { const handleDownload = async (filename: string | undefined) => {
if (!filename) return; if (!filename) return;
const response = await fetch(`/api/v1/download/output/${id}`); const response = await fetch(`/api/v1/download/output/${id}`);
if (!response.ok) { if (!response.ok) {
console.error('Download failed'); console.error('Download failed');
return; return;
@@ -93,6 +45,7 @@ export default function VideoId() {
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = filename; a.download = filename;
document.body.appendChild(a); document.body.appendChild(a);
@@ -101,6 +54,13 @@ export default function VideoId() {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}; };
useEffect(() => {
if (!id) return;
getMetadata(id)
.then((data) => setMetadata(data ?? null))
.catch((err) => console.error("Metadata fetch failed:", err));
}, [id]);
return ( return (
<div className={"grid grid-cols-[70%_30%]"}> <div className={"grid grid-cols-[70%_30%]"}>
@@ -115,10 +75,7 @@ export default function VideoId() {
<ClipConfig <ClipConfig
setWidth={setWidth} setMetadata={setOutputMetadata}
setHeight={setHeight}
setFileSize={setFileSize}
setFps={setFps}
/> />
{metadata && {metadata &&
@@ -141,7 +98,7 @@ export default function VideoId() {
videoRef={videoRef.current} videoRef={videoRef.current}
videoMetadata={metadata} videoMetadata={metadata}
setSliderValue={setPlaybackValue} setSliderValue={setPlaybackValue}
setClipRangeValue={setClipRangeValue} setMetadata={setOutputMetadata}
className={"w-full mb-10 bg-primary"} className={"w-full mb-10 bg-primary"}
/> />
</div>} </div>}
@@ -169,3 +126,5 @@ export default function VideoId() {
</div> </div>
); );
} }
export default ClipEdit;

View File

@@ -1,23 +1,21 @@
import {useState} from "react"; import {useState} from "react";
import {UploadService} from "Frontend/generated/endpoints"; import {useNavigate} from "react-router-dom";
import { useNavigate } from 'react-router-dom'; import { uploadFile } from "../utils/Endpoints"
import "./../index.css";
export default function main() { const clipUpload = () => {
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [error, setError] = useState(false); const [noFileError, setNoFileError] = useState(false);
const press = (() => {
function press() {
if (file) { if (file) {
UploadService.upload(file) uploadFile(file)
.then(uuid => navigate(`video/${uuid}`)) .then(uuid => navigate(`video/${uuid}`))
.catch(e => console.error(e)); .catch(e => console.error(e));
} else { } else {
setError(true); setNoFileError(true);
}
} }
});
return ( return (
<div className={"flex flex-col justify-between"}> <div className={"flex flex-col justify-between"}>
@@ -30,14 +28,16 @@ export default function main() {
className={"block w-full cursor-pointer rounded-lg border border-dashed border-gray-400 bg-white p-4 text-center hover:bg-gray-50 transition"} className={"block w-full cursor-pointer rounded-lg border border-dashed border-gray-400 bg-white p-4 text-center hover:bg-gray-50 transition"}
/> />
<button <button
onClick={() => press()} onClick={press}
className={"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"} className={"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"}
>Upload</button> >Upload</button>
{error && {noFileError &&
<label className={"text-center text-red-500"}>Please choose a file</label> <label className={"text-center text-red-500"}>Please choose a file</label>
} }
</div> </div>
) )
} };
export default clipUpload;

View File

@@ -0,0 +1,96 @@
type VideoMetadata = {
startPoint: number,
endPoint: number,
fps: number,
width: number,
height: number,
fileSize: number
}
const uploadFile = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('api/v1/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
console.log("File uploaded successfully");
return await response.text();
} else {
console.log("File upload failed");
return "";
}
} catch (error) {
console.error('Error uploading file:', error);
return "";
}
};
const editFile = async (uuid: string, videoMetadata: VideoMetadata): Promise<boolean> => {
const formData = new URLSearchParams();
Object.entries(videoMetadata).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
formData.append(key, value.toString());
}
});
try {
const response = await fetch(`/api/v1/edit/${uuid}`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: formData.toString()
});
return response.ok;
} catch (error){
console.error('Error editing file:', error);
return false;
}
}
const processFile = async (uuid: string): Promise<boolean> => {
const response = await fetch(`/api/v1/process/${uuid}`);
return response.ok;
}
const getProgress = async (uuid: string): Promise<number> => {
const response = await fetch(`/api/v1/progress/${uuid}`);
if (response.ok) {
return response.json();
} else {
return 0;
}
}
const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
try {
const response = await fetch(`/api/v1/metadata/${uuid}`);
return response.json();
} catch (error) {
return {
startPoint: 0,
endPoint: 0,
fps: 0,
width: 0,
height: 0,
fileSize: 0
}
}
}
export {
VideoMetadata,
uploadFile,
editFile,
processFile,
getProgress,
getMetadata
}

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "vodSystem", "name": "VoD-System",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

59
pom.xml
View File

@@ -27,14 +27,9 @@
<url/> <url/>
</scm> </scm>
<properties> <properties>
<java.version>24</java.version> <java.version>21</java.version>
<vaadin.version>24.7.3</vaadin.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-devtools</artifactId>
@@ -48,8 +43,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
@@ -66,17 +60,6 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
@@ -108,42 +91,4 @@
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>production</id>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
<exclusions>
<exclusion>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<id>frontend</id>
<phase>compile</phase>
<goals>
<goal>prepare-frontend</goal>
<goal>build-frontend</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project> </project>

View File

@@ -1,28 +0,0 @@
@import "tailwindcss";
@theme {
--font-display: "Satoshi", "sans-serif";
/* Breakpoints */
--breakpoint-3xl: 1920px;
--color-primary: oklch(0.55 0.21 254); /* Modern Blue (#2563EB) */
--color-secondary: oklch(0.94 0.01 250); /* Light Gray (#E5E7EB) */
--color-accent: oklch(0.68 0.20 288); /* Indigo Accent (#6366F1) */
--color-text: oklch(0.17 0.01 270); /* Dark Slate (#111827) */
--color-background: oklch(0.98 0.005 250);/* Soft off-white (#F9FAFB) */
--color-primary-pressed: oklch(0.55 0.21 254 / 0.5);
/* Easing */
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
--ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}
#range-slider .range-slider__range{
background: var(--color-primary);
}
#range-slider .range-slider__thumb{
background: var(--color-primary);
}

View File

@@ -4,8 +4,6 @@ import com.ddf.vodsystem.entities.JobStatus;
import com.ddf.vodsystem.exceptions.JobNotFinished; import com.ddf.vodsystem.exceptions.JobNotFinished;
import com.ddf.vodsystem.exceptions.JobNotFound; import com.ddf.vodsystem.exceptions.JobNotFound;
import com.ddf.vodsystem.entities.Job; import com.ddf.vodsystem.entities.Job;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.Endpoint;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -14,8 +12,6 @@ import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
@Service @Service
@Endpoint
@AnonymousAllowed
public class DownloadService { public class DownloadService {
private final JobService jobService; private final JobService jobService;

View File

@@ -3,13 +3,9 @@ package com.ddf.vodsystem.services;
import com.ddf.vodsystem.entities.VideoMetadata; import com.ddf.vodsystem.entities.VideoMetadata;
import com.ddf.vodsystem.entities.Job; import com.ddf.vodsystem.entities.Job;
import com.ddf.vodsystem.entities.JobStatus; import com.ddf.vodsystem.entities.JobStatus;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.Endpoint;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@Endpoint
@AnonymousAllowed
public class EditService { public class EditService {
private final JobService jobService; private final JobService jobService;

View File

@@ -5,8 +5,6 @@ import com.ddf.vodsystem.entities.VideoMetadata;
import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.FFMPEGException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.Endpoint;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -17,8 +15,6 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@Service @Service
@Endpoint
@AnonymousAllowed
public class MetadataService { public class MetadataService {
private static Logger logger = LoggerFactory.getLogger(MetadataService.class); private static Logger logger = LoggerFactory.getLogger(MetadataService.class);

View File

@@ -2,8 +2,6 @@ package com.ddf.vodsystem.services;
import com.ddf.vodsystem.entities.Job; import com.ddf.vodsystem.entities.Job;
import com.ddf.vodsystem.entities.VideoMetadata; import com.ddf.vodsystem.entities.VideoMetadata;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.Endpoint;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -24,8 +22,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@Service @Service
@Endpoint
@AnonymousAllowed
public class UploadService { public class UploadService {
private static final Logger logger = LoggerFactory.getLogger(UploadService.class); private static final Logger logger = LoggerFactory.getLogger(UploadService.class);

View File

@@ -1,4 +1,3 @@
vaadin.launch-browser=true
spring.application.name=vodSystem spring.application.name=vodSystem
# VODs # VODs

View File

@@ -1,13 +0,0 @@
package com.ddf.vodsystem;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class VodSystemApplicationTests {
@Test
void contextLoads() {
}
}