Image encryption, decryption, styling and other stuff
This commit is contained in:
parent
866ad8b13b
commit
169741d5a1
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
import { Settings } from 'modules';
|
import { Settings } from 'modules';
|
||||||
import BuiltinModule from './BuiltinModule';
|
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 { VueInjector, Reflection } from 'ui';
|
||||||
import { ClientLogger as Logger } from 'common';
|
import { ClientLogger as Logger } from 'common';
|
||||||
import { request } from 'vendor';
|
import { request } from 'vendor';
|
||||||
|
@ -20,7 +20,7 @@ import E2EEMessageButton from './E2EEMessageButton.vue';
|
||||||
import aes256 from 'aes256';
|
import aes256 from 'aes256';
|
||||||
|
|
||||||
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
||||||
const decryptCache = [];
|
const imageCache = [];
|
||||||
|
|
||||||
export default new class E2EE extends BuiltinModule {
|
export default new class E2EE extends BuiltinModule {
|
||||||
|
|
||||||
|
@ -46,6 +46,12 @@ export default new class E2EE extends BuiltinModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
encrypt(key, content, prefix = '') {
|
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);
|
return prefix + aes256.encrypt(key, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +66,7 @@ export default new class E2EE extends BuiltinModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
async enabled(e) {
|
async enabled(e) {
|
||||||
|
window.sec = Security;
|
||||||
this.patchMessageContent();
|
this.patchMessageContent();
|
||||||
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
|
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
|
||||||
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
|
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
|
||||||
|
@ -106,52 +113,49 @@ export default new class E2EE extends BuiltinModule {
|
||||||
|
|
||||||
renderMessageContent(component, args, retVal) {
|
renderMessageContent(component, args, retVal) {
|
||||||
if (!component.props.message.bd_encrypted) return;
|
if (!component.props.message.bd_encrypted) return;
|
||||||
|
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton));
|
||||||
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton, {
|
|
||||||
message: component.props.message
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeRenderImageWrapper(component, args, retVal) {
|
beforeRenderImageWrapper(component, args, retVal) {
|
||||||
if (!component.props || !component.props.src) return;
|
if (!component.props || !component.props.src) return;
|
||||||
if (component.props.decrypting) 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;
|
if (!src.includes('bde2ee')) return;
|
||||||
|
component.props.className = 'bd-encryptedImage';
|
||||||
|
|
||||||
const alreadyDecrypted = decryptCache.find(item => item.src === component.props.src);
|
const haveKey = this.getKey(DiscordApi.currentChannel.id);
|
||||||
if (alreadyDecrypted) {
|
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.className = 'bd-decryptedImage';
|
||||||
component.props.src = component.props.original = alreadyDecrypted.encodedImage;
|
component.props.src = component.props.original = decrypt;
|
||||||
component.props.width = alreadyDecrypted.width;
|
} catch (err) { return } finally { component.props.readyState = 'READY' }
|
||||||
component.props.height = alreadyDecrypted.height;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolution = null;
|
component.props.readyState = 'LOADING';
|
||||||
try {
|
Logger.info('E2EE', 'Decrypting image: ' + src);
|
||||||
resolution = src.match(/_(.*?)\./)[1].split('x');
|
request.get(src, { encoding: 'binary' }).then(res => {
|
||||||
} catch (err) { }
|
|
||||||
|
|
||||||
component.props.className = 'bd-encryptedImage';
|
|
||||||
component.props.decrypting = true;
|
|
||||||
|
|
||||||
request.get(component.props.src, { encoding: 'binary' }).then(res => {
|
|
||||||
const arr = new Uint8Array(new ArrayBuffer(res.length));
|
const arr = new Uint8Array(new ArrayBuffer(res.length));
|
||||||
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
|
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
|
||||||
|
|
||||||
const aobindex = Utils.aobscan(arr, [73, 69, 78, 68]) + 8;
|
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);
|
imageCache.push({ src, image });
|
||||||
const encoded = Utils.arrayBufferToBase64(sliced);
|
|
||||||
const base64enc = 'data:image/png;base64,' + encoded;
|
|
||||||
|
|
||||||
if (!component || !component.props) return;
|
if (!component || !component.props) {
|
||||||
if (resolution && resolution.length >= 2) {
|
Logger.warn('E2EE', 'Component seems to be gone');
|
||||||
component.props.width = parseInt(resolution[0]);
|
return;
|
||||||
component.props.height = parseInt(resolution[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptCache.push({ src, width: component.props.width, height: component.props.height, encodedImage: base64enc });
|
|
||||||
component.props.decrypting = false;
|
component.props.decrypting = false;
|
||||||
component.forceUpdate();
|
component.forceUpdate();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|
|
@ -10,34 +10,40 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bd-e2eeTaContainer">
|
<div class="bd-e2eeTaContainer">
|
||||||
|
<v-popover popoverClass="bd-popover bd-e2eePopover" placement="top">
|
||||||
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
|
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
|
||||||
<MiLock v-tooltip="error" />
|
<MiLock v-tooltip="error" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
|
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
|
||||||
<MiLock v-tooltip="'Loading'" />
|
<MiLock v-tooltip="'Loading'" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn" @click="E2EE.encryptNewMessages = true">
|
|
||||||
|
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn">
|
||||||
<MiLock v-tooltip="'New messages will not be encrypted.'" />
|
<MiLock v-tooltip="'New messages will not be encrypted.'" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok" @click="E2EE.encryptNewMessages = false">
|
|
||||||
|
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok">
|
||||||
<MiLock v-tooltip="'Ready!'" />
|
<MiLock v-tooltip="'Ready!'" />
|
||||||
</div>
|
</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-taDivider"></div>
|
||||||
<div class="bd-e2eeTaBtn bd-e2eeUploadBtn" :class="{'bd-disabled': error}" @click="showUploadDialog">
|
|
||||||
<MiPlus v-tooltip="'Upload Encrypted'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { Utils } from 'common';
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
import { E2EE } from 'builtin';
|
import { E2EE } from 'builtin';
|
||||||
import { DiscordApi, Security } from 'modules';
|
import { DiscordApi, Security } from 'modules';
|
||||||
import { MiLock, MiPlus } from '../ui/components/common/MaterialIcon';
|
import { MiLock, MiPlus } from '../ui/components/common/MaterialIcon';
|
||||||
|
import { Toasts } from 'ui';
|
||||||
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]);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { MiLock, MiPlus },
|
components: { MiLock, MiPlus },
|
||||||
|
@ -49,18 +55,33 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showUploadDialog() {
|
async showUploadDialog() {
|
||||||
const dialogResult = remote.dialog.showOpenDialog({ properties: ['openFile'] });
|
const dialogResult = remote.dialog.showOpenDialog({ properties: ['openFile'] });
|
||||||
if (!dialogResult) return;
|
if (!dialogResult) return;
|
||||||
console.log(dialogResult);
|
|
||||||
const readFile = fs.readFileSync(dialogResult[0]);
|
const readFile = fs.readFileSync(dialogResult[0]);
|
||||||
console.log(readFile);
|
|
||||||
const merge = new Uint8Array([...lock, ...readFile]);
|
|
||||||
const FileActions = _bd.WebpackModules.getModuleByProps(["makeFile"]);
|
const FileActions = _bd.WebpackModules.getModuleByProps(["makeFile"]);
|
||||||
const Uploader = _bd.WebpackModules.getModuleByProps(["instantBatchUpload"]);
|
const Uploader = _bd.WebpackModules.getModuleByProps(["instantBatchUpload"]);
|
||||||
const file = FileActions.makeFile(merge, "encrypted.png");
|
|
||||||
console.log(file);
|
const img = await Utils.getImageFromBuffer(readFile);
|
||||||
Uploader.upload(DiscordApi.currentChannel.id, FileActions.makeFile(merge, "bde2ee_266x200.png"));
|
|
||||||
|
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() {
|
mounted() {
|
||||||
|
|
|
@ -28,12 +28,12 @@ export default new class ReactDevtoolsModule extends BuiltinModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled(e) {
|
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) {
|
disabled(e) {
|
||||||
electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
|
// 7electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
|
||||||
electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
|
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsOpened() {
|
devToolsOpened() {
|
||||||
|
|
|
@ -28,12 +28,12 @@ export default new class VueDevtoolsModule extends BuiltinModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled(e) {
|
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) {
|
disabled(e) {
|
||||||
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
|
// electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
|
||||||
electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
|
// electron.remote.BrowserWindow.getAllWindows()[0].webContents.removeListener('devtools-opened', this.devToolsOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsOpened() {
|
devToolsOpened() {
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
E2EE.encryptNewMessages
|
||||||
.bd-e2eeMdBtn {
|
.bd-e2eeMdBtn {
|
||||||
cursor: pointer;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -211,6 +211,34 @@ export class Utils {
|
||||||
}
|
}
|
||||||
return window.btoa(binary);
|
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 {
|
export class FileUtils {
|
||||||
|
|
Loading…
Reference in New Issue