STR v2.1.0
This commit is contained in:
parent
7b0faabf5d
commit
981f77b9c0
|
@ -1,4 +1,8 @@
|
|||
# [SaveToRedux](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux "SaveToRedux") Changelog
|
||||
### 2.1.0
|
||||
- Fixed plugin after the new context menu changes. If you notice any bugs, [join my support server](https://discord.gg/NYvWdN5)
|
||||
- Fixed incorrect slashes used on Windows.
|
||||
|
||||
### 2.0.14
|
||||
- Fixed plugin being broken.
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ var SaveToRedux = (() => {
|
|||
twitter_username: ''
|
||||
}
|
||||
],
|
||||
version: '2.0.14',
|
||||
version: '2.1.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.',
|
||||
github: 'https://github.com/1Lighty',
|
||||
github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/SaveToRedux/SaveToRedux.plugin.js'
|
||||
|
@ -50,7 +50,7 @@ var SaveToRedux = (() => {
|
|||
{
|
||||
title: 'fixed',
|
||||
type: 'fixed',
|
||||
items: ['Fixed plugin being broken.']
|
||||
items: ['Fixed plugin after the new context menu changes. If you notice any bugs, [join my support server](https://discord.gg/NYvWdN5)', 'Fixed incorrect slashes used on Windows.']
|
||||
}
|
||||
],
|
||||
defaultConfig: [
|
||||
|
@ -103,8 +103,10 @@ var SaveToRedux = (() => {
|
|||
|
||||
/* Build */
|
||||
const buildPlugin = ([Plugin, Api]) => {
|
||||
const { Settings, Modals, Utilities, WebpackModules, DiscordModules, DiscordClasses, ReactComponents, DiscordAPI, Logger, Patcher, PluginUpdater, PluginUtilities } = Api;
|
||||
const { React, ContextMenuActions, GuildStore, DiscordConstants, Dispatcher, SwitchRow, EmojiUtils, RadioGroup, EmojiInfo, ModalStack } = DiscordModules;
|
||||
const { Settings, Modals, Utilities, WebpackModules, DiscordModules, DiscordClasses, ReactComponents, DiscordAPI, Logger, PluginUpdater, PluginUtilities, ReactTools } = Api;
|
||||
const { React, ContextMenuActions, GuildStore, DiscordConstants, Dispatcher, SwitchRow, EmojiUtils, EmojiStore, RadioGroup, EmojiInfo, ModalStack } = DiscordModules;
|
||||
const Patcher = XenoLib.createSmartPatcher(Api.Patcher);
|
||||
|
||||
const TextComponent = WebpackModules.getByDisplayName('Text');
|
||||
const getEmojiURL = Utilities.getNestedProp(WebpackModules.getByProps('getEmojiURL'), 'getEmojiURL');
|
||||
const showAlertModal = Utilities.getNestedProp(
|
||||
|
@ -249,12 +251,7 @@ var SaveToRedux = (() => {
|
|||
if ('string' != typeof r) throw new Error('Input must be string');
|
||||
var l = Buffer.byteLength.bind(Buffer),
|
||||
a = o.bind(null, l),
|
||||
p = r
|
||||
.replace(t, n)
|
||||
.replace(u, n)
|
||||
.replace(i, n)
|
||||
.replace(f, n)
|
||||
.replace(c, n);
|
||||
p = r.replace(t, n).replace(u, n).replace(i, n).replace(f, n).replace(c, n);
|
||||
return a(p, e.extLength);
|
||||
}
|
||||
var t = /[\/\?<>\\:\*\|"]/g,
|
||||
|
@ -283,10 +280,16 @@ var SaveToRedux = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
const isImage = e => /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(e);
|
||||
const isVideo = e => /\.{0,1}(mp4|webm|mov)$/i.test(e);
|
||||
const isAudio = e => /\.{0,1}(mp3|ogg|wav|flac|m4a)$/i.test(e);
|
||||
|
||||
const useIdealExtensions = url => (url.indexOf('/a_') !== -1 ? url.replace('.webp', '.gif').replace('.png', '.gif') : url.replace('.webp', '.png'));
|
||||
|
||||
return class SaveToRedux extends Plugin {
|
||||
constructor() {
|
||||
super();
|
||||
XenoLib.DiscordUtils.bindAll(this, ['handleContextMenu', 'formatFilename']);
|
||||
XenoLib.DiscordUtils.bindAll(this, ['formatFilename']);
|
||||
XenoLib.changeName(__filename, 'SaveToRedux');
|
||||
const oOnStart = this.onStart.bind(this);
|
||||
this.onStart = () => {
|
||||
|
@ -335,7 +338,6 @@ var SaveToRedux = (() => {
|
|||
onStop() {
|
||||
this.promises.state.cancelled = true;
|
||||
Patcher.unpatchAll();
|
||||
XenoLib.unpatchContext(this.handleContextMenu);
|
||||
PluginUtilities.removeStyle(this.short + '-CSS');
|
||||
}
|
||||
|
||||
|
@ -366,8 +368,47 @@ var SaveToRedux = (() => {
|
|||
/* PATCHES */
|
||||
|
||||
patchAll() {
|
||||
XenoLib.patchContext(this.handleContextMenu);
|
||||
Utilities.suppressErrors(this.patchEmojiPicker.bind(this), 'EmojiPicker patch')(this.promises.state);
|
||||
Utilities.suppressErrors(this.patchReactions.bind(this), 'Reaction patch')(this.promises.state);
|
||||
this.patchContextMenus();
|
||||
}
|
||||
|
||||
patchEmojiPicker() {
|
||||
return;
|
||||
const EmojiPickerListRow = WebpackModules.getModule(m => m.default && m.default.displayName == 'EmojiPickerListRow');
|
||||
Patcher.after(EmojiPickerListRow, 'default', (_, __, returnValue) => {
|
||||
for (const emoji of returnValue.props.children) {
|
||||
const emojiObj = emoji.props.children.props.emoji;
|
||||
const url = emojiObj.id ? getEmojiURL({ id: emojiObj.id, animated: emojiObj.animated }) : 'https://discord.com' + EmojiInfo.getURL(emojiObj.surrogates);
|
||||
const oOnContextMenu = emoji.props.onContextMenu;
|
||||
emoji.props.onContextMenu = e => {
|
||||
if (oOnContextMenu) {
|
||||
let timeout = 0;
|
||||
const unpatch = Patcher.before(ContextMenuActions, 'openContextMenu', (_, args) => {
|
||||
const old = args[1];
|
||||
args[1] = e => {
|
||||
try {
|
||||
const _ret = old(e);
|
||||
const ret = _ret.type(_ret.props);
|
||||
const children = Utilities.getNestedProp(ret, 'props.children.0.props.children');
|
||||
if (Array.isArray(children)) children.push(this.constructMenu(url.split('?')[0], 'Emoji', emojiObj.uniqueName));
|
||||
return ret;
|
||||
} catch (err) {
|
||||
Logger.error('Some error happened in emoji picker context menu', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
unpatch();
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
timeout = setTimeout(unpatch, 1000);
|
||||
return oOnContextMenu(e);
|
||||
} else {
|
||||
ContextMenuActions.openContextMenu(e, _ => React.createElement('div', { className: DiscordClasses.ContextMenu.contextMenu }, XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Emoji', emojiObj.uniqueName)])));
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async patchReactions(promiseState) {
|
||||
|
@ -378,8 +419,8 @@ var SaveToRedux = (() => {
|
|||
ret.props.children = e => {
|
||||
try {
|
||||
const oChRet = oChildren(e);
|
||||
const url = _this.props.emoji.id ? getEmojiURL({ id: _this.props.emoji.id, animated: _this.props.emoji.animated }) : EmojiInfo.getURL(_this.props.emoji.name);
|
||||
XenoLib.createSharedContext(oChRet, 'MESSAGE_REACTIONS', () => XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Reaction', _this.props.emoji.name)]));
|
||||
const url = _this.props.emoji.id ? getEmojiURL({ id: _this.props.emoji.id, animated: _this.props.emoji.animated }) : 'https://discord.com' + EmojiInfo.getURL(_this.props.emoji.name);
|
||||
XenoLib.createSharedContext(oChRet, () => XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Reaction', _this.props.emoji.name)]));
|
||||
return oChRet;
|
||||
} catch (e) {
|
||||
Logger.stacktrace('Error in Reaction patch', e);
|
||||
|
@ -391,6 +432,224 @@ var SaveToRedux = (() => {
|
|||
Reaction.forceUpdateAll();
|
||||
}
|
||||
|
||||
patchContextMenus() {
|
||||
this.patchUserContextMenus();
|
||||
this.patchImageContextMenus();
|
||||
this.patchGuildContextMenu();
|
||||
}
|
||||
|
||||
patchUserContextMenus() {
|
||||
const CTXs = WebpackModules.findAll(({ default: { displayName } }) => displayName && (displayName.endsWith('UserContextMenu') || displayName === 'GroupDMContextMenu'));
|
||||
for (const CTX of CTXs) {
|
||||
Patcher.after(CTX, 'default', (_, [props], ret) => {
|
||||
const menu = Utilities.getNestedProp(
|
||||
Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'),
|
||||
'props.children'
|
||||
);
|
||||
if (!Array.isArray(menu)) return;
|
||||
let saveType;
|
||||
let url;
|
||||
let customName;
|
||||
if (props.user && props.user.getAvatarURL) {
|
||||
saveType = 'Avatar';
|
||||
url = props.user.getAvatarURL();
|
||||
if (this.settings.saveOptions.saveByName) customName = props.user.username;
|
||||
} else if (props.channel && props.channel.type === 3 /* group DM */) {
|
||||
url = AvatarModule.getChannelIconURL(props.channel);
|
||||
saveType = 'Icon';
|
||||
} else return Logger.warn('Uknonwn context menu') /* hurr durr? */;
|
||||
if (!url.indexOf('/assets/')) url = 'https://discordapp.com' + url;
|
||||
url = useIdealExtensions(url);
|
||||
try {
|
||||
const submenu = this.constructMenu(url.split('?')[0], saveType, customName);
|
||||
const group = XenoLib.createContextMenuGroup([submenu]);
|
||||
if (this.settings.misc.contextMenuOnBottom) menu.push(group);
|
||||
else menu.unshift(group);
|
||||
} catch (e) {
|
||||
Logger.warn('Failed to parse URL...', url, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
patchImageContextMenus() {
|
||||
const patchHandler = (props, ret, isImageMenu) => {
|
||||
const menu = Utilities.getNestedProp(
|
||||
Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'),
|
||||
'props.children'
|
||||
);
|
||||
if (!Array.isArray(menu)) return;
|
||||
const [state, setState] = React.useState({});
|
||||
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) {
|
||||
const inst = ReactTools.getOwnerInstance(Utilities.getNestedProp(props, 'target.parentNode.parentNode'));
|
||||
proxiedUrl = props.src;
|
||||
if (inst) src = inst.props.original;
|
||||
if (typeof proxiedUrl === 'string') proxiedUrl = proxiedUrl.split('?')[0];
|
||||
/* if src does not have an extension but the proxied URL does, use the proxied URL instead */
|
||||
if (typeof src !== 'string' || src.indexOf('discordapp.com/channels') !== -1 || (!(isImage(src) || isVideo(src) || isAudio(src)) && (isImage(proxiedUrl) || isVideo(proxiedUrl) || isAudio(proxiedUrl)))) {
|
||||
src = proxiedUrl;
|
||||
proxiedUrl = '';
|
||||
}
|
||||
}
|
||||
if (!src) src = Utilities.getNestedProp(props, 'attachment.href') || Utilities.getNestedProp(props, 'attachment.url');
|
||||
/* is that enough specific cases? */
|
||||
if (typeof src === 'string') {
|
||||
src = src.split('?')[0];
|
||||
if (src.indexOf('//giphy.com/gifs/') !== -1) src = `https://i.giphy.com/media/${src.match(/-([^-]+)$/)[1]}/giphy.gif`;
|
||||
else if (src.indexOf('//tenor.com/view/') !== -1) {
|
||||
src = props.src;
|
||||
saveType = 'Video';
|
||||
} else if (src.indexOf('//preview.redd.it/') !== -1) {
|
||||
src = src.replace('preview', 'i');
|
||||
} else if (src.indexOf('twimg.com/') !== -1) saveType = 'Image';
|
||||
}
|
||||
if (!src) {
|
||||
let C = props.target;
|
||||
let proxiedsauce;
|
||||
let sauce;
|
||||
while (null != C) {
|
||||
if (C instanceof HTMLImageElement && null != C.src) proxiedsauce = C.src;
|
||||
if (C instanceof HTMLVideoElement && null != C.src) proxiedsauce = C.src;
|
||||
if (C instanceof HTMLAnchorElement && null != C.href) sauce = C.href;
|
||||
C = C.parentNode;
|
||||
}
|
||||
if (!proxiedsauce && !sauce) return;
|
||||
if (proxiedsauce) proxiedsauce = proxiedsauce.split('?')[0];
|
||||
if (sauce) sauce = sauce.split('?')[0];
|
||||
// Logger.info('sauce', sauce, 'proxiedsauce', proxiedsauce);
|
||||
/* do not check if proxiedsauce is an image video or audio, it will always be video or image!
|
||||
an anchor element however is just a link which could be anything! so best we check it
|
||||
special handler for github links, discord attachments and plugins
|
||||
*/
|
||||
if (sauce && sauce.indexOf('//github.com/') !== -1 && (sauce.indexOf('.plugin.js') === sauce.length - 10 || sauce.indexOf('.theme.css') === sauce.length - 10)) {
|
||||
const split = sauce.slice(sauce.indexOf('//github.com/') + 13).split('/');
|
||||
split.splice(2, 1);
|
||||
sauce = 'https://raw.githubusercontent.com/' + split.join('/');
|
||||
}
|
||||
if (!proxiedsauce && (!sauce || !(isImage(sauce) || isVideo(sauce) || isAudio(sauce) || sauce.indexOf('//cdn.discordapp.com/attachments/') !== -1 || sauce.indexOf('//raw.githubusercontent.com/') !== -1))) return;
|
||||
src = sauce;
|
||||
proxiedUrl = proxiedsauce;
|
||||
/* if src does not have an extension but the proxied URL does, use the proxied URL instead */
|
||||
if (!src || (!(isImage(sauce) || isVideo(sauce) || isAudio(sauce)) && (isImage(proxiedsauce) || isVideo(proxiedsauce) || isAudio(proxiedsauce)))) {
|
||||
src = proxiedsauce;
|
||||
proxiedUrl = '';
|
||||
}
|
||||
if (!src) return;
|
||||
}
|
||||
url = src;
|
||||
if (!url) return;
|
||||
if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image';
|
||||
else if (isVideo(url)) saveType = 'Video';
|
||||
else if (isAudio(url)) saveType = 'Audio';
|
||||
if (url.indexOf('app.com/emojis/') !== -1) {
|
||||
saveType = 'Emoji';
|
||||
const emojiId = url.split('emojis/')[1].split('.')[0];
|
||||
const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId);
|
||||
if (!emoji) {
|
||||
if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return;
|
||||
const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1);
|
||||
if (message && message.content) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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],
|
||||
saveType,
|
||||
customName,
|
||||
targetUrl => {
|
||||
if (state.__STR_requesting || state.__STR_requested) return;
|
||||
if (!isTrustedDomain(targetUrl)) return;
|
||||
state.__STR_requesting = true;
|
||||
RequestModule.head(targetUrl, (err, res) => {
|
||||
if (err) 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
|
||||
);
|
||||
const group = XenoLib.createContextMenuGroup([submenu]);
|
||||
if (this.settings.misc.contextMenuOnBottom) menu.push(group);
|
||||
else menu.unshift(group);
|
||||
} catch (e) {
|
||||
Logger.warn('Failed to parse URL...', url, e);
|
||||
}
|
||||
};
|
||||
|
||||
Patcher.after(
|
||||
WebpackModules.find(({ default: { displayName } }) => displayName === 'NativeImageContextMenu'),
|
||||
'default',
|
||||
(_, [props], ret) => patchHandler(props, ret, true)
|
||||
);
|
||||
Patcher.after(
|
||||
WebpackModules.find(({ default: { displayName } }) => displayName === 'MessageContextMenu'),
|
||||
'default',
|
||||
(_, [props], ret) => patchHandler(props, ret)
|
||||
);
|
||||
Patcher.after(
|
||||
WebpackModules.find(({ default: { displayName } }) => displayName === 'MessageSearchResultContextMenu'),
|
||||
'default',
|
||||
(_, [props], ret) => patchHandler(props, ret)
|
||||
);
|
||||
}
|
||||
|
||||
patchGuildContextMenu() {
|
||||
Patcher.after(
|
||||
WebpackModules.find(({ default: { displayName } }) => displayName === 'GuildContextMenu'),
|
||||
'default',
|
||||
(_, [props], ret) => {
|
||||
const menu = Utilities.getNestedProp(
|
||||
Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'),
|
||||
'props.children'
|
||||
);
|
||||
if (!Array.isArray(menu)) return;
|
||||
let url = props.guild.getIconURL();
|
||||
if (!url) return;
|
||||
let customName;
|
||||
if (this.settings.saveOptions.saveByName) customName = props.guild.name;
|
||||
url = useIdealExtensions(url);
|
||||
try {
|
||||
const submenu = this.constructMenu(url.split('?')[0], 'Icon', customName);
|
||||
const group = XenoLib.createContextMenuGroup([submenu]);
|
||||
if (this.settings.misc.contextMenuOnBottom) menu.push(group);
|
||||
else menu.unshift(group);
|
||||
} catch (e) {
|
||||
Logger.warn('Failed to parse URL...', url, e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* PATCHES */
|
||||
|
||||
rand() {
|
||||
|
@ -420,10 +679,7 @@ var SaveToRedux = (() => {
|
|||
ret = name;
|
||||
break;
|
||||
case 1: // date
|
||||
ret = `${date
|
||||
.toLocaleDateString()
|
||||
.split('/')
|
||||
.join('-')} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
||||
ret = `${date.toLocaleDateString().split('/').join('-')} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
||||
break;
|
||||
case 2: // random
|
||||
ret = rand;
|
||||
|
@ -436,10 +692,7 @@ var SaveToRedux = (() => {
|
|||
ret = Utilities.formatTString(this.settings.saveOptions.customFileName, {
|
||||
rand,
|
||||
file: name,
|
||||
date: date
|
||||
.toLocaleDateString()
|
||||
.split('/')
|
||||
.join('-'),
|
||||
date: date.toLocaleDateString().split('/').join('-'),
|
||||
time: `${date.getMinutes()}-${date.getSeconds()}-${date.getMilliseconds()}`,
|
||||
day: date.getDate(), // note to self: getDate gives you the day of month
|
||||
month: date.getMonth() + 1, // getMonth gives 0-11
|
||||
|
@ -464,11 +717,7 @@ var SaveToRedux = (() => {
|
|||
formatURL(url, requiresSize, customName, fallbackExtension, proxiedUrl, failNum = 0, forceKeepOriginal = 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');
|
||||
else if (url.indexOf('twimg.com/') !== -1) url = url.replace(':small', ':orig').replace(':medium', ':orig').replace(':large', ':orig');
|
||||
else if (url.indexOf('.e621.net/') !== -1 || url.indexOf('.e926.net/') !== -1) {
|
||||
if (failNum <= 1) url = url.replace('preview/', '').replace('sample/', '');
|
||||
if (failNum === 1) url = url.replace(/.jpe?g/, '.png');
|
||||
|
@ -495,14 +744,6 @@ var SaveToRedux = (() => {
|
|||
}
|
||||
|
||||
constructMenu(url, type, customName, onNoExtension = () => {}, fallbackExtension, proxiedUrl) {
|
||||
const createSubMenu = (name, items, callback) =>
|
||||
XenoLib.createContextMenuSubMenu(name, items, {
|
||||
action: () => {
|
||||
if (!callback) return;
|
||||
ContextMenuActions.closeContextMenu();
|
||||
callback();
|
||||
}
|
||||
});
|
||||
const subItems = [];
|
||||
const folderSubMenus = [];
|
||||
const formattedurl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, 0, type === 'Theme' || type === 'Plugin');
|
||||
|
@ -544,7 +785,7 @@ var SaveToRedux = (() => {
|
|||
.pipe(FsModule.createWriteStream(path))
|
||||
.on('finish', () => {
|
||||
if (openOnSave) openItem(path);
|
||||
BdApi.showToast(`Saved to '${path}'`, { type: 'success' });
|
||||
BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' });
|
||||
})
|
||||
.on('error', e => {
|
||||
BdApi.showToast(`Failed to save! ${e}`, { type: 'error', timeout: 10000 });
|
||||
|
@ -767,32 +1008,48 @@ var SaveToRedux = (() => {
|
|||
}
|
||||
};
|
||||
|
||||
const folderSubMenu = folder => {
|
||||
return createSubMenu(
|
||||
const folderSubMenu = (folder, idx) => {
|
||||
return XenoLib.createContextMenuSubMenu(
|
||||
folder.name,
|
||||
[
|
||||
XenoLib.createContextMenuItem('Remove Folder', () => {
|
||||
const index = this.folders.findIndex(m => m === folder);
|
||||
if (index === -1) return BdApi.showToast("Fatal error! Attempted to remove a folder that doesn't exist!", { type: 'error', timeout: 5000 });
|
||||
this.folders.splice(index, 1);
|
||||
XenoLib.createContextMenuItem(
|
||||
'Remove Folder',
|
||||
() => {
|
||||
this.folders.splice(idx, 1);
|
||||
this.saveFolders();
|
||||
BdApi.showToast('Removed!', { type: 'success' });
|
||||
}),
|
||||
XenoLib.createContextMenuItem('Open Folder', () => {
|
||||
},
|
||||
'remove-folder'
|
||||
),
|
||||
XenoLib.createContextMenuItem(
|
||||
'Open Folder',
|
||||
() => {
|
||||
openItem(folder.path);
|
||||
}),
|
||||
XenoLib.createContextMenuItem('Save', () => {
|
||||
this.lastUsedFolder = this.folders.findIndex(m => m === folder);
|
||||
},
|
||||
'open-folder'
|
||||
),
|
||||
XenoLib.createContextMenuItem(
|
||||
'Save',
|
||||
() => {
|
||||
this.lastUsedFolder = idx;
|
||||
const path = folder.path + `/${formattedurl.fileName}`;
|
||||
saveFile(path, folder.path);
|
||||
}),
|
||||
XenoLib.createContextMenuItem('Save As...', () => saveAs(folder)),
|
||||
XenoLib.createContextMenuItem('Save And Open', () => {
|
||||
this.lastUsedFolder = this.folders.findIndex(m => m === folder);
|
||||
},
|
||||
'save'
|
||||
),
|
||||
XenoLib.createContextMenuItem('Save As...', () => saveAs(folder), 'savetoredux-save-as'),
|
||||
XenoLib.createContextMenuItem(
|
||||
'Save And Open',
|
||||
() => {
|
||||
this.lastUsedFolder = idx;
|
||||
const path = folder.path + `/${formattedurl.fileName}`;
|
||||
saveFile(path, folder.path, true);
|
||||
}),
|
||||
XenoLib.createContextMenuItem('Edit', () => {
|
||||
},
|
||||
'save-and-open'
|
||||
),
|
||||
XenoLib.createContextMenuItem(
|
||||
'Edit',
|
||||
() => {
|
||||
let __name = folder.name.slice(0);
|
||||
let __path = folder.path.slice(0);
|
||||
const saveFolder = () => {
|
||||
|
@ -815,19 +1072,26 @@ var SaveToRedux = (() => {
|
|||
size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal')
|
||||
}
|
||||
);
|
||||
})
|
||||
},
|
||||
'edit'
|
||||
)
|
||||
],
|
||||
() => {
|
||||
idx,
|
||||
{
|
||||
action: () => {
|
||||
this.lastUsedFolder = this.folders.findIndex(m => m === folder);
|
||||
const path = folder.path + `/${formattedurl.fileName}`;
|
||||
saveFile(path, folder.path);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
for (const folder of this.folders) folderSubMenus.push(folderSubMenu(folder));
|
||||
for (const folderIDX in this.folders) folderSubMenus.push(folderSubMenu(this.folders[folderIDX], folderIDX));
|
||||
subItems.push(
|
||||
...folderSubMenus,
|
||||
XenoLib.createContextMenuItem('Add Folder', () => {
|
||||
XenoLib.createContextMenuItem(
|
||||
'Add Folder',
|
||||
() => {
|
||||
dialog
|
||||
.showOpenDialog({
|
||||
title: 'Add folder',
|
||||
|
@ -864,8 +1128,12 @@ var SaveToRedux = (() => {
|
|||
}
|
||||
);
|
||||
});
|
||||
}),
|
||||
XenoLib.createContextMenuItem('Save As...', () => {
|
||||
},
|
||||
'add-folder'
|
||||
),
|
||||
XenoLib.createContextMenuItem(
|
||||
'Save As...',
|
||||
() => {
|
||||
dialog
|
||||
.showSaveDialog({
|
||||
defaultPath: formattedurl.fileName,
|
||||
|
@ -886,13 +1154,16 @@ var SaveToRedux = (() => {
|
|||
if (!path) return BdApi.showToast('Maybe next time.');
|
||||
saveFile(path, undefined, false, true);
|
||||
});
|
||||
}),
|
||||
},
|
||||
'save-as'
|
||||
),
|
||||
type === 'Plugin'
|
||||
? XenoLib.createContextMenuItem(
|
||||
`Install Plugin`,
|
||||
() => {
|
||||
saveFile(BdApi.Plugins.folder + `/${formattedurl.fileName}`, undefined, false, true);
|
||||
},
|
||||
'install-plugin',
|
||||
{
|
||||
/* onContextMenu: () => console.log('wee!'), tooltip: 'Right click to install and enable' */
|
||||
tooltip: 'No overwrite warning'
|
||||
|
@ -905,6 +1176,7 @@ var SaveToRedux = (() => {
|
|||
() => {
|
||||
saveFile(BdApi.Themes.folder + `/${formattedurl.fileName}`, undefined, false, true);
|
||||
},
|
||||
'install-theme',
|
||||
{
|
||||
/* onContextMenu: () => console.log('wee!'), tooltip: 'Right click to install and enable' */
|
||||
tooltip: 'No overwrite warning'
|
||||
|
@ -912,170 +1184,17 @@ var SaveToRedux = (() => {
|
|||
)
|
||||
: null
|
||||
);
|
||||
return createSubMenu(`Save ${type} To`, 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' });
|
||||
const folder = this.folders[this.lastUsedFolder];
|
||||
if (!folder) return BdApi.showToast('Folder no longer exists', { type: 'error' });
|
||||
const path = folder.path + `/${formattedurl.fileName}`;
|
||||
saveFile(path, folder.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleContextMenu(_this, ret) {
|
||||
if (!ret) return ret;
|
||||
// Logger.info(_this, ret);
|
||||
const type = _this.props.type;
|
||||
let saveType = 'File';
|
||||
let url = '';
|
||||
let proxiedUrl = '';
|
||||
let customName = '';
|
||||
const isImage = e => /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(e);
|
||||
const isVideo = e => /\.{0,1}(mp4|webm|mov)$/i.test(e);
|
||||
const isAudio = e => /\.{0,1}(mp3|ogg|wav|flac)$/i.test(e);
|
||||
const useCorrectShit = () => {
|
||||
if (url.indexOf('/a_') !== -1) url = url.replace('.webp', '.gif').replace('.png', '.gif');
|
||||
else url = url.replace('.webp', '.png');
|
||||
};
|
||||
if (type === 'NATIVE_IMAGE' || type === 'MESSAGE_MAIN' || type === 'MESSAGE_SEARCH_RESULT') {
|
||||
let src;
|
||||
if (type === 'NATIVE_IMAGE') {
|
||||
src = Utilities.getNestedProp(ret, 'props.children.props.href') || Utilities.getNestedProp(_this, 'props.href');
|
||||
proxiedUrl = Utilities.getNestedProp(ret, 'props.children.props.src') || Utilities.getNestedProp(_this, 'props.src');
|
||||
if (typeof proxiedUrl === 'string') proxiedUrl = proxiedUrl.split('?')[0];
|
||||
/* if src does not have an extension but the proxied URL does, use the proxied URL instead */
|
||||
if (typeof src !== 'string' || src.indexOf('discordapp.com/channels') !== -1 || (!(isImage(src) || isVideo(src) || isAudio(src)) && (isImage(proxiedUrl) || isVideo(proxiedUrl) || isAudio(proxiedUrl)))) {
|
||||
src = proxiedUrl;
|
||||
proxiedUrl = '';
|
||||
}
|
||||
}
|
||||
// Logger.info('src', src, 'proxiedUrl', proxiedUrl, _this, ret);
|
||||
if (!src) src = Utilities.getNestedProp(_this, 'props.attachment.href') || Utilities.getNestedProp(_this, 'props.attachment.url');
|
||||
/* is that enough specific cases? */
|
||||
if (typeof src === 'string') {
|
||||
src = src.split('?')[0];
|
||||
if (src.indexOf('//giphy.com/gifs/') !== -1) src = `https://i.giphy.com/media/${src.match(/-([^-]+)$/)[1]}/giphy.gif`;
|
||||
else if (src.indexOf('//tenor.com/view/') !== -1) {
|
||||
src = _this.props.src;
|
||||
saveType = 'Video';
|
||||
} else if (src.indexOf('//preview.redd.it/') !== -1) {
|
||||
src = src.replace('preview', 'i');
|
||||
} else if (src.indexOf('twimg.com/') !== -1) saveType = 'Image';
|
||||
}
|
||||
if (!src) {
|
||||
let C = _this.props.target;
|
||||
let proxiedsauce;
|
||||
let sauce;
|
||||
while (null != C) {
|
||||
if (C instanceof HTMLImageElement && null != C.src) proxiedsauce = C.src;
|
||||
if (C instanceof HTMLVideoElement && null != C.src) proxiedsauce = C.src;
|
||||
if (C instanceof HTMLAnchorElement && null != C.href) sauce = C.href;
|
||||
C = C.parentNode;
|
||||
}
|
||||
if (!proxiedsauce && !sauce) return;
|
||||
if (proxiedsauce) proxiedsauce = proxiedsauce.split('?')[0];
|
||||
if (sauce) sauce = sauce.split('?')[0];
|
||||
// Logger.info('sauce', sauce, 'proxiedsauce', proxiedsauce);
|
||||
/* do not check if proxiedsauce is an image video or audio, it will always be video or image!
|
||||
an anchor element however is just a link which could be anything! so best we check it
|
||||
special handler for github links, discord attachments and plugins
|
||||
*/
|
||||
if (sauce && sauce.indexOf('//github.com/') !== -1 && (sauce.indexOf('.plugin.js') === sauce.length - 10 || sauce.indexOf('.theme.css') === sauce.length - 10)) {
|
||||
const split = sauce.slice(sauce.indexOf('//github.com/') + 13).split('/');
|
||||
split.splice(2, 1);
|
||||
sauce = 'https://raw.githubusercontent.com/' + split.join('/');
|
||||
}
|
||||
if (!proxiedsauce && (!sauce || !(isImage(sauce) || isVideo(sauce) || isAudio(sauce) || sauce.indexOf('//cdn.discordapp.com/attachments/') !== -1 || sauce.indexOf('//raw.githubusercontent.com/') !== -1))) return;
|
||||
src = sauce;
|
||||
proxiedUrl = proxiedsauce;
|
||||
/* if src does not have an extension but the proxied URL does, use the proxied URL instead */
|
||||
if (!src || (!(isImage(sauce) || isVideo(sauce) || isAudio(sauce)) && (isImage(proxiedsauce) || isVideo(proxiedsauce) || isAudio(proxiedsauce)))) {
|
||||
src = proxiedsauce;
|
||||
proxiedUrl = '';
|
||||
}
|
||||
if (!src) return;
|
||||
}
|
||||
url = src;
|
||||
if (!url) return;
|
||||
if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image';
|
||||
else if (isVideo(url)) saveType = 'Video';
|
||||
else if (isAudio(url)) saveType = 'Audio';
|
||||
if (url.indexOf('app.com/emojis/') !== -1) {
|
||||
saveType = 'Emoji';
|
||||
const emojiId = url.split('emojis/')[1].split('.')[0];
|
||||
const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId);
|
||||
if (!emoji) {
|
||||
if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return;
|
||||
const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1);
|
||||
if (message && message.content) {
|
||||
const group = message.content.match(new RegExp(`<a?:([^:>]*):${emojiId}>`));
|
||||
if (group && group[1]) customName = group[1];
|
||||
}
|
||||
if (!customName) {
|
||||
const alt = _this.props.target.alt;
|
||||
if (alt) customName = alt.split(':')[1] || alt;
|
||||
}
|
||||
} else customName = emoji.name;
|
||||
} else if (_this.state.__STR_extension) {
|
||||
if (isImage(_this.state.__STR_extension)) saveType = 'Image';
|
||||
else if (isVideo(_this.state.__STR_extension)) saveType = 'Video';
|
||||
else if (isAudio(_this.state.__STR_extension)) saveType = 'Audio';
|
||||
} else if (url.indexOf('//discordapp.com/assets/') !== -1 && _this.props.target && _this.props.target.className.indexOf('emoji') !== -1) {
|
||||
const alt = _this.props.target.alt;
|
||||
if (alt) customName = alt.split(':')[1] || alt;
|
||||
} else if (url.indexOf('.plugin.js') === url.length - 10) saveType = 'Plugin';
|
||||
else if (url.indexOf('.theme.css') === url.length - 10) saveType = 'Theme';
|
||||
if (!Array.isArray(ret.props.children)) ret.props.children = [ret.props.children];
|
||||
} else if (type === 'GUILD_ICON_BAR') {
|
||||
saveType = 'Icon';
|
||||
url = _this.props.guild.getIconURL();
|
||||
if (!url) return;
|
||||
if (this.settings.saveOptions.saveByName) customName = _this.props.guild.name;
|
||||
useCorrectShit();
|
||||
} else {
|
||||
if (_this.props.user && _this.props.user.getAvatarURL) {
|
||||
saveType = 'Avatar';
|
||||
url = _this.props.user.getAvatarURL();
|
||||
if (this.settings.saveOptions.saveByName) customName = _this.props.user.username;
|
||||
} else if (_this.props.channel && _this.props.channel.type === 3 /* group DM */) {
|
||||
url = AvatarModule.getChannelIconURL(_this.props.channel);
|
||||
saveType = 'Icon';
|
||||
} else return /* hurr durr? */;
|
||||
useCorrectShit();
|
||||
if (url.startsWith('/assets/')) url = 'https://discordapp.com' + url;
|
||||
}
|
||||
try {
|
||||
const submenu = this.constructMenu(
|
||||
url.split('?')[0],
|
||||
saveType,
|
||||
customName,
|
||||
targetUrl => {
|
||||
if (_this.state.__STR_requesting || _this.state.__STR_requested) return;
|
||||
if (!isTrustedDomain(targetUrl)) return;
|
||||
_this.state.__STR_requesting = true;
|
||||
RequestModule.head(targetUrl, (err, res) => {
|
||||
if (err) return _this.setState({ __STR_requesting: false, __STR_requested: true });
|
||||
const extension = MimeTypesModule.extension(res.headers['content-type']);
|
||||
_this.setState({
|
||||
__STR_requesting: false,
|
||||
__STR_requested: true,
|
||||
__STR_extension: extension
|
||||
});
|
||||
});
|
||||
targetUrl;
|
||||
},
|
||||
_this.state.__STR_extension,
|
||||
proxiedUrl
|
||||
);
|
||||
const group = React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'savetoredux submenu', onError: () => XenoLib.Notifications.error(`[**${this.name}**] An issue has occured and the submenus had to be removed to avoid crashes. Sorry for the inconvenience. More info in console (CTRL + SHIFT + I, click console).`, { timeout: 10000 }) }, XenoLib.createContextMenuGroup([submenu]));
|
||||
const targetGroup = ret.props.children;
|
||||
|
||||
if (this.settings.misc.contextMenuOnBottom) targetGroup.push(group);
|
||||
else targetGroup.unshift(group);
|
||||
} catch (e) {
|
||||
Logger.warn('Failed to parse URL...', url, e);
|
||||
}
|
||||
}
|
||||
|
||||
showChangelog(footer) {
|
||||
XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog);
|
||||
}
|
||||
|
@ -1122,7 +1241,7 @@ var SaveToRedux = (() => {
|
|||
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.14') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.17') && (XenoLibOutdated = !0);
|
||||
n(e, '1.2.17') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.20') && (XenoLibOutdated = !0);
|
||||
}
|
||||
} catch (i) {
|
||||
console.error('Error checking if libraries are out of date', i);
|
||||
|
@ -1164,8 +1283,8 @@ var SaveToRedux = (() => {
|
|||
g = BdApi.findModuleByProps('push', 'update', 'pop', 'popWithKey'),
|
||||
h = BdApi.findModuleByDisplayName('Text'),
|
||||
i = BdApi.findModule(a => a.defaultProps && a.key && 'confirm-modal' === a.key()),
|
||||
j = () => 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.`, 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 (!g || !i || !h) return j();
|
||||
j = () => 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 (!g || !i || !h) return console.error(`Missing components:${(g ? '' : ' ModalStack') + (i ? '' : ' ConfirmationModalComponent') + (h ? '' : 'TextElement')}`), j();
|
||||
class k extends BdApi.React.PureComponent {
|
||||
constructor(a) {
|
||||
super(a), (this.state = { hasError: !1 });
|
||||
|
@ -1182,15 +1301,18 @@ var SaveToRedux = (() => {
|
|||
this.props.onConfirm();
|
||||
}
|
||||
}
|
||||
let m = !1;
|
||||
const n = g.push(
|
||||
a =>
|
||||
BdApi.React.createElement(
|
||||
let m = !1,
|
||||
n = !1;
|
||||
const o = g.push(
|
||||
a => {
|
||||
if (n) return null;
|
||||
try {
|
||||
return BdApi.React.createElement(
|
||||
k,
|
||||
{
|
||||
label: 'missing dependency modal',
|
||||
onError: () => {
|
||||
g.popWithKey(n), j();
|
||||
g.popWithKey(o), j();
|
||||
}
|
||||
},
|
||||
BdApi.React.createElement(
|
||||
|
@ -1208,16 +1330,38 @@ var SaveToRedux = (() => {
|
|||
const a = require('request'),
|
||||
b = require('fs'),
|
||||
c = require('path'),
|
||||
d = () => {
|
||||
(global.XenoLib && !XenoLibOutdated) || a('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (a, d, e) => (a || 200 !== d.statusCode ? (g.popWithKey(n), j()) : void b.writeFile(c.join(BdApi.Plugins.folder, '1XenoLib.plugin.js'), e, () => {})));
|
||||
d = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder,
|
||||
e = () => {
|
||||
(global.XenoLib && !XenoLibOutdated) ||
|
||||
a('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (a, e, f) => {
|
||||
try {
|
||||
if (a || 200 !== e.statusCode) return g.popWithKey(o), j();
|
||||
b.writeFile(c.join(d, '1XenoLib.plugin.js'), f, () => {});
|
||||
} catch (a) {
|
||||
console.error('Fatal error downloading XenoLib', a), g.popWithKey(o), j();
|
||||
}
|
||||
});
|
||||
};
|
||||
!global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? a('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (a, e, f) => (a || 200 !== e.statusCode ? (g.popWithKey(n), j()) : void (b.writeFile(c.join(BdApi.Plugins.folder, '0PluginLibrary.plugin.js'), f, () => {}), d()))) : d();
|
||||
!global.ZeresPluginLibrary || ZeresPluginLibraryOutdated
|
||||
? a('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (a, f, h) => {
|
||||
try {
|
||||
if (a || 200 !== f.statusCode) return g.popWithKey(o), j();
|
||||
b.writeFile(c.join(d, '0PluginLibrary.plugin.js'), h, () => {}), e();
|
||||
} catch (a) {
|
||||
console.error('Fatal error downloading ZeresPluginLibrary', a), g.popWithKey(o), j();
|
||||
}
|
||||
})
|
||||
: e();
|
||||
}
|
||||
},
|
||||
a
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
} catch (a) {
|
||||
return console.error('There has been an error constructing the modal', a), (n = !0), g.popWithKey(o), j(), null;
|
||||
}
|
||||
},
|
||||
void 0,
|
||||
`${this.name}_DEP_MODAL`
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue