Add settings and profile editor
This commit is contained in:
parent
80f11f782e
commit
cc41f25277
14 changed files with 1210 additions and 106 deletions
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue