STR v2.2.0 sticker download/conversion support

This commit is contained in:
1Lighty 2020-10-21 17:27:21 +02:00
parent 64f01b1f57
commit dfca0324aa
4 changed files with 287 additions and 168 deletions

View File

@ -1,4 +1,9 @@
# [SaveToRedux](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux "SaveToRedux") Changelog # [SaveToRedux](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux "SaveToRedux") Changelog
### 2.2.0
- Added support for stickers, they can now be downloaded properly.
- Animated stickers will be turned into a shareable GIF, but the option of downloaing the source is still available.
- Lottie sticker download resolution can be changed in settings, default is 160x160.
### 2.1.5 ### 2.1.5
- Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently. - Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently.

View File

@ -1,29 +1,29 @@
# SaveToRedux [![download](https://i.imgur.com/OAHgjZu.png)](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux&dl=1 "SaveToRedux") # SaveToRedux [![download](https://i.imgur.com/OAHgjZu.png)](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux&dl=1 "SaveToRedux")
Allows you to save images, videos, profile icons, server icons, reactions, emotes and custom status emotes to any folder quickly, as well as install plugins from direct links. Allows you to save images, videos, profile icons, server icons, reactions, emotes, custom status emotes and stickers to any folder quickly, as well as install plugins from direct links.
### Features ### Features
Right click on an image, video, file, user, server icon, group DM or emote to be able to set folders and save to folders, under the **Save * To** context menu submenu. Right click on an image, video, file, user, server icon, group DM, emote or sticker to be able to set folders and save to folders, under the **Save * To** context menu submenu.
With * being what you're saving, eg Image, Video, Emoji, File or Icon With * being what you're saving, eg Image, Video, Emoji, File or Icon
Right clicking a theme or plugin attachment or link will show you the option of installing it. Right clicking a theme or plugin attachment or link will show you the option of installing it.
### Preview ### Preview
Right click on nearly any image, video, file, user, server icon, group DM or emote Right click on nearly any image, video, file, user, server icon, group DM, emote or sticker
![preview](https://i.imgur.com/htOuqtw.png) ![preview](https://i.imgur.com/htOuqtw.png)
![preview2](https://cdn.discordapp.com/attachments/389049952732446733/694622056213512292/5jsZjnlrCBkz.png) ![preview2](https://cdn.discordapp.com/attachments/389049952732446733/694622056213512292/5jsZjnlrCBkz.png)
### Settings ### Settings
#### File Save Settings #### File Save Settings
##### File name ##### File name
Original - Save as original filename Original - Save as original filename
Date - Save as current localized time Date - Save as current localized time
Random - Use random characters Random - Use random characters
Original + Random - Append random to the end of the original file name Original + Random - Append random to the end of the original file name
Custom - Set your own name saving Custom - Set your own name saving
##### Custom File name ##### Custom File name
Set file name when saving, can be anything static, or anything dynamic like file, rand, date, time, day, month, year, hours, minutes and seconds. Set file name when saving, can be anything static, or anything dynamic like file, rand, date, time, day, month, year, hours, minutes and seconds.
Dynamic options must be wrapped like ${OPTION} Dynamic options must be wrapped like ${OPTION}
##### Conflicting Filename Mode ##### Conflicting Filename Mode
Warn - Always warn if a file with the same name exists Warn - Always warn if a file with the same name exists
Overwrite Overwrite
Append number - appends a number in paranthesis Append number - appends a number in paranthesis
Append random Append random
Save As... - lets you enter a custom name instead Save As... - lets you enter a custom name instead
#### Misc #### Misc
##### Context menu option at the bottom instead of top ##### Context menu option at the bottom instead of top

View File

@ -2,24 +2,24 @@
/*@cc_on /*@cc_on
@if (@_jscript) @if (@_jscript)
// Offer to self-install for clueless users that try to run this directly. // Offer to self-install for clueless users that try to run this directly.
var shell = WScript.CreateObject('WScript.Shell'); var shell = WScript.CreateObject('WScript.Shell');
var fs = new ActiveXObject('Scripting.FileSystemObject'); var fs = new ActiveXObject('Scripting.FileSystemObject');
var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins');
var pathSelf = WScript.ScriptFullName; var pathSelf = WScript.ScriptFullName;
// Put the user at ease by addressing them in the first person // Put the user at ease by addressing them in the first person
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); 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);
if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40);
} else if (!fs.FolderExists(pathPlugins)) { } else if (!fs.FolderExists(pathPlugins)) {
shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10);
} else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) {
fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
// Show the user where to put plugins in the future // Show the user where to put plugins in the future
shell.Exec('explorer ' + pathPlugins); shell.Exec('explorer ' + pathPlugins);
shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40);
} }
WScript.Quit(); WScript.Quit();
@else@*/ @else@*/
/* /*
@ -41,16 +41,16 @@ module.exports = (() => {
twitter_username: '' twitter_username: ''
} }
], ],
version: '2.1.5', version: '2.2.0',
description: 'Allows you to save images, videos, profile icons, server icons, reactions, emotes and custom status emotes to any folder quickly, as well as install plugins from direct links.', description: 'Allows you to save images, videos, profile icons, server icons, reactions, emotes, custom status emotes and stickers to any folder quickly, as well as install plugins from direct links.',
github: 'https://github.com/1Lighty', github: 'https://github.com/1Lighty',
github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/SaveToRedux/SaveToRedux.plugin.js' github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/SaveToRedux/SaveToRedux.plugin.js'
}, },
changelog: [ changelog: [
{ {
title: 'fixed', title: 'added',
type: 'fixed', type: 'added',
items: ['Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently.'] items: ['Added support for stickers, they can now be downloaded properly.', 'Animated stickers will be turned into a shareable GIF, but the option of downloaing the source is still available.', 'Lottie sticker download resolution can be changed in settings, default is 160x160.']
} }
], ],
defaultConfig: [ defaultConfig: [
@ -94,7 +94,17 @@ module.exports = (() => {
] ]
}, },
{ name: 'User and Server icons get saved by the users or servers name, instead of randomized', id: 'saveByName', type: 'switch', value: true }, { name: 'User and Server icons get saved by the users or servers name, instead of randomized', id: 'saveByName', type: 'switch', value: true },
{ name: 'Append server name or DM name to image/file name', id: 'appendCurrentName', type: 'switch', value: false } { name: 'Append server name or DM name to image/file name', id: 'appendCurrentName', type: 'switch', value: false },
{
name: 'Lottie sticker save size',
id: 'lottieSize',
type: 'radio',
value: 0,
options: [
{ name: 'Default 160x160', value: 0 },
{ name: 'Max size 320x320', value: 1 }
]
},
] ]
}, },
{ type: 'category', id: 'misc', name: 'Misc', collapsible: true, shown: false, settings: [{ name: 'Context menu option at the bottom instead of top', id: 'contextMenuOnBottom', type: 'switch', value: true }] } { type: 'category', id: 'misc', name: 'Misc', collapsible: true, shown: false, settings: [{ name: 'Context menu option at the bottom instead of top', id: 'contextMenuOnBottom', type: 'switch', value: true }] }
@ -113,7 +123,7 @@ module.exports = (() => {
const ret = ConfirmModal(props); const ret = ConfirmModal(props);
if (props.size) ret.props.size = props.size; if (props.size) ret.props.size = props.size;
return ret; return ret;
} catch(err) { } catch (err) {
if (props.onCancel) props.onCancel(); if (props.onCancel) props.onCancel();
else props.onClose(); else props.onClose();
return null; return null;
@ -125,7 +135,7 @@ module.exports = (() => {
const Modals = { const Modals = {
showModal(title, content, options) { showModal(title, content, options) {
return ModalStack.openModal(e => React.createElement(ConfirmationModal, Object.assign({title, children: content, cancelText: 'Cancel'}, e, options))); return ModalStack.openModal(e => React.createElement(ConfirmationModal, Object.assign({ title, children: content, cancelText: 'Cancel' }, e, options)));
}, },
showConfirmationModal(title, content, options) { showConfirmationModal(title, content, options) {
return this.showModal(title, React.createElement(Markdown, null, content), options); return this.showModal(title, React.createElement(Markdown, null, content), options);
@ -312,6 +322,17 @@ module.exports = (() => {
const useIdealExtensions = url => (url.indexOf('/a_') !== -1 ? url.replace('.webp', '.gif').replace('.png', '.gif') : url.replace('.webp', '.png')); const useIdealExtensions = url => (url.indexOf('/a_') !== -1 ? url.replace('.webp', '.gif').replace('.png', '.gif') : url.replace('.webp', '.png'));
const ImageWrapperClassname = XenoLib.getSingleClass('clickable imageWrapper');
const StickerUtils = WebpackModules.getByProps('getStickerAssetUrl');
const { StickerFormat } = WebpackModules.getByProps('StickerFormat') || {};
const WasmLottie = WebpackModules.getByPrototypes('get_rgba');
const webWorkerData = `importScripts('https://1lighty.github.io/BetterDiscordPlugins/Plugins/SaveToRedux/res/worker.js');`;
const workerDataURL = window.URL.createObjectURL(new Blob([webWorkerData], { type: 'text/javascript' }));
const StickerClasses = WebpackModules.getByProps('pngImage', 'lottieCanvas');
return class SaveToRedux extends Plugin { return class SaveToRedux extends Plugin {
constructor() { constructor() {
super(); super();
@ -359,6 +380,9 @@ module.exports = (() => {
div[id$="-str"] + .${XenoLib.getSingleClass('layerContainer layer')} { div[id$="-str"] + .${XenoLib.getSingleClass('layerContainer layer')} {
z-index: 1; z-index: 1;
} }
div[id$="-str-stickers"] + .${XenoLib.getSingleClass('layerContainer layer')} {
z-index: 2;
}
` `
); );
this.lastUsedFolder = -1; this.lastUsedFolder = -1;
@ -400,6 +424,7 @@ module.exports = (() => {
Utilities.suppressErrors(this.patchEmojiPicker.bind(this), 'EmojiPicker patch')(this.promises.state); Utilities.suppressErrors(this.patchEmojiPicker.bind(this), 'EmojiPicker patch')(this.promises.state);
Utilities.suppressErrors(this.patchReactions.bind(this), 'Reaction patch')(this.promises.state); Utilities.suppressErrors(this.patchReactions.bind(this), 'Reaction patch')(this.promises.state);
this.patchContextMenus(); this.patchContextMenus();
this.patchStickerStorePicker();
} }
patchEmojiPicker() { patchEmojiPicker() {
@ -461,6 +486,25 @@ module.exports = (() => {
Reaction.forceUpdateAll(); Reaction.forceUpdateAll();
} }
patchStickerStorePicker() {
const StickerStorePicker = WebpackModules.find(m => {
if (!m || !m.type) return false;
const typeString = String(m.type);
for (const string of ['.getStickerItemProps', '.inspectedSticker']) if (typeString.indexOf(string) === -1) return false;
return true;
});
Patcher.after(StickerStorePicker, 'type', (_, [props], ret) => {
for (const stickerClickable of ret.props.children) {
const { sticker } = Utilities.findInReactTree(stickerClickable, e => e && e.sticker && e.sticker.id) || {};
if (!sticker) continue;
const url = StickerUtils.getStickerAssetUrl(sticker);
XenoLib.createSharedContext(stickerClickable, () => XenoLib.createContextMenuGroup([
this.constructMenu(url.split('?')[0], 'Sticker', sticker.name, () => { }, undefined, '', { id: sticker.id, type: sticker.format_type })
]));
}
})
}
patchContextMenus() { patchContextMenus() {
this.patchUserContextMenus(); this.patchUserContextMenus();
this.patchImageContextMenus(); this.patchImageContextMenus();
@ -509,12 +553,13 @@ module.exports = (() => {
); );
if (!Array.isArray(menu)) return; if (!Array.isArray(menu)) return;
const [state, setState] = React.useState({}); const [state, setState] = React.useState({});
const extraData = {};
let src; let src;
let saveType = 'File'; let saveType = 'File';
let url = ''; let url = '';
let proxiedUrl = ''; let proxiedUrl = '';
let customName = ''; let customName = '';
if (isImageMenu && (Utilities.getNestedProp(props, 'target.parentNode.className') || '').indexOf(XenoLib.getSingleClass('clickable imageWrapper')) !== -1) { if (isImageMenu && (Utilities.getNestedProp(props, 'target.parentNode.className') || '').indexOf(ImageWrapperClassname) !== -1) {
const inst = ReactTools.getOwnerInstance(Utilities.getNestedProp(props, 'target.parentNode.parentNode')); const inst = ReactTools.getOwnerInstance(Utilities.getNestedProp(props, 'target.parentNode.parentNode'));
proxiedUrl = props.src; proxiedUrl = props.src;
if (inst) src = inst.props.original; if (inst) src = inst.props.original;
@ -525,6 +570,17 @@ module.exports = (() => {
proxiedUrl = ''; proxiedUrl = '';
} }
} }
const targetClassName = Utilities.getNestedProp(props, 'target.className') || '';
if (StickerClasses && (targetClassName.indexOf(StickerClasses.lottieCanvas.split(' ')[0]) !== -1 || targetClassName.indexOf(StickerClasses.pngImage.split(' ')[0]) !== -1)) {
const { memoizedProps } = Utilities.findInTree(ReactTools.getReactInstance(props.target), e => e && e.type && e.type.displayName === 'StickerMessage', { walkable: ['return'] });
const { sticker } = memoizedProps || {};
if (!sticker) return;
src = StickerUtils.getStickerAssetUrl(sticker);
customName = sticker.name;
extraData.type = sticker.format_type;
extraData.id = sticker.id;
saveType = 'Sticker';
}
if (!src) src = Utilities.getNestedProp(props, 'attachment.href') || Utilities.getNestedProp(props, 'attachment.url'); if (!src) src = Utilities.getNestedProp(props, 'attachment.href') || Utilities.getNestedProp(props, 'attachment.url');
/* is that enough specific cases? */ /* is that enough specific cases? */
if (typeof src === 'string') { if (typeof src === 'string') {
@ -572,42 +628,44 @@ module.exports = (() => {
} }
url = src; url = src;
if (!url) return; if (!url) return;
if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image'; if (saveType !== 'Sticker') {
else if (isVideo(url)) saveType = 'Video'; if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image';
else if (isAudio(url)) saveType = 'Audio'; else if (isVideo(url)) saveType = 'Video';
if (url.indexOf('app.com/emojis/') !== -1) { else if (isAudio(url)) saveType = 'Audio';
saveType = 'Emoji'; if (url.indexOf('app.com/emojis/') !== -1) {
const emojiId = url.split('emojis/')[1].split('.')[0]; saveType = 'Emoji';
const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId); const emojiId = url.split('emojis/')[1].split('.')[0];
if (!emoji) { const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId);
if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return; if (!emoji) {
const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1); if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return;
if (message && message.content) { const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1);
const group = message.content.match(new RegExp(`<a?:([^:>]*):${emojiId}>`)); if (message && message.content) {
if (group && group[1]) customName = group[1]; const group = message.content.match(new RegExp(`<a?:([^:>]*):${emojiId}>`));
if (group && group[1]) customName = group[1];
}
if (!customName) {
const alt = props.target.alt;
if (alt) customName = alt.split(':')[1] || alt;
}
} else customName = emoji.name;
} else if (state.__STR_extension) {
if (isImage(state.__STR_extension)) saveType = 'Image';
else if (isVideo(state.__STR_extension)) saveType = 'Video';
else if (isAudio(state.__STR_extension)) saveType = 'Audio';
} else if (url.indexOf('//discordapp.com/assets/') !== -1 && props.target && props.target.className.indexOf('emoji') !== -1) {
const alt = props.target.alt;
if (alt) {
customName = alt.split(':')[1] || alt;
const name = EmojiStore.convertSurrogateToName(customName);
if (name) {
const match = name.match(EmojiStore.EMOJI_NAME_RE);
if (match) customName = EmojiStore.getByName(match[1]).uniqueName;
}
} }
if (!customName) { saveType = 'Emoji';
const alt = props.target.alt; } else if (url.indexOf('.plugin.js') === url.length - 10) saveType = 'Plugin';
if (alt) customName = alt.split(':')[1] || alt; else if (url.indexOf('.theme.css') === url.length - 10) saveType = 'Theme';
} }
} else customName = emoji.name;
} else if (state.__STR_extension) {
if (isImage(state.__STR_extension)) saveType = 'Image';
else if (isVideo(state.__STR_extension)) saveType = 'Video';
else if (isAudio(state.__STR_extension)) saveType = 'Audio';
} else if (url.indexOf('//discordapp.com/assets/') !== -1 && props.target && props.target.className.indexOf('emoji') !== -1) {
const alt = props.target.alt;
if (alt) {
customName = alt.split(':')[1] || alt;
const name = EmojiStore.convertSurrogateToName(customName);
if (name) {
const match = name.match(EmojiStore.EMOJI_NAME_RE);
if (match) customName = EmojiStore.getByName(match[1]).uniqueName;
}
}
saveType = 'Emoji';
} else if (url.indexOf('.plugin.js') === url.length - 10) saveType = 'Plugin';
else if (url.indexOf('.theme.css') === url.length - 10) saveType = 'Theme';
try { try {
const submenu = this.constructMenu( const submenu = this.constructMenu(
url.split('?')[0], url.split('?')[0],
@ -618,14 +676,15 @@ module.exports = (() => {
if (!isTrustedDomain(targetUrl)) return; if (!isTrustedDomain(targetUrl)) return;
state.__STR_requesting = true; state.__STR_requesting = true;
RequestModule.head(targetUrl, (err, res) => { RequestModule.head(targetUrl, (err, res) => {
if (err) return setState({ __STR_requesting: false, __STR_requested: true }); if (err || res.statusCode !== 200) return setState({ __STR_requesting: false, __STR_requested: true });
const extension = MimeTypesModule.extension(res.headers['content-type']); const extension = MimeTypesModule.extension(res.headers['content-type']);
setState({ __STR_requesting: false, __STR_requested: true, __STR_extension: extension }); setState({ __STR_requesting: false, __STR_requested: true, __STR_extension: extension });
}); });
targetUrl; targetUrl;
}, },
state.__STR_extension, state.__STR_extension,
proxiedUrl proxiedUrl,
extraData
); );
const group = XenoLib.createContextMenuGroup([submenu]); const group = XenoLib.createContextMenuGroup([submenu]);
if (this.settings.misc.contextMenuOnBottom) menu.push(group); if (this.settings.misc.contextMenuOnBottom) menu.push(group);
@ -743,7 +802,7 @@ module.exports = (() => {
return ret; return ret;
} }
formatURL(url, requiresSize, customName, fallbackExtension, proxiedUrl, failNum = 0, forceKeepOriginal = false) { formatURL(url, requiresSize, customName, fallbackExtension, proxiedUrl, failNum = 0, forceKeepOriginal = false, forceExtension = false) {
// url = url.replace(/\/$/, ''); // url = url.replace(/\/$/, '');
if (requiresSize) url += '?size=2048'; if (requiresSize) url += '?size=2048';
else if (url.indexOf('twimg.com/') !== -1) url = url.replace(':small', ':orig').replace(':medium', ':orig').replace(':large', ':orig'); else if (url.indexOf('twimg.com/') !== -1) url = url.replace(':small', ':orig').replace(':medium', ':orig').replace(':large', ':orig');
@ -753,7 +812,7 @@ module.exports = (() => {
} }
const match = url.match(/(?:\/)([^\/]+?)(?:(?:\.)([^.\/?:]+)){0,1}(?:[^\w\/\.]+\w+){0,1}(?:(?:\?[^\/]+){0,1}|(?:\/){0,1})$/); const match = url.match(/(?:\/)([^\/]+?)(?:(?:\.)([^.\/?:]+)){0,1}(?:[^\w\/\.]+\w+){0,1}(?:(?:\?[^\/]+){0,1}|(?:\/){0,1})$/);
let name = customName || match[1]; let name = customName || match[1];
let extension = match[2] || fallbackExtension; let extension = forceExtension || match[2] || fallbackExtension;
if (url.indexOf('//media.tenor.co') !== -1) { if (url.indexOf('//media.tenor.co') !== -1) {
extension = name; extension = name;
name = url.match(/\/\/media.tenor.co\/[^\/]+\/([^\/]+)\//)[1]; name = url.match(/\/\/media.tenor.co\/[^\/]+\/([^\/]+)\//)[1];
@ -772,10 +831,17 @@ module.exports = (() => {
return ret; return ret;
} }
constructMenu(url, type, customName, onNoExtension = () => { }, fallbackExtension, proxiedUrl) { constructMenu(url, type, customName, onNoExtension = () => { }, fallbackExtension, proxiedUrl, extraData = {}) {
const subItems = []; const subItems = [];
const folderSubMenus = []; const folderSubMenus = [];
const formattedurl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, 0, type === 'Theme' || type === 'Plugin'); let forcedExtension = false;
if (type === 'Sticker') {
if (extraData.isStickerSubMenu) {
if (extraData.type === StickerFormat.APNG) forcedExtension = 'apng';
}
else forcedExtension = extraData.type === StickerFormat.PNG ? 'png' : 'gif';
}
const formattedurl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, 0, type === 'Theme' || type === 'Plugin', forcedExtension);
if (!formattedurl.extension) onNoExtension(formattedurl.url); if (!formattedurl.extension) onNoExtension(formattedurl.url);
let notifId; let notifId;
let downloadAttempts = 0; let downloadAttempts = 0;
@ -796,23 +862,67 @@ module.exports = (() => {
return `${bytes.toFixed(1)}${noUnit && unit.a === units[u] ? '' : ' ' + units[u]}`; return `${bytes.toFixed(1)}${noUnit && unit.a === units[u] ? '' : ' ' + units[u]}`;
} }
const unit = { a: '' }; const unit = { a: '' };
const update = () => XenoLib.Notifications.update(notifId, { content: `Downloading ${type} ${humanFileSize(receivedBytes, false, true, unit)}/${humanFileSize(totalBytes, false, false, unit)}`, progress: (receivedBytes / totalBytes) * 100 }); const update = () => XenoLib.Notifications.update(notifId, { content: `Downloading ${type} ${humanFileSize(receivedBytes.length, false, true, unit)}/${humanFileSize(totalBytes, false, false, unit)}`, progress: (receivedBytes.length / totalBytes) * 100 });
const throttledUpdate = XenoLib._.throttle(update, 50); const throttledUpdate = XenoLib._.throttle(update, 50);
let totalBytes = 0; let totalBytes = 0;
let receivedBytes = 0; let receivedBytes = '';
const req = RequestModule(formattedurl.url); const req = RequestModule({ url: formattedurl.url, encoding: null }, async (_, __, body) => {
if (type !== 'Sticker' || extraData.type === StickerFormat.PNG || extraData.isStickerSubMenu) return; // do not convert it
XenoLib.Notifications.remove(notifId);
notifId = undefined;
let sNotifId = XenoLib.Notifications.info(`Converting sticker to gif..`, { timeout: 0, loading: true });
let worker = null;
let lottieWASM = null;
try {
worker = new Worker(workerDataURL);
if (extraData.type === StickerFormat.APNG) {
await new Promise(res => {
worker.onmessage = res;
worker.postMessage(['CONVERT-APNG', body]);
});
} else {
lottieWASM = new WasmLottie(receivedBytes);
const size = this.settings.saveOptions.lottieSize ? 320 : 160;
const frames = [];
for (let i = 0, framesCount = lottieWASM.frames; i < framesCount; i++) frames.push(new Uint8ClampedArray(lottieWASM.get_bgra(i, size, size)));
await new Promise(async res => {
worker.onmessage = res;
worker.postMessage(['CONVERT-FRAMES', { width: size, height: size, framerate: 60, frames }]);
});
}
const { data } = await new Promise(res => {
worker.onmessage = res;
worker.postMessage(['DONE']);
});
XenoLib.Notifications.update(sNotifId, { content: `Saving..` });
FsModule.writeFileSync(path, Buffer.from(data));
XenoLib.Notifications.remove(sNotifId);
sNotifId = undefined;
if (openOnSave) openPath(path);
BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' });
} catch (err) {
Logger.stacktrace('Failed converting to GIF', err);
BdApi.showToast(`Failed to save sticker..`, { type: 'error' });
} finally {
if (lottieWASM) lottieWASM.drop();
if (worker) worker.terminate();
}
});
req req
.on('data', chunk => { .on('data', chunk => {
receivedBytes += chunk.length; receivedBytes += chunk;
throttledUpdate(); throttledUpdate();
}) })
.on('response', res => { .on('response', res => {
if (res.statusCode == 200) { if (res.statusCode == 200) {
totalBytes = parseInt(res.headers['content-length']); totalBytes = parseInt(res.headers['content-length']);
update(); update();
if (type === 'Sticker' && extraData.type !== StickerFormat.PNG && !extraData.isStickerSubMenu) return; // do not stream download because we need to convert it first
req req
.pipe(FsModule.createWriteStream(path)) .pipe(FsModule.createWriteStream(path))
.on('finish', () => { .on('finish', () => {
XenoLib.Notifications.remove(notifId);
notifId = undefined;
if (openOnSave) openPath(path); if (openOnSave) openPath(path);
BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' }); BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' });
}) })
@ -824,7 +934,7 @@ module.exports = (() => {
} else if (res.statusCode == 404) { } else if (res.statusCode == 404) {
if (shouldDoMultiAttempts && downloadAttempts < 2) { if (shouldDoMultiAttempts && downloadAttempts < 2) {
downloadAttempts++; downloadAttempts++;
const newUrl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, downloadAttempts, type === 'Theme' || type === 'Plugin').url; const newUrl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, downloadAttempts, type === 'Theme' || type === 'Plugin', forcedExtension).url;
if (newUrl !== formattedurl.url) { if (newUrl !== formattedurl.url) {
formattedurl.url = newUrl; formattedurl.url = newUrl;
return downloadEx(path, openOnSave); return downloadEx(path, openOnSave);
@ -1041,7 +1151,7 @@ module.exports = (() => {
return XenoLib.createContextMenuSubMenu( return XenoLib.createContextMenuSubMenu(
folder.name, folder.name,
[ [
XenoLib.createContextMenuItem( extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Remove Folder', 'Remove Folder',
() => { () => {
this.folders.splice(idx, 1); this.folders.splice(idx, 1);
@ -1050,7 +1160,7 @@ module.exports = (() => {
}, },
'remove-folder' 'remove-folder'
), ),
XenoLib.createContextMenuItem( extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Open Folder', 'Open Folder',
() => { () => {
openPath(folder.path); openPath(folder.path);
@ -1076,7 +1186,7 @@ module.exports = (() => {
}, },
'save-and-open' 'save-and-open'
), ),
XenoLib.createContextMenuItem( extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Edit', 'Edit',
() => { () => {
let __name = folder.name.slice(0); let __name = folder.name.slice(0);
@ -1119,7 +1229,10 @@ module.exports = (() => {
for (const folderIDX in this.folders) folderSubMenus.push(folderSubMenu(this.folders[folderIDX], folderIDX)); for (const folderIDX in this.folders) folderSubMenus.push(folderSubMenu(this.folders[folderIDX], folderIDX));
subItems.push( subItems.push(
...folderSubMenus, ...folderSubMenus,
XenoLib.createContextMenuItem( type === 'Sticker' && !extraData.isStickerSubMenu && extraData.type !== StickerFormat.PNG ?
XenoLib.createContextMenuSubMenu(`Save ${extraData.type === StickerFormat.LOTTIE ? 'Lottie JSON' : 'APNG'}`, this.constructMenu(url, type, customName, onNoExtension, fallbackExtension, proxiedUrl, { ...extraData, onlyItems: true, isStickerSubMenu: true, onlyFolderSave: true }), 'str-stickers')
: null,
extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Add Folder', 'Add Folder',
() => { () => {
dialog dialog
@ -1215,6 +1328,7 @@ module.exports = (() => {
) )
: null : null
); );
if (extraData.onlyItems) return subItems;
return XenoLib.createContextMenuSubMenu(`Save ${type} To`, subItems, 'str', { return XenoLib.createContextMenuSubMenu(`Save ${type} To`, subItems, 'str', {
action: () => { action: () => {
if (this.lastUsedFolder === -1) return BdApi.showToast('No folder has been used yet', { type: 'error' }); if (this.lastUsedFolder === -1) return BdApi.showToast('No folder has been used yet', { type: 'error' });
@ -1272,7 +1386,7 @@ module.exports = (() => {
n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e), n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e),
e = BdApi.getPlugin('ZeresPluginLibrary'), e = BdApi.getPlugin('ZeresPluginLibrary'),
o = BdApi.getPlugin('XenoLib'); o = BdApi.getPlugin('XenoLib');
n(e, '1.2.23') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.26') && (XenoLibOutdated = !0); n(e, '1.2.24') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.29') && (XenoLibOutdated = !0);
} }
} catch (i) { } catch (i) {
console.error('Error checking if libraries are out of date', i); console.error('Error checking if libraries are out of date', i);
@ -1300,87 +1414,87 @@ module.exports = (() => {
stop() { } stop() { }
handleMissingLib() { handleMissingLib() {
const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); const a = BdApi.findModuleByProps('openModal', 'hasModalOpen');
if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return;
const b = !global.XenoLib, const b = !global.XenoLib,
c = !global.ZeresPluginLibrary, c = !global.ZeresPluginLibrary,
d = (b && c) || ((b || c) && (XenoLibOutdated || ZeresPluginLibraryOutdated)), d = (b && c) || ((b || c) && (XenoLibOutdated || ZeresPluginLibraryOutdated)),
e = (() => { e = (() => {
let a = ''; let a = '';
return b || c ? (a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? ' and outdated' : ''} `) : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), (a += `${d ? 'Libraries' : 'Library'} `), a; return b || c ? (a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? ' and outdated' : ''} `) : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), (a += `${d ? 'Libraries' : 'Library'} `), a;
})(), })(),
f = (() => { f = (() => {
let a = `The ${d ? 'libraries' : 'library'} `; let a = `The ${d ? 'libraries' : 'library'} `;
return b || XenoLibOutdated ? ((a += 'XenoLib '), (c || ZeresPluginLibraryOutdated) && (a += 'and ZeresPluginLibrary ')) : (c || ZeresPluginLibraryOutdated) && (a += 'ZeresPluginLibrary '), (a += `required for ${this.name} ${d ? 'are' : 'is'} ${b || c ? 'missing' : ''}${XenoLibOutdated || ZeresPluginLibraryOutdated ? (b || c ? ' and/or outdated' : 'outdated') : ''}.`), a; return b || XenoLibOutdated ? ((a += 'XenoLib '), (c || ZeresPluginLibraryOutdated) && (a += 'and ZeresPluginLibrary ')) : (c || ZeresPluginLibraryOutdated) && (a += 'ZeresPluginLibrary '), (a += `required for ${this.name} ${d ? 'are' : 'is'} ${b || c ? 'missing' : ''}${XenoLibOutdated || ZeresPluginLibraryOutdated ? (b || c ? ' and/or outdated' : 'outdated') : ''}.`), a;
})(), })(),
g = BdApi.findModuleByDisplayName('Text'), g = BdApi.findModuleByDisplayName('Text'),
h = BdApi.findModuleByDisplayName('ConfirmModal'), h = BdApi.findModuleByDisplayName('ConfirmModal'),
i = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null)); i = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null));
if (!a || !h || !g) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (h ? '' : ' ConfirmationModalComponent') + (g ? '' : 'TextElement')}`), i(); if (!a || !h || !g) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (h ? '' : ' ConfirmationModalComponent') + (g ? '' : 'TextElement')}`), i();
class j extends BdApi.React.PureComponent { class j extends BdApi.React.PureComponent {
constructor(a) { constructor(a) {
super(a), (this.state = { hasError: !1 }), (this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a))), (this.render = () => (this.state.hasError ? null : this.props.children)); super(a), (this.state = { hasError: !1 }), (this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a))), (this.render = () => (this.state.hasError ? null : this.props.children));
}
} }
let k = !1, }
l = !1; let k = !1,
const m = a.openModal( l = !1;
b => { const m = a.openModal(
if (l) return null; b => {
try { if (l) return null;
return BdApi.React.createElement( try {
j, return BdApi.React.createElement(
{ label: 'missing dependency modal', onError: () => (a.closeModal(m), i()) }, j,
BdApi.React.createElement( { label: 'missing dependency modal', onError: () => (a.closeModal(m), i()) },
h, BdApi.React.createElement(
Object.assign( h,
{ Object.assign(
header: e, {
children: BdApi.React.createElement(g, { size: g.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] }), header: e,
red: !1, children: BdApi.React.createElement(g, { size: g.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] }),
confirmText: 'Download Now', red: !1,
cancelText: 'Cancel', confirmText: 'Download Now',
onCancel: b.onClose, cancelText: 'Cancel',
onConfirm: () => { onCancel: b.onClose,
if (k) return; onConfirm: () => {
k = !0; if (k) return;
const b = require('request'), k = !0;
c = require('fs'), const b = require('request'),
d = require('path'), c = require('fs'),
e = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, d = require('path'),
f = () => { e = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder,
(global.XenoLib && !XenoLibOutdated) || f = () => {
b('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (b, f, g) => { (global.XenoLib && !XenoLibOutdated) ||
try { b('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (b, f, g) => {
if (b || 200 !== f.statusCode) return a.closeModal(m), i();
c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => {});
} catch (b) {
console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i();
}
});
};
!global.ZeresPluginLibrary || ZeresPluginLibraryOutdated
? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => {
try { try {
if (b || 200 !== g.statusCode) return a.closeModal(m), i(); if (b || 200 !== f.statusCode) return a.closeModal(m), i();
c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => {}), f(); c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => { });
} catch (b) { } catch (b) {
console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i(); console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i();
} }
}) });
: f(); };
} !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated
}, ? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => {
b, try {
{ onClose: () => {} } if (b || 200 !== g.statusCode) return a.closeModal(m), i();
) c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => { }), f();
} catch (b) {
console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i();
}
})
: f();
}
},
b,
{ onClose: () => { } }
) )
); )
} catch (b) { );
return console.error('There has been an error constructing the modal', b), (l = !0), a.closeModal(m), i(), null; } catch (b) {
} return console.error('There has been an error constructing the modal', b), (l = !0), a.closeModal(m), i(), null;
}, }
{ modalKey: `${this.name}_DEP_MODAL` } },
); { modalKey: `${this.name}_DEP_MODAL` }
);
} }
get [Symbol.toStringTag]() { get [Symbol.toStringTag]() {
return 'Plugin'; return 'Plugin';

View File

@ -27,6 +27,6 @@ Show a notification in Discord when someone sends a message, just like on mobile
Saves all deleted and purged messages, as well as all edit history and ghost pings. With highly configurable ignore options, and even restoring deleted messages after restarting Discord. Saves all deleted and purged messages, as well as all edit history and ghost pings. With highly configurable ignore options, and even restoring deleted messages after restarting Discord.
## [SaveToRedux](https://github.com/1Lighty/BetterDiscordPlugins/tree/master/Plugins/SaveToRedux "SaveToRedux") ## [SaveToRedux](https://github.com/1Lighty/BetterDiscordPlugins/tree/master/Plugins/SaveToRedux "SaveToRedux")
Allows you to save images, videos, profile icons, server icons, reactions, emotes and custom status emotes to any folder quickly, as well as install plugins from direct links. Allows you to save images, videos, profile icons, server icons, reactions, emotes, custom status emotes and stickers to any folder quickly, as well as install plugins from direct links.
## [UnreadBadgesRedux](https://github.com/1Lighty/BetterDiscordPlugins/tree/master/Plugins/UnreadBadgesRedux "UnreadBadgesRedux") ## [UnreadBadgesRedux](https://github.com/1Lighty/BetterDiscordPlugins/tree/master/Plugins/UnreadBadgesRedux "UnreadBadgesRedux")
Shows an unread badge on folders, server icons and channels, all toggleable with the count adjustable. Shows an unread badge on folders, server icons and channels, all toggleable with the count adjustable.