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 b3c784b7..bfd18b02 100644
--- a/android/app/src/main/assets/public/blah/en-cat.json
+++ b/android/app/src/main/assets/public/blah/en-cat.json
@@ -120,6 +120,9 @@
"title.captcha.code": "captcha code",
"title.invitation.code": "invitation code",
"title.back.to.register": "back to registration",
+ "title.details": "details",
+ "title.members": "cats",
+
"placeholder.username": "cat name",
"placeholder.captcha.code": "captcha code",
"desc.messages.loading": "loading meows...",
@@ -151,6 +154,7 @@
"title.profile": "cat profile",
"title.settings": "cat settings",
"title.logout": "log out",
+ "title.loading": "sniffing...",
"action.edit.profile": "edit cat profile",
"action.open.dm": "open direct meowchat",
"action.invite.accept": "accept meow invite",
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 7bffea78..3aebb7f2 100644
--- a/android/app/src/main/assets/public/blah/en-us.json
+++ b/android/app/src/main/assets/public/blah/en-us.json
@@ -151,7 +151,10 @@
"title.profile": "Profile",
"title.settings": "Settings",
"title.logout": "Log out",
-
+ "title.loading": "Loading...",
+ "title.details": "Details",
+ "title.members": "Members",
+
"action.edit.profile": "Edit profile",
"action.open.dm": "Open DM",
"action.invite.accept": "Accept invite",
diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html
index c863627b..dd5bc391 100644
--- a/android/app/src/main/assets/public/index.html
+++ b/android/app/src/main/assets/public/index.html
@@ -8,8 +8,9 @@
-
-
+
diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js
index c79302f6..ce9aae5c 100644
--- a/android/app/src/main/assets/public/main.js
+++ b/android/app/src/main/assets/public/main.js
@@ -1266,7 +1266,7 @@ var currentRoomContentHtml = null;
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false) {
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
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
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
@@ -1286,7 +1286,7 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
roomsTopBarTransition.style.opacity = "0";
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
{
mainScreen.classList.remove('mobile-details');
@@ -1298,8 +1298,9 @@ async function switchRoomContent(title, content, showRoomBar, icon = "", skipMob
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
+ let pinCallBtns = showRoomBar ? `${pinBtnHtml}${callBtnHtml}` : '';
- roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}${processBlah(title)}${detailsBtnHtml}`;
+ roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}${processBlah(title)}${pinCallBtns}${detailsBtnHtml}`;
roomDetailsBar.style.display = showRoomBar ? "flex" : "none";
let parser = new DOMParser();
@@ -1350,12 +1351,13 @@ async function switchDetailsContent(title, content) {
roomsTopBarTransition.style.opacity = "0";
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) {
+ if (window.innerWidth > 55 * rem) {
await delay(200);
}
- roomsTopBarTransition.innerHTML = processBlah(title);
+ let pinCallBtns = `${pinBtnHtml}${callBtnHtml}`;
+ roomsTopBarTransition.innerHTML = `${processBlah(title)}${pinCallBtns}`;
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
@@ -1461,8 +1463,8 @@ document.addEventListener("click", (e) => {
if (mainScreen && mainScreen.classList.contains('mobile-details')) {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth <= 52 * rem) {
- if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right')) {
+ if (window.innerWidth <= 55 * rem) {
+ if (!roomDetailsBar.contains(e.target) && !e.target.closest('.mobile-nav-btn.right') && !e.target.closest('.profile-popup-container')) {
mobileNavBack();
}
}
@@ -1527,15 +1529,7 @@ async function openProfile(accountId) {
let popupContainer = document.getElementById("profile-popup-container");
let popupInner = document.getElementById("profile-popup-inner");
- //show loading
- popupInner.innerHTML = `${processBlah('action.profile.loading')}
`;
- popupContainer.style.display = "flex";
-
- //force reflow
- popupContainer.offsetHeight;
-
- popupContainer.classList.add("show");
- isProfileOpen = true;
+ showAction('action.profile.loading', 'profile-loading');
try {
if (!accountId) accountId = window.id; //var fallback
@@ -1644,8 +1638,15 @@ async function openProfile(accountId) {
${actionBtnHtml}
`;
+
+ clearAction('profile-loading');
+ popupContainer.style.display = "flex";
+ popupContainer.offsetHeight;
+ popupContainer.classList.add("show");
+ isProfileOpen = true;
} catch (e) {
- popupInner.innerHTML = `${processBlah('error.profile.load.failed')}
`;
+ clearAction('profile-loading');
+ showNotification(processBlah('error.profile.load.failed'), 'error');
console.error(e);
}
}
@@ -1884,7 +1885,7 @@ async function gotoInbox() {
await switchRoomsBar("title.inbox", inboxRoomBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) {
+ if (window.innerWidth > 55 * rem) {
gotoInvites();
}
}
@@ -2055,7 +2056,7 @@ document.addEventListener('contextmenu', e => {
function handleMobileSwipe() {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) return;
+ if (window.innerWidth > 55 * rem) return;
let diffX = touchEndX - touchStartX;
let diffY = touchEndY - touchStartY;
@@ -2151,6 +2152,38 @@ function escapeHtml(unsafe) {
.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 `
+
+
+
+
+
+ `;
+}
+
async function openDm(dmId, username, targetId) {
try {
showAction("action.dm.opening", "dmopen");
@@ -2172,6 +2205,8 @@ async function openDm(dmId, username, targetId) {
}
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");
if (msgContainer) {
@@ -2914,37 +2949,44 @@ async function gotoSettings(tab = 'account') {
await switchRoomsBar("title.settings", settingsBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) {
+ if (window.innerWidth > 55 * rem) {
switchSettingsTab(tab);
}
}
-function switchSettingsTab(tabName) {
+async function switchSettingsTab(tabName) {
setActiveRoombarItem('tab-settings-' + tabName);
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') {
- switchRoomContent("title.profile", profileSettings, false);
- setTimeout(async () => {
+ showAction('title.loading', 'settings-loading');
+
+ try {
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");
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);
+ if (bioEl && !bio.startsWith("error:")) bioEl.value = bio;
+ } catch (e) {
+ clearAction('settings-loading');
+ await switchRoomContent("title.profile", profileSettings, false);
+ }
} else if (tabName === 'appearance') {
- switchRoomContent("title.appearance", appearanceSettings, false);
- setTimeout(populateLanguageDropdown, 210);
+ await switchRoomContent("title.appearance", appearanceSettings, false);
+ populateLanguageDropdown();
}
}
@@ -3096,10 +3138,12 @@ function initCropper() {
// Set aspect ratio
let targetWidth = cropperType === 'avatar' ? 512 : 494;
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
+ let aspectRatio = targetWidth / targetHeight;
container.style.height = 'auto';
container.style.maxHeight = 'calc(85vh - 16rem)';
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
+ container.style.maxWidth = `min(30rem, calc((85vh - 16rem) * ${aspectRatio}))`;
// Force layout recalculation
void container.offsetHeight;
@@ -3171,63 +3215,119 @@ async function confirmCropper() {
let b64;
let newSrc;
+ let isAnimated = false;
+ let gifFrames = null;
+ let webpDecoded = null;
+ let parsedGif = null;
+
if (cropperOriginalType === 'image/gif') {
+ isAnimated = true;
+ } else if (cropperOriginalType === 'image/webp') {
showAction("action.processing", "uploadmedia");
try {
let res = await fetch(cropperOriginalB64);
let buffer = await res.arrayBuffer();
- let parsedGif = gifuct.parseGIF(buffer);
- let frames = gifuct.decompressFrames(parsedGif, true);
- let webpFrames = [];
+ let webpWorker = new Worker('webp-worker.js', { type: 'module' });
+ webpDecoded = await new Promise((resolve, reject) => {
+ 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 tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
- tempCanvas.width = parsedGif.lsd.width;
- tempCanvas.height = parsedGif.lsd.height;
let gifCanvas = document.createElement('canvas');
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
gifCanvas.width = targetWidth;
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) {
- 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);
+ if (cropperOriginalType === 'image/gif') {
+ let res = await fetch(cropperOriginalB64);
+ let buffer = await res.arrayBuffer();
+ 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);
worker.terminate();
};
- worker.postMessage({ targetWidth, targetHeight, webpFrames });
+ worker.postMessage({ action: 'encode', targetWidth, targetHeight, webpFrames });
});
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
@@ -3258,7 +3358,7 @@ async function confirmCropper() {
});
newSrc = 'data:image/webp;base64,' + b64;
} catch (e) {
- console.error("GIF crop failed", e);
+ console.error("Animated crop failed", e);
b64 = cropperOriginalB64.split(',')[1];
newSrc = cropperOriginalB64;
}
diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js
index ff0954f3..114c18d8 100644
--- a/android/app/src/main/assets/public/screens.js
+++ b/android/app/src/main/assets/public/screens.js
@@ -258,6 +258,8 @@ var messageContextMenu = `
//elements
var detailsBtn = ``;
var backBtnHtml = ``;
+var pinBtnHtml = ``;
+var callBtnHtml = ``;
var settingsBar = `
`;
+
+ clearAction('profile-loading');
+ popupContainer.style.display = "flex";
+ popupContainer.offsetHeight;
+ popupContainer.classList.add("show");
+ isProfileOpen = true;
} catch (e) {
- popupInner.innerHTML = `${processBlah('error.profile.load.failed')}
`;
+ clearAction('profile-loading');
+ showNotification(processBlah('error.profile.load.failed'), 'error');
console.error(e);
}
}
@@ -1884,7 +1885,7 @@ async function gotoInbox() {
await switchRoomsBar("title.inbox", inboxRoomBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) {
+ if (window.innerWidth > 55 * rem) {
gotoInvites();
}
}
@@ -2055,7 +2056,7 @@ document.addEventListener('contextmenu', e => {
function handleMobileSwipe() {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) return;
+ if (window.innerWidth > 55 * rem) return;
let diffX = touchEndX - touchStartX;
let diffY = touchEndY - touchStartY;
@@ -2151,6 +2152,38 @@ function escapeHtml(unsafe) {
.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 `
+
+
+
+
+
+ `;
+}
+
async function openDm(dmId, username, targetId) {
try {
showAction("action.dm.opening", "dmopen");
@@ -2172,6 +2205,8 @@ async function openDm(dmId, username, targetId) {
}
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");
if (msgContainer) {
@@ -2914,37 +2949,44 @@ async function gotoSettings(tab = 'account') {
await switchRoomsBar("title.settings", settingsBar);
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
- if (window.innerWidth > 52 * rem) {
+ if (window.innerWidth > 55 * rem) {
switchSettingsTab(tab);
}
}
-function switchSettingsTab(tabName) {
+async function switchSettingsTab(tabName) {
setActiveRoombarItem('tab-settings-' + tabName);
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') {
- switchRoomContent("title.profile", profileSettings, false);
- setTimeout(async () => {
+ showAction('title.loading', 'settings-loading');
+
+ try {
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");
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);
+ if (bioEl && !bio.startsWith("error:")) bioEl.value = bio;
+ } catch (e) {
+ clearAction('settings-loading');
+ await switchRoomContent("title.profile", profileSettings, false);
+ }
} else if (tabName === 'appearance') {
- switchRoomContent("title.appearance", appearanceSettings, false);
- setTimeout(populateLanguageDropdown, 210);
+ await switchRoomContent("title.appearance", appearanceSettings, false);
+ populateLanguageDropdown();
}
}
@@ -3096,10 +3138,12 @@ function initCropper() {
// Set aspect ratio
let targetWidth = cropperType === 'avatar' ? 512 : 494;
let targetHeight = cropperType === 'avatar' ? targetWidth : 200;
+ let aspectRatio = targetWidth / targetHeight;
container.style.height = 'auto';
container.style.maxHeight = 'calc(85vh - 16rem)';
container.style.aspectRatio = `${targetWidth} / ${targetHeight}`;
+ container.style.maxWidth = `min(30rem, calc((85vh - 16rem) * ${aspectRatio}))`;
// Force layout recalculation
void container.offsetHeight;
@@ -3171,63 +3215,119 @@ async function confirmCropper() {
let b64;
let newSrc;
+ let isAnimated = false;
+ let gifFrames = null;
+ let webpDecoded = null;
+ let parsedGif = null;
+
if (cropperOriginalType === 'image/gif') {
+ isAnimated = true;
+ } else if (cropperOriginalType === 'image/webp') {
showAction("action.processing", "uploadmedia");
try {
let res = await fetch(cropperOriginalB64);
let buffer = await res.arrayBuffer();
- let parsedGif = gifuct.parseGIF(buffer);
- let frames = gifuct.decompressFrames(parsedGif, true);
- let webpFrames = [];
+ let webpWorker = new Worker('webp-worker.js', { type: 'module' });
+ webpDecoded = await new Promise((resolve, reject) => {
+ 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 tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
- tempCanvas.width = parsedGif.lsd.width;
- tempCanvas.height = parsedGif.lsd.height;
let gifCanvas = document.createElement('canvas');
let gifCtx = gifCanvas.getContext('2d', { willReadFrequently: true });
gifCanvas.width = targetWidth;
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) {
- 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);
+ if (cropperOriginalType === 'image/gif') {
+ let res = await fetch(cropperOriginalB64);
+ let buffer = await res.arrayBuffer();
+ 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);
worker.terminate();
};
- worker.postMessage({ targetWidth, targetHeight, webpFrames });
+ worker.postMessage({ action: 'encode', targetWidth, targetHeight, webpFrames });
});
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
@@ -3258,7 +3358,7 @@ async function confirmCropper() {
});
newSrc = 'data:image/webp;base64,' + b64;
} catch (e) {
- console.error("GIF crop failed", e);
+ console.error("Animated crop failed", e);
b64 = cropperOriginalB64.split(',')[1];
newSrc = cropperOriginalB64;
}
diff --git a/webroot/screens.js b/webroot/screens.js
index ff0954f3..114c18d8 100644
--- a/webroot/screens.js
+++ b/webroot/screens.js
@@ -258,6 +258,8 @@ var messageContextMenu = `
//elements
var detailsBtn = ``;
var backBtnHtml = ``;
+var pinBtnHtml = ``;
+var callBtnHtml = ``;
var settingsBar = `