diff --git a/server/routes/resources.go b/server/routes/resources.go
index 8c045ac..ffd904b 100644
--- a/server/routes/resources.go
+++ b/server/routes/resources.go
@@ -24,7 +24,7 @@ func (rt *Router) UploadSimulationResource(w http.ResponseWriter, r *http.Reques
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 {
http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)
return
diff --git a/src/lib/MediaItem.svelte b/src/lib/MediaItem.svelte
index ae78128..13f16d4 100644
--- a/src/lib/MediaItem.svelte
+++ b/src/lib/MediaItem.svelte
@@ -1,6 +1,8 @@
@@ -28,6 +31,11 @@
[Download]
+ {#if isVideo(resourceName)}
+
+ {/if}
{#if isEditing}
{:else if isVideo(resourceName)}
-
+ {#if showVideo}
+
+ {/if}
{:else}
-
@@ -94,6 +99,21 @@
.dl-link:visited {
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 {
font-weight: normal;
margin-left: 10px;
diff --git a/src/routes/simulation/[id]/+page.svelte b/src/routes/simulation/[id]/+page.svelte
index 21ffaa6..7da3a07 100644
--- a/src/routes/simulation/[id]/+page.svelte
+++ b/src/routes/simulation/[id]/+page.svelte
@@ -157,6 +157,8 @@
let newName = $state("");
let uploadFiles = $state(null);
let saveError = $state("");
+ let uploadProgress = $state(0);
+ let isUploading = $state(false);
async function handleSave() {
if (!simulation) return;
@@ -182,32 +184,55 @@
// 2. Upload Logic
if (uploadFiles && uploadFiles.length > 0) {
+ isUploading = true;
for (let i = 0; i < uploadFiles.length; i++) {
- const formData = new FormData();
- formData.append("file", uploadFiles[i]);
+ const file = uploadFiles[i];
+ uploadProgress = 0;
- const uploadRes = await fetch(`/api/simulations/${id}/upload`, {
- method: "POST",
- body: formData,
- });
+ try {
+ const upData: any = await new Promise((resolve, reject) => {
+ 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}`;
+ if (!simulation.resources.includes(upData.filename)) {
+ simulation.resources = [
+ ...simulation.resources,
+ upData.filename,
+ ];
+ }
+ } catch (err: any) {
+ saveError = err.toString();
+ isUploading = false;
return;
}
-
- const upData = await uploadRes.json();
- if (!simulation.resources.includes(upData.filename)) {
- simulation.resources = [
- ...simulation.resources,
- upData.filename,
- ];
- }
}
}
isEditing = false;
+ isUploading = false;
uploadFiles = null;
+ uploadProgress = 0;
}
function cancelEdit() {
@@ -217,6 +242,8 @@
isEditing = false;
saveError = "";
uploadFiles = null;
+ uploadProgress = 0;
+ isUploading = false;
}
async function handleDelete() {
@@ -297,11 +324,15 @@
{#if saveError}
{saveError}
{/if}
+ {#if isUploading}
+
+ {/if}
-
-
+
+
{/if}
@@ -538,11 +569,15 @@
background-color: #e0e0e0;
font-family: inherit;
}
- .edit-btn:hover,
- .save-btn:hover,
- .cancel-btn:hover {
+ .edit-btn:hover:not(:disabled),
+ .save-btn:hover:not(:disabled),
+ .cancel-btn:hover:not(:disabled) {
background-color: #d0d0d0;
}
+ button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
.delete-btn {
background-color: #ffcccc;
color: #990000;
@@ -569,16 +604,39 @@
align-items: center;
gap: 10px;
}
- .form-group label {
+ .edit-form label {
+ display: inline-block;
+ width: 100px;
font-weight: bold;
- min-width: 120px;
}
- .form-group input[type="text"] {
+ .edit-form input[type="text"] {
flex: 1;
- max-width: 400px;
padding: 5px;
- font-family: inherit;
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"] {
font-family: inherit;