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 = `