Media Upload Fix
This commit is contained in:
@@ -24,7 +24,7 @@ func (rt *Router) UploadSimulationResource(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.ParseMultipartForm(500 << 20) // 500 MB max memory/file bounds
|
err = r.ParseMultipartForm(32 << 20) // 32 MB max memory bounds, rest spills to disk
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { simName, resourceName, isEditing, onDelete } = $props();
|
let { simName, resourceName, isEditing, onDelete } = $props();
|
||||||
|
|
||||||
|
let showVideo = $state(false);
|
||||||
|
|
||||||
function getResourceUrl(simName: string, resourceName: string) {
|
function getResourceUrl(simName: string, resourceName: string) {
|
||||||
return `/results/${simName}/${resourceName}`;
|
return `/results/${simName}/${resourceName}`;
|
||||||
}
|
}
|
||||||
@@ -17,7 +19,8 @@
|
|||||||
return (
|
return (
|
||||||
fname.toLowerCase().endsWith(".mp4") ||
|
fname.toLowerCase().endsWith(".mp4") ||
|
||||||
fname.toLowerCase().endsWith(".avi") ||
|
fname.toLowerCase().endsWith(".avi") ||
|
||||||
fname.toLowerCase().endsWith(".webm")
|
fname.toLowerCase().endsWith(".webm") ||
|
||||||
|
fname.toLowerCase().endsWith(".mkv")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -28,6 +31,11 @@
|
|||||||
<a href={getResourceUrl(simName, resourceName)} download class="dl-link"
|
<a href={getResourceUrl(simName, resourceName)} download class="dl-link"
|
||||||
>[Download]</a
|
>[Download]</a
|
||||||
>
|
>
|
||||||
|
{#if isVideo(resourceName)}
|
||||||
|
<button class="watch-link" onclick={() => (showVideo = !showVideo)}>
|
||||||
|
[{showVideo ? "Hide" : "Watch"}]
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<button
|
<button
|
||||||
class="delete-link"
|
class="delete-link"
|
||||||
@@ -41,17 +49,14 @@
|
|||||||
{#if isImage(resourceName)}
|
{#if isImage(resourceName)}
|
||||||
<img src={getResourceUrl(simName, resourceName)} alt={resourceName} />
|
<img src={getResourceUrl(simName, resourceName)} alt={resourceName} />
|
||||||
{:else if isVideo(resourceName)}
|
{:else if isVideo(resourceName)}
|
||||||
<video controls>
|
{#if showVideo}
|
||||||
<source
|
<video controls src={getResourceUrl(simName, resourceName)}>
|
||||||
src={getResourceUrl(simName, resourceName)}
|
<track kind="captions" />
|
||||||
type="video/webm"
|
<a href={getResourceUrl(simName, resourceName)}
|
||||||
/>
|
>Download Video</a
|
||||||
<source
|
>
|
||||||
src={getResourceUrl(simName, resourceName)}
|
|
||||||
type="video/mp4"
|
|
||||||
/>
|
|
||||||
<a href={getResourceUrl(simName, resourceName)}>Download Video</a>
|
|
||||||
</video>
|
</video>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -94,6 +99,21 @@
|
|||||||
.dl-link:visited {
|
.dl-link:visited {
|
||||||
color: #800080;
|
color: #800080;
|
||||||
}
|
}
|
||||||
|
.watch-link {
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #0000ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 14px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.watch-link:hover {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
.delete-link {
|
.delete-link {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
|||||||
@@ -157,6 +157,8 @@
|
|||||||
let newName = $state("");
|
let newName = $state("");
|
||||||
let uploadFiles = $state<FileList | null>(null);
|
let uploadFiles = $state<FileList | null>(null);
|
||||||
let saveError = $state("");
|
let saveError = $state("");
|
||||||
|
let uploadProgress = $state(0);
|
||||||
|
let isUploading = $state(false);
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!simulation) return;
|
if (!simulation) return;
|
||||||
@@ -182,32 +184,55 @@
|
|||||||
|
|
||||||
// 2. Upload Logic
|
// 2. Upload Logic
|
||||||
if (uploadFiles && uploadFiles.length > 0) {
|
if (uploadFiles && uploadFiles.length > 0) {
|
||||||
|
isUploading = true;
|
||||||
for (let i = 0; i < uploadFiles.length; i++) {
|
for (let i = 0; i < uploadFiles.length; i++) {
|
||||||
const formData = new FormData();
|
const file = uploadFiles[i];
|
||||||
formData.append("file", uploadFiles[i]);
|
uploadProgress = 0;
|
||||||
|
|
||||||
const uploadRes = await fetch(`/api/simulations/${id}/upload`, {
|
try {
|
||||||
method: "POST",
|
const upData: any = await new Promise((resolve, reject) => {
|
||||||
body: formData,
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", `/api/simulations/${id}/upload`);
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
uploadProgress = Math.round((event.loaded / event.total) * 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(JSON.parse(xhr.responseText));
|
||||||
|
} else {
|
||||||
|
reject(xhr.responseText || `Failed to upload: ${file.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(`Network error uploading: ${file.name}`);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadRes.ok) {
|
|
||||||
saveError = `Failed to upload: ${uploadFiles[i].name}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const upData = await uploadRes.json();
|
|
||||||
if (!simulation.resources.includes(upData.filename)) {
|
if (!simulation.resources.includes(upData.filename)) {
|
||||||
simulation.resources = [
|
simulation.resources = [
|
||||||
...simulation.resources,
|
...simulation.resources,
|
||||||
upData.filename,
|
upData.filename,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
saveError = err.toString();
|
||||||
|
isUploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
|
isUploading = false;
|
||||||
uploadFiles = null;
|
uploadFiles = null;
|
||||||
|
uploadProgress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
@@ -217,6 +242,8 @@
|
|||||||
isEditing = false;
|
isEditing = false;
|
||||||
saveError = "";
|
saveError = "";
|
||||||
uploadFiles = null;
|
uploadFiles = null;
|
||||||
|
uploadProgress = 0;
|
||||||
|
isUploading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
@@ -297,11 +324,15 @@
|
|||||||
{#if saveError}
|
{#if saveError}
|
||||||
<p class="error">{saveError}</p>
|
<p class="error">{saveError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if isUploading}
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" style={`width: ${uploadProgress}%`}></div>
|
||||||
|
<span class="progress-text">{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="save-btn" onclick={handleSave}>Save</button>
|
<button class="save-btn" onclick={handleSave} disabled={isUploading}>Save</button>
|
||||||
<button class="cancel-btn" onclick={cancelEdit}
|
<button class="cancel-btn" onclick={cancelEdit} disabled={isUploading}>Cancel</button>
|
||||||
>Cancel</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -538,11 +569,15 @@
|
|||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.edit-btn:hover,
|
.edit-btn:hover:not(:disabled),
|
||||||
.save-btn:hover,
|
.save-btn:hover:not(:disabled),
|
||||||
.cancel-btn:hover {
|
.cancel-btn:hover:not(:disabled) {
|
||||||
background-color: #d0d0d0;
|
background-color: #d0d0d0;
|
||||||
}
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
background-color: #ffcccc;
|
background-color: #ffcccc;
|
||||||
color: #990000;
|
color: #990000;
|
||||||
@@ -569,16 +604,39 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.form-group label {
|
.edit-form label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
min-width: 120px;
|
|
||||||
}
|
}
|
||||||
.form-group input[type="text"] {
|
.edit-form input[type="text"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 400px;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-family: inherit;
|
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #ddd;
|
||||||
|
border: 1px solid #000;
|
||||||
|
margin-top: 10px;
|
||||||
|
position: relative;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
transition: width 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
line-height: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.form-group input[type="file"] {
|
.form-group input[type="file"] {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
Reference in New Issue
Block a user