mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Add logging and toasts
This commit is contained in:
@@ -154,21 +154,48 @@ function connect() {
|
||||
handleEvent(body);
|
||||
});
|
||||
fetchAssets();
|
||||
}, (error) => {
|
||||
console.warn('WebSocket connection issue', error);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Live updates connection interrupted. Retrying may be necessary.', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAssets() {
|
||||
fetch(`/api/channels/${broadcaster}/assets`).then((r) => r.json()).then(renderAssets);
|
||||
fetch(`/api/channels/${broadcaster}/assets`)
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load assets');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(renderAssets)
|
||||
.catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to load assets. Please refresh.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchCanvasSettings() {
|
||||
return fetch(`/api/channels/${broadcaster}/canvas`)
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load canvas');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((settings) => {
|
||||
canvasSettings = settings;
|
||||
resizeCanvas();
|
||||
})
|
||||
.catch(() => resizeCanvas());
|
||||
.catch(() => {
|
||||
resizeCanvas();
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
@@ -1350,29 +1377,58 @@ function updateVisibility(asset, hidden) {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hidden })
|
||||
}).then((r) => r.json()).then((updated) => {
|
||||
}).then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to update visibility');
|
||||
}
|
||||
return r.json();
|
||||
}).then((updated) => {
|
||||
storeAsset(updated);
|
||||
if (updated.hidden) {
|
||||
stopAudio(updated.id);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Asset hidden from broadcast.', 'info');
|
||||
}
|
||||
} else if (isAudioAsset(updated)) {
|
||||
playAudioFromCanvas(updated, true);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Asset is now visible and active.', 'success');
|
||||
}
|
||||
} else if (typeof showToast === 'function') {
|
||||
showToast('Asset is now visible.', 'success');
|
||||
}
|
||||
updateRenderState(updated);
|
||||
drawAndList();
|
||||
}).catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to change visibility right now.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAsset(asset) {
|
||||
fetch(`/api/channels/${broadcaster}/assets/${asset.id}`, { method: 'DELETE' }).then(() => {
|
||||
clearMedia(asset.id);
|
||||
assets.delete(asset.id);
|
||||
renderStates.delete(asset.id);
|
||||
zOrderDirty = true;
|
||||
if (selectedAssetId === asset.id) {
|
||||
selectedAssetId = null;
|
||||
}
|
||||
drawAndList();
|
||||
});
|
||||
fetch(`/api/channels/${broadcaster}/assets/${asset.id}`, { method: 'DELETE' })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete asset');
|
||||
}
|
||||
clearMedia(asset.id);
|
||||
assets.delete(asset.id);
|
||||
renderStates.delete(asset.id);
|
||||
zOrderDirty = true;
|
||||
if (selectedAssetId === asset.id) {
|
||||
selectedAssetId = null;
|
||||
}
|
||||
drawAndList();
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Asset deleted.', 'info');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to delete asset. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileSelection(input) {
|
||||
@@ -1391,7 +1447,9 @@ function uploadAsset(file = null) {
|
||||
const fileInput = document.getElementById('asset-file');
|
||||
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
|
||||
if (!selectedFile) {
|
||||
alert('Please choose an image, GIF, video, or audio file to upload.');
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Choose an image, GIF, video, or audio file to upload.', 'info');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = new FormData();
|
||||
@@ -1402,15 +1460,24 @@ function uploadAsset(file = null) {
|
||||
fetch(`/api/channels/${broadcaster}/assets`, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
handleFileSelection(fileInput);
|
||||
}
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Asset uploaded successfully.', 'success');
|
||||
}
|
||||
}).catch(() => {
|
||||
if (fileNameLabel) {
|
||||
fileNameLabel.textContent = 'Upload failed';
|
||||
}
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Upload failed. Please try again with a supported file.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1462,12 +1529,21 @@ function persistTransform(asset, silent = false) {
|
||||
audioPitch: asset.audioPitch,
|
||||
audioVolume: asset.audioVolume
|
||||
})
|
||||
}).then((r) => r.json()).then((updated) => {
|
||||
}).then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Transform failed');
|
||||
}
|
||||
return r.json();
|
||||
}).then((updated) => {
|
||||
storeAsset(updated);
|
||||
updateRenderState(updated);
|
||||
if (!silent) {
|
||||
drawAndList();
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!silent && typeof showToast === 'function') {
|
||||
showToast('Unable to save changes. Please retry.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,19 @@ function connect() {
|
||||
const body = JSON.parse(payload.body);
|
||||
handleEvent(body);
|
||||
});
|
||||
fetch(`/api/channels/${broadcaster}/assets/visible`).then(r => r.json()).then(renderAssets);
|
||||
fetch(`/api/channels/${broadcaster}/assets/visible`)
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load assets');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(renderAssets)
|
||||
.catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to load overlay assets. Retrying may help.', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,12 +63,22 @@ function renderAssets(list) {
|
||||
|
||||
function fetchCanvasSettings() {
|
||||
return fetch(`/api/channels/${broadcaster}/canvas`)
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load canvas');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((settings) => {
|
||||
canvasSettings = settings;
|
||||
resizeCanvas();
|
||||
})
|
||||
.catch(() => resizeCanvas());
|
||||
.catch(() => {
|
||||
resizeCanvas();
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
|
||||
@@ -57,23 +57,44 @@ function renderAdmins(list) {
|
||||
|
||||
function fetchAdmins() {
|
||||
fetch(`/api/channels/${broadcaster}/admins`)
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load admins');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(renderAdmins)
|
||||
.catch(() => renderAdmins([]));
|
||||
.catch(() => {
|
||||
renderAdmins([]);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to load admins right now. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeAdmin(username) {
|
||||
if (!username) return;
|
||||
fetch(`/api/channels/${broadcaster}/admins/${encodeURIComponent(username)}`, {
|
||||
method: 'DELETE'
|
||||
}).then(fetchAdmins);
|
||||
}).then((response) => {
|
||||
if (!response.ok && typeof showToast === 'function') {
|
||||
showToast('Failed to remove admin. Please retry.', 'error');
|
||||
}
|
||||
fetchAdmins();
|
||||
}).catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Failed to remove admin. Please retry.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addAdmin() {
|
||||
const input = document.getElementById('new-admin');
|
||||
const username = input.value.trim();
|
||||
if (!username) {
|
||||
alert('Enter a Twitch username to add as an admin.');
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Enter a Twitch username to add as an admin.', 'info');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,9 +103,20 @@ function addAdmin() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username })
|
||||
})
|
||||
.then(() => {
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Add admin failed');
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(`Added @${username} as an admin.`, 'success');
|
||||
}
|
||||
fetchAdmins();
|
||||
})
|
||||
.catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to add admin right now. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,9 +129,19 @@ function renderCanvasSettings(settings) {
|
||||
|
||||
function fetchCanvasSettings() {
|
||||
fetch(`/api/channels/${broadcaster}/canvas`)
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load canvas settings');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(renderCanvasSettings)
|
||||
.catch(() => renderCanvasSettings({ width: 1920, height: 1080 }));
|
||||
.catch(() => {
|
||||
renderCanvasSettings({ width: 1920, height: 1080 });
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveCanvasSettings() {
|
||||
@@ -109,7 +151,9 @@ function saveCanvasSettings() {
|
||||
const width = parseFloat(widthInput?.value) || 0;
|
||||
const height = parseFloat(heightInput?.value) || 0;
|
||||
if (width <= 0 || height <= 0) {
|
||||
alert('Please enter a valid width and height.');
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Please enter a valid width and height.', 'info');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (status) status.textContent = 'Saving...';
|
||||
@@ -118,16 +162,27 @@ function saveCanvasSettings() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ width, height })
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to save canvas');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((settings) => {
|
||||
renderCanvasSettings(settings);
|
||||
if (status) status.textContent = 'Saved.';
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Canvas size saved successfully.', 'success');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (status) status.textContent = '';
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
if (status) status.textContent = 'Unable to save right now.';
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Unable to save canvas size. Please retry.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
51
src/main/resources/static/js/toast.js
Normal file
51
src/main/resources/static/js/toast.js
Normal file
@@ -0,0 +1,51 @@
|
||||
(function () {
|
||||
const CONTAINER_ID = 'toast-container';
|
||||
const DEFAULT_DURATION = 4200;
|
||||
|
||||
function ensureContainer() {
|
||||
let container = document.getElementById(CONTAINER_ID);
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = CONTAINER_ID;
|
||||
container.className = 'toast-container';
|
||||
container.setAttribute('aria-live', 'polite');
|
||||
container.setAttribute('aria-atomic', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildToast(message, type) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = 'toast-indicator';
|
||||
indicator.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'toast-message';
|
||||
content.textContent = message;
|
||||
|
||||
toast.appendChild(indicator);
|
||||
toast.appendChild(content);
|
||||
return toast;
|
||||
}
|
||||
|
||||
function removeToast(toast) {
|
||||
if (!toast) return;
|
||||
toast.classList.add('toast-exit');
|
||||
setTimeout(() => toast.remove(), 250);
|
||||
}
|
||||
|
||||
window.showToast = function showToast(message, type = 'info', options = {}) {
|
||||
if (!message) return;
|
||||
const normalized = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info';
|
||||
const duration = typeof options.duration === 'number' ? options.duration : DEFAULT_DURATION;
|
||||
const container = ensureContainer();
|
||||
const toast = buildToast(message, normalized);
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => removeToast(toast), Math.max(1200, duration));
|
||||
toast.addEventListener('click', () => removeToast(toast));
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user