| 135 |
135 |
|
var itemId = container.dataset.itemId;
|
| 136 |
136 |
|
if (!itemId) return;
|
| 137 |
137 |
|
|
| 138 |
|
- |
var selectedFile = null;
|
|
138 |
+ |
var fileQueue = [];
|
| 139 |
139 |
|
var targetVersionId = null;
|
| 140 |
140 |
|
|
| 141 |
141 |
|
var uploader = new S3Uploader({
|
| 145 |
145 |
|
speedEl: document.getElementById('version-upload-speed'),
|
| 146 |
146 |
|
});
|
| 147 |
147 |
|
|
| 148 |
|
- |
var versionDropzone = document.getElementById('version-dropzone');
|
| 149 |
148 |
|
var versionFileInput = document.getElementById('version-file-input');
|
|
149 |
+ |
var fileRows = document.getElementById('version-file-rows');
|
| 150 |
150 |
|
|
| 151 |
|
- |
function onNewVersionFile(file) {
|
| 152 |
|
- |
selectedFile = file;
|
| 153 |
|
- |
versionDropzone.querySelector('.upload-text').textContent = file.name;
|
|
151 |
+ |
// Add files button
|
|
152 |
+ |
var addBtn = document.getElementById('add-version-file-btn');
|
|
153 |
+ |
if (addBtn) {
|
|
154 |
+ |
addBtn.addEventListener('click', function() { versionFileInput.click(); });
|
| 154 |
155 |
|
}
|
| 155 |
156 |
|
|
| 156 |
|
- |
initDropzone(versionDropzone, versionFileInput, onNewVersionFile);
|
|
157 |
+ |
if (versionFileInput) {
|
|
158 |
+ |
versionFileInput.addEventListener('change', function() {
|
|
159 |
+ |
for (var i = 0; i < this.files.length; i++) addFileRow(this.files[i]);
|
|
160 |
+ |
this.value = '';
|
|
161 |
+ |
});
|
|
162 |
+ |
}
|
| 157 |
163 |
|
|
| 158 |
|
- |
var existingDropzone = document.getElementById('existing-version-dropzone');
|
| 159 |
|
- |
var existingFileInput = document.getElementById('existing-version-file-input');
|
|
164 |
+ |
function guessLabel(name) {
|
|
165 |
+ |
var n = name.toLowerCase();
|
|
166 |
+ |
if (n.indexOf('aarch64') !== -1 || n.indexOf('arm64') !== -1) return n.indexOf('appimage') !== -1 || n.indexOf('.deb') !== -1 ? 'Linux (aarch64)' : 'macOS (arm)';
|
|
167 |
+ |
if (n.indexOf('x86_64') !== -1 || n.indexOf('amd64') !== -1 || n.indexOf('x64') !== -1) return n.indexOf('.exe') !== -1 || n.indexOf('.msi') !== -1 ? 'Windows (x64)' : 'Linux (x86_64)';
|
|
168 |
+ |
if (n.indexOf('.dmg') !== -1) return 'macOS';
|
|
169 |
+ |
if (n.indexOf('.msi') !== -1 || n.indexOf('.exe') !== -1) return 'Windows';
|
|
170 |
+ |
if (n.indexOf('.appimage') !== -1 || n.indexOf('.deb') !== -1) return 'Linux';
|
|
171 |
+ |
return '';
|
|
172 |
+ |
}
|
| 160 |
173 |
|
|
| 161 |
|
- |
initDropzone(existingDropzone, existingFileInput, function(file) {
|
| 162 |
|
- |
if (targetVersionId) uploadVersionFile(targetVersionId, file);
|
| 163 |
|
- |
});
|
|
174 |
+ |
function addFileRow(file) {
|
|
175 |
+ |
var idx = fileQueue.length;
|
|
176 |
+ |
fileQueue.push({ file: file, idx: idx });
|
|
177 |
+ |
var tr = document.createElement('tr');
|
|
178 |
+ |
tr.dataset.idx = idx;
|
|
179 |
+ |
tr.style.borderBottom = '1px solid var(--border)';
|
|
180 |
+ |
tr.innerHTML =
|
|
181 |
+ |
'<td style="padding: 0.4rem 0.5rem 0.4rem 0; font-size: 0.85rem; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="' + file.name.replace(/"/g, '"') + '">' + escapeHtml(file.name) + '</td>' +
|
|
182 |
+ |
'<td style="padding: 0.4rem 0.5rem;"><input type="text" class="version-label-input" data-idx="' + idx + '" value="' + escapeAttr(guessLabel(file.name)) + '" placeholder="e.g., macOS (arm)" style="width: 100%; padding: 0.25rem 0.4rem; font-size: 0.85rem;"></td>' +
|
|
183 |
+ |
'<td style="padding: 0.4rem 0.5rem;"><button type="button" class="secondary version-remove-file" data-idx="' + idx + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>';
|
|
184 |
+ |
fileRows.appendChild(tr);
|
|
185 |
+ |
|
|
186 |
+ |
tr.querySelector('.version-remove-file').addEventListener('click', function() {
|
|
187 |
+ |
fileQueue[idx] = null;
|
|
188 |
+ |
tr.remove();
|
|
189 |
+ |
});
|
|
190 |
+ |
}
|
| 164 |
191 |
|
|
|
192 |
+ |
function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
193 |
+ |
function escapeAttr(s) { return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); }
|
|
194 |
+ |
|
|
195 |
+ |
// Upload all button
|
| 165 |
196 |
|
document.getElementById('create-version-btn').addEventListener('click', function() {
|
| 166 |
197 |
|
var versionNumber = document.getElementById('new-version-number').value.trim();
|
| 167 |
198 |
|
var changelog = document.getElementById('version-changelog').value.trim();
|
|
199 |
+ |
var entries = fileQueue.filter(function(e) { return e !== null; });
|
| 168 |
200 |
|
|
| 169 |
201 |
|
if (!versionNumber) { showToast('Please enter a version number.'); return; }
|
| 170 |
|
- |
if (!selectedFile) { showToast('Please select a file to upload.'); return; }
|
|
202 |
+ |
if (entries.length === 0) { showToast('Please add at least one file.'); return; }
|
|
203 |
+ |
|
|
204 |
+ |
this.disabled = true;
|
|
205 |
+ |
this.textContent = 'Uploading...';
|
|
206 |
+ |
document.getElementById('new-version-form').classList.add('hidden');
|
|
207 |
+ |
document.getElementById('version-upload-progress').classList.remove('hidden');
|
|
208 |
+ |
|
|
209 |
+ |
uploadSequentially(entries, 0, versionNumber, changelog);
|
|
210 |
+ |
});
|
|
211 |
+ |
|
|
212 |
+ |
function uploadSequentially(entries, i, versionNumber, changelog) {
|
|
213 |
+ |
if (i >= entries.length) {
|
|
214 |
+ |
document.getElementById('version-upload-progress').classList.add('hidden');
|
|
215 |
+ |
document.getElementById('version-upload-success').classList.remove('hidden');
|
|
216 |
+ |
setTimeout(function() {
|
|
217 |
+ |
var filesBtn = document.getElementById('tab-files');
|
|
218 |
+ |
if (filesBtn) filesBtn.click();
|
|
219 |
+ |
}, 1500);
|
|
220 |
+ |
return;
|
|
221 |
+ |
}
|
|
222 |
+ |
|
|
223 |
+ |
var entry = entries[i];
|
|
224 |
+ |
var labelInput = document.querySelector('.version-label-input[data-idx="' + entry.idx + '"]');
|
|
225 |
+ |
var label = labelInput ? labelInput.value.trim() : '';
|
|
226 |
+ |
|
|
227 |
+ |
uploader.filenameEl.textContent = entry.file.name + (entries.length > 1 ? ' (' + (i + 1) + '/' + entries.length + ')' : '');
|
| 171 |
228 |
|
|
| 172 |
|
- |
var label = (document.getElementById('new-version-label') || {}).value || '';
|
| 173 |
229 |
|
fetch('/api/items/' + itemId + '/versions', {
|
| 174 |
230 |
|
method: 'POST',
|
| 175 |
231 |
|
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
| 176 |
232 |
|
body: JSON.stringify({
|
| 177 |
233 |
|
version_number: versionNumber,
|
| 178 |
234 |
|
changelog: changelog || null,
|
| 179 |
|
- |
label: label.trim() || null
|
|
235 |
+ |
label: label || null
|
| 180 |
236 |
|
})
|
| 181 |
237 |
|
})
|
| 182 |
238 |
|
.then(function(res) {
|
| 185 |
241 |
|
});
|
| 186 |
242 |
|
return res.json();
|
| 187 |
243 |
|
})
|
| 188 |
|
- |
.then(function(data) { uploadVersionFile(data.id, selectedFile); })
|
| 189 |
|
- |
.catch(function(err) { showVersionError(err.message || 'Failed to create version'); });
|
| 190 |
|
- |
});
|
|
244 |
+ |
.then(function(data) {
|
|
245 |
+ |
return fetch('/api/versions/' + data.id + '/upload/presign', {
|
|
246 |
+ |
method: 'POST',
|
|
247 |
+ |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
|
248 |
+ |
body: JSON.stringify({
|
|
249 |
+ |
file_name: entry.file.name,
|
|
250 |
+ |
content_type: entry.file.type || 'application/octet-stream'
|
|
251 |
+ |
})
|
|
252 |
+ |
}).then(function(res) {
|
|
253 |
+ |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
|
|
254 |
+ |
throw new Error(d.error || 'Failed to get upload URL');
|
|
255 |
+ |
});
|
|
256 |
+ |
return res.json();
|
|
257 |
+ |
}).then(function(presign) {
|
|
258 |
+ |
if (presign.max_file_bytes && entry.file.size > presign.max_file_bytes) {
|
|
259 |
+ |
var limitMB = (presign.max_file_bytes / (1024 * 1024)).toFixed(0);
|
|
260 |
+ |
var fileMB = (entry.file.size / (1024 * 1024)).toFixed(1);
|
|
261 |
+ |
throw new Error(entry.file.name + ': ' + fileMB + ' MB exceeds ' + limitMB + ' MB limit');
|
|
262 |
+ |
}
|
|
263 |
+ |
return uploader.upload(presign.upload_url, entry.file, presign.s3_key, 'application/octet-stream', presign.cache_control)
|
|
264 |
+ |
.then(function(s3Key) { return { s3Key: s3Key, versionId: data.id }; });
|
|
265 |
+ |
});
|
|
266 |
+ |
})
|
|
267 |
+ |
.then(function(result) {
|
|
268 |
+ |
return fetch('/api/versions/' + result.versionId + '/upload/confirm', {
|
|
269 |
+ |
method: 'POST',
|
|
270 |
+ |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
|
271 |
+ |
body: JSON.stringify({ s3_key: result.s3Key, file_size_bytes: entry.file.size })
|
|
272 |
+ |
});
|
|
273 |
+ |
})
|
|
274 |
+ |
.then(function(res) {
|
|
275 |
+ |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
|
|
276 |
+ |
throw new Error(d.error || 'Failed to confirm upload');
|
|
277 |
+ |
});
|
|
278 |
+ |
uploadSequentially(entries, i + 1, versionNumber, changelog);
|
|
279 |
+ |
})
|
|
280 |
+ |
.catch(function(err) { showVersionError(err.message || 'Upload failed'); });
|
|
281 |
+ |
}
|
|
282 |
+ |
|
|
283 |
+ |
// Existing version upload (single file)
|
|
284 |
+ |
var existingDropzone = document.getElementById('existing-version-dropzone');
|
|
285 |
+ |
var existingFileInput = document.getElementById('existing-version-file-input');
|
|
286 |
+ |
if (existingDropzone && existingFileInput) {
|
|
287 |
+ |
initDropzone(existingDropzone, existingFileInput, function(file) {
|
|
288 |
+ |
if (targetVersionId) uploadSingleFile(targetVersionId, file);
|
|
289 |
+ |
});
|
|
290 |
+ |
}
|
|
291 |
+ |
|
|
292 |
+ |
function uploadSingleFile(versionId, file) {
|
|
293 |
+ |
document.getElementById('new-version-form').classList.add('hidden');
|
|
294 |
+ |
document.getElementById('existing-version-upload').classList.add('hidden');
|
|
295 |
+ |
document.getElementById('version-upload-progress').classList.remove('hidden');
|
|
296 |
+ |
|
|
297 |
+ |
fetch('/api/versions/' + versionId + '/upload/presign', {
|
|
298 |
+ |
method: 'POST',
|
|
299 |
+ |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
|
300 |
+ |
body: JSON.stringify({ file_name: file.name, content_type: file.type || 'application/octet-stream' })
|
|
301 |
+ |
})
|
|
302 |
+ |
.then(function(res) {
|
|
303 |
+ |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) { throw new Error(d.error || 'Presign failed'); });
|
|
304 |
+ |
return res.json();
|
|
305 |
+ |
})
|
|
306 |
+ |
.then(function(data) {
|
|
307 |
+ |
return uploader.upload(data.upload_url, file, data.s3_key, 'application/octet-stream', data.cache_control);
|
|
308 |
+ |
})
|
|
309 |
+ |
.then(function(s3Key) {
|
|
310 |
+ |
return fetch('/api/versions/' + versionId + '/upload/confirm', {
|
|
311 |
+ |
method: 'POST',
|
|
312 |
+ |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
|
313 |
+ |
body: JSON.stringify({ s3_key: s3Key, file_size_bytes: file.size })
|
|
314 |
+ |
});
|
|
315 |
+ |
})
|
|
316 |
+ |
.then(function(res) {
|
|
317 |
+ |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) { throw new Error(d.error || 'Confirm failed'); });
|
|
318 |
+ |
document.getElementById('version-upload-progress').classList.add('hidden');
|
|
319 |
+ |
document.getElementById('version-upload-success').classList.remove('hidden');
|
|
320 |
+ |
setTimeout(function() {
|
|
321 |
+ |
var filesBtn = document.getElementById('tab-files');
|
|
322 |
+ |
if (filesBtn) filesBtn.click();
|
|
323 |
+ |
}, 1500);
|
|
324 |
+ |
})
|
|
325 |
+ |
.catch(function(err) { showVersionError(err.message || 'Upload failed'); });
|
|
326 |
+ |
}
|
| 191 |
327 |
|
|
| 192 |
328 |
|
document.querySelectorAll('.upload-to-version-btn').forEach(function(btn) {
|
| 193 |
329 |
|
btn.addEventListener('click', function() {
|
| 241 |
377 |
|
|
| 242 |
378 |
|
document.getElementById('retry-version-upload-btn').addEventListener('click', resetVersionUpload);
|
| 243 |
379 |
|
|
| 244 |
|
- |
function uploadVersionFile(versionId, file) {
|
| 245 |
|
- |
document.getElementById('new-version-form').classList.add('hidden');
|
| 246 |
|
- |
document.getElementById('existing-version-upload').classList.add('hidden');
|
| 247 |
|
- |
document.getElementById('version-upload-progress').classList.remove('hidden');
|
| 248 |
|
- |
|
| 249 |
|
- |
fetch('/api/versions/' + versionId + '/upload/presign', {
|
| 250 |
|
- |
method: 'POST',
|
| 251 |
|
- |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
| 252 |
|
- |
body: JSON.stringify({
|
| 253 |
|
- |
file_name: file.name,
|
| 254 |
|
- |
content_type: file.type || 'application/octet-stream'
|
| 255 |
|
- |
})
|
| 256 |
|
- |
})
|
| 257 |
|
- |
.then(function(res) {
|
| 258 |
|
- |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
|
| 259 |
|
- |
throw new Error(d.error || 'Failed to get upload URL');
|
| 260 |
|
- |
});
|
| 261 |
|
- |
return res.json();
|
| 262 |
|
- |
})
|
| 263 |
|
- |
.then(function(data) {
|
| 264 |
|
- |
if (data.max_file_bytes && file.size > data.max_file_bytes) {
|
| 265 |
|
- |
var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0);
|
| 266 |
|
- |
var fileMB = (file.size / (1024 * 1024)).toFixed(1);
|
| 267 |
|
- |
throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.');
|
| 268 |
|
- |
}
|
| 269 |
|
- |
return uploader.upload(data.upload_url, file, data.s3_key, 'application/octet-stream', data.cache_control);
|
| 270 |
|
- |
})
|
| 271 |
|
- |
.then(function(s3Key) {
|
| 272 |
|
- |
return fetch('/api/versions/' + versionId + '/upload/confirm', {
|
| 273 |
|
- |
method: 'POST',
|
| 274 |
|
- |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
|
| 275 |
|
- |
body: JSON.stringify({ s3_key: s3Key, file_size_bytes: file.size })
|
| 276 |
|
- |
});
|
| 277 |
|
- |
})
|
| 278 |
|
- |
.then(function(res) {
|
| 279 |
|
- |
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
|
| 280 |
|
- |
throw new Error(d.error || 'Failed to confirm upload');
|
| 281 |
|
- |
});
|
| 282 |
|
- |
document.getElementById('version-upload-progress').classList.add('hidden');
|
| 283 |
|
- |
document.getElementById('version-upload-success').classList.remove('hidden');
|
| 284 |
|
- |
setTimeout(function() {
|
| 285 |
|
- |
var filesBtn = document.getElementById('tab-files');
|
| 286 |
|
- |
if (filesBtn) filesBtn.click();
|
| 287 |
|
- |
}, 1500);
|
| 288 |
|
- |
})
|
| 289 |
|
- |
.catch(function(err) { showVersionError(err.message || 'Upload failed'); });
|
| 290 |
|
- |
}
|
| 291 |
|
- |
|
| 292 |
380 |
|
function showVersionError(message) {
|
| 293 |
381 |
|
document.getElementById('version-upload-progress').classList.add('hidden');
|
| 294 |
382 |
|
document.getElementById('version-upload-error').classList.remove('hidden');
|
| 301 |
389 |
|
document.getElementById('version-upload-progress').classList.add('hidden');
|
| 302 |
390 |
|
document.getElementById('version-upload-success').classList.add('hidden');
|
| 303 |
391 |
|
document.getElementById('version-upload-error').classList.add('hidden');
|
| 304 |
|
- |
versionFileInput.value = '';
|
| 305 |
|
- |
selectedFile = null;
|
|
392 |
+ |
fileQueue = [];
|
|
393 |
+ |
fileRows.innerHTML = '';
|
| 306 |
394 |
|
targetVersionId = null;
|
| 307 |
|
- |
versionDropzone.querySelector('.upload-text').textContent = 'Drop file here or click to upload';
|
|
395 |
+ |
var btn = document.getElementById('create-version-btn');
|
|
396 |
+ |
btn.disabled = false;
|
|
397 |
+ |
btn.textContent = 'Upload All';
|
| 308 |
398 |
|
}
|
| 309 |
399 |
|
}
|
| 310 |
400 |
|
|