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
### 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
- Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently.

View File

@ -1,11 +1,11 @@
# 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
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
Right clicking a theme or plugin attachment or link will show you the option of installing it.
### 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)
![preview2](https://cdn.discordapp.com/attachments/389049952732446733/694622056213512292/5jsZjnlrCBkz.png)
### Settings

View File

@ -41,16 +41,16 @@ module.exports = (() => {
twitter_username: ''
}
],
version: '2.1.5',
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.',
version: '2.2.0',
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_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/SaveToRedux/SaveToRedux.plugin.js'
},
changelog: [
{
title: 'fixed',
type: 'fixed',
items: ['Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently.']
title: 'added',
type: 'added',
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: [
@ -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: '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 }] }
@ -113,7 +123,7 @@ module.exports = (() => {
const ret = ConfirmModal(props);
if (props.size) ret.props.size = props.size;
return ret;
} catch(err) {
} catch (err) {
if (props.onCancel) props.onCancel();
else props.onClose();
return null;
@ -125,7 +135,7 @@ module.exports = (() => {
const Modals = {
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) {
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 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 {
constructor() {
super();
@ -359,6 +380,9 @@ module.exports = (() => {
div[id$="-str"] + .${XenoLib.getSingleClass('layerContainer layer')} {
z-index: 1;
}
div[id$="-str-stickers"] + .${XenoLib.getSingleClass('layerContainer layer')} {
z-index: 2;
}
`
);
this.lastUsedFolder = -1;
@ -400,6 +424,7 @@ module.exports = (() => {
Utilities.suppressErrors(this.patchEmojiPicker.bind(this), 'EmojiPicker patch')(this.promises.state);
Utilities.suppressErrors(this.patchReactions.bind(this), 'Reaction patch')(this.promises.state);
this.patchContextMenus();
this.patchStickerStorePicker();
}
patchEmojiPicker() {
@ -461,6 +486,25 @@ module.exports = (() => {
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() {
this.patchUserContextMenus();
this.patchImageContextMenus();
@ -509,12 +553,13 @@ module.exports = (() => {
);
if (!Array.isArray(menu)) return;
const [state, setState] = React.useState({});
const extraData = {};
let src;
let saveType = 'File';
let url = '';
let proxiedUrl = '';
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'));
proxiedUrl = props.src;
if (inst) src = inst.props.original;
@ -525,6 +570,17 @@ module.exports = (() => {
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');
/* is that enough specific cases? */
if (typeof src === 'string') {
@ -572,6 +628,7 @@ module.exports = (() => {
}
url = src;
if (!url) return;
if (saveType !== 'Sticker') {
if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image';
else if (isVideo(url)) saveType = 'Video';
else if (isAudio(url)) saveType = 'Audio';
@ -608,6 +665,7 @@ module.exports = (() => {
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 {
const submenu = this.constructMenu(
url.split('?')[0],
@ -618,14 +676,15 @@ module.exports = (() => {
if (!isTrustedDomain(targetUrl)) return;
state.__STR_requesting = true;
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']);
setState({ __STR_requesting: false, __STR_requested: true, __STR_extension: extension });
});
targetUrl;
},
state.__STR_extension,
proxiedUrl
proxiedUrl,
extraData
);
const group = XenoLib.createContextMenuGroup([submenu]);
if (this.settings.misc.contextMenuOnBottom) menu.push(group);
@ -743,7 +802,7 @@ module.exports = (() => {
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(/\/$/, '');
if (requiresSize) url += '?size=2048';
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})$/);
let name = customName || match[1];
let extension = match[2] || fallbackExtension;
let extension = forceExtension || match[2] || fallbackExtension;
if (url.indexOf('//media.tenor.co') !== -1) {
extension = name;
name = url.match(/\/\/media.tenor.co\/[^\/]+\/([^\/]+)\//)[1];
@ -772,10 +831,17 @@ module.exports = (() => {
return ret;
}
constructMenu(url, type, customName, onNoExtension = () => { }, fallbackExtension, proxiedUrl) {
constructMenu(url, type, customName, onNoExtension = () => { }, fallbackExtension, proxiedUrl, extraData = {}) {
const subItems = [];
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);
let notifId;
let downloadAttempts = 0;
@ -796,23 +862,67 @@ module.exports = (() => {
return `${bytes.toFixed(1)}${noUnit && unit.a === units[u] ? '' : ' ' + units[u]}`;
}
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);
let totalBytes = 0;
let receivedBytes = 0;
const req = RequestModule(formattedurl.url);
let receivedBytes = '';
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
.on('data', chunk => {
receivedBytes += chunk.length;
receivedBytes += chunk;
throttledUpdate();
})
.on('response', res => {
if (res.statusCode == 200) {
totalBytes = parseInt(res.headers['content-length']);
update();
if (type === 'Sticker' && extraData.type !== StickerFormat.PNG && !extraData.isStickerSubMenu) return; // do not stream download because we need to convert it first
req
.pipe(FsModule.createWriteStream(path))
.on('finish', () => {
XenoLib.Notifications.remove(notifId);
notifId = undefined;
if (openOnSave) openPath(path);
BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' });
})
@ -824,7 +934,7 @@ module.exports = (() => {
} else if (res.statusCode == 404) {
if (shouldDoMultiAttempts && downloadAttempts < 2) {
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) {
formattedurl.url = newUrl;
return downloadEx(path, openOnSave);
@ -1041,7 +1151,7 @@ module.exports = (() => {
return XenoLib.createContextMenuSubMenu(
folder.name,
[
XenoLib.createContextMenuItem(
extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Remove Folder',
() => {
this.folders.splice(idx, 1);
@ -1050,7 +1160,7 @@ module.exports = (() => {
},
'remove-folder'
),
XenoLib.createContextMenuItem(
extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Open Folder',
() => {
openPath(folder.path);
@ -1076,7 +1186,7 @@ module.exports = (() => {
},
'save-and-open'
),
XenoLib.createContextMenuItem(
extraData.onlyFolderSave ? null : XenoLib.createContextMenuItem(
'Edit',
() => {
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));
subItems.push(
...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',
() => {
dialog
@ -1215,6 +1328,7 @@ module.exports = (() => {
)
: null
);
if (extraData.onlyItems) return subItems;
return XenoLib.createContextMenuSubMenu(`Save ${type} To`, subItems, 'str', {
action: () => {
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),
e = BdApi.getPlugin('ZeresPluginLibrary'),
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) {
console.error('Error checking if libraries are out of date', i);
@ -1352,7 +1466,7 @@ module.exports = (() => {
b('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (b, f, g) => {
try {
if (b || 200 !== f.statusCode) return a.closeModal(m), i();
c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => {});
c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => { });
} catch (b) {
console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i();
}
@ -1362,7 +1476,7 @@ module.exports = (() => {
? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => {
try {
if (b || 200 !== g.statusCode) return a.closeModal(m), i();
c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => {}), f();
c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => { }), f();
} catch (b) {
console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i();
}
@ -1371,7 +1485,7 @@ module.exports = (() => {
}
},
b,
{ onClose: () => {} }
{ onClose: () => { } }
)
)
);

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.
## [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")
Shows an unread badge on folders, server icons and channels, all toggleable with the count adjustable.