├── .gitignore ├── LICENSE ├── README.md ├── SplitLargeFiles.plugin.js ├── images ├── chunks.png └── visualReassembly.png ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/SplitLargeFiles/bundled.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jack Hogan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split Large Files 2 | Split Large Files is a BetterDiscord plugin that makes sending large files easy by breaking up big files into smaller ones that get reassembled upon download. 3 | 4 | The Node engine that Discord runs on only supports a max file size of 2 GB, so don't expect to upload files above 1.5 GB. An error may not be displayed on failure if you try to upload files greater than 1 GB. 5 | 6 | If you are unable to install BetterDiscord or Split Large Files and you want to reassemble a set of chunk files someone sent you, go [here](https://imthesquid.github.io/). 7 | 8 | ## Main Features 9 | Automatic file splitting when uploading files larger than the upload limit... 10 | 11 | ![File split into multiple chunks](images/chunks.png) 12 | 13 | That gets visually reassembled once uploading is complete. 14 | 15 | ###### Note that the recipient(s) must also have the plugin installed. 16 | 17 | ![File visually reassembled into original file](images/visualReassembly.png) 18 | 19 | Downloading the file results in the reassembled original file being put in the directory of your choice. 20 | 21 | ## Other Features 22 | - Manual refresh controls both per-message and per-channel in context menus 23 | - Automatic full-file deletion for your own chunk files that doesn't spam Discord's API 24 | - Full support for new multi-upload system with automatic rate limiting to prevent API spam 25 | 26 | ## Installation 27 | ### Github 28 | 1. Download the `SplitLargeFiles.plugin.js` file (click on the file above, click Raw at the top right of the code, then press Ctrl+S) 29 | 3. Drag it into your BetterDiscord plugins directory 30 | 31 | ## Frequently Asked Questions and Common Problems 32 | - Problem: Discord crashes when I try to upload a large file. 33 | 34 | - Solution: Try uploading a different file of similar size. If the new file works, there is something wrong with the original file. You can try moving it somewhere else and see if it works. If the new file doesn't work, contact me and I will take a bug report. 35 | 36 | ## Squid's Other Plugins 37 | - [PiPEmbeds](https://github.com/ImTheSquid/PiPEmbeds) 38 | - [StickerSnatcher](https://github.com/ImTheSquid/StickerSnatcher) 39 | - [SettingsSync](https://github.com/ImTheSquid/SettingsSync) 40 | 41 | ## Changes made in this fork 42 | 43 | This fork allows the user to choose the split size to be 8MB, 25MB, 50MB, 100MB, or 500MB regardless of their Nitro status, and also implements general bug fixes that should probably be in the original repo but aren't. 44 | -------------------------------------------------------------------------------- /SplitLargeFiles.plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name SplitLargeFiles 3 | * @description Splits files larger than the upload limit into smaller chunks that can be redownloaded into a full file later. 4 | * @version 1.9.7 5 | * @author ImTheSquid & Riolubruh 6 | * @authorId 262055523896131584 7 | * @website https://github.com/riolubruh/SplitLargeFiles 8 | * @source https://github.com/riolubruh/SplitLargeFiles 9 | * @updateUrl https://raw.githubusercontent.com/riolubruh/SplitLargeFiles/main/SplitLargeFiles.plugin.js 10 | */ 11 | /*@cc_on 12 | @if (@_jscript) 13 | 14 | // Offer to self-install for clueless users that try to run this directly. 15 | var shell = WScript.CreateObject("WScript.Shell"); 16 | var fs = new ActiveXObject("Scripting.FileSystemObject"); 17 | var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\\BetterDiscord\\plugins"); 18 | var pathSelf = WScript.ScriptFullName; 19 | // Put the user at ease by addressing them in the first person 20 | shell.Popup("It looks like you've mistakenly tried to run me directly. \n(Don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30); 21 | if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { 22 | shell.Popup("I'm in the correct folder already.", 0, "I'm already installed", 0x40); 23 | } else if (!fs.FolderExists(pathPlugins)) { 24 | shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10); 25 | } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) { 26 | fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); 27 | // Show the user where to put plugins in the future 28 | shell.Exec("explorer " + pathPlugins); 29 | shell.Popup("I'm installed!", 0, "Successfully installed", 0x40); 30 | } 31 | WScript.Quit(); 32 | 33 | @else@*/ 34 | const config = { 35 | info: { 36 | name: "SplitLargeFiles", 37 | authors: [ 38 | { 39 | name: "ImTheSquid", 40 | discord_id: "262055523896131584", 41 | github_username: "ImTheSquid", 42 | twitter_username: "ImTheSquid11" 43 | }, { 44 | name: "Riolubruh", 45 | discord_id: "359063827091816448", 46 | github_username: "riolubruh", 47 | twitter_username: "riolubruh" 48 | } 49 | ], 50 | version: "1.9.7", 51 | description: "Splits files larger than the upload limit into smaller chunks that can be redownloaded into a full file later.", 52 | github: "https://github.com/riolubruh/SplitLargeFiles", 53 | github_raw: "https://raw.githubusercontent.com/riolubruh/SplitLargeFiles/main/SplitLargeFiles.plugin.js" 54 | }, 55 | changelog: [ 56 | { 57 | title: "1.9.7", 58 | items: [ 59 | "Improved YABDP4Nitro compatibility." 60 | ] 61 | } 62 | ], 63 | main: "bundled.js" 64 | }; 65 | class Dummy { 66 | constructor() { this._config = config; } 67 | start() { } 68 | stop() { } 69 | } 70 | 71 | if (!global.ZeresPluginLibrary) { 72 | BdApi.showConfirmationModal("Library Missing", `The library plugin needed for ${config.name ?? config.info.name} is missing. Please click Download Now to install it.`, { 73 | confirmText: "Download Now", 74 | cancelText: "Cancel", 75 | onConfirm: () => { 76 | require("request").get("https://betterdiscord.app/gh-redirect?id=9", async (err, resp, body) => { 77 | if (err) return require("electron").shell.openExternal("https://betterdiscord.app/Download?id=9"); 78 | if (resp.statusCode === 302) { 79 | require("request").get(resp.headers.location, async (error, response, content) => { 80 | if (error) return require("electron").shell.openExternal("https://betterdiscord.app/Download?id=9"); 81 | await new Promise(r => require("fs").writeFile(require("path").join(BdApi.Plugins.folder, "0PluginLibrary.plugin.js"), content, r)); 82 | }); 83 | } 84 | else { 85 | await new Promise(r => require("fs").writeFile(require("path").join(BdApi.Plugins.folder, "0PluginLibrary.plugin.js"), body, r)); 86 | } 87 | }); 88 | } 89 | }); 90 | } 91 | 92 | module.exports = !global.ZeresPluginLibrary ? Dummy : (([Plugin, Api]) => { 93 | const plugin = (Plugin, Library) => { 94 | "use strict"; 95 | const { ContextMenu, Webpack, React } = BdApi; 96 | const { byProps } = Webpack.Filters; 97 | const { Logger, Patcher, DiscordModules, DOMTools, PluginUtilities, Settings, PluginUpdater } = Library; 98 | const { SettingPanel, Slider } = Settings; 99 | const { Dispatcher, SelectedChannelStore, SelectedGuildStore, UserStore, MessageStore, Permissions, ChannelStore, MessageActions } = DiscordModules; 100 | const MessageAttachmentManager = Webpack.getModule(byProps("addFiles")); 101 | const FileCheckMod = Webpack.getModule((m) => Object.values(m).filter((v) => v?.toString).map((v) => v.toString()).some((v) => v.includes("getCurrentUser();") && v.includes("getUserMaxFileSize"))); 102 | //const MessageAccessories = Webpack.getByKeys("MessageAccessories").MessageAccessories; 103 | const MessageAccessories = Webpack.getByKeys("$p", "BB").BB; 104 | //const Attachment = Webpack.getByKeys("isMediaAttachment", "default"); 105 | const Attachment = Webpack.getAllByKeys("Z", "g").filter(o => o.g.toString().includes("attachment"))[0]; 106 | const uploadinator = Webpack.getByKeys("G", "d"); 107 | //Hover Download button 108 | const downloadButtonMod = Webpack.getAllByKeys("Z").filter(obj => obj.Z.toString().includes(`MEDIA_DOWNLOAD_BUTTON_TAPPED`))[0]; 109 | const BATCH_SIZE = 10; 110 | const queuedUploads = /* @__PURE__ */ new Map(); 111 | const activeDownloads = /* @__PURE__ */ new Map(); 112 | let registeredDownloads = new Array(); 113 | /* async function downloadId(download) { 114 | if (!download) 115 | return null; 116 | const encoder = new TextEncoder(); 117 | const digested = await crypto.subtle.digest("SHA-256", encoder.encode(download.urls.join(""))); 118 | return Buffer.from(digested).toString("base64"); 119 | } */ 120 | function getFunctionNameFromString(obj, search) { 121 | for (const [k, v] of Object.entries(obj)) { 122 | if (search.every((str) => v?.toString().match(str))) { 123 | return k; 124 | } 125 | } 126 | return null; 127 | } 128 | async function addFileProgress(download, progress) { 129 | if (activeDownloads.has(download.messages[0].id)) { 130 | activeDownloads.set(download.messages[0].id, activeDownloads.get(download.messages[0].id) + progress); 131 | } else { 132 | activeDownloads.set(download.messages[0].id, progress); 133 | } 134 | Dispatcher.dispatch({ 135 | type: "SLF_UPDATE_PROGRESS" 136 | }); 137 | } 138 | const concatTypedArrays = (a, b) => { 139 | var c = new a.constructor(a.length + b.length); 140 | c.set(a, 0); 141 | c.set(b, a.length); 142 | return c; 143 | }; 144 | const isSetLinear = (set) => { 145 | for (let setIndex = 0; setIndex < set.length; setIndex++) { 146 | if (!set.has(setIndex)) { 147 | return false; 148 | } 149 | } 150 | return true; 151 | }; 152 | async function downloadFiles(download) { 153 | const https = require("https"); 154 | const fs = require("fs"); 155 | const path = require("path"); 156 | const electron = require("electron"); 157 | const vals = new Uint8Array(8); 158 | crypto.getRandomValues(vals); 159 | const id = Buffer.from(vals).toString("hex"); 160 | const tempFolder = path.join(process.env.TMP, `dlfc-download-${id}`); 161 | fs.mkdirSync(tempFolder); 162 | BdApi.showToast("Downloading files...", { type: "info" }); 163 | let promises = []; 164 | for (const url of download.urls) { 165 | let chunkName = url.slice(url.lastIndexOf("/") + 1); 166 | chunkName = chunkName.slice(0, chunkName.indexOf("?")); 167 | const dest = path.join(tempFolder, chunkName); 168 | const file = fs.createWriteStream(dest); 169 | const downloadPromise = new Promise((resolve, reject) => { 170 | https.get(url, (response) => { 171 | response.on("end", () => { 172 | file.close(); 173 | resolve(chunkName); 174 | }); 175 | response.on("data", (data) => { 176 | file.write(Buffer.from(data)); 177 | addFileProgress(download, data.length); 178 | }); 179 | }).on("error", (err) => { 180 | fs.unlink(dest); 181 | reject(err); 182 | }); 183 | }); 184 | promises.push(downloadPromise); 185 | } 186 | let movelocation = await BdApi.UI.openDialog({ 187 | mode: "save", 188 | title: "Save As", 189 | showOverwriteConfirmation: true, 190 | defaultPath: process.env.USERPROFILE + "\\Desktop\\" + download.filename 191 | }); 192 | Promise.all(promises).then((names) => { 193 | let fileBuffers = []; 194 | for (let name of names) { 195 | name = name.slice(0, name.indexOf("?")); 196 | 197 | if (name.endsWith(".dlf")) { 198 | name += "c"; 199 | } 200 | fileBuffers.push(fs.readFileSync(path.join(tempFolder, name), null)); 201 | } 202 | fileBuffers = fileBuffers.filter((buffer) => buffer.length >= 5 && buffer[0] === 223 && buffer[1] === 0); 203 | fileBuffers.sort((left, right) => left[2] - right[2]); 204 | let numChunks = 0; 205 | let chunkSet = /* @__PURE__ */ new Set(); 206 | let outputFile = fs.createWriteStream(path.join(tempFolder, `${download.filename}`)); 207 | for (const buffer of fileBuffers) { 208 | if (buffer[2] >= buffer[3] || numChunks !== 0 && buffer[3] > numChunks) { 209 | BdApi.showToast("Reassembly failed: Some chunks are not part of the same file", { type: "error" }); 210 | outputFile.close(); 211 | return; 212 | } 213 | chunkSet.add(buffer[2]); 214 | numChunks = buffer[3]; 215 | outputFile.write(buffer.subarray(4)); 216 | } 217 | if (!isSetLinear(chunkSet) || chunkSet.size === 0) { 218 | BdApi.showToast("Reassembly failed: Some chunks do not exist", { type: "error" }); 219 | outputFile.close(); 220 | return; 221 | } 222 | outputFile.close(() => { 223 | BdApi.showToast("File reassembled successfully", { type: "success" }); 224 | fs.readdirSync(tempFolder).forEach(file => { 225 | if (file.toString().includes(".dlfc")) { 226 | fs.unlinkSync((tempFolder + "\\" + file.toString())); 227 | } 228 | }); 229 | 230 | /* 231 | * Move the downloaded file to movelocation 232 | * Most web browsers actually do something similar to this when downloading files 233 | * (the download starts immediately and then you just choose where it gets saved) 234 | * so this is actually fairly standard even though I had to do it for a completely different reason 235 | */ 236 | fs.rename((path.join(tempFolder, `${download.filename}`)), movelocation.filePath); 237 | //electron.shell.showItemInFolder(path.join(tempFolder, `${download.filename}`)); 238 | BdApi.showToast(("File downloaded to " + (path.join(tempFolder, `${download.filename}`))), { type: "success", timeout: 5000 }); 239 | // downloadId(download).then((id2) => activeDownloads.delete(id2)); 240 | // activeDownloads.delete(download.messages[0].id); 241 | 242 | Dispatcher.dispatch({ 243 | type: "SLF_UPDATE_PROGRESS" 244 | }); 245 | 246 | fs.rmdirSync(tempFolder, { recursive: true }); 247 | }); 248 | }).catch((err) => { 249 | Logger.error(err); 250 | BdApi.showToast("Failed to download file. Check console for error output.", { type: "error" }); 251 | fs.rmdirSync(tempFolder, { recursive: true }); 252 | }); 253 | } 254 | function FileIcon() { 255 | return React.createElement("img", { 256 | className: "dlfcIcon", 257 | alt: "Attachment file type: SplitLargeFiles Chunk File", 258 | title: "SplitLargeFiles Chunk File", 259 | src: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDcyIDk2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPHBhdGggZD0iTTcyLDI5LjNMNzIsODkuNkM3Miw5MS44NCA3Miw5Mi45NiA3MS41Niw5My44MkM3MS4xOCw5NC41NiA3MC41Niw5NS4xOCA2OS44Miw5NS41NkM2OC45Niw5NiA2Ny44NCw5NiA2NS42LDk2TDYuNCw5NkM0LjE2LDk2IDMuMDQsOTYgMi4xOCw5NS41NkMxLjQ0LDk1LjE4IDAuODIsOTQuNTYgMC40NCw5My44MkMwLDkyLjk2IDAsOTEuODQgMCw4OS42TDAsNi40QzAsNC4xNiAwLDMuMDQgMC40NCwyLjE4QzAuODIsMS40NCAxLjQ0LDAuODIgMi4xOCwwLjQ0QzMuMDQsLTAgNC4xNiwtMCA2LjQsLTBMNDIuNywtMEM0NC42NiwtMCA0NS42NCwtMCA0Ni41NiwwLjIyQzQ3LjA2LDAuMzQgNDcuNTQsMC41IDQ4LDAuNzJMNDgsMTcuNkM0OCwxOS44NCA0OCwyMC45NiA0OC40NCwyMS44MkM0OC44MiwyMi41NiA0OS40NCwyMy4xOCA1MC4xOCwyMy41NkM1MS4wNCwyNCA1Mi4xNiwyNCA1NC40LDI0TDcxLjI4LDI0QzcxLjUsMjQuNDYgNzEuNjYsMjQuOTQgNzEuNzgsMjUuNDRDNzIsMjYuMzYgNzIsMjcuMzQgNzIsMjkuM1oiIHN0eWxlPSJmaWxsOnJnYigyMTEsMjE0LDI1Myk7ZmlsbC1ydWxlOm5vbnplcm87Ii8+CiAgICA8cGF0aCBkPSJNNjguMjYsMjAuMjZDNjkuNjQsMjEuNjQgNzAuMzIsMjIuMzIgNzAuODIsMjMuMTRDNzEsMjMuNDIgNzEuMTQsMjMuNyA3MS4yOCwyNEw1NC40LDI0QzUyLjE2LDI0IDUxLjA0LDI0IDUwLjE4LDIzLjU2QzQ5LjQ0LDIzLjE4IDQ4LjgyLDIyLjU2IDQ4LjQ0LDIxLjgyQzQ4LDIwLjk2IDQ4LDE5Ljg0IDQ4LDE3LjZMNDgsMC43MkM0OC4zLDAuODYgNDguNTgsMSA0OC44NiwxLjE4QzQ5LjY4LDEuNjggNTAuMzYsMi4zNiA1MS43NCwzLjc0TDY4LjI2LDIwLjI2WiIgc3R5bGU9ImZpbGw6cmdiKDE0NywxNTUsMjQ5KTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsNC41LDcpIj4KICAgICAgICA8cmVjdCB4PSIxMSIgeT0iNDEiIHdpZHRoPSI0MSIgaGVpZ2h0PSIyOCIgc3R5bGU9ImZpbGw6cmdiKDE0NywxNTUsMjQ5KTsiLz4KICAgIDwvZz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDAuNSwtMiwyMy41KSI+CiAgICAgICAgPHJlY3QgeD0iMjEiIHk9IjM5IiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIHN0eWxlPSJmaWxsOnJnYigxNDcsMTU1LDI0OSk7Ii8+CiAgICA8L2c+CiAgICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwwLjUsMjIsMjMuNSkiPgogICAgICAgIDxyZWN0IHg9IjIxIiB5PSIzOSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBzdHlsZT0iZmlsbDpyZ2IoMTQ3LDE1NSwyNDkpOyIvPgogICAgPC9nPgo8L3N2Zz4K" 260 | }); 261 | } 262 | /* class AttachmentShim extends React.Component { 263 | constructor(props) { 264 | super(props); 265 | this.child = props.children; 266 | this.attachmentID = props.attachmentData.id; 267 | this.state = { 268 | downloadData: null, 269 | downloadProgress: 0 270 | }; 271 | this.onNewDownload = this.onNewDownload.bind(this); 272 | this.onDownloadProgress = this.onDownloadProgress.bind(this); 273 | } 274 | componentDidMount() { 275 | Dispatcher.subscribe("DLFC_REFRESH_DOWNLOADS", this.onNewDownload); 276 | Dispatcher.subscribe("SLF_UPDATE_PROGRESS", this.onDownloadProgress); 277 | } 278 | componentWillUnmount() { 279 | Dispatcher.unsubscribe("DLFC_REFRESH_DOWNLOADS", this.onNewDownload); 280 | Dispatcher.unsubscribe("SLF_UPDATE_PROGRESS", this.onDownloadProgress); 281 | } 282 | onNewDownload(e) { 283 | if (this.state.downloadData) { 284 | return; 285 | } 286 | for (const download of e.downloads) { 287 | if (download.messages[0].attachmentID === this.attachmentID) { 288 | this.setState({ downloadData: download }); 289 | break; 290 | } 291 | } 292 | } 293 | onDownloadProgress() { 294 | downloadId(this.state.downloadData).then((id) => { 295 | if (this.state.downloadData && activeDownloads.has(id)) { 296 | this.setState({ downloadProgress: activeDownloads.get(id) / this.state.downloadData.totalSize }); 297 | } else { 298 | this.setState({ downloadProgress: 0 }); 299 | } 300 | }); 301 | } 302 | render() { 303 | if (this.state.downloadData) { 304 | return React.createElement(Attachment[getFunctionNameFromString(Attachment, ["renderAdjacentContent"])], { 305 | filename: this.state.downloadData.filename + (this.state.downloadProgress > 0 ? ` - Downloading ${Math.round(this.state.downloadProgress * 100)}%` : ""), 306 | url: null, 307 | dlfc: true, 308 | size: this.state.downloadData.totalSize, 309 | onClick: () => { 310 | downloadFiles(this.state.downloadData); 311 | } 312 | }, []); 313 | } else { 314 | return this.child; 315 | } 316 | } 317 | } */ 318 | const defaultSettingsData = { 319 | deletionDelay: 6, 320 | fileSplitSize: 26214400 321 | }; 322 | let settings = null; 323 | const reloadSettings = () => { 324 | settings = PluginUtilities.loadSettings("SplitLargeFiles", defaultSettingsData); 325 | }; 326 | const validActionDelays = [6, 7, 8, 9, 10, 11, 12]; 327 | class SplitLargeFiles extends Plugin { 328 | onStart() { 329 | BdApi.injectCSS("SplitLargeFiles", ` 330 | .dlfcIcon { 331 | width: 30px; 332 | height: 40px; 333 | margin-right: 8px; 334 | } 335 | 336 | .slfClickable { 337 | cursor: pointer; 338 | background: none; 339 | } 340 | .slfIcon { 341 | color: var(--interactive-normal); 342 | } 343 | .slfClickable:hover .slfIcon { 344 | color: var(--interactive-hover); 345 | } 346 | `); 347 | 348 | //explicitly trigger plugin update 349 | PluginUpdater.checkForUpdate(this.getName(), this.getVersion(), this._config.info.github_raw); 350 | 351 | //Disable 500MB (basically a safety check) limit. 352 | const RealMaxFileSizeMod = Webpack.getByKeys("B", "Fm", "Lc", "zz"); 353 | RealMaxFileSizeMod.zz = Number.MAX_SAFE_INTEGER; 354 | 355 | Patcher.instead(uploadinator, "d", (_, e) => { 356 | try { 357 | //console.log(e); 358 | var E = Array.from(e[0]).map((function (e) { 359 | return { 360 | file: e, 361 | platform: 1 362 | } 363 | })) 364 | MessageAttachmentManager.addFiles({ 365 | files: E, 366 | channelId: e[1].id, 367 | showLargeMessageDialog: false, 368 | draftType: e[2] 369 | }) 370 | } catch (err) { 371 | console.error(err); 372 | BdApi.UI.showToast("An error occurred.", { type: "error" }); 373 | } 374 | }); 375 | 376 | reloadSettings(); 377 | this.incompleteDownloads = []; 378 | Patcher.instead(MessageAttachmentManager, "addFiles", (_, [{ files, channelId }], original) => { 379 | let oversizedFiles = [], regularFiles = []; 380 | 381 | for(let i = 0; i < files.length; i++){ 382 | const fileContainer = files[i]; 383 | if(fileContainer.file.clip != undefined){ 384 | continue; 385 | } 386 | 387 | if(fileContainer.file.type.startsWith("video/") && BdApi.Plugins.isEnabled("YABDP4Nitro")){ 388 | 389 | const YABDP4NitroSettings = BdApi.getData("YABDP4Nitro", "settings"); 390 | if(YABDP4NitroSettings.useClipBypass && fileContainer.file.size <= 104857600){ 391 | continue; 392 | } 393 | 394 | } 395 | const [numChunks, numChunksWithHeaders] = this.calcNumChunks(fileContainer.file); 396 | if (numChunks === 1) { 397 | regularFiles.push(fileContainer); 398 | continue; 399 | } else if (numChunksWithHeaders > 255) { 400 | BdApi.showToast("File size exceeds max chunk count of 255.", { type: "error" }); 401 | return; 402 | } 403 | oversizedFiles.push(fileContainer); 404 | } 405 | if (oversizedFiles.length === 0) { 406 | original({ 407 | files: regularFiles, 408 | channelId, 409 | showLargeMessageDialog: false, 410 | draftType: 0 411 | }); 412 | } else { 413 | this.splitLargeFiles(oversizedFiles).then((fileArrayArray) => { 414 | if (fileArrayArray.length === 0) { 415 | return; 416 | } 417 | MessageAttachmentManager.clearAll(this.getCurrentChannel()?.id, 0); 418 | const fileArray = regularFiles.concat.apply([], fileArrayArray); 419 | if (queuedUploads.has(channelId)) { 420 | queuedUploads.get(channelId).push(fileArray); 421 | } else { 422 | queuedUploads.set(channelId, fileArray); 423 | } 424 | original({ 425 | files: queuedUploads.get(channelId).splice(0, BATCH_SIZE), 426 | channelId, 427 | showLargeMessageDialog: false, 428 | draftType: 0 429 | }); 430 | }); 431 | } 432 | 433 | }); 434 | 435 | Patcher.after(MessageAccessories.prototype, "renderAttachments", (_, [arg], ret) => { 436 | if (!ret || arg.attachments.length === 0 || !arg.attachments[0].filename.endsWith(".dlfc")) { 437 | return; 438 | } 439 | /*const component = ret.props.children; 440 | ret.props.children = React.createElement(AttachmentShim, { 441 | attachmentData: arg.attachments[0] 442 | }, component); */ 443 | }); 444 | 445 | Patcher.after(downloadButtonMod, "Z", (_, args, ret) => { 446 | let registeredDownload = registeredDownloads.find((element) => element.urls.find((url) => url === args[0].href)); 447 | 448 | if(registeredDownload){ 449 | //disable default download 450 | ret.props.href = undefined; 451 | 452 | ret.props.onClick = (e) => { 453 | e.stopPropagation(); 454 | downloadFiles(registeredDownload); 455 | } 456 | } 457 | 458 | }); 459 | 460 | Dispatcher.subscribe("SLF_UPDATE_PROGRESS", this.updateProgress); 461 | 462 | Patcher.after(Attachment, getFunctionNameFromString(Attachment, ["renderAdjacentContent"]), (_, args, ret) => { 463 | ret.props.children[0].props.children[1].props.onClick = args[0].onClick; //???? 464 | 465 | if (args[0].fileName.endsWith(".dlfc")) { 466 | //File Icon 467 | ret.props.children[0].props.children[0] = /* @__PURE__ */ React.createElement(FileIcon, null); 468 | 469 | //For readability 470 | let fileAnchor = ret.props.children[0].props.children[1].props.children[0].props.children.props; 471 | 472 | //also for readability. (me when no pass by ref keyword. those who know know) 473 | let fileSize = ret.props.children[0].props.children[1].props.children[1].props; 474 | 475 | //dont show split part number or DLFC file extension (the download doesnt get registered until you hover it) 476 | fileAnchor.children = fileAnchor.children.replace(/^[0-99]-[0-99]_/, "").replace(/.dlfc$/, ""); 477 | 478 | let registeredDownload = registeredDownloads.find((element) => element.messages.find((message) => message.id === args[0].message.id)); 479 | if(registeredDownload) { 480 | fileAnchor.children = registeredDownload.filename.replaceAll("_", " "); 481 | fileSize.children = (registeredDownload.totalSize / 1024 / 1024).toFixed(2) + " MB • DLFC File"; 482 | 483 | //disable default download 484 | fileAnchor.href = undefined; 485 | 486 | //trigger SLF download on click 487 | fileAnchor.onClick = (e) => { 488 | e.stopPropagation(); 489 | downloadFiles(registeredDownload); 490 | } 491 | } 492 | } 493 | }); 494 | this.messageCreate = (e) => { 495 | if (e.channelId === this.getCurrentChannel()?.id) { 496 | if (queuedUploads.has(e.channelId) && e.message.author.id === UserStore.getCurrentUser().id) { 497 | MessageAttachmentManager.addFiles({ 498 | files: queuedUploads.get(e.channelId).splice(0, BATCH_SIZE), 499 | channelId: e.channelId 500 | }); 501 | if (queuedUploads.get(e.channelId).length === 0) { 502 | queuedUploads.delete(e.channelId); 503 | } 504 | } 505 | setTimeout(() => this.findAvailableDownloads(), 500); 506 | } 507 | }; 508 | Dispatcher.subscribe("MESSAGE_CREATE", this.messageCreate); 509 | this.channelSelect = (_) => { 510 | setTimeout(() => this.findAvailableDownloads(), 200); 511 | }; 512 | Dispatcher.subscribe("CHANNEL_SELECT", this.channelSelect); 513 | this.loadMessagesSuccess = (_) => { 514 | this.findAvailableDownloads(); 515 | }; 516 | Dispatcher.subscribe("LOAD_MESSAGES_SUCCESS", this.loadMessagesSuccess); 517 | this.messageContextMenuUnpatch = ContextMenu.patch("message", (tree, props) => { 518 | const incomplete = this.incompleteDownloads.find((download) => download.messages.some((message) => message.id === props.message.id)); 519 | const registered = registeredDownloads.find((download) => download.messages.some((msg) => msg.id === props.message.id)); 520 | if (!(incomplete || registered)) { 521 | return; 522 | } 523 | tree.props.children[2].props.children.push(ContextMenu.buildItem({ type: "separator" }), ContextMenu.buildItem({ 524 | label: "Refresh Downloadables", action: () => { 525 | this.findAvailableDownloads(); 526 | BdApi.showToast("Downloadables refreshed", { type: "success" }); 527 | } 528 | }), ContextMenu.buildItem({ 529 | label: "Copy Download Links", action: () => { 530 | const urls = this.getFileURLsFromMessageId(props.message.id); 531 | if (!urls) { 532 | BdApi.showToast("Failed to Copy Links", { type: "error" }); 533 | } 534 | DiscordNative.clipboard.copy(urls.join(" ")); 535 | } 536 | })); 537 | if (incomplete && this.canDeleteDownload(incomplete)) { 538 | tree.props.children[2].props.children.push(ContextMenu.buildItem({ 539 | label: "Delete Download Fragments", danger: true, action: () => { 540 | this.deleteDownload(incomplete); 541 | this.findAvailableDownloads(); 542 | } 543 | })); 544 | } 545 | if (!incomplete) { 546 | tree.props.children[2].props.children.push(ContextMenu.buildItem({ 547 | label: "Download Large File", action: () => { 548 | downloadFiles(registered); 549 | } 550 | })); 551 | } 552 | }); 553 | this.channelContextMenuUnpatch = ContextMenu.patch("channel-context", (tree, _) => { 554 | tree.props.children[2].props.children.push(ContextMenu.buildItem({ type: "separator" }), ContextMenu.buildItem({ 555 | label: "Refresh Downloadables", action: () => { 556 | this.findAvailableDownloads(); 557 | BdApi.showToast("Downloadables refreshed", { type: "success" }); 558 | } 559 | })); 560 | }); 561 | this.userContextMenuUnpatch = ContextMenu.patch("user-context", (tree, _) => { 562 | tree.props.children[2].props.children.push(ContextMenu.buildItem({ type: "separator" }), ContextMenu.buildItem({ 563 | label: "Refresh Downloadables", action: () => { 564 | this.findAvailableDownloads(); 565 | BdApi.showToast("Downloadables refreshed", { type: "success" }); 566 | } 567 | })); 568 | }); 569 | this.messageDelete = (e) => { 570 | if (e.channelId !== this.getCurrentChannel()?.id) { 571 | return; 572 | } 573 | const download = registeredDownloads.find((element) => element.messages.find((message) => message.id === e.id)); 574 | if (download && this.canDeleteDownload(download)) { 575 | this.deleteDownload(download, e.id); 576 | } 577 | this.findAvailableDownloads(); 578 | }; 579 | Dispatcher.subscribe("MESSAGE_DELETE", this.messageDelete); 580 | Patcher.before(Webpack.getByKeys("Url", "resolve", "resolveObject"), "parse", (_, a) => { 581 | //Fix crashing issue 582 | a[0] = String(a[0]); 583 | }); 584 | //BdApi.showToast("Waiting for BetterDiscord to load before refreshing downloadables...", { type: "info" }); 585 | setTimeout(() => { 586 | //BdApi.showToast("Downloadables refreshed", { type: "success" }); 587 | this.findAvailableDownloads(); 588 | }, 100); 589 | } 590 | updateProgress(){ 591 | activeDownloads.keys().forEach(key => { 592 | let registeredDownload = registeredDownloads.find((element) => element.messages.find((message) => message.id === key)); 593 | if(document.getElementById("message-accessories-" + key)){ 594 | try{ 595 | let element = document.getElementById("message-accessories-" + key); 596 | let attachment = element.children[0].children[0].children[0].children[0].children[1].children[1]; 597 | 598 | attachment.innerHTML = (registeredDownload.totalSize / 1024 / 1024).toFixed(2) + " MB • DLFC File" + " • Downloading " + ( activeDownloads.get(key) / registeredDownload.totalSize * 100 ).toFixed(2) + "%"; 599 | 600 | if(attachment.innerHTML.includes("100.00%")){ 601 | attachment.innerHTML = attachment.innerHTML.replace("Downloading 100.00%", "Download Complete"); 602 | } 603 | }catch(e){ 604 | console.error(e); 605 | } 606 | } 607 | if(activeDownloads.get(key) == registeredDownload.totalSize){ 608 | activeDownloads.delete(key); 609 | } 610 | }); 611 | } 612 | getFileURLsFromMessageId(messageId) { 613 | const download = registeredDownloads.find((download2) => download2.messages.some((msg) => msg.id === messageId)); 614 | return download?.urls; 615 | } 616 | splitLargeFiles(fileContainers) { 617 | BdApi.showToast("Generating file chunks...", { type: "info" }); 618 | let promises = []; 619 | for (const fileContainer of fileContainers) { 620 | const file = fileContainer.file; 621 | promises.push(new Promise((res, rej) => { 622 | file.arrayBuffer().then((buffer) => { 623 | const fileBytes = new Uint8Array(buffer); 624 | const [numChunks, numChunksWithHeaders] = this.calcNumChunks(file); 625 | const fileList = []; 626 | for (let chunk = 0; chunk < numChunksWithHeaders; chunk++) { 627 | const baseOffset = chunk * (this.maxFileUploadSize() - 4); 628 | const headerBytes = new Uint8Array(4); 629 | headerBytes.set([223, 0, chunk & 255, numChunks & 255]); 630 | const bytesToWrite = fileBytes.slice(baseOffset, baseOffset + this.maxFileUploadSize() - 4); 631 | fileList.push({ 632 | file: new File([concatTypedArrays(headerBytes, bytesToWrite)], `${chunk}-${numChunks - 1}_${file.name}.dlfc`), 633 | platform: fileContainer.platform 634 | }); 635 | } 636 | res(fileList); 637 | }).catch((err) => { 638 | Logger.error(err); 639 | BdApi.showToast("Failed to read file, please try again later.", { type: "error" }); 640 | rej(); 641 | }); 642 | })); 643 | } 644 | return Promise.all(promises); 645 | } 646 | calcNumChunks(file) { 647 | return [Math.ceil(file.size / this.maxFileUploadSize()), Math.ceil(file.size / (this.maxFileUploadSize() - 4))]; 648 | } 649 | getSettingsPanel() { 650 | reloadSettings(); 651 | const settingPanel = new SettingPanel(() => { 652 | PluginUtilities.saveSettings("SplitLargeFiles", settings); 653 | }, new Slider("Chunk File Deletion Delay", "How long to wait (in seconds) before deleting each sequential message of a chunk file. If you plan on deleting VERY large files you should set this value high to avoid API spam.", validActionDelays[0], validActionDelays[validActionDelays.length - 1], settings.deletionDelay, (newVal) => { 654 | if (newVal > validActionDelays[validActionDelays.length - 1] || newVal < validActionDelays[0]) { 655 | newVal = validActionDelays[0]; 656 | } 657 | settings.deletionDelay = newVal; 658 | }, { markers: validActionDelays, stickToMarkers: true }), 659 | new Settings.Dropdown("File Split Size", "Changes the size of the split files.", settings.fileSplitSize, [ 660 | { label: "8MB", value: 8387608 }, 661 | { label: "10MB", value: 10485760 }, 662 | { label: "25MB", value: 26214400 }, 663 | { label: "50MB", value: 52428800 }, 664 | { label: "100MB", value: 104857600 }, 665 | { label: "500MB", value: 524288000 }], value => settings.fileSplitSize = value, { searchable: true } 666 | ) 667 | ).getElement(); 668 | settingPanel.style.paddingBottom = "75px" 669 | return settingPanel 670 | } 671 | maxFileUploadSize() { 672 | return settings.fileSplitSize 673 | } 674 | findAvailableDownloads() { 675 | registeredDownloads = []; 676 | this.incompleteDownloads = []; 677 | for (const message of this.getChannelMessages(this.getCurrentChannel()?.id) ?? []) { 678 | if (message.noDLFC) { 679 | continue; 680 | } 681 | let foundDLFCAttachment = false; 682 | for (const attachment of message.attachments) { 683 | if (isNaN(parseInt(attachment.filename)) || !attachment.filename.endsWith(".dlfc")) { 684 | continue; 685 | } 686 | foundDLFCAttachment = true; 687 | const realName = this.extractRealFileName(attachment.filename); 688 | const existingEntry = registeredDownloads.find((element) => element.filename === realName && !element.foundParts.has(parseInt(attachment.filename))); 689 | if (existingEntry) { 690 | existingEntry.urls.push(attachment.url); 691 | existingEntry.messages.push({ id: message.id, date: message.timestamp, attachmentID: attachment.id }); 692 | existingEntry.foundParts.add(parseInt(attachment.filename)); 693 | existingEntry.totalSize += attachment.size; 694 | } else { 695 | registeredDownloads.unshift({ 696 | filename: realName, 697 | owner: message.author.id, 698 | urls: [attachment.url], 699 | messages: [{ id: message.id, date: message.timestamp, attachmentID: attachment.id }], 700 | foundParts: /* @__PURE__ */ new Set([parseInt(attachment.filename)]), 701 | totalSize: attachment.size 702 | }); 703 | } 704 | } 705 | if (!foundDLFCAttachment) { 706 | message.noDLFC = true; 707 | } 708 | } 709 | registeredDownloads = registeredDownloads.filter((value, _, __) => { 710 | const chunkSet = /* @__PURE__ */ new Set(); 711 | let highestChunk = 0; 712 | for (const url of value.urls) { 713 | const filename = url.slice(url.lastIndexOf("/") + 1); 714 | const fileNumber = parseInt(filename); 715 | const fileTotal = parseInt(filename.slice(filename.indexOf("-") + 1)); 716 | chunkSet.add(fileNumber); 717 | if (highestChunk === 0) { 718 | highestChunk = fileTotal; 719 | } else if (highestChunk !== fileTotal) { 720 | this.incompleteDownloads.push(value); 721 | return false; 722 | } 723 | } 724 | const result = isSetLinear(chunkSet) && highestChunk + 1 === chunkSet.size; 725 | if (!result) { 726 | this.incompleteDownloads.push(value); 727 | } 728 | return result; 729 | }); 730 | registeredDownloads.forEach((download) => { 731 | download.messages.sort((first, second) => first.date - second.date); 732 | for (let messageIndex = 1; messageIndex < download.messages.length; messageIndex++) { 733 | if (download.messages[messageIndex].id === download.messages[0].id) { 734 | this.setAttachmentVisibility(download.messages[0].id, messageIndex, false); 735 | } else { 736 | this.setMessageVisibility(download.messages[messageIndex].id, false); 737 | } 738 | } 739 | }); 740 | if (registeredDownloads.length > 0) { 741 | Dispatcher.dispatch({ 742 | type: "DLFC_REFRESH_DOWNLOADS", 743 | downloads: registeredDownloads 744 | }); 745 | } 746 | } 747 | extractRealFileName(name) { 748 | return name.slice(name.indexOf("_") + 1, name.length - 5); 749 | } 750 | setMessageVisibility(id, visible) { 751 | const element = DOMTools.query('#chat-messages-' + BdApi.findModuleByProps("getLastChannelFollowingDestination").getChannelId() + '-' + id); 752 | if (element) { 753 | if (visible) { 754 | element.removeAttribute("hidden"); 755 | } else { 756 | element.setAttribute("hidden", ""); 757 | } 758 | } else { 759 | Logger.warn('Unable to find DOM object with selector #chat-messages-' + BdApi.findModuleByProps("getLastChannelFollowingDestination").getChannelId() + '-' + id); 760 | } 761 | } 762 | setAttachmentVisibility(id, index, visible) { 763 | const parent = DOMTools.query('#message-accessories-' + id); 764 | let element = parent?.lastChild?.children[index]; 765 | 766 | if (element) { 767 | if (visible) { 768 | parent.removeAttribute("style"); 769 | element.removeAttribute("style"); 770 | } else { 771 | parent.setAttribute("style", "grid-row-gap: 0;"); 772 | element.setAttribute("style", "display: none;"); 773 | } 774 | } else { 775 | Logger.error(`Unable to find child DOM object at index ${index} with parent selector #message-accessories-${id}`); 776 | } 777 | } 778 | deleteDownload(download, excludeMessage = null) { 779 | if (download.messages.map((msg) => msg.id).every((id, i, arr) => id === arr[0])) { 780 | return; 781 | } 782 | BdApi.showToast(`Deleting chunks (1 chunk/${settings.deletionDelay} seconds)`, { type: "success" }); 783 | let delayCount = 1; 784 | for (const message of this.getChannelMessages(this.getCurrentChannel().id)) { 785 | const downloadMessage = download.messages.find((dMessage) => dMessage.id === message.id); 786 | if (downloadMessage) { 787 | if (excludeMessage && message.id === excludeMessage.id) { 788 | continue; 789 | } 790 | this.setMessageVisibility(message.id, true); 791 | const downloadMessageIndex = download.messages.indexOf(downloadMessage); 792 | download.messages.splice(downloadMessageIndex, 1); 793 | setTimeout(() => this.deleteMessage(message), delayCount * settings.deletionDelay * 1e3); 794 | delayCount += 1; 795 | } 796 | } 797 | } 798 | canDeleteDownload(download) { 799 | return download.owner === UserStore.getCurrentUser().id || this.canManageMessages(); 800 | } 801 | getCurrentChannel() { 802 | return ChannelStore.getChannel(SelectedChannelStore.getChannelId()) ?? null; 803 | } 804 | getChannelMessages(channelId) { 805 | if (!channelId) { 806 | return null; 807 | } 808 | return MessageStore.getMessages(channelId)._array; 809 | } 810 | canManageMessages() { 811 | const currentChannel = this.getCurrentChannel(); 812 | if (!currentChannel) { 813 | return false; 814 | } 815 | return !!(Permissions.computePermissions(currentChannel) & 0x2000n); 816 | } 817 | deleteMessage(message) { 818 | try { 819 | MessageActions.deleteMessage(message.channel_id, message.id, false); 820 | } catch (err) { 821 | console.error(err); 822 | } 823 | } 824 | onStop() { 825 | Patcher.unpatchAll(); 826 | if (this.messageContextMenuUnpatch) 827 | this.messageContextMenuUnpatch(); 828 | if (this.channelContextMenuUnpatch) 829 | this.channelContextMenuUnpatch(); 830 | if (this.userContextMenuUnpatch) 831 | this.userContextMenuUnpatch(); 832 | Dispatcher.unsubscribe("MESSAGE_CREATE", this.messageCreate); 833 | Dispatcher.unsubscribe("CHANNEL_SELECT", this.channelSelect); 834 | Dispatcher.unsubscribe("MESSAGE_DELETE", this.messageDelete); 835 | Dispatcher.unsubscribe("LOAD_MESSAGES_SUCCESS", this.loadMessagesSuccess); 836 | Dispatcher.unsubscribe("SLF_UPDATE_PROGRESS", this.updateProgress); 837 | BdApi.clearCSS("SplitLargeFiles"); 838 | } 839 | } 840 | ; 841 | return SplitLargeFiles; 842 | }; 843 | return plugin(Plugin, Api); 844 | })(global.ZeresPluginLibrary.buildPlugin(config)); 845 | /*@end@*/ -------------------------------------------------------------------------------- /images/chunks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riolubruh/SplitLargeFiles/2f7b3e200268c1db619f3de82cb828f9d4e1256f/images/chunks.png -------------------------------------------------------------------------------- /images/visualReassembly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riolubruh/SplitLargeFiles/2f7b3e200268c1db619f3de82cb828f9d4e1256f/images/visualReassembly.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splitlargefiles", 3 | "version": "1.8.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "splitlargefiles", 9 | "version": "1.8.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "zerespluginlibrary": "^2.0.6" 13 | } 14 | }, 15 | "node_modules/zerespluginlibrary": { 16 | "version": "2.0.6", 17 | "resolved": "https://registry.npmjs.org/zerespluginlibrary/-/zerespluginlibrary-2.0.6.tgz", 18 | "integrity": "sha512-xFJGAKWWHVhCPCX6Qq1tsVJzOzTHRF7xnz2bu+3DKGaXsNCP7hPxy0LJ3mXtHLeSlH3v9ARZ0kzDs4cj5bCVnw==", 19 | "bin": { 20 | "zpl": "bin/zpl.js" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "zerespluginlibrary": { 26 | "version": "2.0.6", 27 | "resolved": "https://registry.npmjs.org/zerespluginlibrary/-/zerespluginlibrary-2.0.6.tgz", 28 | "integrity": "sha512-xFJGAKWWHVhCPCX6Qq1tsVJzOzTHRF7xnz2bu+3DKGaXsNCP7hPxy0LJ3mXtHLeSlH3v9ARZ0kzDs4cj5bCVnw==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splitlargefiles", 3 | "version": "1.8.0", 4 | "description": "Splits Discord files", 5 | "main": "index.jsx", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "esbuild src/SplitLargeFiles/index.jsx --bundle --outfile=src/SplitLargeFiles/bundled.js --platform=node && zpl build SplitLargeFiles", 9 | "init": "zpl init SplitLargeFiles" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ImTheSquid/SplitLargeFiles.git" 14 | }, 15 | "author": "Jack Hogan", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/ImTheSquid/SplitLargeFiles/issues" 19 | }, 20 | "homepage": "https://github.com/ImTheSquid/SplitLargeFiles#readme", 21 | "dependencies": { 22 | "zerespluginlibrary": "^2.0.6" 23 | }, 24 | "zplConfig": { 25 | "base": "./src", 26 | "out": "./release", 27 | "copyToBD": true, 28 | "addInstallScript": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------