big updat:
- update dependencies - add webp support and webp conversion for profile images
This commit is contained in:
parent
19c3dbb42d
commit
95aaaa69ea
244 changed files with 121382 additions and 86 deletions
192
webroot/main.js
192
webroot/main.js
|
|
@ -449,6 +449,29 @@ async function encryptString(plainText, passphrase) {
|
|||
return uint8ToBase64(combined);
|
||||
}
|
||||
|
||||
async function encryptBytesWithNonce(dataUint8, key, nonce) {
|
||||
const encoder = new TextEncoder();
|
||||
const passphrase = nonce + key;
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
||||
|
||||
const importedKey = await crypto.subtle.importKey(
|
||||
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{name: 'AES-CBC', iv: iv},
|
||||
importedKey,
|
||||
dataUint8
|
||||
);
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
async function decryptString(base64Text, passphrase) {
|
||||
const encoder = new TextEncoder();
|
||||
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
|
||||
|
|
@ -608,11 +631,43 @@ async function fetchEncrypted(request, body = "") {
|
|||
let data = await response.text();
|
||||
if (data.startsWith("error:")) return data;
|
||||
return await decryptString(data, passwordHash);
|
||||
} catch (e) {
|
||||
console.error("fetchEncrypted error:", e);
|
||||
return "error:network.error";
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEncryptedUpload(target, fileBytesUint8) {
|
||||
let release;
|
||||
const lock = new Promise(resolve => release = resolve);
|
||||
const prevMutex = requestMutex;
|
||||
requestMutex = prevMutex.then(() => lock);
|
||||
await prevMutex;
|
||||
try {
|
||||
let nonce = await getNonce(id, passwordHash);
|
||||
|
||||
let response = await fetch(`${url}/upload?id=${id}`, {
|
||||
method: "POST",
|
||||
body: await encryptBytesWithNonce(fileBytesUint8, passwordHash, nonce),
|
||||
headers: {
|
||||
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce),
|
||||
"target": await encryptWithNonce(target, passwordHash, nonce)
|
||||
}
|
||||
});
|
||||
let data = await response.text();
|
||||
if (data.startsWith("error:")) return data;
|
||||
return await decryptString(data, passwordHash);
|
||||
} catch (e) {
|
||||
console.error("fetchEncryptedUpload error:", e);
|
||||
return "error:network.error";
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function power(base, exponent, mod) {
|
||||
let res = 1n;
|
||||
base = base % mod;
|
||||
|
|
@ -914,6 +969,9 @@ function processBlah(blahmessage) {
|
|||
}
|
||||
return message;
|
||||
} catch (e) {
|
||||
if (blahmessage.startsWith("error:")) {
|
||||
return blah["error.unknown.error"] || "Unknown error";
|
||||
}
|
||||
if (prepended) {
|
||||
return blahmessage.substring(1);
|
||||
}
|
||||
|
|
@ -1092,7 +1150,12 @@ id = localStorage.getItem('id');
|
|||
username = localStorage.getItem('username');
|
||||
password = localStorage.getItem('password');
|
||||
host = localStorage.getItem('host');
|
||||
mainJS();
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
mainJS();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
function showFixedContextMenu(rect, html) {
|
||||
|
|
@ -2848,14 +2911,19 @@ async function reactMessagePrompt(msgId, quickReaction = null) {
|
|||
async function gotoSettings(tab = 'account') {
|
||||
fixedContextMenu.classList.remove("show");
|
||||
if (roomsBarContainer) roomsBarContainer.style.display = "";
|
||||
switchRoomsBar("title.settings", settingsBar);
|
||||
switchSettingsTab(tab);
|
||||
await switchRoomsBar("title.settings", settingsBar);
|
||||
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
if (window.innerWidth > 52 * rem) {
|
||||
switchSettingsTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
function switchSettingsTab(tabName) {
|
||||
setActiveRoombarItem('tab-settings-' + tabName);
|
||||
if (tabName === 'account') {
|
||||
switchRoomContent("title.account", accountSettings, false, null, true); //skip mobile slide
|
||||
switchRoomContent("title.account", accountSettings, false);
|
||||
|
||||
} else if (tabName === 'profile') {
|
||||
switchRoomContent("title.profile", profileSettings, false);
|
||||
setTimeout(async () => {
|
||||
|
|
@ -3093,38 +3161,126 @@ async function confirmCropper() {
|
|||
let zoom = parseFloat(document.getElementById('cropper-zoom').value);
|
||||
let container = document.getElementById('cropper-viewport-container');
|
||||
|
||||
let targetWidth = cropperType === 'avatar' ? 512 : 988;
|
||||
let targetHeight = cropperType === 'avatar' ? 512 : 400;
|
||||
let scaleRatio = targetWidth / container.clientWidth;
|
||||
let drawX = cropperPosX * scaleRatio;
|
||||
let drawY = cropperPosY * scaleRatio;
|
||||
let drawW = cropperImageObj.width * zoom * scaleRatio;
|
||||
let drawH = cropperImageObj.height * zoom * scaleRatio;
|
||||
|
||||
let b64;
|
||||
let newSrc;
|
||||
if (cropperOriginalType === 'image/gif') {
|
||||
b64 = cropperOriginalB64.split(',')[1];
|
||||
newSrc = cropperOriginalB64;
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
let webpUint8 = await new Promise((resolve, reject) => {
|
||||
let worker = new Worker('webp-worker.js', { type: 'module' });
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data.success) {
|
||||
resolve(e.data.data);
|
||||
} else {
|
||||
reject(new Error(e.data.error));
|
||||
}
|
||||
worker.terminate();
|
||||
};
|
||||
worker.onerror = (err) => {
|
||||
reject(err);
|
||||
worker.terminate();
|
||||
};
|
||||
worker.postMessage({ targetWidth, targetHeight, webpFrames });
|
||||
});
|
||||
let webpBlob = new Blob([webpUint8], {type: 'image/webp'});
|
||||
|
||||
b64 = await new Promise((resolve) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
resolve(reader.result.split(',')[1]);
|
||||
};
|
||||
reader.readAsDataURL(webpBlob);
|
||||
});
|
||||
newSrc = 'data:image/webp;base64,' + b64;
|
||||
} catch (e) {
|
||||
console.error("GIF crop failed", e);
|
||||
b64 = cropperOriginalB64.split(',')[1];
|
||||
newSrc = cropperOriginalB64;
|
||||
}
|
||||
} else {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
|
||||
let targetWidth = cropperType === 'avatar' ? 512 : 988;
|
||||
let targetHeight = cropperType === 'avatar' ? 512 : 400;
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
|
||||
let scaleRatio = targetWidth / container.clientWidth;
|
||||
let drawX = cropperPosX * scaleRatio;
|
||||
let drawY = cropperPosY * scaleRatio;
|
||||
let drawW = cropperImageObj.width * zoom * scaleRatio;
|
||||
let drawH = cropperImageObj.height * zoom * scaleRatio;
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, targetWidth, targetHeight);
|
||||
ctx.drawImage(cropperImageObj, drawX, drawY, drawW, drawH);
|
||||
|
||||
b64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1];
|
||||
newSrc = 'data:image/jpeg;base64,' + b64;
|
||||
b64 = canvas.toDataURL('image/webp', 0.9).split(',')[1];
|
||||
newSrc = 'data:image/webp;base64,' + b64;
|
||||
}
|
||||
|
||||
let payloadKey = cropperType === 'avatar' ? 'larp.profile.pfp' : 'larp.profile.banner';
|
||||
let payload = { string1: payloadKey, string2: b64 };
|
||||
let fileBytes = base64ToUint8(b64);
|
||||
showAction("action.uploading", "uploadmedia");
|
||||
let res = await fetchEncrypted("user/storage/public/update", JSON.stringify(payload));
|
||||
let res = await fetchEncryptedUpload(payloadKey, fileBytes);
|
||||
clearAction("uploadmedia");
|
||||
if (res && res.startsWith("error:")) {
|
||||
showBlahNotification(res);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue