add better loading for profile settings and user profiles & add details bar
This commit is contained in:
parent
95aaaa69ea
commit
271c573fb4
16 changed files with 504 additions and 174 deletions
|
|
@ -120,6 +120,9 @@
|
||||||
"title.captcha.code": "captcha code",
|
"title.captcha.code": "captcha code",
|
||||||
"title.invitation.code": "invitation code",
|
"title.invitation.code": "invitation code",
|
||||||
"title.back.to.register": "back to registration",
|
"title.back.to.register": "back to registration",
|
||||||
|
"title.details": "details",
|
||||||
|
"title.members": "cats",
|
||||||
|
|
||||||
"placeholder.username": "cat name",
|
"placeholder.username": "cat name",
|
||||||
"placeholder.captcha.code": "captcha code",
|
"placeholder.captcha.code": "captcha code",
|
||||||
"desc.messages.loading": "loading meows...",
|
"desc.messages.loading": "loading meows...",
|
||||||
|
|
@ -151,6 +154,7 @@
|
||||||
"title.profile": "cat profile",
|
"title.profile": "cat profile",
|
||||||
"title.settings": "cat settings",
|
"title.settings": "cat settings",
|
||||||
"title.logout": "log out",
|
"title.logout": "log out",
|
||||||
|
"title.loading": "sniffing...",
|
||||||
"action.edit.profile": "edit cat profile",
|
"action.edit.profile": "edit cat profile",
|
||||||
"action.open.dm": "open direct meowchat",
|
"action.open.dm": "open direct meowchat",
|
||||||
"action.invite.accept": "accept meow invite",
|
"action.invite.accept": "accept meow invite",
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,10 @@
|
||||||
"title.profile": "Profile",
|
"title.profile": "Profile",
|
||||||
"title.settings": "Settings",
|
"title.settings": "Settings",
|
||||||
"title.logout": "Log out",
|
"title.logout": "Log out",
|
||||||
|
"title.loading": "Loading...",
|
||||||
|
"title.details": "Details",
|
||||||
|
"title.members": "Members",
|
||||||
|
|
||||||
"action.edit.profile": "Edit profile",
|
"action.edit.profile": "Edit profile",
|
||||||
"action.open.dm": "Open DM",
|
"action.open.dm": "Open DM",
|
||||||
"action.invite.accept": "Accept invite",
|
"action.invite.accept": "Accept invite",
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
<script src="crypto-pq.js"></script>
|
<script src="crypto-pq.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/eruda"></script> <!-- debug -->
|
<!-- debug
|
||||||
<script>eruda.init();</script>
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>eruda.init();</script> -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1266,7 +1266,7 @@ var currentRoomContentHtml = null;
|
||||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 55 * rem && !skipMobileSlide) {
|
||||||
if (title != "title.splash") { //do not show splash on mobile
|
if (title != "title.splash") { //do not show splash on mobile
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
|
|
@ -1286,7 +1286,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
roomsTopBarTransition.style.opacity = "0";
|
roomsTopBarTransition.style.opacity = "0";
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 55 * rem && !skipMobileSlide) {
|
||||||
if (title != "title.splash") //do not show splash on mobile
|
if (title != "title.splash") //do not show splash on mobile
|
||||||
{
|
{
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
|
|
@ -1298,8 +1298,9 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
|
|
||||||
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
|
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
|
||||||
|
let pinCallBtns = showRoomBar ? `${pinBtnHtml}${callBtnHtml}` : '';
|
||||||
|
|
||||||
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${detailsBtnHtml}`;
|
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}${detailsBtnHtml}`;
|
||||||
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
|
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
|
||||||
|
|
||||||
let parser = new DOMParser();
|
let parser = new DOMParser();
|
||||||
|
|
@ -1350,12 +1351,13 @@ async function switchDetailsContent(title, content) {
|
||||||
roomsTopBarTransition.style.opacity = "0";
|
roomsTopBarTransition.style.opacity = "0";
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
roomsTopBarTransition.innerHTML = processBlah(title);
|
let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`;
|
||||||
|
roomsTopBarTransition.innerHTML = `<inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}`;
|
||||||
|
|
||||||
let parser = new DOMParser();
|
let parser = new DOMParser();
|
||||||
let doc = parser.parseFromString(content, "text/html");
|
let doc = parser.parseFromString(content, "text/html");
|
||||||
|
|
@ -1461,8 +1463,8 @@ document.addEventListener("click", (e) => {
|
||||||
|
|
||||||
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
|
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem) {
|
if (window.innerWidth <= 55 * rem) {
|
||||||
if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right')) {
|
if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right') && !e.target.closest('.profile-popup-container')) {
|
||||||
mobileNavBack();
|
mobileNavBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1527,15 +1529,7 @@ async function openProfile(accountId) {
|
||||||
let popupContainer = document.getElementById("profile-popup-container");
|
let popupContainer = document.getElementById("profile-popup-container");
|
||||||
let popupInner = document.getElementById("profile-popup-inner");
|
let popupInner = document.getElementById("profile-popup-inner");
|
||||||
|
|
||||||
//show loading
|
showAction('action.profile.loading', 'profile-loading');
|
||||||
popupInner.innerHTML = `<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>${processBlah('action.profile.loading')}</p></div>`;
|
|
||||||
popupContainer.style.display = "flex";
|
|
||||||
|
|
||||||
//force reflow
|
|
||||||
popupContainer.offsetHeight;
|
|
||||||
|
|
||||||
popupContainer.classList.add("show");
|
|
||||||
isProfileOpen = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!accountId) accountId = window.id; //var fallback
|
if (!accountId) accountId = window.id; //var fallback
|
||||||
|
|
@ -1644,8 +1638,15 @@ async function openProfile(accountId) {
|
||||||
${actionBtnHtml}
|
${actionBtnHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
clearAction('profile-loading');
|
||||||
|
popupContainer.style.display = "flex";
|
||||||
|
popupContainer.offsetHeight;
|
||||||
|
popupContainer.classList.add("show");
|
||||||
|
isProfileOpen = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popupInner.innerHTML = `<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>${processBlah('error.profile.load.failed')}</p></div>`;
|
clearAction('profile-loading');
|
||||||
|
showNotification(processBlah('error.profile.load.failed'), 'error');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1884,7 +1885,7 @@ async function gotoInbox() {
|
||||||
await switchRoomsBar("title.inbox", inboxRoomBar);
|
await switchRoomsBar("title.inbox", inboxRoomBar);
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
gotoInvites();
|
gotoInvites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2055,7 +2056,7 @@ document.addEventListener('contextmenu', e => {
|
||||||
|
|
||||||
function handleMobileSwipe() {
|
function handleMobileSwipe() {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) return;
|
if (window.innerWidth > 55 * rem) return;
|
||||||
|
|
||||||
let diffX = touchEndX - touchStartX;
|
let diffX = touchEndX - touchStartX;
|
||||||
let diffY = touchEndY - touchStartY;
|
let diffY = touchEndY - touchStartY;
|
||||||
|
|
@ -2151,6 +2152,38 @@ function escapeHtml(unsafe) {
|
||||||
.replaceAll(/'/g, "'");
|
.replaceAll(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateDmMembersHtml(targetId, targetUsername) {
|
||||||
|
let myIdStr = id + ";" + host;
|
||||||
|
let myUsername = await fetchAsync(`${url}/idtoname?id=${myIdStr}`);
|
||||||
|
let myPfp = await getAvatarUrl(myIdStr, myUsername);
|
||||||
|
|
||||||
|
let targetPfp = await getAvatarUrl(targetId, targetUsername);
|
||||||
|
let targetDisplayName = targetUsername.split(':')[0];
|
||||||
|
let myDisplayName = myUsername.split(':')[0];
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="list-container no-flex-grow" style="padding: 0 0.5rem; width: 100%;">
|
||||||
|
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%;">
|
||||||
|
<span style="padding-left: 0.6rem; font-size: 0.8rem; font-weight: bold; opacity: 0.5; text-transform: uppercase;"><blah>title.members</blah></span>
|
||||||
|
</div>
|
||||||
|
<button class="room-entry" onclick="openProfile('${targetId}')" style="margin-bottom: 0;">
|
||||||
|
<img src="${targetPfp}" class="room-pfp">
|
||||||
|
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||||
|
<span class="room-name">${targetDisplayName}</span>
|
||||||
|
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="room-entry" onclick="openProfile('${myIdStr}')" style="margin-bottom: 0;">
|
||||||
|
<img src="${myPfp}" class="room-pfp">
|
||||||
|
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||||
|
<span class="room-name">${myDisplayName}</span>
|
||||||
|
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${myUsername}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function openDm(dmId, username, targetId) {
|
async function openDm(dmId, username, targetId) {
|
||||||
try {
|
try {
|
||||||
showAction("action.dm.opening", "dmopen");
|
showAction("action.dm.opening", "dmopen");
|
||||||
|
|
@ -2172,6 +2205,8 @@ async function openDm(dmId, username, targetId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
||||||
|
let membersHtml = await generateDmMembersHtml(targetId, username);
|
||||||
|
await switchDetailsContent("title.details", membersHtml);
|
||||||
|
|
||||||
let msgContainer = document.getElementById("chat-messages");
|
let msgContainer = document.getElementById("chat-messages");
|
||||||
if (msgContainer) {
|
if (msgContainer) {
|
||||||
|
|
@ -2914,37 +2949,44 @@ async function gotoSettings(tab = 'account') {
|
||||||
await switchRoomsBar("title.settings", settingsBar);
|
await switchRoomsBar("title.settings", settingsBar);
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
switchSettingsTab(tab);
|
switchSettingsTab(tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchSettingsTab(tabName) {
|
async function switchSettingsTab(tabName) {
|
||||||
setActiveRoombarItem('tab-settings-' + tabName);
|
setActiveRoombarItem('tab-settings-' + tabName);
|
||||||
if (tabName === 'account') {
|
if (tabName === 'account') {
|
||||||
switchRoomContent("title.account", accountSettings, false);
|
await switchRoomContent("title.account", accountSettings, false);
|
||||||
|
let userEl = document.getElementById("settings-username");
|
||||||
|
if (userEl && window.username) userEl.value = window.username;
|
||||||
} else if (tabName === 'profile') {
|
} else if (tabName === 'profile') {
|
||||||
switchRoomContent("title.profile", profileSettings, false);
|
showAction('title.loading', 'settings-loading');
|
||||||
setTimeout(async () => {
|
|
||||||
|
try {
|
||||||
let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
|
let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
|
||||||
|
let bannerUrl = `${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.banner`;
|
||||||
|
if (window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
|
||||||
|
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
|
||||||
|
|
||||||
|
clearAction('settings-loading');
|
||||||
|
await switchRoomContent("title.profile", profileSettings, false);
|
||||||
|
|
||||||
let pfpEl = document.getElementById("settings-avatar-preview");
|
let pfpEl = document.getElementById("settings-avatar-preview");
|
||||||
if (pfpEl) pfpEl.src = pfpUrl;
|
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");
|
let bannerEl = document.getElementById("settings-banner-preview");
|
||||||
if (bannerEl) bannerEl.src = bannerUrl;
|
if (bannerEl) bannerEl.src = bannerUrl;
|
||||||
|
|
||||||
let bioEl = document.getElementById("settings-bio");
|
let bioEl = document.getElementById("settings-bio");
|
||||||
if (bioEl) {
|
if (bioEl && !bio.startsWith("error:")) bioEl.value = bio;
|
||||||
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
|
} catch (e) {
|
||||||
if (!bio.startsWith("error:")) bioEl.value = bio;
|
clearAction('settings-loading');
|
||||||
}
|
await switchRoomContent("title.profile", profileSettings, false);
|
||||||
}, 210);
|
}
|
||||||
} else if (tabName === 'appearance') {
|
} else if (tabName === 'appearance') {
|
||||||
switchRoomContent("title.appearance", appearanceSettings, false);
|
await switchRoomContent("title.appearance", appearanceSettings, false);
|
||||||
setTimeout(populateLanguageDropdown, 210);
|
populateLanguageDropdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3096,10 +3138,12 @@ function initCropper() {
|
||||||
// Set aspect ratio
|
// Set aspect ratio
|
||||||
let targetWidth = cropperType === 'avatar' ? 512 : 494;
|
let targetWidth = cropperType === 'avatar' ? 512 : 494;
|
||||||
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
||||||
|
let aspectRatio = targetWidth / targetHeight;
|
||||||
|
|
||||||
container.style.height = 'auto';
|
container.style.height = 'auto';
|
||||||
container.style.maxHeight = 'calc(85vh - 16rem)';
|
container.style.maxHeight = 'calc(85vh - 16rem)';
|
||||||
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
|
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
|
||||||
|
container.style.maxWidth = `min(30rem, calc((85vh - 16rem) * ${aspectRatio}))`;
|
||||||
|
|
||||||
// Force layout recalculation
|
// Force layout recalculation
|
||||||
void container.offsetHeight;
|
void container.offsetHeight;
|
||||||
|
|
@ -3171,63 +3215,119 @@ async function confirmCropper() {
|
||||||
|
|
||||||
let b64;
|
let b64;
|
||||||
let newSrc;
|
let newSrc;
|
||||||
|
let isAnimated = false;
|
||||||
|
let gifFrames = null;
|
||||||
|
let webpDecoded = null;
|
||||||
|
let parsedGif = null;
|
||||||
|
|
||||||
if (cropperOriginalType === 'image/gif') {
|
if (cropperOriginalType === 'image/gif') {
|
||||||
|
isAnimated = true;
|
||||||
|
} else if (cropperOriginalType === 'image/webp') {
|
||||||
showAction("action.processing", "uploadmedia");
|
showAction("action.processing", "uploadmedia");
|
||||||
try {
|
try {
|
||||||
let res = await fetch(cropperOriginalB64);
|
let res = await fetch(cropperOriginalB64);
|
||||||
let buffer = await res.arrayBuffer();
|
let buffer = await res.arrayBuffer();
|
||||||
let parsedGif = gifuct.parseGIF(buffer);
|
let webpWorker = new Worker('webp-worker.js', { type: 'module' });
|
||||||
let frames = gifuct.decompressFrames(parsedGif, true);
|
webpDecoded = await new Promise((resolve, reject) => {
|
||||||
let webpFrames = [];
|
webpWorker.onmessage = (e) => {
|
||||||
|
if (e.data.success) resolve(e.data.data);
|
||||||
|
else reject(new Error(e.data.error));
|
||||||
|
webpWorker.terminate();
|
||||||
|
};
|
||||||
|
webpWorker.onerror = (err) => { reject(err); webpWorker.terminate(); };
|
||||||
|
webpWorker.postMessage({ action: 'decode', data: new Uint8Array(buffer) });
|
||||||
|
});
|
||||||
|
if (webpDecoded && webpDecoded.length > 1) {
|
||||||
|
isAnimated = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("WebP decode check failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnimated) {
|
||||||
|
showAction("action.processing", "uploadmedia");
|
||||||
|
try {
|
||||||
|
let webpFrames = [];
|
||||||
let tempCanvas = document.createElement('canvas');
|
let tempCanvas = document.createElement('canvas');
|
||||||
let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
tempCanvas.width = parsedGif.lsd.width;
|
|
||||||
tempCanvas.height = parsedGif.lsd.height;
|
|
||||||
|
|
||||||
let gifCanvas = document.createElement('canvas');
|
let gifCanvas = document.createElement('canvas');
|
||||||
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
|
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
gifCanvas.width = targetWidth;
|
gifCanvas.width = targetWidth;
|
||||||
gifCanvas.height = targetHeight;
|
gifCanvas.height = targetHeight;
|
||||||
|
|
||||||
let frameImageData = null;
|
|
||||||
let previousImageData = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < frames.length; i++) {
|
|
||||||
let frame = frames[i];
|
|
||||||
|
|
||||||
if (frame.disposalType === 3) {
|
|
||||||
previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
|
|
||||||
frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
frameImageData.data.set(frame.patch);
|
|
||||||
|
|
||||||
let patchCanvas = document.createElement('canvas');
|
|
||||||
patchCanvas.width = frame.dims.width;
|
|
||||||
patchCanvas.height = frame.dims.height;
|
|
||||||
patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
|
|
||||||
|
|
||||||
tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
|
|
||||||
|
|
||||||
gifCtx.fillStyle = '#000';
|
|
||||||
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
|
||||||
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
|
||||||
|
|
||||||
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
|
||||||
webpFrames.push({
|
|
||||||
data: frameImgData.data,
|
|
||||||
duration: Math.max(20, frame.delay || 0),
|
|
||||||
config: { lossless: 0, quality: 90 }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (frame.disposalType === 2) {
|
if (cropperOriginalType === 'image/gif') {
|
||||||
tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
|
let res = await fetch(cropperOriginalB64);
|
||||||
} else if (frame.disposalType === 3 && previousImageData) {
|
let buffer = await res.arrayBuffer();
|
||||||
tempCtx.putImageData(previousImageData, 0, 0);
|
parsedGif = gifuct.parseGIF(buffer);
|
||||||
|
gifFrames = gifuct.decompressFrames(parsedGif, true);
|
||||||
|
|
||||||
|
tempCanvas.width = parsedGif.lsd.width;
|
||||||
|
tempCanvas.height = parsedGif.lsd.height;
|
||||||
|
|
||||||
|
let frameImageData = null;
|
||||||
|
let previousImageData = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < gifFrames.length; i++) {
|
||||||
|
let frame = gifFrames[i];
|
||||||
|
|
||||||
|
if (frame.disposalType === 3) {
|
||||||
|
previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
|
||||||
|
frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameImageData.data.set(frame.patch);
|
||||||
|
|
||||||
|
let patchCanvas = document.createElement('canvas');
|
||||||
|
patchCanvas.width = frame.dims.width;
|
||||||
|
patchCanvas.height = frame.dims.height;
|
||||||
|
patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
|
||||||
|
|
||||||
|
tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
|
||||||
|
|
||||||
|
gifCtx.fillStyle = '#000';
|
||||||
|
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
||||||
|
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
||||||
|
|
||||||
|
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
webpFrames.push({
|
||||||
|
data: frameImgData.data,
|
||||||
|
duration: Math.max(20, frame.delay || 0),
|
||||||
|
config: { lossless: 0, quality: 90 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (frame.disposalType === 2) {
|
||||||
|
tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
|
||||||
|
} else if (frame.disposalType === 3 && previousImageData) {
|
||||||
|
tempCtx.putImageData(previousImageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cropperOriginalType === 'image/webp' && webpDecoded) {
|
||||||
|
tempCanvas.width = webpDecoded[0].width;
|
||||||
|
tempCanvas.height = webpDecoded[0].height;
|
||||||
|
for (let i = 0; i < webpDecoded.length; i++) {
|
||||||
|
let frame = webpDecoded[i];
|
||||||
|
let frameImageData = new ImageData(
|
||||||
|
new Uint8ClampedArray(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength),
|
||||||
|
frame.width,
|
||||||
|
frame.height
|
||||||
|
);
|
||||||
|
tempCtx.putImageData(frameImageData, 0, 0);
|
||||||
|
|
||||||
|
gifCtx.fillStyle = '#000';
|
||||||
|
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
||||||
|
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
||||||
|
|
||||||
|
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
webpFrames.push({
|
||||||
|
data: frameImgData.data,
|
||||||
|
duration: Math.max(20, frame.duration || 0),
|
||||||
|
config: { lossless: 0, quality: 90 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3245,7 +3345,7 @@ async function confirmCropper() {
|
||||||
reject(err);
|
reject(err);
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
};
|
};
|
||||||
worker.postMessage({ targetWidth, targetHeight, webpFrames });
|
worker.postMessage({ action: 'encode', targetWidth, targetHeight, webpFrames });
|
||||||
});
|
});
|
||||||
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
|
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
|
||||||
|
|
||||||
|
|
@ -3258,7 +3358,7 @@ async function confirmCropper() {
|
||||||
});
|
});
|
||||||
newSrc = 'data:image/webp;base64,' + b64;
|
newSrc = 'data:image/webp;base64,' + b64;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("GIF crop failed", e);
|
console.error("Animated crop failed", e);
|
||||||
b64 = cropperOriginalB64.split(',')[1];
|
b64 = cropperOriginalB64.split(',')[1];
|
||||||
newSrc = cropperOriginalB64;
|
newSrc = cropperOriginalB64;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ var messageContextMenu = `
|
||||||
//elements
|
//elements
|
||||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||||
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;
|
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;
|
||||||
|
var pinBtnHtml = `<button class="topbar-action-btn room-action-pin right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280q0-33 23.5-56.5T400-840h160q33 0 56.5 23.5T640-760v280Z"/></svg></button>`;
|
||||||
|
var callBtnHtml = `<button class="topbar-action-btn room-action-call right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12Z"/></svg></button>`;
|
||||||
|
|
||||||
var settingsBar = `
|
var settingsBar = `
|
||||||
<div class="sidebar-section-header">
|
<div class="sidebar-section-header">
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,8 @@ roomtopbar {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: var(--border-width) solid var(--light-border-color);
|
border-bottom: var(--border-width) solid var(--light-border-color);
|
||||||
|
|
@ -414,6 +416,33 @@ space{
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-action-btn {
|
||||||
|
display: flex;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 2.4rem;
|
||||||
|
width: 2.4rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.topbar-action-btn:active, .topbar-action-btn.is-pressed {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: var(--press-scale);
|
||||||
|
}
|
||||||
|
.topbar-action-btn.right {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
.topbar-action-btn svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
.mobile-nav-btn {
|
.mobile-nav-btn {
|
||||||
display: none;
|
display: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -435,7 +464,7 @@ space{
|
||||||
}
|
}
|
||||||
.mobile-nav-btn.right {
|
.mobile-nav-btn.right {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: 0.8rem;
|
margin-left: 0.2rem;
|
||||||
}
|
}
|
||||||
.mobile-nav-btn svg {
|
.mobile-nav-btn svg {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
|
@ -444,7 +473,7 @@ space{
|
||||||
.flex-spacer {
|
.flex-spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
@media (max-width: 52rem) {
|
@media (max-width: 55rem) {
|
||||||
.mobile-nav-btn {
|
.mobile-nav-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -977,3 +1006,22 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roomcontent roomtopbar .room-action-pin,
|
||||||
|
roomcontent roomtopbar .room-action-call {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#roomdetailsbar roomtopbar .room-action-pin,
|
||||||
|
#roomdetailsbar roomtopbar .room-action-call {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 65rem) and (min-width: 55.001rem) {
|
||||||
|
roomcontent roomtopbar .room-action-pin { display: none !important; }
|
||||||
|
#roomdetailsbar roomtopbar .room-action-pin { display: flex !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 58rem) and (min-width: 55.001rem) {
|
||||||
|
roomcontent roomtopbar .room-action-call { display: none !important; }
|
||||||
|
#roomdetailsbar roomtopbar .room-action-call { display: flex !important; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { encodeAnimation } from './wasm-webp/index.js';
|
import { encodeAnimation, decodeAnimation } from './wasm-webp/index.js';
|
||||||
|
|
||||||
self.onmessage = async (e) => {
|
self.onmessage = async (e) => {
|
||||||
let { targetWidth, targetHeight, webpFrames } = e.data;
|
let action = e.data.action || 'encode';
|
||||||
try {
|
try {
|
||||||
let webpUint8 = await encodeAnimation(targetWidth, targetHeight, true, webpFrames);
|
if (action === 'decode') {
|
||||||
self.postMessage({ success: true, data: webpUint8 }, [webpUint8.buffer]);
|
let decoded = await decodeAnimation(e.data.data, true);
|
||||||
|
let transferables = decoded.map(f => f.data.buffer);
|
||||||
|
self.postMessage({ success: true, data: decoded, action }, transferables);
|
||||||
|
} else {
|
||||||
|
let { targetWidth, targetHeight, webpFrames } = e.data;
|
||||||
|
let webpUint8 = await encodeAnimation(targetWidth, targetHeight, true, webpFrames);
|
||||||
|
self.postMessage({ success: true, data: webpUint8, action }, [webpUint8.buffer]);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.postMessage({ success: false, error: err.toString() });
|
self.postMessage({ success: false, error: err.toString(), action });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
icons/icon.png
BIN
icons/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -120,6 +120,9 @@
|
||||||
"title.captcha.code": "captcha code",
|
"title.captcha.code": "captcha code",
|
||||||
"title.invitation.code": "invitation code",
|
"title.invitation.code": "invitation code",
|
||||||
"title.back.to.register": "back to registration",
|
"title.back.to.register": "back to registration",
|
||||||
|
"title.details": "details",
|
||||||
|
"title.members": "cats",
|
||||||
|
|
||||||
"placeholder.username": "cat name",
|
"placeholder.username": "cat name",
|
||||||
"placeholder.captcha.code": "captcha code",
|
"placeholder.captcha.code": "captcha code",
|
||||||
"desc.messages.loading": "loading meows...",
|
"desc.messages.loading": "loading meows...",
|
||||||
|
|
@ -151,6 +154,7 @@
|
||||||
"title.profile": "cat profile",
|
"title.profile": "cat profile",
|
||||||
"title.settings": "cat settings",
|
"title.settings": "cat settings",
|
||||||
"title.logout": "log out",
|
"title.logout": "log out",
|
||||||
|
"title.loading": "sniffing...",
|
||||||
"action.edit.profile": "edit cat profile",
|
"action.edit.profile": "edit cat profile",
|
||||||
"action.open.dm": "open direct meowchat",
|
"action.open.dm": "open direct meowchat",
|
||||||
"action.invite.accept": "accept meow invite",
|
"action.invite.accept": "accept meow invite",
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,10 @@
|
||||||
"title.profile": "Profile",
|
"title.profile": "Profile",
|
||||||
"title.settings": "Settings",
|
"title.settings": "Settings",
|
||||||
"title.logout": "Log out",
|
"title.logout": "Log out",
|
||||||
|
"title.loading": "Loading...",
|
||||||
|
"title.details": "Details",
|
||||||
|
"title.members": "Members",
|
||||||
|
|
||||||
"action.edit.profile": "Edit profile",
|
"action.edit.profile": "Edit profile",
|
||||||
"action.open.dm": "Open DM",
|
"action.open.dm": "Open DM",
|
||||||
"action.invite.accept": "Accept invite",
|
"action.invite.accept": "Accept invite",
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
<script src="crypto-pq.js"></script>
|
<script src="crypto-pq.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/eruda"></script> <!-- debug -->
|
<!-- debug
|
||||||
<script>eruda.init();</script>
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>eruda.init();</script> -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
258
webroot/main.js
258
webroot/main.js
|
|
@ -1266,7 +1266,7 @@ var currentRoomContentHtml = null;
|
||||||
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
|
||||||
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 55 * rem && !skipMobileSlide) {
|
||||||
if (title != "title.splash") { //do not show splash on mobile
|
if (title != "title.splash") { //do not show splash on mobile
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
mainScreen.classList.add('mobile-content');
|
mainScreen.classList.add('mobile-content');
|
||||||
|
|
@ -1286,7 +1286,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
roomsTopBarTransition.style.opacity = "0";
|
roomsTopBarTransition.style.opacity = "0";
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
|
if (window.innerWidth <= 55 * rem && !skipMobileSlide) {
|
||||||
if (title != "title.splash") //do not show splash on mobile
|
if (title != "title.splash") //do not show splash on mobile
|
||||||
{
|
{
|
||||||
mainScreen.classList.remove('mobile-details');
|
mainScreen.classList.remove('mobile-details');
|
||||||
|
|
@ -1298,8 +1298,9 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
|
||||||
|
|
||||||
|
|
||||||
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
|
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
|
||||||
|
let pinCallBtns = showRoomBar ? `${pinBtnHtml}${callBtnHtml}` : '';
|
||||||
|
|
||||||
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${detailsBtnHtml}`;
|
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}<space></space><inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}${detailsBtnHtml}`;
|
||||||
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
|
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
|
||||||
|
|
||||||
let parser = new DOMParser();
|
let parser = new DOMParser();
|
||||||
|
|
@ -1350,12 +1351,13 @@ async function switchDetailsContent(title, content) {
|
||||||
roomsTopBarTransition.style.opacity = "0";
|
roomsTopBarTransition.style.opacity = "0";
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
roomsTopBarTransition.innerHTML = processBlah(title);
|
let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`;
|
||||||
|
roomsTopBarTransition.innerHTML = `<inherit>${processBlah(title)}</inherit><div class="flex-spacer"></div>${pinCallBtns}`;
|
||||||
|
|
||||||
let parser = new DOMParser();
|
let parser = new DOMParser();
|
||||||
let doc = parser.parseFromString(content, "text/html");
|
let doc = parser.parseFromString(content, "text/html");
|
||||||
|
|
@ -1461,8 +1463,8 @@ document.addEventListener("click", (e) => {
|
||||||
|
|
||||||
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
|
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth <= 52 * rem) {
|
if (window.innerWidth <= 55 * rem) {
|
||||||
if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right')) {
|
if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right') && !e.target.closest('.profile-popup-container')) {
|
||||||
mobileNavBack();
|
mobileNavBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1527,15 +1529,7 @@ async function openProfile(accountId) {
|
||||||
let popupContainer = document.getElementById("profile-popup-container");
|
let popupContainer = document.getElementById("profile-popup-container");
|
||||||
let popupInner = document.getElementById("profile-popup-inner");
|
let popupInner = document.getElementById("profile-popup-inner");
|
||||||
|
|
||||||
//show loading
|
showAction('action.profile.loading', 'profile-loading');
|
||||||
popupInner.innerHTML = `<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>${processBlah('action.profile.loading')}</p></div>`;
|
|
||||||
popupContainer.style.display = "flex";
|
|
||||||
|
|
||||||
//force reflow
|
|
||||||
popupContainer.offsetHeight;
|
|
||||||
|
|
||||||
popupContainer.classList.add("show");
|
|
||||||
isProfileOpen = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!accountId) accountId = window.id; //var fallback
|
if (!accountId) accountId = window.id; //var fallback
|
||||||
|
|
@ -1644,8 +1638,15 @@ async function openProfile(accountId) {
|
||||||
${actionBtnHtml}
|
${actionBtnHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
clearAction('profile-loading');
|
||||||
|
popupContainer.style.display = "flex";
|
||||||
|
popupContainer.offsetHeight;
|
||||||
|
popupContainer.classList.add("show");
|
||||||
|
isProfileOpen = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popupInner.innerHTML = `<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>${processBlah('error.profile.load.failed')}</p></div>`;
|
clearAction('profile-loading');
|
||||||
|
showNotification(processBlah('error.profile.load.failed'), 'error');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1884,7 +1885,7 @@ async function gotoInbox() {
|
||||||
await switchRoomsBar("title.inbox", inboxRoomBar);
|
await switchRoomsBar("title.inbox", inboxRoomBar);
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
gotoInvites();
|
gotoInvites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2055,7 +2056,7 @@ document.addEventListener('contextmenu', e => {
|
||||||
|
|
||||||
function handleMobileSwipe() {
|
function handleMobileSwipe() {
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) return;
|
if (window.innerWidth > 55 * rem) return;
|
||||||
|
|
||||||
let diffX = touchEndX - touchStartX;
|
let diffX = touchEndX - touchStartX;
|
||||||
let diffY = touchEndY - touchStartY;
|
let diffY = touchEndY - touchStartY;
|
||||||
|
|
@ -2151,6 +2152,38 @@ function escapeHtml(unsafe) {
|
||||||
.replaceAll(/'/g, "'");
|
.replaceAll(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateDmMembersHtml(targetId, targetUsername) {
|
||||||
|
let myIdStr = id + ";" + host;
|
||||||
|
let myUsername = await fetchAsync(`${url}/idtoname?id=${myIdStr}`);
|
||||||
|
let myPfp = await getAvatarUrl(myIdStr, myUsername);
|
||||||
|
|
||||||
|
let targetPfp = await getAvatarUrl(targetId, targetUsername);
|
||||||
|
let targetDisplayName = targetUsername.split(':')[0];
|
||||||
|
let myDisplayName = myUsername.split(':')[0];
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="list-container no-flex-grow" style="padding: 0 0.5rem; width: 100%;">
|
||||||
|
<div class="sidebar-section-header" style="margin-top: 0.5rem; margin-bottom: 0.5rem; width: 100%;">
|
||||||
|
<span style="padding-left: 0.6rem; font-size: 0.8rem; font-weight: bold; opacity: 0.5; text-transform: uppercase;"><blah>title.members</blah></span>
|
||||||
|
</div>
|
||||||
|
<button class="room-entry" onclick="openProfile('${targetId}')" style="margin-bottom: 0;">
|
||||||
|
<img src="${targetPfp}" class="room-pfp">
|
||||||
|
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||||
|
<span class="room-name">${targetDisplayName}</span>
|
||||||
|
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="room-entry" onclick="openProfile('${myIdStr}')" style="margin-bottom: 0;">
|
||||||
|
<img src="${myPfp}" class="room-pfp">
|
||||||
|
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%; margin-left: 0.37rem;">
|
||||||
|
<span class="room-name">${myDisplayName}</span>
|
||||||
|
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${myUsername}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function openDm(dmId, username, targetId) {
|
async function openDm(dmId, username, targetId) {
|
||||||
try {
|
try {
|
||||||
showAction("action.dm.opening", "dmopen");
|
showAction("action.dm.opening", "dmopen");
|
||||||
|
|
@ -2172,6 +2205,8 @@ async function openDm(dmId, username, targetId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
||||||
|
let membersHtml = await generateDmMembersHtml(targetId, username);
|
||||||
|
await switchDetailsContent("title.details", membersHtml);
|
||||||
|
|
||||||
let msgContainer = document.getElementById("chat-messages");
|
let msgContainer = document.getElementById("chat-messages");
|
||||||
if (msgContainer) {
|
if (msgContainer) {
|
||||||
|
|
@ -2914,37 +2949,44 @@ async function gotoSettings(tab = 'account') {
|
||||||
await switchRoomsBar("title.settings", settingsBar);
|
await switchRoomsBar("title.settings", settingsBar);
|
||||||
|
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
if (window.innerWidth > 52 * rem) {
|
if (window.innerWidth > 55 * rem) {
|
||||||
switchSettingsTab(tab);
|
switchSettingsTab(tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchSettingsTab(tabName) {
|
async function switchSettingsTab(tabName) {
|
||||||
setActiveRoombarItem('tab-settings-' + tabName);
|
setActiveRoombarItem('tab-settings-' + tabName);
|
||||||
if (tabName === 'account') {
|
if (tabName === 'account') {
|
||||||
switchRoomContent("title.account", accountSettings, false);
|
await switchRoomContent("title.account", accountSettings, false);
|
||||||
|
let userEl = document.getElementById("settings-username");
|
||||||
|
if (userEl && window.username) userEl.value = window.username;
|
||||||
} else if (tabName === 'profile') {
|
} else if (tabName === 'profile') {
|
||||||
switchRoomContent("title.profile", profileSettings, false);
|
showAction('title.loading', 'settings-loading');
|
||||||
setTimeout(async () => {
|
|
||||||
|
try {
|
||||||
let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
|
let pfpUrl = await getAvatarUrl(window.id, window.username + ":" + window.host);
|
||||||
|
let bannerUrl = `${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.banner`;
|
||||||
|
if (window.profileUpdateTimestamp) bannerUrl += `&t=${window.profileUpdateTimestamp}`;
|
||||||
|
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
|
||||||
|
|
||||||
|
clearAction('settings-loading');
|
||||||
|
await switchRoomContent("title.profile", profileSettings, false);
|
||||||
|
|
||||||
let pfpEl = document.getElementById("settings-avatar-preview");
|
let pfpEl = document.getElementById("settings-avatar-preview");
|
||||||
if (pfpEl) pfpEl.src = pfpUrl;
|
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");
|
let bannerEl = document.getElementById("settings-banner-preview");
|
||||||
if (bannerEl) bannerEl.src = bannerUrl;
|
if (bannerEl) bannerEl.src = bannerUrl;
|
||||||
|
|
||||||
let bioEl = document.getElementById("settings-bio");
|
let bioEl = document.getElementById("settings-bio");
|
||||||
if (bioEl) {
|
if (bioEl && !bio.startsWith("error:")) bioEl.value = bio;
|
||||||
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${window.id}&e=larp.profile.bio`);
|
} catch (e) {
|
||||||
if (!bio.startsWith("error:")) bioEl.value = bio;
|
clearAction('settings-loading');
|
||||||
}
|
await switchRoomContent("title.profile", profileSettings, false);
|
||||||
}, 210);
|
}
|
||||||
} else if (tabName === 'appearance') {
|
} else if (tabName === 'appearance') {
|
||||||
switchRoomContent("title.appearance", appearanceSettings, false);
|
await switchRoomContent("title.appearance", appearanceSettings, false);
|
||||||
setTimeout(populateLanguageDropdown, 210);
|
populateLanguageDropdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3096,10 +3138,12 @@ function initCropper() {
|
||||||
// Set aspect ratio
|
// Set aspect ratio
|
||||||
let targetWidth = cropperType === 'avatar' ? 512 : 494;
|
let targetWidth = cropperType === 'avatar' ? 512 : 494;
|
||||||
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
|
||||||
|
let aspectRatio = targetWidth / targetHeight;
|
||||||
|
|
||||||
container.style.height = 'auto';
|
container.style.height = 'auto';
|
||||||
container.style.maxHeight = 'calc(85vh - 16rem)';
|
container.style.maxHeight = 'calc(85vh - 16rem)';
|
||||||
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
|
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
|
||||||
|
container.style.maxWidth = `min(30rem, calc((85vh - 16rem) * ${aspectRatio}))`;
|
||||||
|
|
||||||
// Force layout recalculation
|
// Force layout recalculation
|
||||||
void container.offsetHeight;
|
void container.offsetHeight;
|
||||||
|
|
@ -3171,63 +3215,119 @@ async function confirmCropper() {
|
||||||
|
|
||||||
let b64;
|
let b64;
|
||||||
let newSrc;
|
let newSrc;
|
||||||
|
let isAnimated = false;
|
||||||
|
let gifFrames = null;
|
||||||
|
let webpDecoded = null;
|
||||||
|
let parsedGif = null;
|
||||||
|
|
||||||
if (cropperOriginalType === 'image/gif') {
|
if (cropperOriginalType === 'image/gif') {
|
||||||
|
isAnimated = true;
|
||||||
|
} else if (cropperOriginalType === 'image/webp') {
|
||||||
showAction("action.processing", "uploadmedia");
|
showAction("action.processing", "uploadmedia");
|
||||||
try {
|
try {
|
||||||
let res = await fetch(cropperOriginalB64);
|
let res = await fetch(cropperOriginalB64);
|
||||||
let buffer = await res.arrayBuffer();
|
let buffer = await res.arrayBuffer();
|
||||||
let parsedGif = gifuct.parseGIF(buffer);
|
let webpWorker = new Worker('webp-worker.js', { type: 'module' });
|
||||||
let frames = gifuct.decompressFrames(parsedGif, true);
|
webpDecoded = await new Promise((resolve, reject) => {
|
||||||
let webpFrames = [];
|
webpWorker.onmessage = (e) => {
|
||||||
|
if (e.data.success) resolve(e.data.data);
|
||||||
|
else reject(new Error(e.data.error));
|
||||||
|
webpWorker.terminate();
|
||||||
|
};
|
||||||
|
webpWorker.onerror = (err) => { reject(err); webpWorker.terminate(); };
|
||||||
|
webpWorker.postMessage({ action: 'decode', data: new Uint8Array(buffer) });
|
||||||
|
});
|
||||||
|
if (webpDecoded && webpDecoded.length > 1) {
|
||||||
|
isAnimated = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("WebP decode check failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnimated) {
|
||||||
|
showAction("action.processing", "uploadmedia");
|
||||||
|
try {
|
||||||
|
let webpFrames = [];
|
||||||
let tempCanvas = document.createElement('canvas');
|
let tempCanvas = document.createElement('canvas');
|
||||||
let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
let tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
tempCanvas.width = parsedGif.lsd.width;
|
|
||||||
tempCanvas.height = parsedGif.lsd.height;
|
|
||||||
|
|
||||||
let gifCanvas = document.createElement('canvas');
|
let gifCanvas = document.createElement('canvas');
|
||||||
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
|
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
gifCanvas.width = targetWidth;
|
gifCanvas.width = targetWidth;
|
||||||
gifCanvas.height = targetHeight;
|
gifCanvas.height = targetHeight;
|
||||||
|
|
||||||
let frameImageData = null;
|
|
||||||
let previousImageData = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < frames.length; i++) {
|
|
||||||
let frame = frames[i];
|
|
||||||
|
|
||||||
if (frame.disposalType === 3) {
|
|
||||||
previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
|
|
||||||
frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
frameImageData.data.set(frame.patch);
|
|
||||||
|
|
||||||
let patchCanvas = document.createElement('canvas');
|
|
||||||
patchCanvas.width = frame.dims.width;
|
|
||||||
patchCanvas.height = frame.dims.height;
|
|
||||||
patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
|
|
||||||
|
|
||||||
tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
|
|
||||||
|
|
||||||
gifCtx.fillStyle = '#000';
|
|
||||||
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
|
||||||
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
|
||||||
|
|
||||||
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
|
||||||
webpFrames.push({
|
|
||||||
data: frameImgData.data,
|
|
||||||
duration: Math.max(20, frame.delay || 0),
|
|
||||||
config: { lossless: 0, quality: 90 }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (frame.disposalType === 2) {
|
if (cropperOriginalType === 'image/gif') {
|
||||||
tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
|
let res = await fetch(cropperOriginalB64);
|
||||||
} else if (frame.disposalType === 3 && previousImageData) {
|
let buffer = await res.arrayBuffer();
|
||||||
tempCtx.putImageData(previousImageData, 0, 0);
|
parsedGif = gifuct.parseGIF(buffer);
|
||||||
|
gifFrames = gifuct.decompressFrames(parsedGif, true);
|
||||||
|
|
||||||
|
tempCanvas.width = parsedGif.lsd.width;
|
||||||
|
tempCanvas.height = parsedGif.lsd.height;
|
||||||
|
|
||||||
|
let frameImageData = null;
|
||||||
|
let previousImageData = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < gifFrames.length; i++) {
|
||||||
|
let frame = gifFrames[i];
|
||||||
|
|
||||||
|
if (frame.disposalType === 3) {
|
||||||
|
previousImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frameImageData || frameImageData.width !== frame.dims.width || frameImageData.height !== frame.dims.height) {
|
||||||
|
frameImageData = tempCtx.createImageData(frame.dims.width, frame.dims.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameImageData.data.set(frame.patch);
|
||||||
|
|
||||||
|
let patchCanvas = document.createElement('canvas');
|
||||||
|
patchCanvas.width = frame.dims.width;
|
||||||
|
patchCanvas.height = frame.dims.height;
|
||||||
|
patchCanvas.getContext('2d').putImageData(frameImageData, 0, 0);
|
||||||
|
|
||||||
|
tempCtx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
|
||||||
|
|
||||||
|
gifCtx.fillStyle = '#000';
|
||||||
|
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
||||||
|
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
||||||
|
|
||||||
|
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
webpFrames.push({
|
||||||
|
data: frameImgData.data,
|
||||||
|
duration: Math.max(20, frame.delay || 0),
|
||||||
|
config: { lossless: 0, quality: 90 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (frame.disposalType === 2) {
|
||||||
|
tempCtx.clearRect(frame.dims.left, frame.dims.top, frame.dims.width, frame.dims.height);
|
||||||
|
} else if (frame.disposalType === 3 && previousImageData) {
|
||||||
|
tempCtx.putImageData(previousImageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cropperOriginalType === 'image/webp' && webpDecoded) {
|
||||||
|
tempCanvas.width = webpDecoded[0].width;
|
||||||
|
tempCanvas.height = webpDecoded[0].height;
|
||||||
|
for (let i = 0; i < webpDecoded.length; i++) {
|
||||||
|
let frame = webpDecoded[i];
|
||||||
|
let frameImageData = new ImageData(
|
||||||
|
new Uint8ClampedArray(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength),
|
||||||
|
frame.width,
|
||||||
|
frame.height
|
||||||
|
);
|
||||||
|
tempCtx.putImageData(frameImageData, 0, 0);
|
||||||
|
|
||||||
|
gifCtx.fillStyle = '#000';
|
||||||
|
gifCtx.fillRect(0, 0, targetWidth, targetHeight);
|
||||||
|
gifCtx.drawImage(tempCanvas, drawX, drawY, drawW, drawH);
|
||||||
|
|
||||||
|
let frameImgData = gifCtx.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
webpFrames.push({
|
||||||
|
data: frameImgData.data,
|
||||||
|
duration: Math.max(20, frame.duration || 0),
|
||||||
|
config: { lossless: 0, quality: 90 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3245,7 +3345,7 @@ async function confirmCropper() {
|
||||||
reject(err);
|
reject(err);
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
};
|
};
|
||||||
worker.postMessage({ targetWidth, targetHeight, webpFrames });
|
worker.postMessage({ action: 'encode', targetWidth, targetHeight, webpFrames });
|
||||||
});
|
});
|
||||||
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
|
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
|
||||||
|
|
||||||
|
|
@ -3258,7 +3358,7 @@ async function confirmCropper() {
|
||||||
});
|
});
|
||||||
newSrc = 'data:image/webp;base64,' + b64;
|
newSrc = 'data:image/webp;base64,' + b64;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("GIF crop failed", e);
|
console.error("Animated crop failed", e);
|
||||||
b64 = cropperOriginalB64.split(',')[1];
|
b64 = cropperOriginalB64.split(',')[1];
|
||||||
newSrc = cropperOriginalB64;
|
newSrc = cropperOriginalB64;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ var messageContextMenu = `
|
||||||
//elements
|
//elements
|
||||||
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
|
||||||
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;
|
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;
|
||||||
|
var pinBtnHtml = `<button class="topbar-action-btn room-action-pin right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280q0-33 23.5-56.5T400-840h160q33 0 56.5 23.5T640-760v280Z"/></svg></button>`;
|
||||||
|
var callBtnHtml = `<button class="topbar-action-btn room-action-call right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M798-120q-125 0-247-54.5T329-329Q229-429 174.5-551T120-798q0-18 12-30t30-12h162q14 0 25 9.5t13 22.5l26 140q2 16-1 27t-11 19l-97 98q20 37 47.5 71.5T387-386q31 31 65 57.5t72 48.5l94-94q9-9 23.5-13.5T670-390l138 28q14 4 23 14.5t9 23.5v162q0 18-12 30t-30 12Z"/></svg></button>`;
|
||||||
|
|
||||||
var settingsBar = `
|
var settingsBar = `
|
||||||
<div class="sidebar-section-header">
|
<div class="sidebar-section-header">
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,8 @@ roomtopbar {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: var(--border-width) solid var(--light-border-color);
|
border-bottom: var(--border-width) solid var(--light-border-color);
|
||||||
|
|
@ -414,6 +416,33 @@ space{
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-action-btn {
|
||||||
|
display: flex;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 2.4rem;
|
||||||
|
width: 2.4rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.topbar-action-btn:active, .topbar-action-btn.is-pressed {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: var(--press-scale);
|
||||||
|
}
|
||||||
|
.topbar-action-btn.right {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
.topbar-action-btn svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
.mobile-nav-btn {
|
.mobile-nav-btn {
|
||||||
display: none;
|
display: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -435,7 +464,7 @@ space{
|
||||||
}
|
}
|
||||||
.mobile-nav-btn.right {
|
.mobile-nav-btn.right {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: 0.8rem;
|
margin-left: 0.2rem;
|
||||||
}
|
}
|
||||||
.mobile-nav-btn svg {
|
.mobile-nav-btn svg {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
|
@ -444,7 +473,7 @@ space{
|
||||||
.flex-spacer {
|
.flex-spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
@media (max-width: 52rem) {
|
@media (max-width: 55rem) {
|
||||||
.mobile-nav-btn {
|
.mobile-nav-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -977,3 +1006,22 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roomcontent roomtopbar .room-action-pin,
|
||||||
|
roomcontent roomtopbar .room-action-call {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#roomdetailsbar roomtopbar .room-action-pin,
|
||||||
|
#roomdetailsbar roomtopbar .room-action-call {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 65rem) and (min-width: 55.001rem) {
|
||||||
|
roomcontent roomtopbar .room-action-pin { display: none !important; }
|
||||||
|
#roomdetailsbar roomtopbar .room-action-pin { display: flex !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 58rem) and (min-width: 55.001rem) {
|
||||||
|
roomcontent roomtopbar .room-action-call { display: none !important; }
|
||||||
|
#roomdetailsbar roomtopbar .room-action-call { display: flex !important; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { encodeAnimation } from './wasm-webp/index.js';
|
import { encodeAnimation, decodeAnimation } from './wasm-webp/index.js';
|
||||||
|
|
||||||
self.onmessage = async (e) => {
|
self.onmessage = async (e) => {
|
||||||
let { targetWidth, targetHeight, webpFrames } = e.data;
|
let action = e.data.action || 'encode';
|
||||||
try {
|
try {
|
||||||
let webpUint8 = await encodeAnimation(targetWidth, targetHeight, true, webpFrames);
|
if (action === 'decode') {
|
||||||
self.postMessage({ success: true, data: webpUint8 }, [webpUint8.buffer]);
|
let decoded = await decodeAnimation(e.data.data, true);
|
||||||
|
let transferables = decoded.map(f => f.data.buffer);
|
||||||
|
self.postMessage({ success: true, data: decoded, action }, transferables);
|
||||||
|
} else {
|
||||||
|
let { targetWidth, targetHeight, webpFrames } = e.data;
|
||||||
|
let webpUint8 = await encodeAnimation(targetWidth, targetHeight, true, webpFrames);
|
||||||
|
self.postMessage({ success: true, data: webpUint8, action }, [webpUint8.buffer]);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.postMessage({ success: false, error: err.toString() });
|
self.postMessage({ success: false, error: err.toString(), action });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue