STR v2.1.0

This commit is contained in:
_Lighty_ 2020-05-19 17:27:02 +02:00
parent 7b0faabf5d
commit 981f77b9c0
2 changed files with 495 additions and 347 deletions

View File

@ -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.

View File

@ -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 });
@ -581,7 +822,7 @@ var SaveToRedux = (() => {
confirmText: Messages.MASKED_LINK_CONFIRM,
minorText: Messages.MASKED_LINK_TRUST_THIS_DOMAIN,
onConfirm: onOk,
onConfirmSecondary: function() {
onConfirmSecondary: function () {
WebpackModules.getByProps('trustDomain').trustDomain(formattedurl.url);
onOk();
}
@ -767,132 +1008,162 @@ 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);
this.saveFolders();
BdApi.showToast('Removed!', { type: 'success' });
}),
XenoLib.createContextMenuItem('Open Folder', () => {
openItem(folder.path);
}),
XenoLib.createContextMenuItem('Save', () => {
XenoLib.createContextMenuItem(
'Remove Folder',
() => {
this.folders.splice(idx, 1);
this.saveFolders();
BdApi.showToast('Removed!', { type: 'success' });
},
'remove-folder'
),
XenoLib.createContextMenuItem(
'Open Folder',
() => {
openItem(folder.path);
},
'open-folder'
),
XenoLib.createContextMenuItem(
'Save',
() => {
this.lastUsedFolder = idx;
const path = folder.path + `/${formattedurl.fileName}`;
saveFile(path, folder.path);
},
'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);
},
'save-and-open'
),
XenoLib.createContextMenuItem(
'Edit',
() => {
let __name = folder.name.slice(0);
let __path = folder.path.slice(0);
const saveFolder = () => {
if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' });
folder.name = __name;
folder.path = __path;
this.saveFolders();
};
Modals.showModal(
'Edit folder',
React.createElement(FolderEditor, {
name: __name,
path: __path,
onNameChange: e => (__name = e),
onPathChange: e => (__path = e)
}),
{
confirmText: 'Create',
onConfirm: saveFolder,
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);
}),
XenoLib.createContextMenuItem('Save As...', () => saveAs(folder)),
XenoLib.createContextMenuItem('Save And Open', () => {
this.lastUsedFolder = this.folders.findIndex(m => m === folder);
const path = folder.path + `/${formattedurl.fileName}`;
saveFile(path, folder.path, true);
}),
XenoLib.createContextMenuItem('Edit', () => {
let __name = folder.name.slice(0);
let __path = folder.path.slice(0);
const saveFolder = () => {
if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' });
folder.name = __name;
folder.path = __path;
this.saveFolders();
};
Modals.showModal(
'Edit folder',
React.createElement(FolderEditor, {
name: __name,
path: __path,
onNameChange: e => (__name = e),
onPathChange: e => (__path = e)
}),
{
confirmText: 'Create',
onConfirm: saveFolder,
size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal')
}
);
})
],
() => {
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', () => {
dialog
.showOpenDialog({
title: 'Add folder',
properties: ['openDirectory', 'createDirectory']
})
.then(({ filePaths: [path] }) => {
if (!path) return BdApi.showToast('Maybe next time.');
let idx;
if ((idx = this.folders.findIndex(m => m.path === path)) !== -1) return BdApi.showToast(`Folder already exists as ${this.folders[idx].name}!`, { type: 'error', timeout: 5000 });
const folderName = PathModule.basename(path);
let __name = folderName;
let __path = path.slice(0);
const saveFolder = () => {
if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' });
this.folders.push({
path: __path,
name: __name || 'Unnamed'
});
this.saveFolders();
BdApi.showToast('Added!', { type: 'success' });
};
Modals.showModal(
'Create New Folder',
React.createElement(FolderEditor, {
name: folderName,
path: path,
onNameChange: e => (__name = e),
onPathChange: e => (__path = e)
}),
{
confirmText: 'Create',
onConfirm: saveFolder,
size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal')
}
);
});
}),
XenoLib.createContextMenuItem('Save As...', () => {
dialog
.showSaveDialog({
defaultPath: formattedurl.fileName,
filters: formattedurl.extension
? [
{
name: /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(formattedurl.extension) ? 'Images' : /\.{0,1}(mp4|webm|mov)$/i.test(formattedurl.extension) ? 'Videos' : /\.{0,1}(mp3|ogg|wav|flac)$/i.test(formattedurl.extension) ? 'Audio' : 'Files',
extensions: [formattedurl.extension]
},
{
name: 'All Files',
extensions: ['*']
}
]
: undefined
})
.then(({ filePath: path }) => {
if (!path) return BdApi.showToast('Maybe next time.');
saveFile(path, undefined, false, true);
});
}),
XenoLib.createContextMenuItem(
'Add Folder',
() => {
dialog
.showOpenDialog({
title: 'Add folder',
properties: ['openDirectory', 'createDirectory']
})
.then(({ filePaths: [path] }) => {
if (!path) return BdApi.showToast('Maybe next time.');
let idx;
if ((idx = this.folders.findIndex(m => m.path === path)) !== -1) return BdApi.showToast(`Folder already exists as ${this.folders[idx].name}!`, { type: 'error', timeout: 5000 });
const folderName = PathModule.basename(path);
let __name = folderName;
let __path = path.slice(0);
const saveFolder = () => {
if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' });
this.folders.push({
path: __path,
name: __name || 'Unnamed'
});
this.saveFolders();
BdApi.showToast('Added!', { type: 'success' });
};
Modals.showModal(
'Create New Folder',
React.createElement(FolderEditor, {
name: folderName,
path: path,
onNameChange: e => (__name = e),
onPathChange: e => (__path = e)
}),
{
confirmText: 'Create',
onConfirm: saveFolder,
size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal')
}
);
});
},
'add-folder'
),
XenoLib.createContextMenuItem(
'Save As...',
() => {
dialog
.showSaveDialog({
defaultPath: formattedurl.fileName,
filters: formattedurl.extension
? [
{
name: /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(formattedurl.extension) ? 'Images' : /\.{0,1}(mp4|webm|mov)$/i.test(formattedurl.extension) ? 'Videos' : /\.{0,1}(mp3|ogg|wav|flac)$/i.test(formattedurl.extension) ? 'Audio' : 'Files',
extensions: [formattedurl.extension]
},
{
name: 'All Files',
extensions: ['*']
}
]
: undefined
})
.then(({ filePath: path }) => {
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, () => {
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);
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,42 +1301,67 @@ var SaveToRedux = (() => {
this.props.onConfirm();
}
}
let m = !1;
const n = g.push(
a =>
BdApi.React.createElement(
k,
{
label: 'missing dependency modal',
onError: () => {
g.popWithKey(n), j();
}
},
BdApi.React.createElement(
l,
Object.assign(
{
header: e,
children: [BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] })],
red: !1,
confirmText: 'Download Now',
cancelText: 'Cancel',
onConfirm: () => {
if (m) return;
m = !0;
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, () => {})));
};
!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();
}
},
a
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(o), j();
}
},
BdApi.React.createElement(
l,
Object.assign(
{
header: e,
children: [BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] })],
red: !1,
confirmText: 'Download Now',
cancelText: 'Cancel',
onConfirm: () => {
if (m) return;
m = !0;
const a = require('request'),
b = require('fs'),
c = require('path'),
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, 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`
);