├── README.md
├── SendAudio.plugin.js
└── src
├── config.json
└── index.js
/README.md:
--------------------------------------------------------------------------------
1 | # Send Audio Plugin
2 | Record and send audios in chat
3 |
4 | ### Preview
5 | 
6 |
7 | ### Send
8 | 
9 |
10 | ### Settings
11 | 
12 |
--------------------------------------------------------------------------------
/SendAudio.plugin.js:
--------------------------------------------------------------------------------
1 | //META{"name":"SendAudio","displayName":"SendAudio","website":"https://github.com/MKSx/EnviarAudio-BetterDiscord","source":""}*//
2 | /*@cc_on
3 | @if (@_jscript)
4 |
5 | // Offer to self-install for clueless users that try to run this directly.
6 | var shell = WScript.CreateObject("WScript.Shell");
7 | var fs = new ActiveXObject("Scripting.FileSystemObject");
8 | var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\BetterDiscord\plugins");
9 | var pathSelf = WScript.ScriptFullName;
10 | // Put the user at ease by addressing them in the first person
11 | shell.Popup("It looks like you've mistakenly tried to run me directly. \n(Don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
12 | if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
13 | shell.Popup("I'm in the correct folder already.", 0, "I'm already installed", 0x40);
14 | } else if (!fs.FolderExists(pathPlugins)) {
15 | shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
16 | } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
17 | fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
18 | // Show the user where to put plugins in the future
19 | shell.Exec("explorer " + pathPlugins);
20 | shell.Popup("I'm installed!", 0, "Successfully installed", 0x40);
21 | }
22 | WScript.Quit();
23 |
24 | @else@*/
25 |
26 | if(!('SendAudio_downloadModal' in global))
27 | global.SendAudio_downloadModal = false;
28 |
29 | if(!('SendAudio_ZLibrary' in global))
30 | global.SendAudio_ZLibrary = ('ZLibrary' in global);
31 |
32 | module.exports = (() => {
33 | const config = {"info":{"name":"Send Audio","authors":[{"name":"Matues","discord_id":"301016626579505162","github_username":"MKSx"}],"version":"1.1.5","description":"Record and send audios in chat","github":"https://github.com/MKSx/EnviarAudio-BetterDiscord","github_raw":"https://raw.githubusercontent.com/MKSx/Send-Audio-Plugin-BetterDiscord/master/SendAudio.plugin.js"},"main":"index.js","defaultConfig":[{"type":"switch","name":"Preview record","id":"preview","value":false,"note":"Allows audio to be heard before being sent"},{"type":"switch","name":"Using nitro","id":"nitro","value":false,"note":"If you are using discord nitro increases the file size that can be used from 8 MB to 50 MB"},{"type":"dropdown","name":"Audio input","id":"devices","note":"The audio recording device that will be used","options":[{"label":"Default","value":"default"}],"value":"default"},{"type":"dropdown","name":"File format","id":"mimetype","note":"The type of file that will be sent","options":[{"label":"mp3","value":"audio/mp3"},{"label":"ogg","value":"audio/ogg"},{"label":"wav","value":"audio/wav"},{"label":"opus","value":"audio/webm;codecs=opus"},{"label":"webm","value":"audio/webm"}],"value":"audio/mp3"}]};
34 |
35 |
36 | //local lib not found
37 | if(typeof global.ZeresPluginLibrary != 'function'){
38 | //Add remote lib
39 | if (!global.ZLibrary && !global.ZLibraryPromise) global.ZLibraryPromise = new Promise((resolve, reject) => {
40 | require("request").get({url: "https://rauenzi.github.io/BDPluginLibrary/release/ZLibrary.js", timeout: 10000}, (err, res, body) => {
41 | if (err || 200 !== res.statusCode) return reject(err || res.statusMessage);
42 | try {const vm = require("vm"), script = new vm.Script(body, {displayErrors: true}); resolve(script.runInThisContext());}
43 | catch(err) {reject(err);}
44 | });
45 | });
46 |
47 | const title = "Library Missing";
48 | const ModalStack = BdApi.findModuleByProps("push", "update", "pop", "popWithKey");
49 | const TextElement = BdApi.findModuleByProps("Sizes", "Weights");
50 | const ConfirmationModal = BdApi.findModule(m => m.defaultProps && m.key && m.key() == "confirm-modal");
51 |
52 |
53 | if (!ModalStack || !ConfirmationModal || !TextElement) {
54 | return BdApi.alert(title, `The library plugin needed for ${config.info.name} is missing.
Click here to download the library!`);
55 | }
56 |
57 | //download lib
58 | if(!global.SendAudio_downloadModal){
59 | global.SendAudio_downloadModal = true;
60 | ModalStack.push(function(props){
61 |
62 | return BdApi.React.createElement(ConfirmationModal, Object.assign({
63 | header: title,
64 | children: [BdApi.React.createElement(TextElement, {color: TextElement.Colors.PRIMARY, children: [`The library plugin needed for ${config.info.name} is missing. Please click Download Now to install it.`]})],
65 | red: false,
66 | confirmText: "Download Now",
67 | cancelText: "Cancel",
68 | onConfirm: () => {
69 | require("request").get("https://rauenzi.github.io/BDPluginLibrary/release/0PluginLibrary.plugin.js", async (error, response, body) => {
70 | if (error)
71 | return require("electron").shell.openExternal("https://betterdiscord.net/ghdl?url=https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js");
72 |
73 | await new Promise(r => require("fs").writeFile(require("path").join(ContentManager.pluginsFolder, "0PluginLibrary.plugin.js"), body, r));
74 |
75 | //remove remote lib
76 | if(!global.SendAudio_ZLibrary){
77 | setTimeout(_ => {
78 | global.ZLibrary = undefined;
79 | }, 10000);
80 | }
81 |
82 | });
83 | }
84 | }, props));
85 | });
86 | }
87 | }
88 | const compilePlugin = ([Plugin, Api]) => {
89 | const plugin = (Plugin, Api) => {
90 | const { DOMTools, Logger, DiscordAPI, DiscordModules, PluginUtilities } = Api;
91 |
92 | const SelectedChannelStore = DiscordModules.SelectedChannelStore;
93 | const ChannelStore = DiscordModules.ChannelStore;
94 | const Upload = BdApi.findModule(m => m.upload && typeof m.upload === 'function');
95 |
96 | const addButton = (attr, svg) => DOMTools.createElement(``);
97 | const addPanel = (html, attr, remove=false) => {
98 | let dom = document.querySelector('.panels-j1Uci_');
99 |
100 | if(!(dom instanceof Element))
101 | return false;
102 |
103 | if(document.querySelector(`div[${attr}]`) instanceof Element){
104 | if(!remove)
105 | return false;
106 |
107 | document.querySelector(`div[${attr}]`).remove();
108 | }
109 | return dom.insertBefore(DOMTools.createElement(`
${html}
`), dom.firstChild);
110 | };
111 |
112 | const getChannelName = () => {
113 | const channel = DiscordAPI.currentChannel;
114 |
115 | if(channel.type == 'GROUP_DM'){
116 | return (DiscordAPI.currentChannel ? DiscordAPI.currentChannel.name : 'Unknown channel');
117 | }
118 | else if(channel.type == 'DM'){
119 | const recipient = DiscordAPI.currentChannel.recipient;
120 | return '@' + recipient.username + '#' + recipient.discriminator;
121 | }
122 | return '#' + channel.name;
123 | };
124 | const getGuildName = () => {
125 | const channel = DiscordAPI.currentChannel;
126 |
127 | if(channel.type != 'GUILD_TEXT'){
128 | return channel.type;
129 | }
130 |
131 | if(DiscordAPI.currentGuild)
132 | return DiscordAPI.currentGuild.name;
133 |
134 | return 'Unknown server';
135 | };
136 | const convertToTime = (seconds) => {
137 | seconds = parseInt(seconds);
138 | return typeof seconds == 'number' && seconds > -1 ? new Date(parseInt(seconds) * 1000).toISOString().substr(11, 8) : '00:00:00';
139 | };
140 | const convertData = (size) => {
141 | //MB
142 | if(size >= 1048576){
143 | temp = (size / 1024) % 1024;
144 |
145 | return `${parseInt((size / 1024) / 1024)}${temp != 0 ? ('.' + temp).slice(0, 3) : ''} MB`;
146 | }
147 | temp = size % 1024;
148 | return `${parseInt(size / 1024)}${temp != 0 ? ('.' + temp).slice(0,3) : ''} KB`;
149 | };
150 | const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
151 | const randomKey = (len) => {
152 | if(len < 1)
153 | return '';
154 |
155 | let ret = '';
156 | for(let i = 0, j = parseInt(len / 3) + (len % 3); i < j; ++i)
157 | ret = ret + String.fromCharCode(randomInt(65, 90)) + String.fromCharCode(randomInt(48, 57)) + String.fromCharCode(randomInt(97, 122));
158 |
159 | return ret.slice(0, len);
160 | };
161 | const sendAudio = (channel, blob) => {
162 |
163 | let date = new Date();
164 |
165 | date = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString();
166 |
167 | return Upload.upload(channel, new File([blob], randomKey(3) + '-' + date.substr(0, 10) + '-' + date.substr(11, 8).replace(/:/g, '-') + '.mp3'), {content: '', tts: false});
168 | };
169 |
170 | const DiscordPlayer = (() => {
171 |
172 | const addPlayer = (attr) => {
173 | return addPanel(`
174 | Loading audio
175 |
176 |
184 |
185 |
188 |
189 | 00:00:00
190 | /
191 | --:--:--
192 |
193 |
198 |
209 |
210 |
221 |
`, attr);
222 | };
223 | return class{
224 | constructor(){
225 | this.audio = false;
226 |
227 | this.events = {
228 | 'end': null,
229 | 'start': null,
230 | 'close': null
231 | };
232 | this.setDuration = false;
233 | this.id = DiscordModules.KeyGenerator();
234 | this.audioReady = false;
235 | this.playerRef = addPlayer(`id='${this.id}'`);
236 | this.playerLoading = this.playerRef.find(`h2[class='DiscordPlayerLoading']`);
237 | this.playerTime = {current: null, duration: null};
238 | this.playerVolume = {lastValue: 1, moving: false, timer: null, button: null, bar: null, ref: null, bounding: null, svg: null};
239 | this.playerDuration = {ref: null, moving: false, current: null, preview: null, tooltip: null, bounding: null, tooltipHeight: 0, lastUpdate: 0, paused: false};
240 | this.playerButton = null;
241 | this.playerClose = null;
242 | this.playerTitle = null;
243 |
244 | this.playerVolume.button = this.playerRef.find(`div[class="flex-1O1GKY da-flex"]`);
245 | this.playerVolume.ref = this.playerRef.find(`div[class~="audioVolumeWrapper-2t9juP"]`);
246 | this.playerVolume.bar = this.playerVolume.ref.find(`div[class~='mediaBarProgress-1xaPtl']`);
247 | this.playerVolume.svg = this.playerVolume.button.find(`div[class="flex-1O1GKY da-flex"] svg`);
248 | this.playerVolume.bounding = this.playerVolume.bar.parentNode.getBoundingClientRect();
249 |
250 | this.playerDuration.ref = this.playerRef.find(`div[class="mediaBarInteraction-37i2O4 da-mediaBarInteraction"] div`);
251 | this.playerDuration.current = this.playerDuration.ref.find(`div[class~="mediaBarProgress-1xaPtl"]`);
252 | this.playerDuration.preview = this.playerDuration.ref.find(`div[class~="mediaBarPreview-1jfyFs"]`);
253 | this.playerDuration.tooltip = this.playerDuration.ref.find(`div[class~="bubble-3qRl2J"]`);
254 | this.playerDuration.bounding = this.playerDuration.ref.parentNode.getBoundingClientRect();
255 | this.playerDuration.tooltipHeight = this.playerDuration.tooltip.getBoundingClientRect().height;
256 |
257 | this.playerDuration.lastUpdate = Date.now();
258 | this.playerClose = this.playerRef.find(`div[role=header] div`);
259 | this.playerTitle = this.playerRef.find(`div[role=header] input`);
260 |
261 | this.playerClose.addEventListener('click', _ => this.close());
262 |
263 | this.playerTime.duration = this.playerRef.find(`div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']`).lastElementChild;
264 | this.playerTime.current = this.playerRef.find(`div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']`).firstElementChild;
265 |
266 | this.playerButton = this.playerRef.find(`div[role="button"]`);
267 |
268 | this.playerButton.addEventListener('click', _ => {
269 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
270 | return;
271 |
272 | if(this.audio.paused || this.audio.ended)
273 | this.audio.play();
274 | else
275 | this.audio.pause();
276 | });
277 | this.playerVolume.button.addEventListener('click', _ => {
278 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
279 | return;
280 |
281 | if(this.audio.volume > 0){
282 | this.playerVolume.lastValue = this.audio.volume;
283 | this.audio.volume = 0;
284 | }
285 | else{
286 | this.audio.volume = this.playerVolume.lastValue;
287 | }
288 | });
289 |
290 | const showVolumeBar = () => {
291 | this.playerVolume.ref.style.visibility = 'visible';
292 | this.playerVolume.svg.style.opacity = '1';
293 |
294 | if(this.playerVolume.timer != null){
295 | clearInterval(this.playerVolume.timer);
296 | this.playerVolume.timer = null;
297 | }
298 | };
299 | const hideVolumeBar = () => {
300 | if(this.playerVolume.timer == null){
301 | this.playerVolume.timer = setTimeout(plugin => {
302 | plugin.playerVolume.ref.style.visibility = '';
303 | plugin.playerVolume.svg.style.opacity = '';
304 | this.playerVolume.timer = null;
305 | }, 1000, this);
306 | }
307 | };
308 | const moveVolumeBar = (e, click=false) => {
309 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
310 | return;
311 |
312 | const value = Math.min(1, Math.max(0, (this.playerVolume.bounding.bottom - e.clientY) / this.playerVolume.bounding.height)) * 100;
313 |
314 | if(click || this.playerVolume.moving){
315 | //player.volume.bar.style.width = `${value}%`;
316 | this.audio.volume = value / 100;
317 | }
318 | };
319 | const moveBar = (e, click=false) => {
320 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
321 | return;
322 |
323 | const value = Math.min(1, Math.max(0, (e.clientX - this.playerDuration.bounding.left) / this.playerDuration.bounding.width)) * 100;
324 |
325 | if(click || this.playerDuration.moving){
326 | if(this.playerDuration.current instanceof Element){
327 | //player.duration.current.style.width = `${value}%`;
328 | //player.audio.currentTime = (player.audio.duration / 100) * value;
329 | this.audio.currentTime = (value / 100) * this.audio.duration;
330 |
331 | if(this.audio.currentTime != this.audio.duration)
332 | if(this.playerButton.firstElementChild.getAttribute('name') == "Replay")
333 | this.playerButton.innerHTML = ``;
334 |
335 | }
336 | }
337 | if(this.playerDuration.tooltip instanceof Element){
338 | if(Date.now() - this.playerDuration.lastUpdate > 10000)
339 | this.playerDuration.bounding = this.playerDuration.ref.parentNode.getBoundingClientRect();
340 |
341 | this.playerDuration.tooltip.innerHTML = convertToTime((value / 100) * this.audio.duration);
342 | this.playerDuration.tooltip.style.left = `${this.playerDuration.bounding.left + ((this.playerDuration.bounding.width / 100) * value)}px`;
343 | this.playerDuration.tooltip.style.top = `${this.playerDuration.bounding.top - this.playerDuration.tooltipHeight - 10}px`;
344 | }
345 | if(this.playerDuration.preview instanceof Element)
346 | this.playerDuration.preview.style.width = `${value}%`;
347 |
348 | };
349 |
350 | this.playerVolume.button.addEventListener('mouseover', showVolumeBar);
351 | this.playerVolume.button.addEventListener('mouseout', hideVolumeBar);
352 | this.playerVolume.ref.firstElementChild.addEventListener('mouseover', showVolumeBar);
353 | this.playerVolume.ref.firstElementChild.addEventListener('mouseout', hideVolumeBar);
354 |
355 | this.playerVolume.bar.parentNode.addEventListener('click', e => moveVolumeBar(e, true));
356 | this.playerVolume.bar.parentNode.addEventListener('mousemove', e => moveVolumeBar(e));
357 | this.playerVolume.bar.parentNode.addEventListener('mousedown', _ => this.playerVolume.moving = true);
358 | this.playerVolume.bar.parentNode.addEventListener('mouseup', _ => this.playerVolume.moving = false);
359 | this.playerVolume.bar.parentNode.addEventListener('mouseleave', _ => this.playerVolume.moving = false);
360 |
361 | this.playerDuration.ref.addEventListener('click', e => moveBar(e, true));
362 | this.playerDuration.ref.addEventListener('mousemove', e => moveBar(e));
363 |
364 | this.playerDuration.ref.addEventListener('mousedown', _ => {
365 | if(this.audio instanceof Audio && this.audio.readyState != 0){
366 | this.playerDuration.moving = true;
367 |
368 | if(!(this.playerDuration.paused = this.audio.paused || this.audio.ended))
369 | this.audio.pause();
370 | }
371 | });
372 | const dragEnd = () => {
373 | if(this.playerDuration.preview instanceof Element)
374 | this.playerDuration.preview.style.width = `0%`;
375 |
376 | if(this.playerDuration.moving){
377 | if(!this.playerDuration.paused)
378 | this.audio.play().catch(err => Logger.error(err));
379 |
380 | this.playerDuration.moving = false;
381 | }
382 | };
383 |
384 | this.playerDuration.ref.addEventListener('mouseup', dragEnd);
385 | this.playerDuration.ref.addEventListener('mouseleave', dragEnd);
386 |
387 | this.playerRef.style.display = 'none';
388 |
389 | }
390 | close(){
391 | if(this.audio instanceof Audio && (!this.audio.paused && !this.audio.ended))
392 | this.audio.pause();
393 |
394 | if(this.playerRef instanceof Element)
395 | this.playerRef.remove();
396 |
397 | if(typeof this.events.close == 'function')
398 | this.events.close();
399 | }
400 | async setAudio(audio){
401 | if(!(audio instanceof Audio))
402 | return false;
403 |
404 | if(this.audio instanceof Audio){
405 | if(!this.audio.paused && !this.audio.ended)
406 | this.audio.pause();
407 |
408 | this.playerDuration.moving = false;
409 | this.playerVolume.moving = false;
410 |
411 | this.audio.currentTime = 0;
412 | this.audio.volume = 1;
413 | this.audio = null;
414 | }
415 |
416 | /*
417 | Fix audio duration:
418 | https://stackoverflow.com/questions/21522036/html-audio-tag-duration-always-infinity/52375280#52375280
419 | */
420 | const fix_audio = async () => {
421 | this.playerLoading.style.display = 'block';
422 | this.playerRef.find(`div[class='DiscordPlayer']`).style.display = 'none';
423 |
424 | while(audio.duration == Infinity || isNaN(audio.duration)){
425 | audio.currentTime = 10000000 * Math.random();
426 | await new Promise(resolve => setTimeout(resolve, 1000));
427 | }
428 | audio.currentTime = 0;
429 | this.playerButton.innerHTML = ``;
430 | await new Promise(resolve => setTimeout(resolve, 1000));
431 | this.playerLoading.style.display = 'none';
432 | this.playerRef.find(`div[class='DiscordPlayer']`).style.display = 'block';
433 | };
434 |
435 | this.audioReady = audio.readyState != 0;
436 | if(audio.readyState == 0){
437 | this.playerTime.duration.innerHTML = '--:--:--';
438 | audio.addEventListener('loadedmetadata', async _ => {
439 | await fix_audio();
440 | this.audioReady = this.audio.readyState != 0;
441 | this.playerTime.duration.innerHTML = convertToTime(audio.duration);
442 |
443 | });
444 | }
445 | else{
446 | await fix_audio();
447 | this.playerTime.duration.innerHTML = convertToTime(audio.duration);
448 | }
449 | this.audio = audio;
450 | this.playerVolume.bar.style.width = `${this.audio.volume * 100}%`;
451 | this.playerTime.current.innerHTML = '00:00:00';
452 | this.audio.addEventListener('timeupdate', _ => {
453 | this.playerDuration.current.style.width = `${(this.audio.currentTime / this.audio.duration) * 100}%`;
454 | this.playerTime.current.innerHTML = convertToTime(this.audio.currentTime);
455 | });
456 | this.audio.addEventListener('volumechange', _ => {
457 | this.playerVolume.bar.style.width = `${this.audio.volume * 100}%`;
458 | if(this.audio.volume == 0){
459 | this.playerVolume.button.innerHTML = ``;
460 |
461 | this.playerVolume.svg = this.playerVolume.button.find('svg');
462 | this.playerVolume.svg.style.opacity = '1';
463 | }
464 | else{
465 | if(this.playerVolume.svg.getAttribute('name') != 'Speaker'){
466 | this.playerVolume.button.innerHTML = ``;
467 | this.playerVolume.svg = this.playerVolume.button.find('svg');
468 | this.playerVolume.svg.style.opacity = '1';
469 | }
470 | }
471 | });
472 | this.audio.addEventListener('play', _ => {
473 | this.playerButton.innerHTML = ``;
474 | });
475 |
476 | this.audio.addEventListener('pause', _ => {
477 | this.playerButton.innerHTML = ``;
478 | });
479 |
480 | this.audio.addEventListener('ended', _ => {
481 | this.playerButton.innerHTML = ``;
482 | });
483 | return true;
484 | }
485 | setAudioLink(link){return this.setAudio(new Audio(link));}
486 | setTitle(title){this.playerTitle.value = title;}
487 | getTitle(){return this.playerTitle.value;}
488 | getAudio(){return this.audio;}
489 | play(){
490 | if(this.audio instanceof Audio && this.audio.readyState != 0){
491 | if(this.audio.paused || this.audio.ended)
492 | this.audio.play();
493 |
494 | return true;
495 | }
496 | return false;
497 | }
498 | pause(){
499 | if(this.audio instanceof Audio && this.audio.readyState != 0){
500 | if(!(this.audio.paused && this.audio.ended))
501 | this.audio.pause();
502 |
503 | return true;
504 | }
505 | return false;
506 | }
507 | setTime(time){
508 | if(this.audio instanceof Audio && this.audio.readyState != 0){
509 | this.audio.currentTime = time;
510 | return true;
511 | }
512 | return false;
513 | }
514 | setVolume(volume){
515 | if(this.audio instanceof Audio){
516 | this.audio.volume = volume;
517 | return true;
518 | }
519 | return false;
520 | }
521 | getTime(){return this.audio instanceof Audio ? this.audio.currentTime : 0;}
522 | setMuted(e){
523 | if(this.audio instanceof Audio){
524 | if(e){
525 | this.playerVolume.lastValue = this.audio.volume > 0 ? this.audio.volume : 1;
526 | this.audio.volume = 0;
527 | }
528 | else
529 | this.audio.volume = this.playerVolume.lastValue;
530 |
531 | return true;
532 | }
533 | return false;
534 | }
535 | getVolume(){ return this.audio instanceof Audio ? this.audio.volume : 0;}
536 | on(event, call){
537 | if(event in this.events)
538 | this.events[event] = call;
539 | }
540 | display(value){this.playerRef.style.display = value;}
541 | };
542 | })();
543 |
544 | const Tooltip = ((KeyGenerator, DOMTools) => {
545 | const classList = {
546 | 'top': 'tooltipBottom-3ARrEK',
547 | 'left': 'tooltipRight-2JM5PQ',
548 | 'bottom': 'tooltipTop-XDDSxx',
549 | 'right': 'tooltipLeft-3EDOk1'
550 | };
551 |
552 | const root = document.querySelector(`div[class='layerContainer-yqaFcK da-layerContainer']`);
553 |
554 | if(!root instanceof Element)
555 | return null;
556 |
557 | const calcAxis = (type, node, tooltip, tooltipPointer) => {
558 | if(type == 'bottom')
559 | return {'top': `${node.y - tooltipPointer.height - tooltip.height}px`,'left': `${node.x + (node.width / 2) - (tooltip.width / 2)}px`};
560 | else if(type == 'top')
561 | return {'top': `${node.y + node.height + tooltipPointer.height}px`,'left': `${node.x + (node.width / 2) - (tooltip.width / 2)}px`};
562 | else if(type == 'right')
563 | return {'top': `${node.y + (node.height / 2) - (tooltip.height / 2)}px`,'left': `${node.x - tooltip.width - tooltipPointer.width}px`};
564 | else if(type == 'left')
565 | return {'top': `${node.y + (node.height / 2) - (tooltip.height / 2)}px`,'left': `${node.x + node.width + tooltipPointer.width}px`};
566 |
567 | return {'top': '0px', 'left': '0px'};
568 | };
569 | return class Tooltip{
570 | constructor(node, text, type='bottom'){
571 | if(!(type in classList))
572 | type = 'bottom';
573 |
574 | this.props = {
575 | 'id': KeyGenerator(),
576 | 'type': type,
577 | 'text': text,
578 | 'ref': null,
579 | 'target': node,
580 | 'lastUpdate': 0,
581 | 'update': 10000 //10 seconds
582 | };
583 | this.props.ref = root.appendChild(DOMTools.createElement(``));
584 |
585 | this.removed = false;
586 | this.enabled = true;
587 | this.Offsets = {
588 | target: null,
589 | ref: null,
590 | pointerRef: null
591 | };
592 |
593 | this.props.ref.style.display = 'none';
594 | if(node instanceof Element){
595 | this.update();
596 | this.props.target.setAttribute('tooltip-id', this.props.id);
597 | node.addEventListener('mouseenter', this.show);
598 | node.addEventListener('mouseleave', this.hide);
599 | }
600 | }
601 | show(){
602 | if(this.enabled && !this.removed && this.props.ref instanceof Element){
603 | if(this.props.lastUpdate + this.props.update < Date.now())
604 | this.update();
605 |
606 | this.props.ref.style.display = 'block';
607 | return true;
608 | }
609 | return false;
610 | }
611 | hide(){
612 | if(this.enabled && !this.removed && this.props.ref instanceof Element){
613 | this.props.ref.style.display = 'none';
614 | return true;
615 | }
616 | return false;
617 | }
618 | enable(e){
619 | this.enabled = e;
620 | }
621 | toggle(){
622 | if(this.enabled && this.props.ref instanceof Element)
623 | return this.props.ref.style.display == 'none' ? this.show() : this.hide();
624 |
625 | return false;
626 | }
627 | create(force=false){
628 | if(this.removed || force){
629 | this.props.ref = root.appendChild(DOMTools.createElement(``))
630 | this.props.ref.style.display = 'none';
631 | this.removed = false;
632 |
633 | this.update();
634 | }
635 | }
636 | setTarget(node){
637 | if(node instanceof Element){
638 | if(this.props.target instanceof Element){
639 | this.props.target.removeEventListener('mouseenter', this.show);
640 | this.props.target.removeEventListener('mouseleave', this.hide);
641 | }
642 | this.props.target = node;
643 | node.addEventListener('mouseenter', this.show);
644 | node.addEventListener('mouseleave', this.hide);
645 |
646 | if(this.removed)
647 | this.create();
648 | else
649 | this.update();
650 |
651 | return true;
652 | }
653 | return false;
654 | }
655 | setText(text){
656 | if(!this.removed){
657 | this.props.text = text;
658 |
659 | if(this.props.ref instanceof Element){
660 | const node = this.props.ref.find('div[role=text]')
661 | if(node instanceof Element){
662 | node.innerHTML = text;
663 | return true;
664 | }
665 |
666 | }
667 | }
668 | return false;
669 | }
670 | setType(type){
671 | if(!this.removed && type in classList){
672 | if(this.props.ref instanceof Element){
673 | this.props.ref.removeClass(classList[this.props.type]);
674 | this.props.ref.addClass(classList[type]);
675 | this.props.type = type;
676 |
677 | this.update();
678 | return true;
679 | }
680 | }
681 | return false;
682 | }
683 | remove(){
684 | if(!this.removed){
685 |
686 | if(this.props.target instanceof Element){
687 | this.props.target.removeEventListener('mouseenter', this.show);
688 | this.props.target.removeEventListener('mouseleave', this.hide);
689 | }
690 |
691 | if(this.props.ref instanceof Element)
692 | this.props.ref.remove();
693 |
694 | this.props.ref = null;
695 | this.props.target = null;
696 |
697 | this.removed = true;
698 | }
699 | }
700 | update(){
701 | let displayValue = [false, false];
702 |
703 | if(this.props.ref.style.display != 'none')
704 | displayValue[0] = true;
705 | else
706 | this.props.ref.style.display = 'block';
707 |
708 | if(this.props.target.style.display != 'none')
709 | displayValue[1] = true;
710 | else
711 | this.props.target.style.display = 'block';
712 |
713 | this.Offsets = {
714 | target: this.props.target.getBoundingClientRect(),
715 | ref: this.props.ref.getBoundingClientRect(),
716 | pointerRef: this.props.ref.querySelector(`div[class~='da-tooltipPointer']`).getBoundingClientRect()
717 | };
718 |
719 | const { top, left } = calcAxis(this.props.type, this.Offsets.target, this.Offsets.ref, this.Offsets.pointerRef);
720 |
721 | this.props.ref.style.top = top;
722 | this.props.ref.style.left = left;
723 |
724 | if(!displayValue[0])
725 | this.props.ref.style.display = 'none';
726 |
727 | if(!displayValue[1])
728 | this.props.target.style.display = 'none';
729 |
730 | this.props.lastUpdate = Date.now();
731 | }
732 | };
733 | })(DiscordModules.KeyGenerator, DOMTools);
734 | return class SendAudio extends Plugin {
735 |
736 | onStart() {
737 | if(!document.getElementById('css-' + this.getName())){
738 | PluginUtilities.addStyle('css-' + this.getName(),`
739 | :root{--sendaudio-record: var(--interactive-normal);--sendaudio-record-hover: var(--interactive-active);--sendaudio-cancel: var(--interactive-normal);--sendaudio-cancel-hover: var(--interactive-active);--sendaudio-pause: var(--interactive-normal);--sendaudio-pause-hover: var(--interactive-active);--sendaudio-play: var(--interactive-normal);--sendaudio-play-hover: var(--interactive-active);--sendaudio-save: var(--interactive-normal);--sendaudio-save-hover: var(--interactive-active);--sendaudio-send: var(--interactive-normal);--sendaudio-send-hover: var(--interactive-active);}
740 | .flash-record{width: 14px;height: 14px;border-radius:50%;position: relative;top: 2px;margin: 0 2px;animation: flash linear 1s infinite;}
741 | @keyframes flash{0% {opacity: 1;}50% {opacity: .1;}100% {opacity: 1;}}
742 | .DiscordPlayer div[role='header']{color: var(--text-normal);display: flex;-webkit-box-align: center;align-items: center;border-bottom: 5px;}
743 | .DiscordPlayer div[role='header'] input{border: none;background: transparent;color: var(--header-primary);font-weight: 600;}
744 | .DiscordPlayer div[role='header'] div{position: absolute;right: 0;opacity: .3;cursor: pointer;}
745 | .DiscordPlayer div[role='header'] div:hover{opacity: 1 !important;}
746 | .DiscordPlayer div[role='controls']{color: white;display: flex;-webkit-box-align: center;align-items: center;}
747 | .DiscordPlayerLoading{display:none;color: var(--header-primary);font-weight: 600;text-align: center;padding: 5px;}
748 | .DiscordPlayerLoading path{fill: var(--header-primary);stroke: var(--header-primary);stroke-width: 5px;}
749 | .DiscordPlayerLoading svg{width: 16px;height: 16px;}
750 | .DiscordPlayer div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']{color: var(--text-normal);}
751 | .DiscordPlayer div[role=button] svg{color: var(--interactive-normal);opacity: 1;}
752 | .DiscordPlayer div[role=button]:hover svg{color: var(--interactive-active);opacity: 1;}
753 | .DiscordPlayer div[role='control-bar'], .DiscordPlayer div[role='control-bar']:after, .DiscordPlayer div[role='control-bar']:before{background-color: var(--interactive-muted);}
754 | .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs'], .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs']:after, .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs']:before{background-color: var(--text-normal);}
755 | .SendAudio-Info input{border: none;background: transparent;color: var(--header-primary);font-weight: 600;}
756 | #sendAudioButtons button[role=send] polygon{fill: var(--sendaudio-send) !important;}
757 | #sendAudioButtons button[role=send]:hover polygon{fill: var(--sendaudio-send-hover) !important;}
758 | #sendAudioButtons button[role=save] svg{border: 1px solid var(--sendaudio-save) !important;}
759 | #sendAudioButtons button[role=save] svg path:last-child{fill: var(--sendaudio-save) !important;}
760 |
761 | #sendAudioButtons button[role=save]:hover svg{border: 1px solid var(--sendaudio-save-hover) !important;}
762 | #sendAudioButtons button[role=save]:hover svg path:last-child{fill: var(--sendaudio-save-hover) !important;}
763 | #sendAudioButtons button[role=play] svg{color: var(--sendaudio-play) !important;}
764 | #sendAudioButtons button[role=play]:hover svg{color: var(--sendaudio-play-hover) !important;}
765 | #sendAudioButtons button[role=pause] path{fill: var(--sendaudio-pause) !important;}
766 | #sendAudioButtons button[role=pause]:hover path{fill: var(--sendaudio-pause-hover) !important;}
767 | #sendAudioButtons button[role=cancel] path{fill: var(--sendaudio-cancel) !important;}
768 | #sendAudioButtons button[role=cancel]:hover path{fill: var(--sendaudio-cancel-hover) !important;}
769 | #sendAudioButtons button[role=record] svg{color: var(--sendaudio-record) !important;}
770 | #sendAudioButtons button[role=record]:hover svg{color: var(--sendaudio-record-hover) !important;}
771 | .plugin-settings[id="plugin-settings-${this.getName()}"] > div{padding: 20px;}
772 | `);
773 | }
774 |
775 | this.devices = false;
776 |
777 | this.media = false;
778 |
779 | this.mediaInfo = {
780 | ready: false,
781 | error: false
782 | };
783 | this.panelInfo = [false, false];
784 |
785 | this.player = null;
786 |
787 | this.record = {
788 | chunks: [],
789 | blob: null,
790 | max_size: this.settings.nitro ? 52428800 : 8388608,
791 | size: 0,
792 | time: 0,
793 | limitStop: false,
794 | notSave: false,
795 | channel: null,
796 | previewing: false
797 | };
798 |
799 | this.tooltips = {
800 | 'record': new Tooltip(null, 'Record'),
801 | 'cancel': new Tooltip(null, 'Cancel'),
802 | 'pause': new Tooltip(null, 'Pause'),
803 | 'play': new Tooltip(null, 'Play'),
804 | 'resume': new Tooltip(null, 'Resume'),
805 | 'save': new Tooltip(null, 'Save'),
806 | 'send': new Tooltip(null, 'Send'),
807 | 'info': new Tooltip(null, 'Info')
808 | };
809 |
810 | //Init devices
811 |
812 | let plugin = this;
813 |
814 | navigator.mediaDevices.enumerateDevices().then(devices => {
815 | plugin.devices = devices.filter(device => device.kind == 'audioinput' && device.deviceId != 'default');
816 |
817 | let addeds = [];
818 |
819 | plugin.devices.forEach(device => {
820 | if(addeds.indexOf(device.groupId) == -1){
821 | plugin._config.defaultConfig.filter(k => k.id == 'devices')[0].options.push({label: device.label, value: device.deviceId});
822 | addeds.push(device.groupId);
823 | }
824 | });
825 | }).catch(error => {
826 | Logger.warn('Unable to get recording devices:', error);
827 | });
828 |
829 | this.onSwitch();
830 |
831 | this.changeMedia();
832 | }
833 |
834 | onStop() {
835 |
836 | this.mediaInfo.notSave = true;
837 | this.recordStop();
838 |
839 | if(typeof this.buttons == 'object' && this.buttons.group instanceof Element)
840 | this.buttons.group.remove();
841 |
842 | this.recordReset();
843 |
844 | PluginUtilities.removeStyle('css-' + this.getName());
845 |
846 | for(let k in this.tooltips){
847 | this.tooltips[k].remove();
848 | }
849 | }
850 |
851 | onSwitch(){
852 | if(!this.addButtons())
853 | return;
854 | }
855 |
856 | addButtons(){
857 | if(document.getElementById('sendAudioButtons') instanceof Element)
858 | return false;
859 |
860 | let buttons_ = document.querySelector(`div[class="buttons-3JBrkn da-buttons"]`);
861 | let uploadButton = document.getElementsByClassName('attachButton-2WznTc');
862 |
863 | if(!(buttons_ instanceof Element) || (!uploadButton || !(uploadButton[0] instanceof Element)))
864 | return false;
865 |
866 | this.buttons = {
867 | group: DOMTools.createElement(``),
868 | record: addButton(`role='record'`, ``),
869 | cancel: addButton(`role='cancel' style='display: none;'`, ``),
870 | pause: addButton(`role='pause' style='display: none;'`, ``),
871 | play: addButton(`role='play' style='display: none;'`, ``),
872 | save: addButton(`role='save' style='display: none;'`, ``),
873 | send: addButton(`role='send' style='display: none;'`, ``)
874 | };
875 |
876 | this.buttons.group.append(this.buttons.record);
877 | this.buttons.group.append(this.buttons.cancel);
878 | this.buttons.group.append(this.buttons.pause);
879 | this.buttons.group.append(this.buttons.play);
880 | this.buttons.group.append(this.buttons.save);
881 | this.buttons.group.append(this.buttons.send);
882 |
883 | buttons_.append(this.buttons.group);
884 |
885 | for(let k in this.tooltips){
886 | if(k in this.buttons){
887 | this.tooltips[k].setTarget(this.buttons[k]);
888 | this.tooltips[k].props.lastUpdate = 0;
889 | }
890 | }
891 |
892 | const plugin = this;
893 |
894 |
895 | this.buttons.record.addEventListener('click', _ => {
896 | if(!plugin.recordStart()){
897 | Logger.warn("Cannot start recording");
898 | }
899 | });
900 | this.buttons.cancel.addEventListener('click', _ => {
901 | if(plugin.record.previewing == true){
902 | plugin.recordReset();
903 |
904 | plugin.setDisplay({all: 'none', record: ''});
905 | }
906 | else{
907 | plugin.mediaInfo.notSave = true;
908 | plugin.recordStop();
909 | }
910 | });
911 | this.buttons.pause.addEventListener('click', _ => plugin.recordPause());
912 | this.buttons.play.addEventListener('click', _ => plugin.recordResume());
913 | this.buttons.save.addEventListener('click', _ => plugin.recordStop());
914 | this.buttons.send.addEventListener('click', _ => {
915 | if(plugin.record.previewing == true){
916 | if(plugin.record.channel){
917 | sendAudio(plugin.record.channel.id, plugin.record.blob);
918 |
919 | plugin.recordReset();
920 | plugin.setDisplay({all: 'none', record: ''});
921 | }
922 | }
923 | else plugin.recordStop();
924 | });
925 | if(!this.mediaInfo.ready || this.mediaInfo.error)
926 | this.buttons.record.disabled = true;
927 |
928 | if(this.mediaInfo.ready && !this.mediaInfo.error && this.media instanceof MediaRecorder){
929 | const channel = DiscordAPI.currentChannel;
930 |
931 | if(this.record.previewing)
932 | this.setDisplay({record: 'none', cancel: '', send: ''});
933 |
934 | else if(this.media.state != 'inactive'){
935 | if(this.settings.preview)
936 | this.setDisplay({record: 'none', cancel: '', save: ''});
937 | else
938 | this.setDisplay({record: 'none', cancel: '', send: ''});
939 |
940 | if(typeof channel == 'object' && typeof this.record.channel == 'object' && channel.id != this.record.channel.id){
941 | this.buttons.send.disabled = true;
942 | this.buttons.save.disabled = true;
943 | }
944 | if(this.media.state == 'paused')
945 | this.setDisplay({pause: 'none', play: ''});
946 | else
947 | this.setDisplay({pause: '', play: 'none'});
948 | }
949 |
950 | }
951 | return true;
952 | }
953 | addPanelRecording(){
954 | const data = `Recording in ${getChannelName()} (${getGuildName()})`;
955 |
956 | const panel = addPanel(``, `id='SendAudio-RecordingInfo'`);
957 |
958 | if(panel instanceof Element){
959 | panel.addClass('SendAudio-Info');
960 | this.panelInfo[0] = panel;
961 | this.panelInfo[1] = panel.querySelector('input[role=time]');
962 |
963 | this.tooltips.info.setText(data);
964 | this.tooltips.info.setTarget(panel);
965 | return true;
966 | }
967 | return false;
968 | }
969 | removePanelRecording(){
970 | if(this.panelInfo[0] instanceof Element)
971 | this.panelInfo[0].remove();
972 |
973 | this.panelInfo = [false, false];
974 | }
975 |
976 | setDisplay(values){
977 | if(typeof values != 'object' || typeof this.buttons != 'object' || !(this.buttons.group instanceof Element))
978 | return false;
979 |
980 | if(values.all)
981 | for(let key in this.buttons)
982 | if(key != 'group' && this.buttons[key] instanceof Element)
983 | this.buttons[key].style.display = values.all;
984 |
985 | delete values.all;
986 |
987 | for(let key in values)
988 | if(key in this.buttons && this.buttons[key] instanceof Element)
989 | this.buttons[key].style.display = values[key];
990 |
991 | return true;
992 | }
993 |
994 | getSettingsPanel(){
995 | const settings = this.buildSettingsPanel();
996 | if(settings){
997 | settings.addListener((id, value) => {
998 | if(id == 'devices'){
999 | this.changeMedia();
1000 | }
1001 | else if(id == 'preview'){
1002 | if(this.media instanceof MediaRecorder && this.media.state != 'inactive'){
1003 | if(value)
1004 | this.setDisplay({save: '', send: 'none'});
1005 | else
1006 | this.setDisplay({save: 'none', send: ''});
1007 | }
1008 | }
1009 | else if(id == 'nitro'){
1010 | this.record.max_size = value ? 52428800 : 8388608;
1011 | }
1012 | });
1013 | return settings.getElement();
1014 | }
1015 | Logger.warn("Failed to display settings");
1016 | }
1017 |
1018 | changeMedia(device=false, changing=false){
1019 | const plugin = this;
1020 |
1021 | const change = () => {
1022 | plugin.media = null;
1023 | plugin.mediaInfo.ready = false;
1024 | plugin.mediaInfo.error = false;
1025 |
1026 | navigator.mediaDevices.getUserMedia({audio: (device != false ? device : plugin.settings.devices)}).then(s => {
1027 | plugin.media = new MediaRecorder(s);
1028 | Logger.log("MediaRecorder started successfully");
1029 |
1030 | plugin.media.addEventListener('dataavailable', e => plugin.onRecordingData(e.data));
1031 | plugin.media.addEventListener('start', _ => plugin.onRecordingStateChange(0));
1032 | plugin.media.addEventListener('stop', _ => plugin.onRecordingStateChange(1));
1033 | plugin.media.addEventListener('pause', _ => plugin.onRecordingStateChange(2));
1034 | plugin.media.addEventListener('resume', _ => plugin.onRecordingStateChange(3));
1035 |
1036 | plugin.media.addEventListener('error', error => {
1037 | Logger.error('Recording error:', error);
1038 | });
1039 | plugin.mediaInfo.ready = true;
1040 | plugin.mediaInfo.error = false;
1041 | if(typeof plugin.buttons == 'object' && plugin.buttons.record instanceof Element)
1042 | plugin.buttons.record.disabled = false;
1043 |
1044 | }).catch(err => {
1045 | plugin.mediaInfo.ready = false;
1046 | plugin.mediaInfo.error = true;
1047 | if(typeof plugin.buttons == 'object' &&plugin.buttons.record instanceof Element)
1048 | plugin.buttons.record.disabled = true;
1049 |
1050 | Logger.error("Failed to start MediaRecorder:", err.message);
1051 |
1052 | if(!changing){
1053 | Logger.log("Changing to the default");
1054 | plugin.changeMedia('default', true);
1055 | }
1056 | });
1057 | };
1058 |
1059 | if(this.media instanceof MediaRecorder){
1060 | this.mediaInfo.notSave = true;
1061 | if(this.media.state != 'inactive')
1062 | this.media.stop().then(_ => change()).catch(_ => change());
1063 | }
1064 | else{
1065 | change();
1066 | }
1067 | }
1068 |
1069 | /* Record methods */
1070 | recordStart(){
1071 | if(this.media instanceof MediaRecorder){
1072 | if(this.media.state == 'inactive'){
1073 | this.record.chunks = [];
1074 | this.record.size = 0;
1075 | this.record.time = 0;
1076 | this.media.start(1000);
1077 | }
1078 | return this.media.state == 'recording';
1079 | }
1080 | return false;
1081 | }
1082 | recordStop(){
1083 | if(this.media instanceof MediaRecorder){
1084 | if(this.media.state != 'inactive')
1085 | this.media.stop();
1086 |
1087 | return this.media.state == 'inactive';
1088 | }
1089 | return false;
1090 | }
1091 | recordPause(){
1092 | if(this.media instanceof MediaRecorder){
1093 | if(this.media.state == 'recording')
1094 | this.media.pause();
1095 |
1096 | return this.media.state == 'paused';
1097 | }
1098 | return false;
1099 | }
1100 | recordResume(){
1101 | if(this.media instanceof MediaRecorder){
1102 | if(this.media.state == 'paused')
1103 | this.media.resume();
1104 |
1105 | return this.media.state == 'recording';
1106 | }
1107 | return false;
1108 | }
1109 | recordReset(){
1110 | this.record.chunks = [];
1111 | this.record.time = 0;
1112 | this.record.size = 0;
1113 | this.record.notSave = false;
1114 | this.record.blob = null;
1115 | this.record.channel = null;
1116 | this.record.previewing = false;
1117 | this.record.limitStop = false;
1118 |
1119 | if(typeof this.buttons == 'object' && 'group' in this.buttons && this.buttons.group instanceof Element){
1120 | this.buttons.save.disabled = false;
1121 | this.buttons.send.disabled = false;
1122 | }
1123 |
1124 | if(this.player != null)
1125 | this.player.close();
1126 | }
1127 |
1128 | /* Record Event */
1129 | onRecordingStateChange(new_state){
1130 | //start
1131 | switch(new_state){
1132 | //start
1133 | case 0:{
1134 | this.addPanelRecording();
1135 | this.record.channel = DiscordAPI.currentChannel;
1136 | if(this.settings.preview)
1137 | this.setDisplay({record: 'none', cancel: 'block', pause: 'block', save: 'block'});
1138 | else
1139 | this.setDisplay({record: 'none', cancel: 'block', pause: 'block', send: 'block'});
1140 | break;
1141 | }
1142 | //stop
1143 | case 1:{
1144 | this.removePanelRecording();
1145 |
1146 | if(this.mediaInfo.notSave){
1147 | this.mediaInfo.notSave = false;
1148 | this.setDisplay({all: 'none', record: ''});
1149 | this.recordReset();
1150 | return;
1151 | }
1152 | if(this.record.size > 0){
1153 |
1154 | if(this.record.size > this.record.max_size){
1155 | let tempChunks = [];
1156 | let tempSize = 0;
1157 |
1158 | for(let i = 0; i < this.record.chunks.length; ++i){
1159 | if(tempSize + this.record.chunks[i].size <= this.record.max_size){
1160 | tempChunks.push(this.record.chunks[i]);
1161 | tempSize += this.record.chunks[i].size;
1162 | }
1163 | }
1164 | this.record.chunks = tempChunks;
1165 | this.record.size = tempSize;
1166 | }
1167 |
1168 | this.record.blob = new Blob(this.record.chunks, {type: this.settings.mimetype})
1169 | if(!this.settings.preview){
1170 | if(this.record.channel)
1171 | sendAudio(this.record.channel.id, this.record.blob);
1172 | }
1173 | else{
1174 | Logger.log("PREVIEW");
1175 | this.player = new DiscordPlayer();
1176 | this.player.setAudio(new Audio(URL.createObjectURL(this.record.blob)));
1177 | this.player.display('block');
1178 | this.record.previewing = true;
1179 | this.setDisplay({all: 'none', cancel: 'block', send: 'block'});
1180 | return;
1181 | }
1182 | }
1183 | this.recordReset();
1184 | this.setDisplay({all: 'none', record: ''});
1185 | break;
1186 | }
1187 | //pause
1188 | case 2:{
1189 | this.setDisplay({play: '', pause: 'none'});
1190 | break;
1191 | }
1192 | //resume
1193 | case 3:{
1194 | this.setDisplay({play: 'none', pause: ''});
1195 | break;
1196 | }
1197 | default:
1198 | break;
1199 | }
1200 |
1201 | }
1202 | onRecordingData(data){
1203 | this.record.time ++;
1204 |
1205 | if(data.size > 0){
1206 | if(this.record.size + data.size > this.record.max_size){
1207 | this.record.limitStop = true;
1208 | this.recordStop();
1209 | return;
1210 | }
1211 | this.record.chunks.push(data);
1212 | this.record.size += data.size;
1213 | }
1214 | if(this.panelInfo[1] instanceof Element)
1215 | this.panelInfo[1].value = `${convertToTime(this.record.time)} - ${convertData(this.record.size)}`;
1216 | }
1217 | };
1218 | };
1219 | return plugin(Plugin, Api);
1220 | };
1221 | if(!global.ZLibrary && typeof global.ZeresPluginLibrary != 'function'){
1222 | return class {
1223 | getName() {return config.info.name.replace(" ", "");} getAuthor() {return config.info.authors.map(a => a.name).join(", ");} getDescription() {return config.info.description;} getVersion() {return config.info.version;} stop() {}
1224 | showAlert() {window.BdApi.alert("Loading Error",`Something went wrong trying to load the library for the plugin. You can try using a local copy of the library to fix this.
Click here to download the library!`);}
1225 | async load() {
1226 | try {await global.ZLibraryPromise;}
1227 | catch(err) {return this.showAlert();}
1228 | const vm = require("vm"), plugin = compilePlugin(global.ZLibrary.buildPlugin(config));
1229 | try {new vm.Script(plugin, {displayErrors: true});} catch(err) {return bdpluginErrors.push({name: this.getName(), file: this.getName() + ".plugin.js", reason: "Plugin could not be compiled.", error: {message: err.message, stack: err.stack}});}
1230 | global[this.getName()] = plugin;
1231 | try {new vm.Script(`new global["${this.getName()}"]();`, {displayErrors: true});} catch(err) {return bdpluginErrors.push({name: this.getName(), file: this.getName() + ".plugin.js", reason: "Plugin could not be constructed", error: {message: err.message, stack: err.stack}});}
1232 | bdplugins[this.getName()].plugin = new global[this.getName()]();
1233 | bdplugins[this.getName()].plugin.load();
1234 | }
1235 | async start() {
1236 | try {await global.ZLibraryPromise;}
1237 | catch(err) {return this.showAlert();}
1238 | bdplugins[this.getName()].plugin.start();
1239 | }
1240 | };
1241 | }
1242 | //Loading plugin using remote lib or local lib
1243 | return typeof global.ZeresPluginLibrary != 'function' ? compilePlugin(global.ZLibrary.buildPlugin(config)) : compilePlugin(global.ZeresPluginLibrary.buildPlugin(config));
1244 | })();
1245 | /*@end@*/
1246 |
--------------------------------------------------------------------------------
/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "name": "Send Audio",
4 | "authors": [{
5 | "name": "Matues",
6 | "discord_id": "301016626579505162",
7 | "github_username": "MKSx"
8 | }],
9 | "version": "1.1.4",
10 | "description": "Record and send audios in chat",
11 | "github": "https://github.com/MKSx/EnviarAudio-BetterDiscord",
12 | "github_raw": "https://raw.githubusercontent.com/MKSx/Send-Audio-Plugin-BetterDiscord/master/SendAudio.plugin.js"
13 | },
14 | "main": "index.js",
15 | "defaultConfig": [
16 | {"type": "switch", "name": "Preview record", "id": "preview", "value": false, "note": "Allows audio to be heard before being sent"},
17 | {"type": "switch", "name": "Using nitro", "id": "nitro", "value": false, "note": "If you are using discord nitro increases the file size that can be used from 8 MB to 50 MB"},
18 | {"type": "dropdown", "name": "Audio input", "id": "devices", "note": "The audio recording device that will be used", "options": [{"label": "Default", "value": "default"}], "value": "default"},
19 | {"type": "dropdown", "name": "File format", "id": "mimetype", "note": "The type of file that will be sent", "options": [{"label": "mp3", "value": "audio/mp3"}, {"label": "ogg", "value": "audio/ogg"}, {"label": "wav", "value": "audio/wav"}, {"label": "opus", "value": "audio/webm;codecs=opus"}, {"label": "webm", "value": "audio/webm"}], "value": "audio/mp3"}
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports = (Plugin, Api) => {
2 | const { DOMTools, Logger, DiscordAPI, DiscordModules, PluginUtilities } = Api;
3 |
4 | const SelectedChannelStore = DiscordModules.SelectedChannelStore;
5 | const ChannelStore = DiscordModules.ChannelStore;
6 | const Upload = BdApi.findModule(m => m.upload && typeof m.upload === 'function');
7 |
8 | const addButton = (attr, svg) => DOMTools.createElement(``);
9 | const addPanel = (html, attr, remove=false) => {
10 | let dom = document.querySelector('.panels-j1Uci_');
11 |
12 | if(!(dom instanceof Element))
13 | return false;
14 |
15 | if(document.querySelector(`div[${attr}]`) instanceof Element){
16 | if(!remove)
17 | return false;
18 |
19 | document.querySelector(`div[${attr}]`).remove();
20 | }
21 | return dom.insertBefore(DOMTools.createElement(`${html}
`), dom.firstChild);
22 | };
23 |
24 | const getChannelName = () => {
25 | const channel = DiscordAPI.currentChannel;
26 |
27 | if(channel.type == 'GROUP_DM'){
28 | return (DiscordAPI.currentChannel ? DiscordAPI.currentChannel.name : 'Unknown channel');
29 | }
30 | else if(channel.type == 'DM'){
31 | const recipient = DiscordAPI.currentChannel.recipient;
32 | return '@' + recipient.username + '#' + recipient.discriminator;
33 | }
34 | return '#' + channel.name;
35 | };
36 | const getGuildName = () => {
37 | const channel = DiscordAPI.currentChannel;
38 |
39 | if(channel.type != 'GUILD_TEXT'){
40 | return channel.type;
41 | }
42 |
43 | if(DiscordAPI.currentGuild)
44 | return DiscordAPI.currentGuild.name;
45 |
46 | return 'Unknown server';
47 | };
48 | const convertToTime = (seconds) => {
49 | seconds = parseInt(seconds);
50 | return typeof seconds == 'number' && seconds > -1 ? new Date(parseInt(seconds) * 1000).toISOString().substr(11, 8) : '00:00:00';
51 | };
52 | const convertData = (size) => {
53 | //MB
54 | if(size >= 1048576){
55 | temp = (size / 1024) % 1024;
56 |
57 | return `${parseInt((size / 1024) / 1024)}${temp != 0 ? ('.' + temp).slice(0, 3) : ''} MB`;
58 | }
59 | temp = size % 1024;
60 | return `${parseInt(size / 1024)}${temp != 0 ? ('.' + temp).slice(0,3) : ''} KB`;
61 | };
62 | const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
63 | const randomKey = (len) => {
64 | if(len < 1)
65 | return '';
66 |
67 | let ret = '';
68 | for(let i = 0, j = parseInt(len / 3) + (len % 3); i < j; ++i)
69 | ret = ret + String.fromCharCode(randomInt(65, 90)) + String.fromCharCode(randomInt(48, 57)) + String.fromCharCode(randomInt(97, 122));
70 |
71 | return ret.slice(0, len);
72 | };
73 | const sendAudio = (channel, blob) => {
74 |
75 | let date = new Date();
76 |
77 | date = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString();
78 |
79 | return Upload.upload(channel, new File([blob], randomKey(3) + '-' + date.substr(0, 10) + '-' + date.substr(11, 8).replace(/:/g, '-') + '.mp3'), {content: '', tts: false});
80 | };
81 |
82 | const DiscordPlayer = (() => {
83 |
84 | const addPlayer = (attr) => {
85 | return addPanel(`
86 | Loading audio
87 |
88 |
96 |
97 |
100 |
101 | 00:00:00
102 | /
103 | --:--:--
104 |
105 |
110 |
121 |
122 |
133 |
`, attr);
134 | };
135 | return class{
136 | constructor(){
137 | this.audio = false;
138 |
139 | this.events = {
140 | 'end': null,
141 | 'start': null,
142 | 'close': null
143 | };
144 | this.setDuration = false;
145 | this.id = DiscordModules.KeyGenerator();
146 | this.audioReady = false;
147 | this.playerRef = addPlayer(`id='${this.id}'`);
148 | this.playerLoading = this.playerRef.find(`h2[class='DiscordPlayerLoading']`);
149 | this.playerTime = {current: null, duration: null};
150 | this.playerVolume = {lastValue: 1, moving: false, timer: null, button: null, bar: null, ref: null, bounding: null, svg: null};
151 | this.playerDuration = {ref: null, moving: false, current: null, preview: null, tooltip: null, bounding: null, tooltipHeight: 0, lastUpdate: 0, paused: false};
152 | this.playerButton = null;
153 | this.playerClose = null;
154 | this.playerTitle = null;
155 |
156 | this.playerVolume.button = this.playerRef.find(`div[class="flex-1O1GKY da-flex"]`);
157 | this.playerVolume.ref = this.playerRef.find(`div[class~="audioVolumeWrapper-2t9juP"]`);
158 | this.playerVolume.bar = this.playerVolume.ref.find(`div[class~='mediaBarProgress-1xaPtl']`);
159 | this.playerVolume.svg = this.playerVolume.button.find(`div[class="flex-1O1GKY da-flex"] svg`);
160 | this.playerVolume.bounding = this.playerVolume.bar.parentNode.getBoundingClientRect();
161 |
162 | this.playerDuration.ref = this.playerRef.find(`div[class="mediaBarInteraction-37i2O4 da-mediaBarInteraction"] div`);
163 | this.playerDuration.current = this.playerDuration.ref.find(`div[class~="mediaBarProgress-1xaPtl"]`);
164 | this.playerDuration.preview = this.playerDuration.ref.find(`div[class~="mediaBarPreview-1jfyFs"]`);
165 | this.playerDuration.tooltip = this.playerDuration.ref.find(`div[class~="bubble-3qRl2J"]`);
166 | this.playerDuration.bounding = this.playerDuration.ref.parentNode.getBoundingClientRect();
167 | this.playerDuration.tooltipHeight = this.playerDuration.tooltip.getBoundingClientRect().height;
168 |
169 | this.playerDuration.lastUpdate = Date.now();
170 | this.playerClose = this.playerRef.find(`div[role=header] div`);
171 | this.playerTitle = this.playerRef.find(`div[role=header] input`);
172 |
173 | this.playerClose.on('click', _ => this.close());
174 |
175 | this.playerTime.duration = this.playerRef.find(`div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']`).lastElementChild;
176 | this.playerTime.current = this.playerRef.find(`div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']`).firstElementChild;
177 |
178 | this.playerButton = this.playerRef.find(`div[role="button"]`);
179 |
180 | this.playerButton.on('click', _ => {
181 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
182 | return;
183 |
184 | if(this.audio.paused || this.audio.ended)
185 | this.audio.play();
186 | else
187 | this.audio.pause();
188 | });
189 | this.playerVolume.button.on('click', _ => {
190 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
191 | return;
192 |
193 | if(this.audio.volume > 0){
194 | this.playerVolume.lastValue = this.audio.volume;
195 | this.audio.volume = 0;
196 | }
197 | else{
198 | this.audio.volume = this.playerVolume.lastValue;
199 | }
200 | });
201 |
202 | const showVolumeBar = () => {
203 | this.playerVolume.ref.style.visibility = 'visible';
204 | this.playerVolume.svg.style.opacity = '1';
205 |
206 | if(this.playerVolume.timer != null){
207 | clearInterval(this.playerVolume.timer);
208 | this.playerVolume.timer = null;
209 | }
210 | };
211 | const hideVolumeBar = () => {
212 | if(this.playerVolume.timer == null){
213 | this.playerVolume.timer = setTimeout(plugin => {
214 | plugin.playerVolume.ref.style.visibility = '';
215 | plugin.playerVolume.svg.style.opacity = '';
216 | this.playerVolume.timer = null;
217 | }, 1000, this);
218 | }
219 | };
220 | const moveVolumeBar = (e, click=false) => {
221 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
222 | return;
223 |
224 | const value = Math.min(1, Math.max(0, (this.playerVolume.bounding.bottom - e.clientY) / this.playerVolume.bounding.height)) * 100;
225 |
226 | if(click || this.playerVolume.moving){
227 | //player.volume.bar.style.width = `${value}%`;
228 | this.audio.volume = value / 100;
229 | }
230 | };
231 | const moveBar = (e, click=false) => {
232 | if(!(this.audio instanceof Audio) || this.audio.readyState == 0)
233 | return;
234 |
235 | const value = Math.min(1, Math.max(0, (e.clientX - this.playerDuration.bounding.left) / this.playerDuration.bounding.width)) * 100;
236 |
237 | if(click || this.playerDuration.moving){
238 | if(this.playerDuration.current instanceof Element){
239 | //player.duration.current.style.width = `${value}%`;
240 | //player.audio.currentTime = (player.audio.duration / 100) * value;
241 | this.audio.currentTime = (value / 100) * this.audio.duration;
242 |
243 | if(this.audio.currentTime != this.audio.duration)
244 | if(this.playerButton.firstElementChild.getAttribute('name') == "Replay")
245 | this.playerButton.innerHTML = ``;
246 |
247 | }
248 | }
249 | if(this.playerDuration.tooltip instanceof Element){
250 | if(Date.now() - this.playerDuration.lastUpdate > 10000)
251 | this.playerDuration.bounding = this.playerDuration.ref.parentNode.getBoundingClientRect();
252 |
253 | this.playerDuration.tooltip.innerHTML = convertToTime((value / 100) * this.audio.duration);
254 | this.playerDuration.tooltip.style.left = `${this.playerDuration.bounding.left + ((this.playerDuration.bounding.width / 100) * value)}px`;
255 | this.playerDuration.tooltip.style.top = `${this.playerDuration.bounding.top - this.playerDuration.tooltipHeight - 10}px`;
256 | }
257 | if(this.playerDuration.preview instanceof Element)
258 | this.playerDuration.preview.style.width = `${value}%`;
259 |
260 | };
261 |
262 | this.playerVolume.button.on('mouseover', showVolumeBar);
263 | this.playerVolume.button.on('mouseout', hideVolumeBar);
264 | this.playerVolume.ref.firstElementChild.on('mouseover', showVolumeBar);
265 | this.playerVolume.ref.firstElementChild.on('mouseout', hideVolumeBar);
266 |
267 | this.playerVolume.bar.parentNode.on('click', e => moveVolumeBar(e, true));
268 | this.playerVolume.bar.parentNode.on('mousemove', e => moveVolumeBar(e));
269 | this.playerVolume.bar.parentNode.on('mousedown', _ => this.playerVolume.moving = true);
270 | this.playerVolume.bar.parentNode.on('mouseup', _ => this.playerVolume.moving = false);
271 | this.playerVolume.bar.parentNode.on('mouseleave', _ => this.playerVolume.moving = false);
272 |
273 | this.playerDuration.ref.on('click', e => moveBar(e, true));
274 | this.playerDuration.ref.on('mousemove', e => moveBar(e));
275 |
276 | this.playerDuration.ref.on('mousedown', _ => {
277 | if(this.audio instanceof Audio && this.audio.readyState != 0){
278 | this.playerDuration.moving = true;
279 |
280 | if(!(this.playerDuration.paused = this.audio.paused || this.audio.ended))
281 | this.audio.pause();
282 | }
283 | });
284 | const dragEnd = () => {
285 | if(this.playerDuration.preview instanceof Element)
286 | this.playerDuration.preview.style.width = `0%`;
287 |
288 | if(this.playerDuration.moving){
289 | if(!this.playerDuration.paused)
290 | this.audio.play().catch(err => Logger.error(err));
291 |
292 | this.playerDuration.moving = false;
293 | }
294 | };
295 |
296 | this.playerDuration.ref.on('mouseup', dragEnd);
297 | this.playerDuration.ref.on('mouseleave', dragEnd);
298 |
299 | this.playerRef.style.display = 'none';
300 |
301 | }
302 | close(){
303 | if(this.audio instanceof Audio && (!this.audio.paused && !this.audio.ended))
304 | this.audio.pause();
305 |
306 | if(this.playerRef instanceof Element)
307 | this.playerRef.remove();
308 |
309 | if(typeof this.events.close == 'function')
310 | this.events.close();
311 | }
312 | async setAudio(audio){
313 | if(!(audio instanceof Audio))
314 | return false;
315 |
316 | if(this.audio instanceof Audio){
317 | if(!this.audio.paused && !this.audio.ended)
318 | this.audio.pause();
319 |
320 | this.playerDuration.moving = false;
321 | this.playerVolume.moving = false;
322 |
323 | this.audio.currentTime = 0;
324 | this.audio.volume = 1;
325 | this.audio = null;
326 | }
327 |
328 | /*
329 | Fix audio duration:
330 | https://stackoverflow.com/questions/21522036/html-audio-tag-duration-always-infinity/52375280#52375280
331 | */
332 | const fix_audio = async () => {
333 | this.playerLoading.style.display = 'block';
334 | this.playerRef.find(`div[class='DiscordPlayer']`).style.display = 'none';
335 |
336 | while(audio.duration == Infinity || isNaN(audio.duration)){
337 | audio.currentTime = 10000000 * Math.random();
338 | await new Promise(resolve => setTimeout(resolve, 1000));
339 | }
340 | audio.currentTime = 0;
341 | this.playerButton.innerHTML = ``;
342 | await new Promise(resolve => setTimeout(resolve, 1000));
343 | this.playerLoading.style.display = 'none';
344 | this.playerRef.find(`div[class='DiscordPlayer']`).style.display = 'block';
345 | };
346 |
347 | this.audioReady = audio.readyState != 0;
348 | if(audio.readyState == 0){
349 | this.playerTime.duration.innerHTML = '--:--:--';
350 | audio.on('loadedmetadata', async _ => {
351 | await fix_audio();
352 | this.audioReady = this.audio.readyState != 0;
353 | this.playerTime.duration.innerHTML = convertToTime(audio.duration);
354 |
355 | });
356 | }
357 | else{
358 | await fix_audio();
359 | this.playerTime.duration.innerHTML = convertToTime(audio.duration);
360 | }
361 | this.audio = audio;
362 | this.playerVolume.bar.style.width = `${this.audio.volume * 100}%`;
363 | this.playerTime.current.innerHTML = '00:00:00';
364 | this.audio.on('timeupdate', _ => {
365 | this.playerDuration.current.style.width = `${(this.audio.currentTime / this.audio.duration) * 100}%`;
366 | this.playerTime.current.innerHTML = convertToTime(this.audio.currentTime);
367 | });
368 | this.audio.on('volumechange', _ => {
369 | this.playerVolume.bar.style.width = `${this.audio.volume * 100}%`;
370 | if(this.audio.volume == 0){
371 | this.playerVolume.button.innerHTML = ``;
372 |
373 | this.playerVolume.svg = this.playerVolume.button.find('svg');
374 | this.playerVolume.svg.style.opacity = '1';
375 | }
376 | else{
377 | if(this.playerVolume.svg.getAttribute('name') != 'Speaker'){
378 | this.playerVolume.button.innerHTML = ``;
379 | this.playerVolume.svg = this.playerVolume.button.find('svg');
380 | this.playerVolume.svg.style.opacity = '1';
381 | }
382 | }
383 | });
384 | this.audio.on('play', _ => {
385 | this.playerButton.innerHTML = ``;
386 | });
387 |
388 | this.audio.on('pause', _ => {
389 | this.playerButton.innerHTML = ``;
390 | });
391 |
392 | this.audio.on('ended', _ => {
393 | this.playerButton.innerHTML = ``;
394 | });
395 | return true;
396 | }
397 | setAudioLink(link){return this.setAudio(new Audio(link));}
398 | setTitle(title){this.playerTitle.value = title;}
399 | getTitle(){return this.playerTitle.value;}
400 | getAudio(){return this.audio;}
401 | play(){
402 | if(this.audio instanceof Audio && this.audio.readyState != 0){
403 | if(this.audio.paused || this.audio.ended)
404 | this.audio.play();
405 |
406 | return true;
407 | }
408 | return false;
409 | }
410 | pause(){
411 | if(this.audio instanceof Audio && this.audio.readyState != 0){
412 | if(!(this.audio.paused && this.audio.ended))
413 | this.audio.pause();
414 |
415 | return true;
416 | }
417 | return false;
418 | }
419 | setTime(time){
420 | if(this.audio instanceof Audio && this.audio.readyState != 0){
421 | this.audio.currentTime = time;
422 | return true;
423 | }
424 | return false;
425 | }
426 | setVolume(volume){
427 | if(this.audio instanceof Audio){
428 | this.audio.volume = volume;
429 | return true;
430 | }
431 | return false;
432 | }
433 | getTime(){return this.audio instanceof Audio ? this.audio.currentTime : 0;}
434 | setMuted(e){
435 | if(this.audio instanceof Audio){
436 | if(e){
437 | this.playerVolume.lastValue = this.audio.volume > 0 ? this.audio.volume : 1;
438 | this.audio.volume = 0;
439 | }
440 | else
441 | this.audio.volume = this.playerVolume.lastValue;
442 |
443 | return true;
444 | }
445 | return false;
446 | }
447 | getVolume(){ return this.audio instanceof Audio ? this.audio.volume : 0;}
448 | on(event, call){
449 | if(event in this.events)
450 | this.events[event] = call;
451 | }
452 | display(value){this.playerRef.style.display = value;}
453 | };
454 | })();
455 |
456 | const Tooltip = ((KeyGenerator, DOMTools) => {
457 | const classList = {
458 | 'top': 'tooltipBottom-3ARrEK',
459 | 'left': 'tooltipRight-2JM5PQ',
460 | 'bottom': 'tooltipTop-XDDSxx',
461 | 'right': 'tooltipLeft-3EDOk1'
462 | };
463 |
464 | const root = document.querySelector(`div[class='layerContainer-yqaFcK da-layerContainer']`);
465 |
466 | if(!root instanceof Element)
467 | return null;
468 |
469 | const calcAxis = (type, node, tooltip, tooltipPointer) => {
470 | if(type == 'bottom')
471 | return {'top': `${node.y - tooltipPointer.height - tooltip.height}px`,'left': `${node.x + (node.width / 2) - (tooltip.width / 2)}px`};
472 | else if(type == 'top')
473 | return {'top': `${node.y + node.height + tooltipPointer.height}px`,'left': `${node.x + (node.width / 2) - (tooltip.width / 2)}px`};
474 | else if(type == 'right')
475 | return {'top': `${node.y + (node.height / 2) - (tooltip.height / 2)}px`,'left': `${node.x - tooltip.width - tooltipPointer.width}px`};
476 | else if(type == 'left')
477 | return {'top': `${node.y + (node.height / 2) - (tooltip.height / 2)}px`,'left': `${node.x + node.width + tooltipPointer.width}px`};
478 |
479 | return {'top': '0px', 'left': '0px'};
480 | };
481 | return class Tooltip{
482 | constructor(node, text, type='bottom'){
483 | if(!(type in classList))
484 | type = 'bottom';
485 |
486 | this.props = {
487 | 'id': KeyGenerator(),
488 | 'type': type,
489 | 'text': text,
490 | 'ref': null,
491 | 'target': node,
492 | 'lastUpdate': 0,
493 | 'update': 10000 //10 seconds
494 | };
495 | this.props.ref = root.appendChild(DOMTools.createElement(``));
496 |
497 | this.removed = false;
498 | this.enabled = true;
499 | this.Offsets = {
500 | target: null,
501 | ref: null,
502 | pointerRef: null
503 | };
504 |
505 | this.props.ref.style.display = 'none';
506 | if(node instanceof Element){
507 | this.update();
508 | this.props.target.setAttribute('tooltip-id', this.props.id);
509 | node.on('mouseenter.Tooltip', _ => this.show());
510 | node.on('mouseleave.Tooltip', _ => this.hide());
511 | }
512 | }
513 | show(){
514 | if(this.enabled && !this.removed && this.props.ref instanceof Element){
515 | if(this.props.lastUpdate + this.props.update < Date.now())
516 | this.update();
517 |
518 | this.props.ref.style.display = 'block';
519 | return true;
520 | }
521 | return false;
522 | }
523 | hide(){
524 | if(this.enabled && !this.removed && this.props.ref instanceof Element){
525 | this.props.ref.style.display = 'none';
526 | return true;
527 | }
528 | return false;
529 | }
530 | enable(e){
531 | this.enabled = e;
532 | }
533 | toggle(){
534 | if(this.enabled && this.props.ref instanceof Element)
535 | return this.props.ref.style.display == 'none' ? this.show() : this.hide();
536 |
537 | return false;
538 | }
539 | create(force=false){
540 | if(this.removed || force){
541 | this.props.ref = root.appendChild(DOMTools.createElement(``))
542 | this.props.ref.style.display = 'none';
543 | this.removed = false;
544 |
545 | this.update();
546 | }
547 | }
548 | setTarget(node){
549 | if(node instanceof Element){
550 | if(this.props.target instanceof Element){
551 | this.props.target.off('mouseenter.Tooltip');
552 | this.props.target.off('mouseleave.Tooltip');
553 | }
554 | this.props.target = node;
555 | node.on('mouseenter.Tooltip', _ => this.show());
556 | node.on('mouseleave.Tooltip', _ => this.hide());
557 |
558 | if(this.removed)
559 | this.create();
560 | else
561 | this.update();
562 |
563 | return true;
564 | }
565 | return false;
566 | }
567 | setText(text){
568 | if(!this.removed){
569 | this.props.text = text;
570 |
571 | if(this.props.ref instanceof Element){
572 | const node = this.props.ref.find('div[role=text]')
573 | if(node instanceof Element){
574 | node.innerHTML = text;
575 | return true;
576 | }
577 |
578 | }
579 | }
580 | return false;
581 | }
582 | setType(type){
583 | if(!this.removed && type in classList){
584 | if(this.props.ref instanceof Element){
585 | this.props.ref.removeClass(classList[this.props.type]);
586 | this.props.ref.addClass(classList[type]);
587 | this.props.type = type;
588 |
589 | this.update();
590 | return true;
591 | }
592 | }
593 | return false;
594 | }
595 | remove(){
596 | if(!this.removed){
597 |
598 | if(this.props.target instanceof Element){
599 | this.props.target.off('mouseenter.Tooltip');
600 | this.props.target.off('mouseleave.Tooltip');
601 | }
602 |
603 | if(this.props.ref instanceof Element)
604 | this.props.ref.remove();
605 |
606 | this.props.ref = null;
607 | this.props.target = null;
608 |
609 | this.removed = true;
610 | }
611 | }
612 | update(){
613 | let displayValue = [false, false];
614 |
615 | if(this.props.ref.style.display != 'none')
616 | displayValue[0] = true;
617 | else
618 | this.props.ref.style.display = 'block';
619 |
620 | if(this.props.target.style.display != 'none')
621 | displayValue[1] = true;
622 | else
623 | this.props.target.style.display = 'block';
624 |
625 | this.Offsets = {
626 | target: this.props.target.getBoundingClientRect(),
627 | ref: this.props.ref.getBoundingClientRect(),
628 | pointerRef: this.props.ref.find(`div[class~='da-tooltipPointer']`).getBoundingClientRect()
629 | };
630 |
631 | const { top, left } = calcAxis(this.props.type, this.Offsets.target, this.Offsets.ref, this.Offsets.pointerRef);
632 |
633 | this.props.ref.style.top = top;
634 | this.props.ref.style.left = left;
635 |
636 | if(!displayValue[0])
637 | this.props.ref.style.display = 'none';
638 |
639 | if(!displayValue[1])
640 | this.props.target.style.display = 'none';
641 |
642 | this.props.lastUpdate = Date.now();
643 | }
644 | };
645 | })(DiscordModules.KeyGenerator, DOMTools);
646 | return class SendAudio extends Plugin {
647 |
648 | onStart() {
649 | if(!document.getElementById('css-' + this.getName())){
650 | PluginUtilities.addStyle('css-' + this.getName(),`
651 | :root{--sendaudio-record: var(--interactive-normal);--sendaudio-record-hover: var(--interactive-active);--sendaudio-cancel: var(--interactive-normal);--sendaudio-cancel-hover: var(--interactive-active);--sendaudio-pause: var(--interactive-normal);--sendaudio-pause-hover: var(--interactive-active);--sendaudio-play: var(--interactive-normal);--sendaudio-play-hover: var(--interactive-active);--sendaudio-save: var(--interactive-normal);--sendaudio-save-hover: var(--interactive-active);--sendaudio-send: var(--interactive-normal);--sendaudio-send-hover: var(--interactive-active);}
652 | .flash-record{width: 14px;height: 14px;border-radius:50%;position: relative;top: 2px;margin: 0 2px;animation: flash linear 1s infinite;}
653 | @keyframes flash{0% {opacity: 1;}50% {opacity: .1;}100% {opacity: 1;}}
654 | .DiscordPlayer div[role='header']{color: var(--text-normal);display: flex;-webkit-box-align: center;align-items: center;border-bottom: 5px;}
655 | .DiscordPlayer div[role='header'] input{border: none;background: transparent;color: var(--header-primary);font-weight: 600;}
656 | .DiscordPlayer div[role='header'] div{position: absolute;right: 0;opacity: .3;cursor: pointer;}
657 | .DiscordPlayer div[role='header'] div:hover{opacity: 1 !important;}
658 | .DiscordPlayer div[role='controls']{color: white;display: flex;-webkit-box-align: center;align-items: center;}
659 | .DiscordPlayerLoading{display:none;color: var(--header-primary);font-weight: 600;text-align: center;padding: 5px;}
660 | .DiscordPlayerLoading path{fill: var(--header-primary);stroke: var(--header-primary);stroke-width: 5px;}
661 | .DiscordPlayerLoading svg{width: 16px;height: 16px;}
662 | .DiscordPlayer div[class='durationTimeWrapper-OugPFt da-durationTimeWrapper']{color: var(--text-normal);}
663 | .DiscordPlayer div[role=button] svg{color: var(--interactive-normal);opacity: 1;}
664 | .DiscordPlayer div[role=button]:hover svg{color: var(--interactive-active);opacity: 1;}
665 | .DiscordPlayer div[role='control-bar'], .DiscordPlayer div[role='control-bar']:after, .DiscordPlayer div[role='control-bar']:before{background-color: var(--interactive-muted);}
666 | .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs'], .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs']:after, .DiscordPlayer div[role='control-bar'] div[class~='mediaBarPreview-1jfyFs']:before{background-color: var(--text-normal);}
667 | .SendAudio-Info input{border: none;background: transparent;color: var(--header-primary);font-weight: 600;}
668 | #sendAudioButtons button[role=send] polygon{fill: var(--sendaudio-send) !important;}
669 | #sendAudioButtons button[role=send]:hover polygon{fill: var(--sendaudio-send-hover) !important;}
670 | #sendAudioButtons button[role=save] svg{border: 1px solid var(--sendaudio-save) !important;}
671 | #sendAudioButtons button[role=save] svg path:last-child{fill: var(--sendaudio-save) !important;}
672 |
673 | #sendAudioButtons button[role=save]:hover svg{border: 1px solid var(--sendaudio-save-hover) !important;}
674 | #sendAudioButtons button[role=save]:hover svg path:last-child{fill: var(--sendaudio-save-hover) !important;}
675 | #sendAudioButtons button[role=play] svg{color: var(--sendaudio-play) !important;}
676 | #sendAudioButtons button[role=play]:hover svg{color: var(--sendaudio-play-hover) !important;}
677 | #sendAudioButtons button[role=pause] path{fill: var(--sendaudio-pause) !important;}
678 | #sendAudioButtons button[role=pause]:hover path{fill: var(--sendaudio-pause-hover) !important;}
679 | #sendAudioButtons button[role=cancel] path{fill: var(--sendaudio-cancel) !important;}
680 | #sendAudioButtons button[role=cancel]:hover path{fill: var(--sendaudio-cancel-hover) !important;}
681 | #sendAudioButtons button[role=record] svg{color: var(--sendaudio-record) !important;}
682 | #sendAudioButtons button[role=record]:hover svg{color: var(--sendaudio-record-hover) !important;}
683 | .plugin-settings[id="plugin-settings-SendAudio 3"] > div{padding: 20px;}
684 | `);
685 | }
686 |
687 | this.devices = false;
688 |
689 | this.media = false;
690 |
691 | this.mediaInfo = {
692 | ready: false,
693 | error: false
694 | };
695 | this.panelInfo = [false, false];
696 |
697 | this.player = null;
698 |
699 | this.record = {
700 | chunks: [],
701 | blob: null,
702 | max_size: this.settings.nitro ? 52428800 : 8388608,
703 | size: 0,
704 | time: 0,
705 | limitStop: false,
706 | notSave: false,
707 | channel: null,
708 | previewing: false
709 | };
710 |
711 | this.tooltips = {
712 | 'record': new Tooltip(null, 'Record'),
713 | 'cancel': new Tooltip(null, 'Cancel'),
714 | 'pause': new Tooltip(null, 'Pause'),
715 | 'play': new Tooltip(null, 'Play'),
716 | 'resume': new Tooltip(null, 'Resume'),
717 | 'save': new Tooltip(null, 'Save'),
718 | 'send': new Tooltip(null, 'Send'),
719 | 'info': new Tooltip(null, 'Info')
720 | };
721 |
722 | //Init devices
723 |
724 | let plugin = this;
725 |
726 | navigator.mediaDevices.enumerateDevices().then(devices => {
727 | plugin.devices = devices.filter(device => device.kind == 'audioinput' && device.deviceId != 'default');
728 |
729 | let addeds = [];
730 |
731 | plugin.devices.forEach(device => {
732 | if(addeds.indexOf(device.groupId) == -1){
733 | plugin._config.defaultConfig.filter(k => k.id == 'devices')[0].options.push({label: device.label, value: device.deviceId});
734 | addeds.push(device.groupId);
735 | }
736 | });
737 | }).catch(error => {
738 | Logger.warn('Unable to get recording devices:', error);
739 | });
740 |
741 | this.onSwitch();
742 |
743 | this.changeMedia();
744 | }
745 |
746 | onStop() {
747 |
748 | this.mediaInfo.notSave = true;
749 | this.recordStop();
750 |
751 | if(typeof this.buttons == 'object' && this.buttons.group instanceof Element)
752 | this.buttons.group.remove();
753 |
754 | this.recordReset();
755 |
756 | PluginUtilities.removeStyle('css-' + this.getName());
757 |
758 | for(let k in this.tooltips){
759 | this.tooltips[k].remove();
760 | }
761 | }
762 |
763 | onSwitch(){
764 | if(!this.addButtons())
765 | return;
766 | }
767 |
768 | addButtons(){
769 | if(document.getElementById('sendAudioButtons') instanceof Element)
770 | return false;
771 |
772 | let buttons_ = document.querySelector(`div[class="buttons-3JBrkn da-buttons"]`);
773 |
774 | if(!(buttons_ instanceof Element))
775 | return false;
776 |
777 | this.buttons = {
778 | group: DOMTools.createElement(``),
779 | record: addButton(`role='record'`, ``),
780 | cancel: addButton(`role='cancel' style='display: none;'`, ``),
781 | pause: addButton(`role='pause' style='display: none;'`, ``),
782 | play: addButton(`role='play' style='display: none;'`, ``),
783 | save: addButton(`role='save' style='display: none;'`, ``),
784 | send: addButton(`role='send' style='display: none;'`, ``)
785 | };
786 |
787 | this.buttons.group.append(this.buttons.record);
788 | this.buttons.group.append(this.buttons.cancel);
789 | this.buttons.group.append(this.buttons.pause);
790 | this.buttons.group.append(this.buttons.play);
791 | this.buttons.group.append(this.buttons.save);
792 | this.buttons.group.append(this.buttons.send);
793 |
794 | buttons_.append(this.buttons.group);
795 |
796 | for(let k in this.tooltips){
797 | if(k in this.buttons){
798 | this.tooltips[k].setTarget(this.buttons[k]);
799 | this.tooltips[k].props.lastUpdate = 0;
800 | }
801 | }
802 |
803 | const plugin = this;
804 |
805 |
806 | this.buttons.record.on('click', _ => {
807 | if(!plugin.recordStart()){
808 | Logger.warn("Cannot start recording");
809 | }
810 | });
811 | this.buttons.cancel.on('click', _ => {
812 | if(plugin.record.previewing == true){
813 | plugin.recordReset();
814 |
815 | plugin.setDisplay({all: 'none', record: ''});
816 | }
817 | else{
818 | plugin.mediaInfo.notSave = true;
819 | plugin.recordStop();
820 | }
821 | });
822 | this.buttons.pause.on('click', _ => plugin.recordPause());
823 | this.buttons.play.on('click', _ => plugin.recordResume());
824 | this.buttons.save.on('click', _ => plugin.recordStop());
825 | this.buttons.send.on('click', _ => {
826 | if(plugin.record.previewing == true){
827 | if(plugin.record.channel){
828 | sendAudio(plugin.record.channel.id, plugin.record.blob);
829 |
830 | plugin.recordReset();
831 | plugin.setDisplay({all: 'none', record: ''});
832 | }
833 | }
834 | else plugin.recordStop();
835 | });
836 | if(!this.mediaInfo.ready || this.mediaInfo.error)
837 | this.buttons.record.disabled = true;
838 |
839 | if(this.mediaInfo.ready && !this.mediaInfo.error && this.media instanceof MediaRecorder){
840 | const channel = DiscordAPI.currentChannel;
841 |
842 | if(this.record.previewing)
843 | this.setDisplay({record: 'none', cancel: '', send: ''});
844 |
845 | else if(this.media.state != 'inactive'){
846 | if(this.settings.preview)
847 | this.setDisplay({record: 'none', cancel: '', save: ''});
848 | else
849 | this.setDisplay({record: 'none', cancel: '', send: ''});
850 |
851 | if(typeof channel == 'object' && typeof this.record.channel == 'object' && channel.id != this.record.channel.id){
852 | this.buttons.send.disabled = true;
853 | this.buttons.save.disabled = true;
854 | }
855 | if(this.media.state == 'paused')
856 | this.setDisplay({pause: 'none', play: ''});
857 | else
858 | this.setDisplay({pause: '', play: 'none'});
859 | }
860 |
861 | }
862 | return true;
863 | }
864 | addPanelRecording(){
865 | const data = `Recording in ${getChannelName()} (${getGuildName()})`;
866 |
867 | const panel = addPanel(``, `id='SendAudio-RecordingInfo'`);
868 |
869 | if(panel instanceof Element){
870 | panel.addClass('SendAudio-Info');
871 | this.panelInfo[0] = panel;
872 | this.panelInfo[1] = panel.querySelector('input[role=time]');
873 |
874 | this.tooltips.info.setText(data);
875 | this.tooltips.info.setTarget(panel);
876 | return true;
877 | }
878 | return false;
879 | }
880 | removePanelRecording(){
881 | if(this.panelInfo[0] instanceof Element)
882 | this.panelInfo[0].remove();
883 |
884 | this.panelInfo = [false, false];
885 | }
886 |
887 | setDisplay(values){
888 | if(typeof values != 'object' || typeof this.buttons != 'object' || !(this.buttons.group instanceof Element))
889 | return false;
890 |
891 | if(values.all)
892 | for(let key in this.buttons)
893 | if(key != 'group' && this.buttons[key] instanceof Element)
894 | this.buttons[key].style.display = values.all;
895 |
896 | delete values.all;
897 |
898 | for(let key in values)
899 | if(key in this.buttons && this.buttons[key] instanceof Element)
900 | this.buttons[key].style.display = values[key];
901 |
902 | return true;
903 | }
904 |
905 | getSettingsPanel(){
906 | const settings = this.buildSettingsPanel();
907 | if(settings){
908 | settings.addListener((id, value) => {
909 | if(id == 'devices'){
910 | this.changeMedia();
911 | }
912 | else if(id == 'preview'){
913 | if(this.media instanceof MediaRecorder && this.media.state != 'inactive'){
914 | if(value)
915 | this.setDisplay({save: '', send: 'none'});
916 | else
917 | this.setDisplay({save: 'none', send: ''});
918 | }
919 | }
920 | else if(id == 'nitro'){
921 | this.record.max_size = value ? 52428800 : 8388608;
922 | }
923 | });
924 | return settings.getElement();
925 | }
926 | Logger.warn("Failed to display settings");
927 | }
928 |
929 | changeMedia(device=false, changing=false){
930 | const plugin = this;
931 |
932 | const change = () => {
933 | plugin.media = null;
934 | plugin.mediaInfo.ready = false;
935 | plugin.mediaInfo.error = false;
936 |
937 | navigator.mediaDevices.getUserMedia({audio: (device != false ? device : plugin.settings.devices)}).then(s => {
938 | plugin.media = new MediaRecorder(s);
939 | Logger.log("MediaRecorder started successfully");
940 |
941 | plugin.media.addEventListener('dataavailable', e => plugin.onRecordingData(e.data));
942 | plugin.media.addEventListener('start', _ => plugin.onRecordingStateChange(0));
943 | plugin.media.addEventListener('stop', _ => plugin.onRecordingStateChange(1));
944 | plugin.media.addEventListener('pause', _ => plugin.onRecordingStateChange(2));
945 | plugin.media.addEventListener('resume', _ => plugin.onRecordingStateChange(3));
946 |
947 | plugin.media.addEventListener('error', error => {
948 | Logger.error('Recording error:', error);
949 | });
950 | plugin.mediaInfo.ready = true;
951 | plugin.mediaInfo.error = false;
952 | if(typeof plugin.buttons == 'object' && plugin.buttons.record instanceof Element)
953 | plugin.buttons.record.disabled = false;
954 |
955 | }).catch(err => {
956 | plugin.mediaInfo.ready = false;
957 | plugin.mediaInfo.error = true;
958 | if(typeof plugin.buttons == 'object' &&plugin.buttons.record instanceof Element)
959 | plugin.buttons.record.disabled = true;
960 |
961 | Logger.error("Failed to start MediaRecorder:", err.message);
962 |
963 | if(!changing){
964 | Logger.log("Changing to the default");
965 | plugin.changeMedia('default', true);
966 | }
967 | });
968 | };
969 |
970 | if(this.media instanceof MediaRecorder){
971 | this.mediaInfo.notSave = true;
972 | if(this.media.state != 'inactive')
973 | this.media.stop().then(_ => change()).catch(_ => change());
974 | }
975 | else{
976 | change();
977 | }
978 | }
979 |
980 | /* Record methods */
981 | recordStart(){
982 | if(this.media instanceof MediaRecorder){
983 | if(this.media.state == 'inactive'){
984 | this.record.chunks = [];
985 | this.record.size = 0;
986 | this.record.time = 0;
987 | this.media.start(1000);
988 | }
989 | return this.media.state == 'recording';
990 | }
991 | return false;
992 | }
993 | recordStop(){
994 | if(this.media instanceof MediaRecorder){
995 | if(this.media.state != 'inactive')
996 | this.media.stop();
997 |
998 | return this.media.state == 'inactive';
999 | }
1000 | return false;
1001 | }
1002 | recordPause(){
1003 | if(this.media instanceof MediaRecorder){
1004 | if(this.media.state == 'recording')
1005 | this.media.pause();
1006 |
1007 | return this.media.state == 'paused';
1008 | }
1009 | return false;
1010 | }
1011 | recordResume(){
1012 | if(this.media instanceof MediaRecorder){
1013 | if(this.media.state == 'paused')
1014 | this.media.resume();
1015 |
1016 | return this.media.state == 'recording';
1017 | }
1018 | return false;
1019 | }
1020 | recordReset(){
1021 | this.record.chunks = [];
1022 | this.record.time = 0;
1023 | this.record.size = 0;
1024 | this.record.notSave = false;
1025 | this.record.blob = null;
1026 | this.record.channel = null;
1027 | this.record.previewing = false;
1028 | this.record.limitStop = false;
1029 |
1030 | if(typeof this.buttons == 'object' && 'group' in this.buttons && this.buttons.group instanceof Element){
1031 | this.buttons.save.disabled = false;
1032 | this.buttons.send.disabled = false;
1033 | }
1034 |
1035 | if(this.player != null)
1036 | this.player.close();
1037 | }
1038 |
1039 | /* Record Event */
1040 | onRecordingStateChange(new_state){
1041 | //start
1042 | switch(new_state){
1043 | //start
1044 | case 0:{
1045 | this.addPanelRecording();
1046 | this.record.channel = DiscordAPI.currentChannel;
1047 | if(this.settings.preview)
1048 | this.setDisplay({record: 'none', cancel: 'block', pause: 'block', save: 'block'});
1049 | else
1050 | this.setDisplay({record: 'none', cancel: 'block', pause: 'block', send: 'block'});
1051 | break;
1052 | }
1053 | //stop
1054 | case 1:{
1055 | this.removePanelRecording();
1056 |
1057 | if(this.mediaInfo.notSave){
1058 | this.mediaInfo.notSave = false;
1059 | this.setDisplay({all: 'none', record: ''});
1060 | this.recordReset();
1061 | return;
1062 | }
1063 | if(this.record.size > 0){
1064 |
1065 | if(this.record.size > this.record.max_size){
1066 | let tempChunks = [];
1067 | let tempSize = 0;
1068 |
1069 | for(let i = 0; i < this.record.chunks.length; ++i){
1070 | if(tempSize + this.record.chunks[i].size <= this.record.max_size){
1071 | tempChunks.push(this.record.chunks[i]);
1072 | tempSize += this.record.chunks[i].size;
1073 | }
1074 | }
1075 | this.record.chunks = tempChunks;
1076 | this.record.size = tempSize;
1077 | }
1078 |
1079 | this.record.blob = new Blob(this.record.chunks, {type: this.settings.mimetype})
1080 | if(!this.settings.preview){
1081 | if(this.record.channel)
1082 | sendAudio(this.record.channel.id, this.record.blob);
1083 | }
1084 | else{
1085 | Logger.log("PREVIEW");
1086 | this.player = new DiscordPlayer();
1087 | this.player.setAudio(new Audio(URL.createObjectURL(this.record.blob)));
1088 | this.player.display('block');
1089 | this.record.previewing = true;
1090 | this.setDisplay({all: 'none', cancel: 'block', send: 'block'});
1091 | return;
1092 | }
1093 | }
1094 | this.recordReset();
1095 | this.setDisplay({all: 'none', record: ''});
1096 | break;
1097 | }
1098 | //pause
1099 | case 2:{
1100 | this.setDisplay({play: '', pause: 'none'});
1101 | break;
1102 | }
1103 | //resume
1104 | case 3:{
1105 | this.setDisplay({play: 'none', pause: ''});
1106 | break;
1107 | }
1108 | default:
1109 | break;
1110 | }
1111 |
1112 | }
1113 | onRecordingData(data){
1114 | this.record.time ++;
1115 |
1116 | if(data.size > 0){
1117 | if(this.record.size + data.size > this.record.max_size){
1118 | this.record.limitStop = true;
1119 | this.recordStop();
1120 | return;
1121 | }
1122 | this.record.chunks.push(data);
1123 | this.record.size += data.size;
1124 | }
1125 | if(this.panelInfo[1] instanceof Element)
1126 | this.panelInfo[1].value = `${convertToTime(this.record.time)} - ${convertData(this.record.size)}`;
1127 | }
1128 | };
1129 | };
1130 |
--------------------------------------------------------------------------------