From cc41f252777945630c02a25ccf8e354703cff2b2 Mon Sep 17 00:00:00 2001 From: olcxja Date: Tue, 23 Jun 2026 23:41:27 +0200 Subject: [PATCH] Add settings and profile editor --- .../src/main/assets/public/blah/en-cat.json | 25 +- .../src/main/assets/public/blah/en-us.json | 26 +- .../src/main/assets/public/blah/index.json | 4 + android/app/src/main/assets/public/index.html | 22 ++ android/app/src/main/assets/public/main.js | 372 +++++++++++++++++- android/app/src/main/assets/public/screens.js | 102 ++++- android/app/src/main/assets/public/style.css | 107 +++-- webroot/blah/en-cat.json | 25 +- webroot/blah/en-us.json | 26 +- webroot/blah/index.json | 4 + webroot/index.html | 22 ++ webroot/main.js | 372 +++++++++++++++++- webroot/screens.js | 102 ++++- webroot/style.css | 107 +++-- 14 files changed, 1210 insertions(+), 106 deletions(-) create mode 100644 android/app/src/main/assets/public/blah/index.json create mode 100644 webroot/blah/index.json diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index 9364849d..1e6eb432 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -33,6 +33,9 @@ "username.conditions.allowed": "cat name can only include {all}", "password.not.hashed.properly": "meow word is not hashed properly", "invalid.username.or.password": "invalid cat name or meow word", + "error.account.not.exist": "Meowww???", + "error.storage.limit.exceeded": "Meow too big!", + "error.username.taken": "Meeeeeow", "account.not.exist": "cat with this name doesn't exist", "invalid.nonce": "invalid nonce. try again", "username.changed": "cat name changed successfully", @@ -150,5 +153,25 @@ "action.invite.send": "invite to meowchat", "action.profile.loading": "sniffing profile...", "error.profile.load.failed": "failed to sniff profile.", - "unknown.author": "somecat" + "unknown.author": "somecat", + "title.account": "Cat", + "title.delete.account": "Delete cat", + "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", + "title.avatar": "Cat picture", + "title.banner": "Cat banner", + "title.bio": "Cat bio", + "title.appearance": "Appearance", + "title.language": "Meownguage", + "action.save.profile": "Save cat profile", + "action.save.bio": "Save meow-io", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving meow-io...", + "action.account.saving": "Saving cat...", + "action.account.deleting": "Deleting cat...", + "success:profile.updated": "Cat profile updated successfully :3", + "success:avatar.updated": "Cat picture updated successfully :3", + "success:banner.updated": "Cat banner updated successfully :3", + "action.save": "Save :3", + "action.delete.account": "Delete cat", + "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3" } \ No newline at end of file diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index 4fd12f81..a6ca50bb 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -33,6 +33,9 @@ "username.conditions.allowed": "Username can only include {all}", "password.not.hashed.properly": "Password is not hashed properly", "invalid.username.or.password": "Invalid username or password", + "error.account.not.exist": "Account Does Not Exist", + "error.storage.limit.exceeded": "Storage Limit Exceeded", + "error.username.taken": "Username is Taken", "account.not.exist": "Account with this name doesn't exist", "invalid.nonce": "Invalid nonce. Try again", "username.changed": "Username changed successfully", @@ -143,6 +146,7 @@ "title.profile": "Profile", "title.settings": "Settings", "title.logout": "Log out", + "action.edit.profile": "Edit profile", "action.open.dm": "Open DM", "action.invite.accept": "Accept invite", @@ -150,5 +154,25 @@ "action.invite.send": "Invite to DM", "action.profile.loading": "Loading profile...", "error.profile.load.failed": "Failed to load profile.", - "unknown.author": "Someone" + "unknown.author": "Someone", + "title.account": "Account", + "title.delete.account": "Delete account", + "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", + "title.avatar": "Avatar", + "title.banner": "Banner", + "title.bio": "Bio", + "title.appearance": "Appearance", + "title.language": "Language", + "action.save.profile": "Save profile", + "action.save.bio": "Save bio", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving bio...", + "action.account.saving": "Saving account...", + "action.account.deleting": "Deleting account...", + "success:profile.updated": "Profile updated successfully", + "success:avatar.updated": "Avatar updated successfully", + "success:banner.updated": "Banner updated successfully", + "action.save": "Save", + "action.delete.account": "Delete account", + "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process." } \ No newline at end of file diff --git a/android/app/src/main/assets/public/blah/index.json b/android/app/src/main/assets/public/blah/index.json new file mode 100644 index 00000000..4571bb39 --- /dev/null +++ b/android/app/src/main/assets/public/blah/index.json @@ -0,0 +1,4 @@ +{ + "en-us": { "english": "English (US)", "native": "English (US)" }, + "en-cat": { "english": "English (Cat)", "native": "Meowlish" } +} diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html index 1aa1684a..1a84f2fb 100644 --- a/android/app/src/main/assets/public/index.html +++ b/android/app/src/main/assets/public/index.html @@ -7,6 +7,10 @@ + + + + @@ -89,6 +93,24 @@
+ +
+
+
+

Crop Image

+
+ +
+
+ Zoom + +
+
+ + +
+
+
diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index ad507860..aefe4545 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -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 = ``; 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 = ``; + actionBtnHtml = ``; } 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); } \ No newline at end of file diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index 1eb46ce0..ff0954f3 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -143,7 +143,7 @@ var chatScreen = `
- `; var backBtnHtml = ``; +var settingsBar = ` + + + +`; + +var accountSettings = ` +
+ + title.account +
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+
+ +

desc.delete.account

+ +
+
+`; + +var profileSettings = ` +
+ + title.profile +
+ +
+ +
+
title.avatar
+ + +
+
+
+
+
+ +
+
+
title.banner
+ + +
+
+
+ +
+ + +
+
+ +
+
+`; + +var appearanceSettings = ` +
+ + title.appearance +
+
+ + +
+
+`; diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index 626ab32d..fde60f9b 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -110,6 +110,9 @@ input, textarea { user-select: text !important; -webkit-user-select: text !important; } +textarea { + resize: none; +} .chat-message-text, .chat-message-author, .chat-message-timestamp, .profile-name, .profile-bio, .profile-id-host, @@ -810,15 +813,15 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-popup-content { position: relative; - width: 90vw; - max-width: 31rem; + width: 31em; height: 85vh; - max-height: 44rem; + max-height: 44em; + font-size: min(1rem, calc(90vw / 31)); background: var(--main-bg-color); border: var(--border-width) solid var(--light-border-color); - border-radius: 0.9rem; + border-radius: 0.9em; overflow: hidden; - box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5); + box-shadow: 0 1em 3em rgba(0, 0, 0, 0.5); opacity: 0; transform: scale(0.6); transition: all 0.2s ease; @@ -832,16 +835,16 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-popup-close-btn { position: absolute; - top: 1rem; - right: 1rem; + top: 1em; + right: 1em; z-index: 10; background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(0.25rem); - -webkit-backdrop-filter: blur(0.25rem); + backdrop-filter: blur(0.25em); + -webkit-backdrop-filter: blur(0.25em); border: none; - width: 2.5rem; - height: 2.5rem; - border-radius: 0.8rem; + width: 2.5em; + height: 2.5em; + border-radius: 0.8em; display: flex; justify-content: center; align-items: center; @@ -850,8 +853,8 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { cursor: pointer; } .profile-popup-close-btn svg { - width: 1.5rem; - height: 1.5rem; + width: 1.5em; + height: 1.5em; } .profile-popup-close-btn:hover { background: rgba(255, 255, 255, 0.2); @@ -865,62 +868,104 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-banner { width: 100%; - height: 12.5rem; + aspect-ratio: 494 / 200; background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.1)); object-fit: cover; flex-shrink: 0; } .profile-avatar-container { position: relative; - margin-top: -3.75rem; - margin-left: 2rem; - width: 7.5rem; - height: 7.5rem; + margin-top: -3.75em; + margin-left: 2em; + width: 7.5em; + height: 7.5em; } .profile-avatar { width: 100%; height: 100%; - border-radius: 0.8rem; + border-radius: 0.8em; border: var(--border-width) solid rgba(255, 255, 255, 0.2); object-fit: cover; background: var(--main-bg-color); } .profile-info { - padding: 1rem 2rem 2rem 2rem; + padding: 1em 2em 2em 2em; display: flex; flex-direction: column; - gap: 1rem; + gap: 1em; } .profile-name { - font-size: 2rem; + font-size: 2em; font-weight: 800; line-height: 1.1; - margin-bottom: 0.2rem; + margin-bottom: 0.2em; word-break: break-word; } .profile-id-host { - font-size: 1rem; + font-size: 1em; opacity: 0.6; font-weight: 600; } .profile-bio { - font-size: 1.05rem; + font-size: 1.05em; line-height: 1.5; white-space: pre-wrap; background: rgba(255, 255, 255, 0.03); - padding: 1rem; - border-radius: 0.8rem; + padding: 1em; + border-radius: 0.8em; border: var(--border-width) solid var(--light-border-color); word-break: break-word; } .profile-action-btn { align-self: flex-start; - padding: 0.6rem 1.5rem; + padding: 0.6em 1.5em; font-weight: 700; background: var(--text-color); color: var(--main-bg-color); - border-radius: 0.8rem; - margin-top: 0.5rem; + border-radius: 0.8em; + margin-top: 0.5em; margin-left: 0; height: auto; } + +/* Custom Zoom Slider Styling */ +#cropper-zoom { + -webkit-appearance: none; + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; + margin: 10px 0; +} + +#cropper-zoom::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-color); + cursor: pointer; + transition: transform 0.2s, background 0.2s; +} + +#cropper-zoom::-webkit-slider-thumb:hover { + transform: scale(1.2); + background: #fff; +} + +#cropper-zoom::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-color); + cursor: pointer; + transition: transform 0.2s, background 0.2s; + border: none; +} + +#cropper-zoom::-moz-range-thumb:hover { + transform: scale(1.2); + background: #fff; +} diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index 9364849d..1e6eb432 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -33,6 +33,9 @@ "username.conditions.allowed": "cat name can only include {all}", "password.not.hashed.properly": "meow word is not hashed properly", "invalid.username.or.password": "invalid cat name or meow word", + "error.account.not.exist": "Meowww???", + "error.storage.limit.exceeded": "Meow too big!", + "error.username.taken": "Meeeeeow", "account.not.exist": "cat with this name doesn't exist", "invalid.nonce": "invalid nonce. try again", "username.changed": "cat name changed successfully", @@ -150,5 +153,25 @@ "action.invite.send": "invite to meowchat", "action.profile.loading": "sniffing profile...", "error.profile.load.failed": "failed to sniff profile.", - "unknown.author": "somecat" + "unknown.author": "somecat", + "title.account": "Cat", + "title.delete.account": "Delete cat", + "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", + "title.avatar": "Cat picture", + "title.banner": "Cat banner", + "title.bio": "Cat bio", + "title.appearance": "Appearance", + "title.language": "Meownguage", + "action.save.profile": "Save cat profile", + "action.save.bio": "Save meow-io", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving meow-io...", + "action.account.saving": "Saving cat...", + "action.account.deleting": "Deleting cat...", + "success:profile.updated": "Cat profile updated successfully :3", + "success:avatar.updated": "Cat picture updated successfully :3", + "success:banner.updated": "Cat banner updated successfully :3", + "action.save": "Save :3", + "action.delete.account": "Delete cat", + "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3" } \ No newline at end of file diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 4fd12f81..a6ca50bb 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -33,6 +33,9 @@ "username.conditions.allowed": "Username can only include {all}", "password.not.hashed.properly": "Password is not hashed properly", "invalid.username.or.password": "Invalid username or password", + "error.account.not.exist": "Account Does Not Exist", + "error.storage.limit.exceeded": "Storage Limit Exceeded", + "error.username.taken": "Username is Taken", "account.not.exist": "Account with this name doesn't exist", "invalid.nonce": "Invalid nonce. Try again", "username.changed": "Username changed successfully", @@ -143,6 +146,7 @@ "title.profile": "Profile", "title.settings": "Settings", "title.logout": "Log out", + "action.edit.profile": "Edit profile", "action.open.dm": "Open DM", "action.invite.accept": "Accept invite", @@ -150,5 +154,25 @@ "action.invite.send": "Invite to DM", "action.profile.loading": "Loading profile...", "error.profile.load.failed": "Failed to load profile.", - "unknown.author": "Someone" + "unknown.author": "Someone", + "title.account": "Account", + "title.delete.account": "Delete account", + "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", + "title.avatar": "Avatar", + "title.banner": "Banner", + "title.bio": "Bio", + "title.appearance": "Appearance", + "title.language": "Language", + "action.save.profile": "Save profile", + "action.save.bio": "Save bio", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving bio...", + "action.account.saving": "Saving account...", + "action.account.deleting": "Deleting account...", + "success:profile.updated": "Profile updated successfully", + "success:avatar.updated": "Avatar updated successfully", + "success:banner.updated": "Banner updated successfully", + "action.save": "Save", + "action.delete.account": "Delete account", + "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process." } \ No newline at end of file diff --git a/webroot/blah/index.json b/webroot/blah/index.json new file mode 100644 index 00000000..4571bb39 --- /dev/null +++ b/webroot/blah/index.json @@ -0,0 +1,4 @@ +{ + "en-us": { "english": "English (US)", "native": "English (US)" }, + "en-cat": { "english": "English (Cat)", "native": "Meowlish" } +} diff --git a/webroot/index.html b/webroot/index.html index 1aa1684a..1a84f2fb 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -7,6 +7,10 @@ + + + + @@ -89,6 +93,24 @@
+ +
+
+
+

Crop Image

+
+ +
+
+ Zoom + +
+
+ + +
+
+
diff --git a/webroot/main.js b/webroot/main.js index ad507860..aefe4545 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -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 = ``; 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 = ``; + actionBtnHtml = ``; } 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); } \ No newline at end of file diff --git a/webroot/screens.js b/webroot/screens.js index 1eb46ce0..ff0954f3 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -143,7 +143,7 @@ var chatScreen = `
- `; var backBtnHtml = ``; +var settingsBar = ` + + + +`; + +var accountSettings = ` +
+ + title.account +
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+
+ +

desc.delete.account

+ +
+
+`; + +var profileSettings = ` +
+ + title.profile +
+ +
+ +
+
title.avatar
+ + +
+
+
+
+
+ +
+
+
title.banner
+ + +
+
+
+ +
+ + +
+
+ +
+
+`; + +var appearanceSettings = ` +
+ + title.appearance +
+
+ + +
+
+`; diff --git a/webroot/style.css b/webroot/style.css index 626ab32d..fde60f9b 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -110,6 +110,9 @@ input, textarea { user-select: text !important; -webkit-user-select: text !important; } +textarea { + resize: none; +} .chat-message-text, .chat-message-author, .chat-message-timestamp, .profile-name, .profile-bio, .profile-id-host, @@ -810,15 +813,15 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-popup-content { position: relative; - width: 90vw; - max-width: 31rem; + width: 31em; height: 85vh; - max-height: 44rem; + max-height: 44em; + font-size: min(1rem, calc(90vw / 31)); background: var(--main-bg-color); border: var(--border-width) solid var(--light-border-color); - border-radius: 0.9rem; + border-radius: 0.9em; overflow: hidden; - box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5); + box-shadow: 0 1em 3em rgba(0, 0, 0, 0.5); opacity: 0; transform: scale(0.6); transition: all 0.2s ease; @@ -832,16 +835,16 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-popup-close-btn { position: absolute; - top: 1rem; - right: 1rem; + top: 1em; + right: 1em; z-index: 10; background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(0.25rem); - -webkit-backdrop-filter: blur(0.25rem); + backdrop-filter: blur(0.25em); + -webkit-backdrop-filter: blur(0.25em); border: none; - width: 2.5rem; - height: 2.5rem; - border-radius: 0.8rem; + width: 2.5em; + height: 2.5em; + border-radius: 0.8em; display: flex; justify-content: center; align-items: center; @@ -850,8 +853,8 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { cursor: pointer; } .profile-popup-close-btn svg { - width: 1.5rem; - height: 1.5rem; + width: 1.5em; + height: 1.5em; } .profile-popup-close-btn:hover { background: rgba(255, 255, 255, 0.2); @@ -865,62 +868,104 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) { } .profile-banner { width: 100%; - height: 12.5rem; + aspect-ratio: 494 / 200; background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.1)); object-fit: cover; flex-shrink: 0; } .profile-avatar-container { position: relative; - margin-top: -3.75rem; - margin-left: 2rem; - width: 7.5rem; - height: 7.5rem; + margin-top: -3.75em; + margin-left: 2em; + width: 7.5em; + height: 7.5em; } .profile-avatar { width: 100%; height: 100%; - border-radius: 0.8rem; + border-radius: 0.8em; border: var(--border-width) solid rgba(255, 255, 255, 0.2); object-fit: cover; background: var(--main-bg-color); } .profile-info { - padding: 1rem 2rem 2rem 2rem; + padding: 1em 2em 2em 2em; display: flex; flex-direction: column; - gap: 1rem; + gap: 1em; } .profile-name { - font-size: 2rem; + font-size: 2em; font-weight: 800; line-height: 1.1; - margin-bottom: 0.2rem; + margin-bottom: 0.2em; word-break: break-word; } .profile-id-host { - font-size: 1rem; + font-size: 1em; opacity: 0.6; font-weight: 600; } .profile-bio { - font-size: 1.05rem; + font-size: 1.05em; line-height: 1.5; white-space: pre-wrap; background: rgba(255, 255, 255, 0.03); - padding: 1rem; - border-radius: 0.8rem; + padding: 1em; + border-radius: 0.8em; border: var(--border-width) solid var(--light-border-color); word-break: break-word; } .profile-action-btn { align-self: flex-start; - padding: 0.6rem 1.5rem; + padding: 0.6em 1.5em; font-weight: 700; background: var(--text-color); color: var(--main-bg-color); - border-radius: 0.8rem; - margin-top: 0.5rem; + border-radius: 0.8em; + margin-top: 0.5em; margin-left: 0; height: auto; } + +/* Custom Zoom Slider Styling */ +#cropper-zoom { + -webkit-appearance: none; + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; + margin: 10px 0; +} + +#cropper-zoom::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-color); + cursor: pointer; + transition: transform 0.2s, background 0.2s; +} + +#cropper-zoom::-webkit-slider-thumb:hover { + transform: scale(1.2); + background: #fff; +} + +#cropper-zoom::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-color); + cursor: pointer; + transition: transform 0.2s, background 0.2s; + border: none; +} + +#cropper-zoom::-moz-range-thumb:hover { + transform: scale(1.2); + background: #fff; +}