Image encryption, decryption, styling and other stuff

This commit is contained in:
Jiiks 2018-08-12 15:39:00 +03:00
parent 866ad8b13b
commit 169741d5a1
6 changed files with 199 additions and 62 deletions

View File

@ -10,7 +10,7 @@
import { Settings } from 'modules';
import BuiltinModule from './BuiltinModule';
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi } from 'modules';
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules';
import { VueInjector, Reflection } from 'ui';
import { ClientLogger as Logger } from 'common';
import { request } from 'vendor';
@ -20,7 +20,7 @@ import E2EEMessageButton from './E2EEMessageButton.vue';
import aes256 from 'aes256';
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
const decryptCache = [];
const imageCache = [];
export default new class E2EE extends BuiltinModule {
@ -46,6 +46,12 @@ export default new class E2EE extends BuiltinModule {
}
encrypt(key, content, prefix = '') {
if (!content) {
// Get key for current channel and encrypt
const haveKey = this.getKey(DiscordApi.currentChannel.id);
if (!haveKey) return 'nokey';
return this.encrypt(this.decrypt(this.decrypt(seed, this.master), haveKey), key);
}
return prefix + aes256.encrypt(key, content);
}
@ -60,6 +66,7 @@ export default new class E2EE extends BuiltinModule {
}
async enabled(e) {
window.sec = Security;
this.patchMessageContent();
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
@ -106,52 +113,49 @@ export default new class E2EE extends BuiltinModule {
renderMessageContent(component, args, retVal) {
if (!component.props.message.bd_encrypted) return;
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton, {
message: component.props.message
}));
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton));
}
beforeRenderImageWrapper(component, args, retVal) {
if (!component.props || !component.props.src) return;
if (component.props.decrypting) return;
component.props.decrypting = true;
const src = component.props.src;
const src = component.props.original || component.props.src.split('?')[0];
if (!src.includes('bde2ee')) return;
component.props.className = 'bd-encryptedImage';
const alreadyDecrypted = decryptCache.find(item => item.src === component.props.src);
if (alreadyDecrypted) {
component.props.className = 'bd-decryptedImage';
component.props.src = component.props.original = alreadyDecrypted.encodedImage;
component.props.width = alreadyDecrypted.width;
component.props.height = alreadyDecrypted.height;
const haveKey = this.getKey(DiscordApi.currentChannel.id);
if (!haveKey) return;
const cached = imageCache.find(item => item.src === src);
if (cached) {
Logger.info('E2EE', 'Returning encrypted image from cache');
try {
const decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), haveKey), cached.image);
component.props.className = 'bd-decryptedImage';
component.props.src = component.props.original = decrypt;
} catch (err) { return } finally { component.props.readyState = 'READY' }
return;
}
let resolution = null;
try {
resolution = src.match(/_(.*?)\./)[1].split('x');
} catch (err) { }
component.props.className = 'bd-encryptedImage';
component.props.decrypting = true;
request.get(component.props.src, { encoding: 'binary' }).then(res => {
component.props.readyState = 'LOADING';
Logger.info('E2EE', 'Decrypting image: ' + src);
request.get(src, { encoding: 'binary' }).then(res => {
const arr = new Uint8Array(new ArrayBuffer(res.length));
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
const aobindex = Utils.aobscan(arr, [73, 69, 78, 68]) + 8;
const sliced = arr.slice(aobindex);
const image = new TextDecoder().decode(sliced);
const sliced = arr.slice(aobindex, arr.length - aobindex);
const encoded = Utils.arrayBufferToBase64(sliced);
const base64enc = 'data:image/png;base64,' + encoded;
imageCache.push({ src, image });
if (!component || !component.props) return;
if (resolution && resolution.length >= 2) {
component.props.width = parseInt(resolution[0]);
component.props.height = parseInt(resolution[1]);
if (!component || !component.props) {
Logger.warn('E2EE', 'Component seems to be gone');
return;
}
decryptCache.push({ src, width: component.props.width, height: component.props.height, encodedImage: base64enc });
component.props.decrypting = false;
component.forceUpdate();
}).catch(err => {

View File

@ -10,34 +10,40 @@
<template>
<div class="bd-e2eeTaContainer">
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
<MiLock v-tooltip="error" />
</div>
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
<MiLock v-tooltip="'Loading'" />
</div>
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn" @click="E2EE.encryptNewMessages = true">
<MiLock v-tooltip="'New messages will not be encrypted.'" />
</div>
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok" @click="E2EE.encryptNewMessages = false">
<MiLock v-tooltip="'Ready!'" />
</div>
<v-popover popoverClass="bd-popover bd-e2eePopover" placement="top">
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
<MiLock v-tooltip="error" />
</div>
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
<MiLock v-tooltip="'Loading'" />
</div>
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn">
<MiLock v-tooltip="'New messages will not be encrypted.'" />
</div>
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok">
<MiLock v-tooltip="'Ready!'" />
</div>
<template slot="popover">
<div @click="toggleEncrypt" :class="{'bd-warn': !E2EE.encryptNewMessages, 'bd-ok': E2EE.encryptNewMessages}"><MiLock size="16" v-tooltip="'Toggle Encryption'" /></div>
<div v-close-popover @click="showUploadDialog" v-if="!error"><MiPlus size="16" v-tooltip="'Upload Encrypted Image'" /></div>
</template>
</v-popover>
<div class="bd-taDivider"></div>
<div class="bd-e2eeTaBtn bd-e2eeUploadBtn" :class="{'bd-disabled': error}" @click="showUploadDialog">
<MiPlus v-tooltip="'Upload Encrypted'" />
</div>
</div>
</template>
<script>
import fs from 'fs';
import { Utils } from 'common';
import { remote } from 'electron';
import { E2EE } from 'builtin';
import { DiscordApi, Security } from 'modules';
import { MiLock, MiPlus } from '../ui/components/common/MaterialIcon';
const lock = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 32, 0, 0, 0, 32, 8, 4, 0, 0, 0, 217, 115, 178, 127, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, 177, 143, 11, 252, 97, 5, 0, 0, 0, 32, 99, 72, 82, 77, 0, 0, 122, 38, 0, 0, 128, 132, 0, 0, 250, 0, 0, 0, 128, 232, 0, 0, 117, 48, 0, 0, 234, 96, 0, 0, 58, 152, 0, 0, 23, 112, 156, 186, 81, 60, 0, 0, 0, 2, 98, 75, 71, 68, 0, 0, 170, 141, 35, 50, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 13, 215, 0, 0, 13, 215, 1, 66, 40, 155, 120, 0, 0, 0, 7, 116, 73, 77, 69, 7, 226, 8, 11, 6, 2, 48, 96, 75, 242, 117, 0, 0, 1, 117, 73, 68, 65, 84, 72, 199, 173, 213, 61, 75, 92, 65, 20, 198, 241, 159, 113, 13, 98, 4, 139, 192, 18, 180, 13, 18, 155, 84, 146, 8, 166, 75, 165, 164, 216, 70, 240, 43, 88, 199, 94, 44, 44, 211, 111, 229, 103, 16, 12, 249, 4, 75, 138, 93, 69, 76, 101, 145, 198, 194, 16, 133, 20, 194, 238, 54, 162, 39, 141, 108, 238, 234, 220, 151, 93, 115, 6, 134, 195, 156, 231, 249, 51, 247, 206, 153, 123, 39, 228, 197, 91, 13, 203, 150, 113, 228, 200, 129, 31, 70, 136, 41, 187, 110, 68, 102, 220, 216, 53, 85, 213, 62, 231, 100, 96, 60, 119, 62, 200, 79, 204, 85, 3, 236, 11, 161, 111, 91, 29, 212, 109, 235, 11, 97, 191, 138, 125, 93, 8, 87, 22, 135, 86, 23, 93, 9, 97, 189, 28, 208, 18, 194, 198, 163, 245, 13, 33, 180, 202, 236, 147, 122, 194, 105, 178, 118, 42, 244, 76, 14, 47, 62, 123, 32, 90, 50, 131, 118, 18, 208, 198, 140, 165, 98, 192, 27, 112, 150, 4, 156, 101, 20, 185, 128, 26, 184, 77, 2, 110, 51, 138, 92, 192, 200, 241, 100, 192, 240, 134, 102, 173, 128, 143, 166, 19, 218, 85, 176, 226, 171, 110, 26, 214, 112, 61, 212, 255, 121, 227, 90, 35, 13, 104, 87, 178, 135, 200, 30, 243, 196, 32, 155, 119, 129, 150, 47, 37, 15, 253, 217, 7, 44, 248, 245, 240, 29, 60, 7, 23, 14, 74, 0, 155, 25, 245, 255, 56, 133, 98, 192, 154, 67, 135, 214, 138, 36, 181, 130, 218, 59, 223, 192, 39, 239, 115, 110, 71, 201, 14, 182, 18, 217, 72, 128, 187, 68, 54, 18, 160, 121, 223, 113, 93, 205, 241, 0, 29, 59, 96, 71, 103, 60, 0, 253, 204, 60, 22, 160, 147, 153, 115, 162, 86, 8, 56, 182, 138, 227, 241, 1, 124, 47, 169, 63, 189, 149, 255, 221, 198, 23, 126, 155, 245, 199, 207, 18, 199, 107, 47, 117, 189, 210, 123, 92, 106, 86, 254, 30, 228, 244, 69, 221, 158, 203, 82, 243, 165, 189, 251, 127, 38, 248, 11, 109, 255, 171, 183, 250, 206, 128, 34, 0, 0, 0, 37, 116, 69, 88, 116, 100, 97, 116, 101, 58, 99, 114, 101, 97, 116, 101, 0, 50, 48, 49, 56, 45, 48, 56, 45, 49, 49, 84, 48, 54, 58, 48, 50, 58, 52, 56, 43, 48, 50, 58, 48, 48, 90, 233, 185, 110, 0, 0, 0, 37, 116, 69, 88, 116, 100, 97, 116, 101, 58, 109, 111, 100, 105, 102, 121, 0, 50, 48, 49, 56, 45, 48, 56, 45, 49, 49, 84, 48, 54, 58, 48, 50, 58, 52, 56, 43, 48, 50, 58, 48, 48, 43, 180, 1, 210, 0, 0, 0, 25, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, 0, 119, 119, 119, 46, 105, 110, 107, 115, 99, 97, 112, 101, 46, 111, 114, 103, 155, 238, 60, 26, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]);
import { Toasts } from 'ui';
export default {
components: { MiLock, MiPlus },
@ -49,18 +55,33 @@
};
},
methods: {
showUploadDialog() {
async showUploadDialog() {
const dialogResult = remote.dialog.showOpenDialog({ properties: ['openFile'] });
if (!dialogResult) return;
console.log(dialogResult);
const readFile = fs.readFileSync(dialogResult[0]);
console.log(readFile);
const merge = new Uint8Array([...lock, ...readFile]);
const FileActions = _bd.WebpackModules.getModuleByProps(["makeFile"]);
const Uploader = _bd.WebpackModules.getModuleByProps(["instantBatchUpload"]);
const file = FileActions.makeFile(merge, "encrypted.png");
console.log(file);
Uploader.upload(DiscordApi.currentChannel.id, FileActions.makeFile(merge, "bde2ee_266x200.png"));
const img = await Utils.getImageFromBuffer(readFile);
const canvas = document.createElement('canvas');
canvas.height = img.height;
canvas.width = img.width;
const arrBuffer = await Utils.canvasToArrayBuffer(canvas);
const encodedBytes = new TextEncoder().encode(E2EE.encrypt(img.src));
Uploader.upload(DiscordApi.currentChannel.id, FileActions.makeFile(new Uint8Array([...new Uint8Array(arrBuffer), ...encodedBytes]), 'bde2ee.png'));
},
toggleEncrypt() {
const newState = !this.E2EE.encryptNewMessages;
this.E2EE.encryptNewMessages = newState;
if (!newState) {
Toasts.warning('New messages will not be encrypted');
return;
}
Toasts.success('New messages will be encrypted');
}
},
mounted() {

View File

@ -28,12 +28,12 @@ export default new class ReactDevtoolsModule extends BuiltinModule {
}
enabled(e) {
electron.remote.BrowserWindow.getAllWindows()[0].webContents.on('devtools-opened', this.devToolsOpened);
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.on('devtools-opened', this.devToolsOpened);
}
disabled(e) {
electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
// 7electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
}
devToolsOpened() {

View File

@ -28,16 +28,16 @@ export default new class VueDevtoolsModule extends BuiltinModule {
}
enabled(e) {
electron.remote.BrowserWindow.getAllWindows()[0].webContents.on('devtools-opened', this.devToolsOpened);
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.on('devtools-opened', this.devToolsOpened);
}
disabled(e) {
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
// electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
}
devToolsOpened() {
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
electron.webFrame.registerURLSchemeAsPrivileged('chrome-extension');
try {
const res = electron.remote.BrowserWindow.addDevToolsExtension(path.join(Globals.getPath('ext'), 'extensions', 'vdt'));

View File

@ -45,7 +45,7 @@
position: relative;
display: inline-block;
top: 4px;
E2EE.encryptNewMessages
.bd-e2eeMdBtn {
cursor: pointer;
@ -82,3 +82,87 @@
}
}
}
.bd-e2eePopover {
background: #484b51;
margin: 0;
margin-top: 15px;
.bd-ok svg {
fill: $colbdgreen;
}
.bd-warn svg {
fill: #ccab3e;
}
.bd-popover-wrapper,
.bd-popoverWrapper {
.bd-popover-inner,
.bd-popoverInner {
display: flex;
> div:first-child {
display: flex;
> div:not(:first-child) {
margin-left: 10px;
}
}
.bd-material-design-icon,
.bd-materialDesignIcon {
display: flex;
fill: #7e8084;
cursor: pointer;
&:hover {
fill: #FFF;
}
}
}
}
.bd-popover-arrow,
.bd-popoverArrow {
display: none;
}
}
.bd-encryptedImage::before {
content: "Encrypted Image";
position: absolute;
background: #3ec99a;
width: 100%;
height: 100%;
border-radius: 3px;
display: flex;
justify-content: center;
align-items: flex-start;
font-size: 1.2em;
font-weight: 700;
color: #2c2c2c;
line-height: 30px;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHZlcnNpb249IjEuMCIgICB3aWR0aD0iNTAwIiAgIGhlaWdodD0iNTAwIiAgIGlkPSJzdmcyMzk0Ij4gIDxkZWZzICAgICBpZD0iZGVmczIzOTYiIC8+ICA8ZyAgICAgaWQ9ImxheWVyMSI+ICAgIDxwYXRoICAgICAgIGQ9Ik0gOTcuMzU3MTI5LDQzNC43MDMwNiBDIDk5LjM4MDc5MSw0NDYuODA5NjggMTA5Ljg4OTc3LDQ1Ni4wMDUwMSAxMjIuNTY0NDMsNDU2LjAwNTAxIEwgMzgzLjg2ODUsNDU2LjAwNTAxIEMgMzk2LjU0MzE1LDQ1Ni4wMDUwMSA0MDcuMDUyMTMsNDQ2LjgwOTY4IDQwOS4wNzU4LDQzNC43MDMwNiBMIDk3LjM1NzEyOSw0MzQuNzAzMDYgeiBNIDEyMi41NjQ0MywyMzUuODg0NzkgQyAxMDguNDM0MTUsMjM1Ljg4NDc5IDk3LjAwMjA1OCwyNDcuMzE2ODMgOTcuMDAyMDU4LDI2MS40NDcxMyBMIDk3LjAwMjA1OCw0MTYuOTUxNDIgTCA0MDkuNDMwODgsNDE2Ljk1MTQyIEwgNDA5LjQzMDg4LDI2MS40NDcxMyBDIDQwOS40MzA4OCwyNDcuMzE2ODMgMzk3Ljk5ODc4LDIzNS44ODQ3OSAzODMuODY4NSwyMzUuODg0NzkgTCAzNjguMjgyNTUsMjM1Ljg4NDc5IEMgMzY4LjQ5NTU5LDIzMi45Mzc5OSAzNjguNjAyMSwxNjkuNjM1NjcgMzY4LjYwMjEsMTY2LjY1MzM5IEMgMzY4LjYwMjEsMTAyLjkyNTAxIDMxNi45NDQ4NCw1MS4yNjc3NTIgMjUzLjIxNjQ2LDUxLjI2Nzc1MiBDIDE4OS40ODgxLDUxLjI2Nzc1MiAxMzcuODMwODMsMTAyLjkyNTAxIDEzNy44MzA4MywxNjYuNjUzMzkgQyAxMzcuODMwODMsMTY5LjYzNTY3IDEzNy45MzczNCwyMzIuOTM3OTkgMTM4LjE1MDM5LDIzNS44ODQ3OSBMIDEyMi41NjQ0MywyMzUuODg0NzkgeiBNIDI5OC41MTg2MiwyMzUuODg0NzkgTCAyMDcuOTE0MzEsMjM1Ljg4NDc5IEMgMjA3LjM0NjI1LDIzMy4wMDkgMjA3LjA2MjIsMTY5LjcwNjY2IDIwNy4wNjIyLDE2Ni42NTMzOSBDIDIwNy4wNjIyLDE0MS4xNjIwNSAyMjcuNzI1MTMsMTIwLjQ5OTE3IDI1My4yMTY0NiwxMjAuNDk5MTcgQyAyNzguNzA3ODIsMTIwLjQ5OTE3IDI5OS4zNzA3NCwxNDEuMTYyMDUgMjk5LjM3MDc0LDE2Ni42NTMzOSBDIDI5OS4zNzA3NCwxNjkuNzA2NjYgMjk5LjA4NjcsMjMzLjAwOSAyOTguNTE4NjIsMjM1Ljg4NDc5IHoiICAgICAgIGlkPSJwYXRoMTQiICAgICAgIHN0eWxlPSJmaWxsOiMyYzJjMmM7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQiIC8+ICA8L2c+PC9zdmc+);
background-size: 50% 50%;
background-repeat: no-repeat;
background-position: center;
}
.bd-decryptedImage::before {
content: "";
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHZlcnNpb249IjEuMCIgICB3aWR0aD0iNTAwIiAgIGhlaWdodD0iNTAwIiAgIGlkPSJzdmcyMzk0Ij4gIDxkZWZzICAgICBpZD0iZGVmczIzOTYiIC8+ICA8ZyAgICAgaWQ9ImxheWVyMSI+ICAgIDxwYXRoICAgICAgIGQ9Ik0gOTcuMzU3MTI5LDQzNC43MDMwNiBDIDk5LjM4MDc5MSw0NDYuODA5NjggMTA5Ljg4OTc3LDQ1Ni4wMDUwMSAxMjIuNTY0NDMsNDU2LjAwNTAxIEwgMzgzLjg2ODUsNDU2LjAwNTAxIEMgMzk2LjU0MzE1LDQ1Ni4wMDUwMSA0MDcuMDUyMTMsNDQ2LjgwOTY4IDQwOS4wNzU4LDQzNC43MDMwNiBMIDk3LjM1NzEyOSw0MzQuNzAzMDYgeiBNIDEyMi41NjQ0MywyMzUuODg0NzkgQyAxMDguNDM0MTUsMjM1Ljg4NDc5IDk3LjAwMjA1OCwyNDcuMzE2ODMgOTcuMDAyMDU4LDI2MS40NDcxMyBMIDk3LjAwMjA1OCw0MTYuOTUxNDIgTCA0MDkuNDMwODgsNDE2Ljk1MTQyIEwgNDA5LjQzMDg4LDI2MS40NDcxMyBDIDQwOS40MzA4OCwyNDcuMzE2ODMgMzk3Ljk5ODc4LDIzNS44ODQ3OSAzODMuODY4NSwyMzUuODg0NzkgTCAzNjguMjgyNTUsMjM1Ljg4NDc5IEMgMzY4LjQ5NTU5LDIzMi45Mzc5OSAzNjguNjAyMSwxNjkuNjM1NjcgMzY4LjYwMjEsMTY2LjY1MzM5IEMgMzY4LjYwMjEsMTAyLjkyNTAxIDMxNi45NDQ4NCw1MS4yNjc3NTIgMjUzLjIxNjQ2LDUxLjI2Nzc1MiBDIDE4OS40ODgxLDUxLjI2Nzc1MiAxMzcuODMwODMsMTAyLjkyNTAxIDEzNy44MzA4MywxNjYuNjUzMzkgQyAxMzcuODMwODMsMTY5LjYzNTY3IDEzNy45MzczNCwyMzIuOTM3OTkgMTM4LjE1MDM5LDIzNS44ODQ3OSBMIDEyMi41NjQ0MywyMzUuODg0NzkgeiBNIDI5OC41MTg2MiwyMzUuODg0NzkgTCAyMDcuOTE0MzEsMjM1Ljg4NDc5IEMgMjA3LjM0NjI1LDIzMy4wMDkgMjA3LjA2MjIsMTY5LjcwNjY2IDIwNy4wNjIyLDE2Ni42NTMzOSBDIDIwNy4wNjIyLDE0MS4xNjIwNSAyMjcuNzI1MTMsMTIwLjQ5OTE3IDI1My4yMTY0NiwxMjAuNDk5MTcgQyAyNzguNzA3ODIsMTIwLjQ5OTE3IDI5OS4zNzA3NCwxNDEuMTYyMDUgMjk5LjM3MDc0LDE2Ni42NTMzOSBDIDI5OS4zNzA3NCwxNjkuNzA2NjYgMjk5LjA4NjcsMjMzLjAwOSAyOTguNTE4NjIsMjM1Ljg4NDc5IHoiICAgICAgIGlkPSJwYXRoMTQiICAgICAgIHN0eWxlPSJmaWxsOiMyYzJjMmM7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQiIC8+ICA8L2c+PC9zdmc+);
width: 11px;
height: 11px;
position: absolute;
z-index: 1;
display: block;
background-size: cover;
background-color: #3ecb9c;
background-repeat: no-repeat;
border-radius: 100%;
border: 2px solid #3ecb9c;
top: 5px;
left: 5px;
opacity: .5;
}

View File

@ -211,6 +211,34 @@ export class Utils {
}
return window.btoa(binary);
}
static async getImageFromBuffer(buffer) {
if (!(buffer instanceof Blob)) buffer = new Blob([buffer]);
const reader = new FileReader();
reader.readAsDataURL(buffer);
await new Promise(r => {
reader.onload = r
});
const img = new Image();
img.src = reader.result;
return await new Promise(resolve => {
img.onload = () => {
resolve(img);
}
});
}
static async canvasToArrayBuffer(canvas, mime = 'image/png') {
const reader = new FileReader();
return new Promise(resolve => {
canvas.toBlob(blob => {
reader.addEventListener('loadend', () => {
resolve(reader.result);
});
reader.readAsArrayBuffer(blob);
}, mime);
});
}
}
export class FileUtils {