Add settings and profile editor

This commit is contained in:
olcxja 2026-06-23 23:41:27 +02:00
commit cc41f25277
14 changed files with 1210 additions and 106 deletions

View file

@ -131,7 +131,12 @@ function base64ToUint8(base64) {
}
function uint8ToBase64(uint8) {
return fixBase64Padding(btoa(String.fromCharCode(...uint8)));
let binary = '';
const chunkSize = 8192;
for (let i = 0; i < uint8.length; i += chunkSize) {
binary += String.fromCharCode.apply(null, uint8.subarray(i, i + chunkSize));
}
return fixBase64Padding(btoa(binary));
}
function concatUint8(a, b) {
@ -441,7 +446,7 @@ async function encryptString(plainText, passphrase) {
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return fixBase64Padding(btoa(String.fromCharCode(...combined)));
return uint8ToBase64(combined);
}
async function decryptString(base64Text, passphrase) {
@ -601,6 +606,7 @@ async function fetchEncrypted(request, body = "") {
}
});
let data = await response.text();
if (data.startsWith("error:")) return data;
return await decryptString(data, passwordHash);
} finally {
release();
@ -780,6 +786,7 @@ function createAvatarSvg(name, size = 512) {
async function getAvatarUrl(id, username) {
try {
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
if (id === window.id && window.profileUpdateTimestamp) pfpUrl += `&t=${window.profileUpdateTimestamp}`;
if ((await fetchAsync(pfpUrl)) == "") {
throw Error();
}
@ -839,7 +846,6 @@ async function initBlahs() {
res = await fetchAsync(`${path}blah/en-us.json`);
blah = JSON.parse(res);
}
let blahTags = document.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
@ -1056,15 +1062,15 @@ async function ensureUserKeys() {
}
async function mainJS() {
if (localStorage.getItem('lang') != null) {
lang = localStorage.getItem('lang');
}
await initBlahs();
await loadEmotes();
passwordHash = await hashSHA3_512(password);
if (localStorage.getItem('lang') != null) {
lang = localStorage.getItem('lang');
}
if (host) {
await updateProtocolAndUrl(host);
} else {
@ -1488,6 +1494,7 @@ async function openProfile(accountId) {
let pfpUrl = await getAvatarUrl(accountId, nameWithHost);
let bannerUrl = `${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.banner`;
if (accountId === window.id && window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
let bannerHtml = `<img src="${bannerUrl}" class="profile-banner" onerror="this.outerHTML='<div class=\\'profile-banner\\'></div>'">`;
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.bio`);
@ -1498,7 +1505,7 @@ async function openProfile(accountId) {
let isOwnProfile = accountId === id;
let actionBtnHtml = "";
if (isOwnProfile) {
actionBtnHtml = `<button class="profile-action-btn">${processBlah('action.edit.profile')}</button>`;
actionBtnHtml = `<button class="profile-action-btn" onclick="gotoSettings('profile');closeProfile();">${processBlah('action.edit.profile')}</button>`;
} else {
let hasDm = false;
let dmRoomId = "";
@ -1920,7 +1927,7 @@ document.addEventListener('touchstart', e => {
activeTouchButton.classList.add('is-pressed');
pressDate = Date.now();
}
}, {passive: false});
}, {passive: true});
document.addEventListener('touchmove', e => {
if (!touchMoved) {
@ -1940,7 +1947,7 @@ document.addEventListener('touchmove', e => {
}
}
}
}, {passive: false});
}, {passive: true});
document.addEventListener('touchend', async (e) => {
touchEndX = e.changedTouches[0].screenX;
@ -1952,14 +1959,6 @@ document.addEventListener('touchend', async (e) => {
if (activeTouchButton) {
let elapsed = Date.now() - pressDate; //nie pomoglo ale zostawie
if (elapsed < 100) {
const remaining = 100 - elapsed;
await delay(remaining);
}
activeTouchButton.classList.remove('is-pressed');
if (!touchMoved) {
if (activeTouchButton.tagName !== 'INPUT' && activeTouchButton.tagName !== 'TEXTAREA') {
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
@ -1974,6 +1973,14 @@ document.addEventListener('touchend', async (e) => {
activeTouchButton.focus();
}
}
let elapsed = Date.now() - pressDate; //nie pomoglo ale zostawie
if (elapsed < 100) {
const remaining = 100 - elapsed;
await delay(remaining);
}
activeTouchButton.classList.remove('is-pressed');
activeTouchButton = null;
}
});
@ -2450,7 +2457,7 @@ function setupChatScrollListener() {
let replyingToMsgId = null;
async function sendMessage(sendbutton) {
async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) {
if (!currentDmId || !currentDmKey) return;
let input = document.getElementById("chat-input");
@ -2843,4 +2850,331 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
showBlahNotification("error:message.react.failed");
}
clearAction("msgreact");
}
// --- Settings Logic ---
async function gotoSettings(tab = 'account') {
fixedContextMenu.classList.remove("show");
if (roomsBarContainer) roomsBarContainer.style.display = "";
switchRoomsBar("title.settings", settingsBar);
switchSettingsTab(tab);
}
function switchSettingsTab(tabName) {
setActiveRoombarItem('tab-settings-' + tabName);
if (tabName === 'account') {
switchRoomContent("title.account", accountSettings, false);
} else if (tabName === 'profile') {
switchRoomContent("title.profile", profileSettings, false);
setTimeout(async () => {
let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
let pfpEl = document.getElementById("settings-avatar-preview");
if (pfpEl) pfpEl.src = pfpUrl;
let bannerUrl = `${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.banner`;
if (window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
let bannerEl = document.getElementById("settings-banner-preview");
if (bannerEl) bannerEl.src = bannerUrl;
let bioEl = document.getElementById("settings-bio");
if (bioEl) {
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
if (!bio.startsWith("error:")) bioEl.value = bio;
}
}, 210);
} else if (tabName === 'appearance') {
switchRoomContent("title.appearance", appearanceSettings, false);
setTimeout(populateLanguageDropdown, 210);
}
}
async function populateLanguageDropdown() {
let sel = document.getElementById("settings-language");
if (!sel) return;
try {
let path = window.location.pathname.replace("/login/index.html", "/").replace("index.html", "");
let res = await fetchAsync(`${path}blah/index.json`);
let json = JSON.parse(res);
sel.innerHTML = "";
for (let code in json) {
let opt = document.createElement("option");
opt.value = code;
opt.textContent = json[code].native + " (" + json[code].english + ")";
if (code === lang) opt.selected = true;
sel.appendChild(opt);
}
} catch (e) {
console.error("Failed to load languages", e);
}
}
function changeLanguage(newLang) {
if (!newLang) return;
localStorage.setItem('lang', newLang);
window.location.reload();
}
async function changeUsername() {
let input = document.getElementById("settings-username");
let name = input.value;
if (!name) return;
showAction("action.account.saving", "accountsave");
let res = await fetchPostEnc(`${url}/chname?id=${window.id}`, name);
clearAction("accountsave");
let text = res;
if (text.startsWith("success:")) {
showBlahNotification(text);
loginUsername = name;
localStorage.setItem("username", name);
} else {
showBlahNotification(text);
}
}
async function changePassword() {
let input = document.getElementById("settings-password");
let newPass = input.value;
if (!newPass) return;
showAction("action.account.saving", "accountsave");
let newPassHash = await hashSHA3_512(newPass);
let newUserKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, newPass);
let payload = JSON.stringify({
string1: newPassHash,
string2: JSON.stringify({
string1: newUserKeysEncrypted,
string2: JSON.stringify(userKeysPublic)
})
});
let res = await fetchPostEnc(`${url}/chpass?id=${window.id}`, payload);
clearAction("accountsave");
let text = res;
if (text.startsWith("success:")) {
password = newPass;
localStorage.setItem("password", newPass);
passwordHash = newPassHash;
userKeysEncrypted = newUserKeysEncrypted;
localStorage.setItem(`userKeys.enc.v1:${window.id}`, userKeysEncrypted);
showBlahNotification(text);
} else {
showBlahNotification(text);
}
}
async function deleteAccount() {
if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return;
showAction("action.account.deleting", "accountdel");
let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, "");
clearAction("accountdel");
let text = res;
if (text.startsWith("success:")) {
alert(processBlah(text));
logout();
} else {
showBlahNotification(text);
}
}
async function saveProfile() {
let bioInput = document.getElementById("settings-bio");
if (bioInput) {
let payload = { string1: "larp.profile.bio", string2: bioInput.value || "" };
showAction("action.bio.saving", "savebio");
let res = await fetchEncrypted("user/storage/public/update", JSON.stringify(payload));
clearAction("savebio");
if (res && res.startsWith("error:")) { showBlahNotification(res); return; }
}
showBlahNotification("success:profile.updated");
}
// --- Cropper Logic ---
let cropperType = 'avatar'; // 'avatar' or 'banner'
let cropperImageObj = null;
let cropperPosX = 0;
let cropperPosY = 0;
let cropperBaseZoom = 1;
let cropperOriginalType = '';
let cropperOriginalB64 = '';
let isDraggingCropper = false;
let startDragX = 0;
let startDragY = 0;
let startPosX = 0;
let startPosY = 0;
function openCropper(input, type) {
if (!input.files || input.files.length === 0) return;
let file = input.files[0];
cropperType = type;
let reader = new FileReader();
reader.onload = (e) => {
let img = new Image();
img.onload = () => {
cropperImageObj = img;
initCropper();
};
cropperOriginalType = file.type;
cropperOriginalB64 = e.target.result;
img.src = e.target.result;
document.getElementById('cropper-image').src = e.target.result;
};
reader.readAsDataURL(file);
input.value = ""; // reset input
}
function initCropper() {
let container = document.getElementById('cropper-viewport-container');
let imgEl = document.getElementById('cropper-image');
let zoomSlider = document.getElementById('cropper-zoom');
// Set aspect ratio
let targetWidth = cropperType === 'avatar' ? 512 : 494;
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
let containerWidth = container.clientWidth || 300;
container.style.height = (containerWidth * (targetHeight / targetWidth)) + 'px';
let imgWidth = cropperImageObj.width;
let imgHeight = cropperImageObj.height;
let scaleX = containerWidth / imgWidth;
let scaleY = (containerWidth * (targetHeight / targetWidth)) / imgHeight;
cropperBaseZoom = Math.max(scaleX, scaleY);
zoomSlider.min = cropperBaseZoom;
zoomSlider.max = cropperBaseZoom * 5;
zoomSlider.value = cropperBaseZoom;
cropperPosX = 0;
cropperPosY = 0;
updateCropperTransform();
document.getElementById('cropper-popup-container').classList.add('show');
}
function updateCropperZoom() {
updateCropperTransform();
}
function updateCropperTransform() {
let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let imgEl = document.getElementById('cropper-image');
let container = document.getElementById('cropper-viewport-container');
if (cropperImageObj) {
let scaledW = cropperImageObj.width * zoom;
let scaledH = cropperImageObj.height * zoom;
let minX = container.clientWidth - scaledW;
let maxX = 0;
let minY = container.clientHeight - scaledH;
let maxY = 0;
if (minX > maxX) minX = maxX;
if (minY > maxY) minY = maxY;
if (cropperPosX < minX) cropperPosX = minX;
if (cropperPosX > maxX) cropperPosX = maxX;
if (cropperPosY < minY) cropperPosY = minY;
if (cropperPosY > maxY) cropperPosY = maxY;
}
imgEl.style.transform = `translate(${cropperPosX}px, ${cropperPosY}px) scale(${zoom})`;
}
function closeCropper() {
document.getElementById('cropper-popup-container').classList.remove('show');
}
async function confirmCropper() {
let zoom = parseFloat(document.getElementById('cropper-zoom').value);
let container = document.getElementById('cropper-viewport-container');
let b64;
let newSrc;
if (cropperOriginalType === 'image/gif') {
b64 = cropperOriginalB64.split(',')[1];
newSrc = cropperOriginalB64;
} else {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let targetWidth = cropperType === 'avatar' ? 512 : 988;
let targetHeight = cropperType === 'avatar' ? 512 : 400;
canvas.width = targetWidth;
canvas.height = targetHeight;
let scaleRatio = targetWidth / container.clientWidth;
let drawX = cropperPosX * scaleRatio;
let drawY = cropperPosY * scaleRatio;
let drawW = cropperImageObj.width * zoom * scaleRatio;
let drawH = cropperImageObj.height * zoom * scaleRatio;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, targetWidth, targetHeight);
ctx.drawImage(cropperImageObj, drawX, drawY, drawW, drawH);
b64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1];
newSrc = 'data:image/jpeg;base64,' + b64;
}
let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
let payload = { string1: payloadKey, string2: b64 };
showAction("action.uploading", "uploadmedia");
let res = await fetchEncrypted("user/storage/public/update", JSON.stringify(payload));
clearAction("uploadmedia");
if (res && res.startsWith("error:")) {
showBlahNotification(res);
} else {
showBlahNotification(`success:${cropperType}.updated`);
window.profileUpdateTimestamp = Date.now();
// Update all matching images on the page instantly
document.querySelectorAll('img').forEach(img => {
if (img.src.includes(`id=${window.id}&e=${payloadKey}`) || img.id === `settings-${cropperType}-preview` || (cropperType === 'avatar' && (img.id === 'my-avatar-img' || img.id === 'sidebar-pfp'))) {
img.src = newSrc;
}
});
}
closeCropper();
}
let container = document.getElementById('cropper-viewport-container');
if (container) {
const startDrag = (e) => {
isDraggingCropper = true;
startDragX = e.clientX || (e.touches && e.touches[0].clientX);
startDragY = e.clientY || (e.touches && e.touches[0].clientY);
startPosX = cropperPosX;
startPosY = cropperPosY;
if (e.type !== "touchstart") e.preventDefault();
};
const onDrag = (e) => {
if (!isDraggingCropper) return;
let clientX = e.clientX || (e.touches && e.touches[0].clientX);
let clientY = e.clientY || (e.touches && e.touches[0].clientY);
cropperPosX = startPosX + (clientX - startDragX);
cropperPosY = startPosY + (clientY - startDragY);
updateCropperTransform();
};
const endDrag = () => {
isDraggingCropper = false;
};
container.addEventListener('mousedown', startDrag);
container.addEventListener('touchstart', startDrag, {passive: true});
document.addEventListener('mousemove', onDrag);
document.addEventListener('touchmove', onDrag, {passive: true});
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
}