gfds
This commit is contained in:
+114
-18
@@ -151,7 +151,10 @@
|
||||
const allowedTypes = ['image/gif', 'image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
if (chooseBtn && fileInput) {
|
||||
chooseBtn.addEventListener('click', () => fileInput.click());
|
||||
chooseBtn.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#avatar-remove-btn')) return; // removing, not choosing
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files[0];
|
||||
@@ -194,8 +197,24 @@
|
||||
progressWrapper.style.display = 'flex';
|
||||
progressFill.style.width = '0%';
|
||||
progressText.textContent = '0%';
|
||||
progressWrapper.setAttribute('aria-valuenow', '0');
|
||||
showStatus(i18n.uploading || 'Uploading...', '');
|
||||
|
||||
// Show the chosen image in the live preview right away; the progress overlay sits on top of it
|
||||
chooseBtn.classList.add('is-uploading');
|
||||
const liveAvatar = document.getElementById('live-preview-avatar');
|
||||
const prevAvatarSrc = liveAvatar ? liveAvatar.src : null;
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
if (liveAvatar) liveAvatar.src = localUrl;
|
||||
const finishUpload = (ok) => {
|
||||
progressWrapper.style.display = 'none';
|
||||
chooseBtn.classList.remove('is-uploading');
|
||||
if (!ok && liveAvatar && prevAvatarSrc) liveAvatar.src = prevAvatarSrc;
|
||||
// Keep the local image until the server copy has loaded, then free it
|
||||
if (ok && liveAvatar) liveAvatar.addEventListener('load', () => URL.revokeObjectURL(localUrl), { once: true });
|
||||
else URL.revokeObjectURL(localUrl);
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
@@ -205,14 +224,19 @@
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
progressFill.style.width = percent + '%';
|
||||
progressText.textContent = percent + '%';
|
||||
progressWrapper.setAttribute('aria-valuenow', String(percent));
|
||||
// At 100% the bytes are sent but the server is still processing
|
||||
if (percent >= 100) progressText.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
||||
else progressText.textContent = percent + '%';
|
||||
}
|
||||
});
|
||||
|
||||
let uploadOk = false;
|
||||
xhr.addEventListener('load', () => {
|
||||
try {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
if (xhr.status === 200 && res.success) {
|
||||
uploadOk = true;
|
||||
showStatus(res.msg || 'Avatar uploaded!', 'success');
|
||||
|
||||
// Update preview
|
||||
@@ -236,7 +260,15 @@
|
||||
|
||||
// Show remove button if not present
|
||||
const existingRemoveBtn = document.getElementById('avatar-remove-btn');
|
||||
if (!existingRemoveBtn) {
|
||||
if (!existingRemoveBtn && chooseBtn.classList.contains('editable-avatar-container')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.id = 'avatar-remove-btn';
|
||||
btn.className = 'avatar-quick-remove';
|
||||
btn.title = 'Remove custom avatar';
|
||||
btn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||
chooseBtn.appendChild(btn);
|
||||
} else if (!existingRemoveBtn) {
|
||||
const actionsDiv = document.querySelector('.avatar-upload-actions');
|
||||
if (actionsDiv) {
|
||||
const btn = document.createElement('button');
|
||||
@@ -258,14 +290,14 @@
|
||||
showStatus('Upload failed: Invalid response', 'error');
|
||||
}
|
||||
|
||||
progressWrapper.style.display = 'none';
|
||||
finishUpload(uploadOk);
|
||||
uploadBtn.disabled = true;
|
||||
chooseBtn.disabled = false;
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
showStatus('Upload failed: Network error', 'error');
|
||||
progressWrapper.style.display = 'none';
|
||||
finishUpload(false);
|
||||
uploadBtn.disabled = true;
|
||||
chooseBtn.disabled = false;
|
||||
});
|
||||
@@ -654,11 +686,43 @@
|
||||
|
||||
if (bannerUploadBtn) bannerUploadBtn.disabled = true;
|
||||
if (bannerChooseBtn) bannerChooseBtn.disabled = true;
|
||||
if (bannerProgressWrapper) { bannerProgressWrapper.style.display = 'flex'; }
|
||||
const bannerLabel = (inner) => '<i class="fa-solid fa-image"></i> ' + inner;
|
||||
if (bannerProgressWrapper) { bannerProgressWrapper.style.display = 'block'; bannerProgressWrapper.setAttribute('aria-valuenow', '0'); }
|
||||
if (bannerProgressFill) bannerProgressFill.style.width = '0%';
|
||||
if (bannerProgressText) bannerProgressText.textContent = '0%';
|
||||
if (bannerProgressText) bannerProgressText.innerHTML = bannerLabel('0%');
|
||||
showBannerStatus('Uploading...', '');
|
||||
|
||||
// Show the cropped banner on the live card right away; roll back if the upload fails
|
||||
const liveCard = document.getElementById('live-profile-preview-box');
|
||||
const prevBanner = liveCard ? {
|
||||
img: liveCard.style.getPropertyValue('--author-banner'),
|
||||
size: liveCard.style.getPropertyValue('--author-banner-size'),
|
||||
pos: liveCard.style.getPropertyValue('--author-banner-position')
|
||||
} : null;
|
||||
const localBannerUrl = URL.createObjectURL(payload.file);
|
||||
if (liveCard) {
|
||||
liveCard.classList.add('is-banner-uploading');
|
||||
liveCard.style.setProperty('--author-banner', `url('${localBannerUrl}')`);
|
||||
liveCard.style.setProperty('--author-banner-size', payload.banner_size);
|
||||
liveCard.style.setProperty('--author-banner-position', payload.banner_position);
|
||||
}
|
||||
const finishBannerUpload = (ok) => {
|
||||
if (bannerProgressWrapper) bannerProgressWrapper.style.display = 'none';
|
||||
if (bannerChooseBtn) bannerChooseBtn.disabled = false;
|
||||
if (liveCard) {
|
||||
liveCard.classList.remove('is-banner-uploading');
|
||||
if (!ok && prevBanner) {
|
||||
['--author-banner', '--author-banner-size', '--author-banner-position'].forEach((prop, i) => {
|
||||
const v = [prevBanner.img, prevBanner.size, prevBanner.pos][i];
|
||||
if (v) liveCard.style.setProperty(prop, v); else liveCard.style.removeProperty(prop);
|
||||
});
|
||||
}
|
||||
}
|
||||
// The server URL replaced the local one on success; give the browser time to swap before freeing it
|
||||
setTimeout(() => URL.revokeObjectURL(localBannerUrl), ok ? 5000 : 0);
|
||||
};
|
||||
let bannerOk = false;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', payload.file);
|
||||
if (payload.file_orig) formData.append('file_orig', payload.file_orig);
|
||||
@@ -671,7 +735,9 @@
|
||||
if (e.lengthComputable) {
|
||||
const pct = Math.round((e.loaded / e.total) * 100);
|
||||
if (bannerProgressFill) bannerProgressFill.style.width = pct + '%';
|
||||
if (bannerProgressText) bannerProgressText.textContent = pct + '%';
|
||||
if (bannerProgressWrapper) bannerProgressWrapper.setAttribute('aria-valuenow', String(pct));
|
||||
// At 100% the bytes are sent but the server is still processing
|
||||
if (bannerProgressText) bannerProgressText.innerHTML = bannerLabel(pct >= 100 ? '<i class="fa-solid fa-spinner fa-spin"></i>' : pct + '%');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -679,6 +745,7 @@
|
||||
try {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
if (xhr.status === 200 && res.success) {
|
||||
bannerOk = true;
|
||||
showBannerStatus(res.msg || 'Banner uploaded!', 'success');
|
||||
|
||||
const bannerPreview = document.getElementById('banner-preview');
|
||||
@@ -710,7 +777,19 @@
|
||||
}
|
||||
|
||||
const existingRemoveBtn = document.getElementById('banner-remove-btn');
|
||||
if (!existingRemoveBtn) {
|
||||
const descControls = document.getElementById('desc-edit-controls');
|
||||
if (!existingRemoveBtn && descControls) {
|
||||
// Live card: add Reposition / Remove next to Change Banner, same as the server render
|
||||
const mk = (id, cls, icon, label) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button'; b.id = id; b.className = cls;
|
||||
b.style.cssText = 'padding: 4px 10px; font-size: 0.8em;';
|
||||
b.innerHTML = `<i class="fa-solid ${icon}"></i> ${label}`;
|
||||
return b;
|
||||
};
|
||||
descControls.appendChild(mk('banner-edit-btn', 'button button-sm', 'fa-arrows-up-down-left-right', 'Reposition'));
|
||||
descControls.appendChild(mk('banner-remove-btn', 'button button-danger button-sm', 'fa-trash', 'Remove Banner'));
|
||||
} else if (!existingRemoveBtn) {
|
||||
const actionsDiv = bannerUploadBtn ? bannerUploadBtn.closest('.avatar-upload-actions') : null;
|
||||
if (actionsDiv) {
|
||||
const editBtn = document.createElement('button');
|
||||
@@ -762,14 +841,12 @@
|
||||
showBannerStatus('Upload failed: Invalid response', 'error');
|
||||
}
|
||||
|
||||
if (bannerProgressWrapper) bannerProgressWrapper.style.display = 'none';
|
||||
if (bannerChooseBtn) bannerChooseBtn.disabled = false;
|
||||
finishBannerUpload(bannerOk);
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
showBannerStatus('Upload failed: Network error', 'error');
|
||||
if (bannerProgressWrapper) bannerProgressWrapper.style.display = 'none';
|
||||
if (bannerChooseBtn) bannerChooseBtn.disabled = false;
|
||||
finishBannerUpload(false);
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/v2/settings/uploadBanner');
|
||||
@@ -1604,9 +1681,12 @@
|
||||
onaraEnabledToggle.checked = stored !== null ? stored === 'true' : sessionOnara;
|
||||
// Reflect current state into window.onara so isOnaraActive() sees it
|
||||
window.onara = onaraEnabledToggle.checked;
|
||||
try {
|
||||
document.cookie = `f0ck_onara=${onaraEnabledToggle.checked ? '1' : '0'}; path=/; max-age=31536000; SameSite=Lax`;
|
||||
} catch {}
|
||||
// Only persist an explicit choice; without one the config default keeps applying
|
||||
if (stored !== null) {
|
||||
try {
|
||||
document.cookie = `f0ck_onara=${onaraEnabledToggle.checked ? '1' : '0'}; path=/; max-age=31536000; SameSite=Lax`;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onaraEnabledToggle.addEventListener('change', () => {
|
||||
const enabled = onaraEnabledToggle.checked;
|
||||
@@ -1950,6 +2030,17 @@
|
||||
const liveDesc = document.getElementById('live-preview-description');
|
||||
const descControls = document.getElementById('desc-edit-controls');
|
||||
|
||||
// Inline-editable profile fields are text inputs: keep their key events away from the site-wide
|
||||
// shortcut handlers (bubble-phase on document/window), not all of which skip contenteditable.
|
||||
// The fields' own listeners (Enter / Ctrl+Enter to save, Escape) sit on the element and still run.
|
||||
document.querySelectorAll('.settings [contenteditable="true"]').forEach((el) => {
|
||||
if (el.dataset.keysIsolated) return;
|
||||
el.dataset.keysIsolated = '1';
|
||||
['keydown', 'keyup', 'keypress'].forEach((type) => {
|
||||
el.addEventListener(type, (e) => e.stopPropagation());
|
||||
});
|
||||
});
|
||||
|
||||
if (liveDesc && descriptionTextarea) {
|
||||
liveDesc.addEventListener('focus', () => {
|
||||
if (descControls) descControls.style.display = 'flex';
|
||||
@@ -2072,7 +2163,9 @@
|
||||
if (passwordForm) {
|
||||
passwordForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const current_password = document.getElementById('current_password').value;
|
||||
// Absent on passkey-only accounts (no current password; setting one re-enables password login)
|
||||
const currentInput = document.getElementById('current_password');
|
||||
const current_password = currentInput ? currentInput.value : '';
|
||||
const new_password = document.getElementById('new_password').value;
|
||||
const new_password_confirm = document.getElementById('new_password_confirm').value;
|
||||
|
||||
@@ -2082,6 +2175,7 @@
|
||||
}
|
||||
|
||||
const btn = passwordForm.querySelector('button');
|
||||
const btnLabel = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Updating...';
|
||||
|
||||
@@ -2098,6 +2192,8 @@
|
||||
if (data.success) {
|
||||
showAccountStatus(passwordStatus, data.msg || 'Password updated correctly', 'success');
|
||||
passwordForm.reset();
|
||||
// Password login was off: reload so the account section shows the normal state again
|
||||
if (!currentInput) setTimeout(() => location.reload(), 900);
|
||||
} else {
|
||||
showAccountStatus(passwordStatus, data.msg || 'Failed to update password', 'error');
|
||||
}
|
||||
@@ -2105,7 +2201,7 @@
|
||||
showAccountStatus(passwordStatus, 'Request failed', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Update Password';
|
||||
btn.textContent = btnLabel;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user