fixing permissions
This commit is contained in:
parent
3f6ee6c687
commit
a26b52c2fa
|
|
@ -193,6 +193,7 @@
|
||||||
const oldWs = M.ws;
|
const oldWs = M.ws;
|
||||||
M.ws = null;
|
M.ws = null;
|
||||||
oldWs.onclose = null;
|
oldWs.onclose = null;
|
||||||
|
oldWs.onerror = null;
|
||||||
oldWs.close();
|
oldWs.close();
|
||||||
}
|
}
|
||||||
M.currentChannelId = id;
|
M.currentChannelId = id;
|
||||||
|
|
@ -200,6 +201,9 @@
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
M.ws = new WebSocket(proto + "//" + location.host + "/api/channels/" + id + "/ws");
|
M.ws = new WebSocket(proto + "//" + location.host + "/api/channels/" + id + "/ws");
|
||||||
|
|
||||||
|
// Track if we've ever connected successfully
|
||||||
|
let wasConnected = false;
|
||||||
|
|
||||||
M.ws.onmessage = (e) => {
|
M.ws.onmessage = (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
// Handle channel list updates
|
// Handle channel list updates
|
||||||
|
|
@ -225,6 +229,7 @@
|
||||||
const oldWs = M.ws;
|
const oldWs = M.ws;
|
||||||
M.ws = null;
|
M.ws = null;
|
||||||
oldWs.onclose = null;
|
oldWs.onclose = null;
|
||||||
|
oldWs.onerror = null;
|
||||||
oldWs.close();
|
oldWs.close();
|
||||||
}
|
}
|
||||||
M.updateUI();
|
M.updateUI();
|
||||||
|
|
@ -279,18 +284,25 @@
|
||||||
M.handleUpdate(data);
|
M.handleUpdate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
M.ws.onerror = () => {
|
||||||
|
console.log("[WS] Connection error");
|
||||||
|
};
|
||||||
|
|
||||||
M.ws.onclose = () => {
|
M.ws.onclose = () => {
|
||||||
M.synced = false;
|
M.synced = false;
|
||||||
M.ws = null;
|
M.ws = null;
|
||||||
M.$("#sync-indicator").classList.add("disconnected");
|
M.$("#sync-indicator").classList.add("disconnected");
|
||||||
M.updateUI();
|
M.updateUI();
|
||||||
// Auto-reconnect if user wants to be synced
|
// Auto-reconnect if user wants to be synced
|
||||||
|
// Use faster retry (2s) if never connected, slower (3s) if disconnected after connecting
|
||||||
if (M.wantSync) {
|
if (M.wantSync) {
|
||||||
setTimeout(() => M.connectChannel(id), 3000);
|
const delay = wasConnected ? 3000 : 2000;
|
||||||
|
setTimeout(() => M.connectChannel(id), delay);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
M.ws.onopen = () => {
|
M.ws.onopen = () => {
|
||||||
|
wasConnected = true;
|
||||||
M.synced = true;
|
M.synced = true;
|
||||||
M.$("#sync-indicator").classList.remove("disconnected");
|
M.$("#sync-indicator").classList.remove("disconnected");
|
||||||
M.updateUI();
|
M.updateUI();
|
||||||
|
|
|
||||||
|
|
@ -56,22 +56,35 @@
|
||||||
<div id="channels-list"></div>
|
<div id="channels-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="library-panel">
|
<div id="library-panel">
|
||||||
<div id="scan-progress" class="scan-progress hidden"></div>
|
<div class="panel-tabs">
|
||||||
|
<button class="panel-tab active" data-tab="library">Library</button>
|
||||||
|
<button class="panel-tab" data-tab="tasks">Tasks</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-views">
|
||||||
|
<div id="library-view" class="panel-view active">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Library</h3>
|
|
||||||
<input type="text" id="library-search" placeholder="Search..." class="search-input">
|
<input type="text" id="library-search" placeholder="Search..." class="search-input">
|
||||||
<input type="file" id="file-input" multiple accept=".mp3,.ogg,.flac,.wav,.m4a,.aac,.opus,.wma,.mp4" style="display:none">
|
<input type="file" id="file-input" multiple accept=".mp3,.ogg,.flac,.wav,.m4a,.aac,.opus,.wma,.mp4" style="display:none">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="scan-progress" class="scan-progress hidden"></div>
|
||||||
<div id="library"></div>
|
<div id="library"></div>
|
||||||
<div id="upload-progress" class="upload-progress hidden">
|
<div id="add-panel" class="add-panel hidden">
|
||||||
<div class="upload-progress-bar"></div>
|
<button id="btn-add-close" class="add-panel-close">Close</button>
|
||||||
<span class="upload-progress-text"></span>
|
<div class="add-panel-content">
|
||||||
|
<button id="btn-upload-files" class="add-option">Upload files...</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-upload" class="upload-btn">Upload files...</button>
|
</div>
|
||||||
|
<button id="btn-add" class="add-btn">Add to library...</button>
|
||||||
<div id="upload-dropzone" class="upload-dropzone hidden">
|
<div id="upload-dropzone" class="upload-dropzone hidden">
|
||||||
<div class="dropzone-content">Drop audio files here</div>
|
<div class="dropzone-content">Drop audio files here</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tasks-view" class="panel-view">
|
||||||
|
<div id="tasks-list"></div>
|
||||||
|
<div id="tasks-empty" class="tasks-empty">No active tasks</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="queue-panel">
|
<div id="queue-panel">
|
||||||
<h3 id="queue-title">Queue</h3>
|
<h3 id="queue-title">Queue</h3>
|
||||||
<div id="now-playing-bar" class="now-playing-bar hidden" title="Click to scroll to current track"></div>
|
<div id="now-playing-bar" class="now-playing-bar hidden" title="Click to scroll to current track"></div>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,27 @@
|
||||||
console.log(`TrackStorage: ${M.cachedTracks.size} tracks cached`);
|
console.log(`TrackStorage: ${M.cachedTracks.size} tracks cached`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup panel tab switching
|
||||||
|
function initPanelTabs() {
|
||||||
|
const tabs = document.querySelectorAll(".panel-tab");
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.onclick = () => {
|
||||||
|
const tabId = tab.dataset.tab;
|
||||||
|
const panel = tab.closest("#library-panel, #queue-panel");
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
// Update active tab
|
||||||
|
panel.querySelectorAll(".panel-tab").forEach(t => t.classList.remove("active"));
|
||||||
|
tab.classList.add("active");
|
||||||
|
|
||||||
|
// Update active view
|
||||||
|
panel.querySelectorAll(".panel-view").forEach(v => v.classList.remove("active"));
|
||||||
|
const view = panel.querySelector(`#${tabId}-view`);
|
||||||
|
if (view) view.classList.add("active");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Setup history panel handlers
|
// Setup history panel handlers
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const btnHistory = M.$("#btn-history");
|
const btnHistory = M.$("#btn-history");
|
||||||
|
|
@ -34,6 +55,8 @@
|
||||||
if (btnClose) {
|
if (btnClose) {
|
||||||
btnClose.onclick = () => M.toggleToastHistory();
|
btnClose.onclick = () => M.toggleToastHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initPanelTabs();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the application
|
// Initialize the application
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,49 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe
|
||||||
#channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; }
|
#channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; }
|
||||||
#channels-list .listener-mult { color: #666; font-size: 0.55rem; }
|
#channels-list .listener-mult { color: #666; font-size: 0.55rem; }
|
||||||
#library-panel, #queue-panel { flex: 0 0 700px; min-width: 0; overflow: hidden; background: #1a1a1a; border-radius: 6px; padding: 0.5rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; position: relative; }
|
#library-panel, #queue-panel { flex: 0 0 700px; min-width: 0; overflow: hidden; background: #1a1a1a; border-radius: 6px; padding: 0.5rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; position: relative; }
|
||||||
.scan-progress { font-size: 0.7rem; color: #ea4; padding: 0.2rem 0.4rem; background: #2a2a1a; border-radius: 3px; margin-bottom: 0.3rem; display: flex; align-items: center; gap: 0.4rem; }
|
.panel-tabs { display: flex; gap: 0; margin-bottom: 0; flex-shrink: 0; }
|
||||||
|
.panel-tab { background: #252525; border: none; color: #666; font-family: inherit; font-size: 0.8rem; font-weight: bold; padding: 0.3rem 0.6rem; cursor: pointer; border-radius: 4px 4px 0 0; text-transform: uppercase; letter-spacing: 0.05em; margin-right: 2px; }
|
||||||
|
.panel-tab:hover { color: #aaa; background: #2a2a2a; }
|
||||||
|
.panel-tab.active { color: #eee; background: #222; }
|
||||||
|
.panel-views { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; position: relative; background: #222; border-radius: 0 4px 4px 4px; }
|
||||||
|
.panel-view { display: none; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; position: relative; padding: 0.4rem; }
|
||||||
|
.panel-view.active { display: flex; }
|
||||||
|
.tasks-empty { color: #666; font-size: 0.85rem; padding: 1rem; text-align: center; }
|
||||||
|
.tasks-empty.hidden { display: none; }
|
||||||
|
#tasks-list { display: flex; flex-direction: column; gap: 0.3rem; overflow-y: auto; flex: 1; }
|
||||||
|
.task-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; background: #2a2a1a; border-radius: 4px; font-size: 0.8rem; color: #ea4; position: relative; overflow: hidden; }
|
||||||
|
.task-item.complete { background: #1a2a1a; color: #4e8; }
|
||||||
|
.task-item.error { background: #2a1a1a; color: #e44; }
|
||||||
|
.task-item .task-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #ea4; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0; }
|
||||||
|
.task-item.complete .task-spinner { display: none; }
|
||||||
|
.task-item.error .task-spinner { display: none; }
|
||||||
|
.task-item .task-icon { flex-shrink: 0; }
|
||||||
|
.task-item.complete .task-icon::before { content: "✓"; }
|
||||||
|
.task-item.error .task-icon::before { content: "✗"; }
|
||||||
|
.task-item .task-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.task-item .task-progress { font-size: 0.7rem; color: #888; flex-shrink: 0; }
|
||||||
|
.task-item .task-bar { position: absolute; left: 0; bottom: 0; height: 2px; background: #ea4; transition: width 0.2s; }
|
||||||
|
.task-item.complete .task-bar { background: #4e8; }
|
||||||
|
.scan-progress { font-size: 0.7rem; color: #ea4; padding: 0.2rem 0.4rem; background: #2a2a1a; border-radius: 3px; margin-bottom: 0.3rem; display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; }
|
||||||
.scan-progress.hidden { display: none; }
|
.scan-progress.hidden { display: none; }
|
||||||
.scan-progress.complete { color: #4e8; background: #1a2a1a; }
|
.scan-progress.complete { color: #4e8; background: #1a2a1a; }
|
||||||
.scan-progress .spinner { display: inline-block; width: 10px; height: 10px; border: 2px solid #ea4; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
.scan-progress .spinner { display: inline-block; width: 10px; height: 10px; border: 2px solid #ea4; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
.upload-btn { width: 100%; padding: 0.5rem; background: #2a3a2a; border: 1px dashed #4e8; border-radius: 4px; color: #4e8; font-size: 0.85rem; cursor: pointer; margin-top: 0.3rem; flex-shrink: 0; }
|
.upload-btn { width: 100%; padding: 0.5rem; background: #2a3a2a; border: 1px dashed #4e8; border-radius: 4px; color: #4e8; font-size: 0.85rem; cursor: pointer; margin-top: 0.3rem; flex-shrink: 0; }
|
||||||
.upload-btn:hover { background: #3a4a3a; }
|
.upload-btn:hover { background: #3a4a3a; }
|
||||||
|
.add-btn { width: 100%; padding: 0.5rem; background: #252525; border: 1px solid #444; border-radius: 4px; color: #888; font-size: 0.85rem; cursor: pointer; margin-top: 0.3rem; flex-shrink: 0; transition: all 0.2s; }
|
||||||
|
.add-btn:hover { background: #2a2a2a; border-color: #666; color: #aaa; }
|
||||||
|
.add-btn.hidden { display: none; }
|
||||||
|
.add-panel { position: absolute; bottom: 0; left: 0; right: 0; background: #1a1a1a; border-top: 1px solid #333; border-radius: 0 0 6px 6px; display: flex; flex-direction: column; height: 50%; overflow: hidden; animation: panelSlideUp 0.2s ease-out; }
|
||||||
|
.add-panel.hidden { display: none; }
|
||||||
|
.add-panel.closing { animation: panelSlideDown 0.2s ease-in forwards; }
|
||||||
|
@keyframes panelSlideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||||
|
@keyframes panelSlideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
|
||||||
|
.add-panel-close { width: calc(100% - 1rem); margin: 0.5rem; padding: 0.5rem; background: #252525; border: 1px solid #444; border-radius: 4px; color: #888; font-size: 0.85rem; cursor: pointer; flex-shrink: 0; }
|
||||||
|
.add-panel-close:hover { background: #2a2a2a; border-color: #666; color: #aaa; }
|
||||||
|
.add-panel-content { flex: 1; overflow-y: auto; padding: 0 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 0.3rem; }
|
||||||
|
.add-option { width: 100%; padding: 0.6rem 0.8rem; background: #222; border: 1px solid #333; border-radius: 4px; color: #aaa; font-size: 0.85rem; cursor: pointer; text-align: left; transition: all 0.15s; }
|
||||||
|
.add-option:hover { background: #2a3a2a; border-color: #4e8; color: #4e8; }
|
||||||
.upload-progress { display: flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.5rem; background: #2a2a1a; border-radius: 4px; margin-top: 0.3rem; flex-shrink: 0; position: relative; overflow: hidden; }
|
.upload-progress { display: flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.5rem; background: #2a2a1a; border-radius: 4px; margin-top: 0.3rem; flex-shrink: 0; position: relative; overflow: hidden; }
|
||||||
.upload-progress.hidden { display: none; }
|
.upload-progress.hidden { display: none; }
|
||||||
.upload-progress-bar { position: absolute; left: 0; top: 0; bottom: 0; background: #4a6a4a; transition: width 0.2s; }
|
.upload-progress-bar { position: absolute; left: 0; top: 0; bottom: 0; background: #4a6a4a; transition: width 0.2s; }
|
||||||
|
|
|
||||||
183
public/upload.js
183
public/upload.js
|
|
@ -2,28 +2,57 @@
|
||||||
const M = window.MusicRoom;
|
const M = window.MusicRoom;
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const uploadBtn = M.$("#btn-upload");
|
const addBtn = M.$("#btn-add");
|
||||||
|
const addPanel = M.$("#add-panel");
|
||||||
|
const addCloseBtn = M.$("#btn-add-close");
|
||||||
|
const uploadFilesBtn = M.$("#btn-upload-files");
|
||||||
const fileInput = M.$("#file-input");
|
const fileInput = M.$("#file-input");
|
||||||
const dropzone = M.$("#upload-dropzone");
|
const dropzone = M.$("#upload-dropzone");
|
||||||
const libraryPanel = M.$("#library-panel");
|
const libraryPanel = M.$("#library-panel");
|
||||||
const progressEl = M.$("#upload-progress");
|
const tasksList = M.$("#tasks-list");
|
||||||
const progressBar = progressEl?.querySelector(".upload-progress-bar");
|
const tasksEmpty = M.$("#tasks-empty");
|
||||||
const progressText = progressEl?.querySelector(".upload-progress-text");
|
|
||||||
|
|
||||||
if (!uploadBtn || !fileInput || !dropzone) return;
|
if (!addBtn || !fileInput || !dropzone) return;
|
||||||
|
|
||||||
// Click upload button opens file dialog
|
function openPanel() {
|
||||||
uploadBtn.onclick = () => {
|
addPanel.classList.remove("hidden", "closing");
|
||||||
|
addBtn.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
addPanel.classList.add("closing");
|
||||||
|
addBtn.classList.remove("hidden");
|
||||||
|
addPanel.addEventListener("animationend", () => {
|
||||||
|
if (addPanel.classList.contains("closing")) {
|
||||||
|
addPanel.classList.add("hidden");
|
||||||
|
addPanel.classList.remove("closing");
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open add panel
|
||||||
|
addBtn.onclick = () => {
|
||||||
if (!M.currentUser) {
|
if (!M.currentUser) {
|
||||||
M.showToast("Sign in to upload");
|
M.showToast("Sign in to add tracks");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
openPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close add panel
|
||||||
|
addCloseBtn.onclick = () => {
|
||||||
|
closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload files option
|
||||||
|
uploadFilesBtn.onclick = () => {
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
// File input change
|
// File input change
|
||||||
fileInput.onchange = () => {
|
fileInput.onchange = () => {
|
||||||
if (fileInput.files.length > 0) {
|
if (fileInput.files.length > 0) {
|
||||||
|
closePanel();
|
||||||
uploadFiles(fileInput.files);
|
uploadFiles(fileInput.files);
|
||||||
fileInput.value = "";
|
fileInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
@ -71,19 +100,91 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function showProgress(current, total, filename) {
|
// Task management
|
||||||
if (!progressEl) return;
|
function updateTasksEmpty() {
|
||||||
const pct = Math.round((current / total) * 100);
|
const hasTasks = tasksList.children.length > 0;
|
||||||
progressBar.style.width = pct + "%";
|
tasksEmpty.classList.toggle("hidden", hasTasks);
|
||||||
progressText.textContent = `Uploading ${current}/${total}: ${filename}`;
|
|
||||||
progressEl.classList.remove("hidden");
|
|
||||||
uploadBtn.classList.add("hidden");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideProgress() {
|
function createTask(filename) {
|
||||||
if (!progressEl) return;
|
const task = document.createElement("div");
|
||||||
progressEl.classList.add("hidden");
|
task.className = "task-item";
|
||||||
uploadBtn.classList.remove("hidden");
|
task.innerHTML = `
|
||||||
|
<span class="task-spinner"></span>
|
||||||
|
<span class="task-icon"></span>
|
||||||
|
<span class="task-name">${filename}</span>
|
||||||
|
<span class="task-progress">0%</span>
|
||||||
|
<div class="task-bar" style="width: 0%"></div>
|
||||||
|
`;
|
||||||
|
tasksList.appendChild(task);
|
||||||
|
updateTasksEmpty();
|
||||||
|
|
||||||
|
// Switch to tasks tab
|
||||||
|
const tasksTab = document.querySelector('.panel-tab[data-tab="tasks"]');
|
||||||
|
if (tasksTab) tasksTab.click();
|
||||||
|
|
||||||
|
return {
|
||||||
|
setProgress(percent) {
|
||||||
|
task.querySelector(".task-progress").textContent = `${Math.round(percent)}%`;
|
||||||
|
task.querySelector(".task-bar").style.width = `${percent}%`;
|
||||||
|
},
|
||||||
|
setComplete() {
|
||||||
|
task.classList.add("complete");
|
||||||
|
task.querySelector(".task-progress").textContent = "Done";
|
||||||
|
task.querySelector(".task-bar").style.width = "100%";
|
||||||
|
// Remove after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
task.remove();
|
||||||
|
updateTasksEmpty();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
setError(msg) {
|
||||||
|
task.classList.add("error");
|
||||||
|
task.querySelector(".task-progress").textContent = msg || "Failed";
|
||||||
|
// Remove after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
task.remove();
|
||||||
|
updateTasksEmpty();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const task = createTask(file.name);
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percent = (e.loaded / e.total) * 100;
|
||||||
|
task.setProgress(percent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
task.setComplete();
|
||||||
|
resolve({ success: true });
|
||||||
|
} else if (xhr.status === 409) {
|
||||||
|
task.setError("Duplicate");
|
||||||
|
resolve({ success: false, duplicate: true });
|
||||||
|
} else {
|
||||||
|
task.setError("Failed");
|
||||||
|
resolve({ success: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
task.setError("Error");
|
||||||
|
resolve({ success: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.open("POST", "/api/upload");
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFiles(files) {
|
async function uploadFiles(files) {
|
||||||
|
|
@ -100,44 +201,30 @@
|
||||||
|
|
||||||
let uploaded = 0;
|
let uploaded = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
let duplicates = 0;
|
|
||||||
const total = audioFiles.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < audioFiles.length; i++) {
|
// Upload files in parallel (max 3 concurrent)
|
||||||
const file = audioFiles[i];
|
const concurrency = 3;
|
||||||
showProgress(i + 1, total, file.name);
|
const queue = [...audioFiles];
|
||||||
|
const active = [];
|
||||||
|
|
||||||
try {
|
while (queue.length > 0 || active.length > 0) {
|
||||||
const formData = new FormData();
|
while (active.length < concurrency && queue.length > 0) {
|
||||||
formData.append("file", file);
|
const file = queue.shift();
|
||||||
|
const promise = uploadFile(file).then(result => {
|
||||||
const res = await fetch("/api/upload", {
|
if (result.success) uploaded++;
|
||||||
method: "POST",
|
else failed++;
|
||||||
body: formData
|
active.splice(active.indexOf(promise), 1);
|
||||||
});
|
});
|
||||||
|
active.push(promise);
|
||||||
if (res.ok) {
|
|
||||||
uploaded++;
|
|
||||||
} else if (res.status === 409) {
|
|
||||||
// File already exists
|
|
||||||
M.showToast(`Already uploaded: ${file.name}`, "warning");
|
|
||||||
duplicates++;
|
|
||||||
} else {
|
|
||||||
const err = await res.json().catch(() => ({}));
|
|
||||||
console.error(`Upload failed for ${file.name}:`, err.error || res.status);
|
|
||||||
failed++;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
if (active.length > 0) {
|
||||||
console.error(`Upload error for ${file.name}:`, e);
|
await Promise.race(active);
|
||||||
failed++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideProgress();
|
|
||||||
|
|
||||||
if (uploaded > 0) {
|
if (uploaded > 0) {
|
||||||
M.showToast(`Uploaded ${uploaded} track${uploaded > 1 ? 's' : ''}${failed > 0 ? `, ${failed} failed` : ''}`);
|
M.showToast(`Uploaded ${uploaded} track${uploaded > 1 ? 's' : ''}${failed > 0 ? `, ${failed} failed` : ''}`);
|
||||||
} else {
|
} else if (failed > 0) {
|
||||||
M.showToast(`Upload failed`);
|
M.showToast(`Upload failed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ function userHasPermission(user: ReturnType<typeof getUser>, resourceType: strin
|
||||||
if (user.is_guest && permission === "control") return false;
|
if (user.is_guest && permission === "control") return false;
|
||||||
|
|
||||||
// Check default permissions from config
|
// Check default permissions from config
|
||||||
if (resourceType === "channel" && config.defaultPermissions.channel?.includes(permission)) {
|
if (resourceType === "channel" && config.defaultPermissions?.includes(permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -651,8 +651,8 @@ serve({
|
||||||
const permissions = getUserPermissions(user.id);
|
const permissions = getUserPermissions(user.id);
|
||||||
// Add default permissions for all users (except control for guests)
|
// Add default permissions for all users (except control for guests)
|
||||||
const effectivePermissions = [...permissions];
|
const effectivePermissions = [...permissions];
|
||||||
if (config.defaultPermissions.channel) {
|
if (config.defaultPermissions) {
|
||||||
for (const perm of config.defaultPermissions.channel) {
|
for (const perm of config.defaultPermissions) {
|
||||||
// Guests can never have control permission
|
// Guests can never have control permission
|
||||||
if (user.is_guest && perm === "control") continue;
|
if (user.is_guest && perm === "control") continue;
|
||||||
effectivePermissions.push({
|
effectivePermissions.push({
|
||||||
|
|
@ -999,7 +999,7 @@ serve({
|
||||||
|
|
||||||
// Check default permissions or user-specific permissions
|
// Check default permissions or user-specific permissions
|
||||||
const canControl = user.is_admin
|
const canControl = user.is_admin
|
||||||
|| config.defaultPermissions.channel?.includes("control")
|
|| config.defaultPermissions?.includes("control")
|
||||||
|| hasPermission(userId, "channel", ws.data.channelId, "control");
|
|| hasPermission(userId, "channel", ws.data.channelId, "control");
|
||||||
if (!canControl) {
|
if (!canControl) {
|
||||||
console.log("[WS] User lacks control permission:", user.username);
|
console.log("[WS] User lacks control permission:", user.username);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue