Merge pull request #253 from JsSucks/package-installer

Package installer
This commit is contained in:
Alexei Stukov 2018-08-31 08:36:45 +03:00 committed by GitHub
commit 990453cdad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1057 additions and 513 deletions

View File

@ -36,5 +36,5 @@ export default new class TwentyFourHour extends BuiltinModule {
if (matched[3] === 'AM') return returnValue.replace(matched[0], `${matched[1] === '12' ? '00' : matched[1].padStart(2, '0')}:${matched[2]}`) if (matched[3] === 'AM') return returnValue.replace(matched[0], `${matched[1] === '12' ? '00' : matched[1].padStart(2, '0')}:${matched[2]}`)
return returnValue.replace(matched[0], `${parseInt(matched[1]) + 12}:${matched[2]}`) return returnValue.replace(matched[0], `${parseInt(matched[1]) + 12}:${matched[2]}`)
} }
} }

View File

@ -222,6 +222,13 @@
{ {
"id": "default", "id": "default",
"settings": [ "settings": [
{
"id": "unsafe-content",
"type": "bool",
"text": "Allow unverified content",
"hint": "Allow loading unverified plugins/themes",
"value": "false"
},
{ {
"id": "tracking-protection", "id": "tracking-protection",
"type": "bool", "type": "bool",

View File

@ -8,14 +8,18 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import asar from 'asar';
import path, { dirname } from 'path';
import rimraf from 'rimraf';
import Content from './content'; import Content from './content';
import Globals from './globals'; import Globals from './globals';
import Database from './database'; import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common'; import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingsSet, ErrorEvent } from 'structs'; import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui'; import { Modals } from 'ui';
import path from 'path';
import Combokeys from 'combokeys'; import Combokeys from 'combokeys';
import Settings from './settings';
/** /**
* Base class for managing external content * Base class for managing external content
@ -77,12 +81,20 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath); const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) { for (const dir of directories) {
try { const packed = dir.endsWith('.bd');
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; } if (!packed) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
}
try { try {
await this.preloadContent(dir); if (packed) {
await this.preloadPackedContent(dir);
} else {
await this.preloadContent(dir);
}
} catch (err) { } catch (err) {
this.errors.push(new ErrorEvent({ this.errors.push(new ErrorEvent({
module: this.moduleName, module: this.moduleName,
@ -120,6 +132,8 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath); const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) { for (const dir of directories) {
const packed = dir.endsWith('.bd');
// If content is already loaded this should resolve // If content is already loaded this should resolve
if (this.getContentByDirName(dir)) continue; if (this.getContentByDirName(dir)) continue;
@ -173,6 +187,30 @@ export default class {
} }
} }
static async preloadPackedContent(pkg, reload = false, index) {
try {
const packagePath = path.join(this.contentPath, pkg);
const packageName = pkg.replace('.bd', '');
await FileUtils.fileExists(packagePath);
const config = JSON.parse(asar.extractFile(packagePath, 'config.json').toString());
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({
config,
contentPath: unpackedPath,
packagePath: packagePath,
pkg,
packageName,
packed: true
}, reload, index);
} catch (err) {
throw err;
}
}
/** /**
* Common loading procedure for loading content before passing it to the actual loader * Common loading procedure for loading content before passing it to the actual loader
* @param {any} dirName Base directory for content * @param {any} dirName Base directory for content
@ -181,7 +219,15 @@ export default class {
*/ */
static async preloadContent(dirName, reload = false, index) { static async preloadContent(dirName, reload = false, index) {
try { try {
const contentPath = path.join(this.contentPath, dirName); const unsafeAllowed = Settings.getSetting('security', 'default', 'unsafe-content').value;
const packed = typeof dirName === 'object' && dirName.packed;
// Block any unpacked content as they can't be verified
if (!packed && !unsafeAllowed) {
throw 'Blocked unsafe content';
}
const contentPath = packed ? dirName.contentPath : path.join(this.contentPath, dirName);
await FileUtils.directoryExists(contentPath); await FileUtils.directoryExists(contentPath);
@ -189,7 +235,7 @@ export default class {
throw { 'message': `Attempted to load already loaded user content: ${path}` }; throw { 'message': `Attempted to load already loaded user content: ${path}` };
const configPath = path.resolve(contentPath, 'config.json'); const configPath = path.resolve(contentPath, 'config.json');
const readConfig = await FileUtils.readJsonFromFile(configPath); const readConfig = packed ? dirName.config : await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js'); const mainPath = path.join(contentPath, readConfig.main || 'index.js');
const defaultConfig = new SettingsSet({ const defaultConfig = new SettingsSet({
@ -213,7 +259,7 @@ export default class {
} }
} catch (err) { } catch (err) {
// We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config // We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config
Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`, err]); Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${packed ? dirName.pkg : dirName}`, err]);
} }
userConfig.config = defaultConfig.clone({ settings: userConfig.config }); userConfig.config = defaultConfig.clone({ settings: userConfig.config });
@ -243,16 +289,22 @@ export default class {
mainPath mainPath
}; };
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport); const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport, packed ? dirName : false);
if (!content) return undefined; if (!content) return undefined;
if (!reload && this.getContentById(content.id)) if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`}; throw { message: `A ${this.contentType} with the ID ${content.id} already exists.` };
if (reload) this.localContent.splice(index, 1, content); if (reload) this.localContent.splice(index, 1, content);
else this.localContent.push(content); else this.localContent.push(content);
return content; return content;
} catch (err) { } catch (err) {
throw err; throw err;
} finally {
if (typeof dirName === 'object' && dirName.packed) {
rimraf(dirName.contentPath, err => {
if (err) Logger.err(err);
});
}
} }
} }
@ -309,14 +361,7 @@ export default class {
if (this.unloadContentHook) this.unloadContentHook(content); if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) { if (reload) return content.packed ? this.preloadPackedContent(content.packed.pkg, true, index) : this.preloadContent(content.dirName, true, index);
const newcontent = await this.preloadContent(content.dirName, true, index);
if (newcontent.enabled) {
newcontent.userConfig.enabled = false;
newcontent.start(false);
}
return newcontent;
}
this.localContent.splice(index, 1); this.localContent.splice(index, 1);
} catch (err) { } catch (err) {

View File

@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import path from 'path';
import sparkplug from 'sparkplug'; import sparkplug from 'sparkplug';
import { ClientIPC } from 'common'; import { ClientIPC } from 'common';
import Module from './module'; import Module from './module';
@ -35,6 +36,10 @@ export default new class extends Module {
async first() { async first() {
const config = await ClientIPC.send('getConfig'); const config = await ClientIPC.send('getConfig');
config.paths.push({
id: 'tmp',
path: path.join(config.paths.find(p => p.id === 'base').path, 'tmp')
});
this.setState({ config }); this.setState({ config });
// This is for Discord to stop error reporting :3 // This is for Discord to stop error reporting :3

View File

@ -27,3 +27,4 @@ export { default as Connectivity } from './connectivity';
export { default as Security } from './security'; export { default as Security } from './security';
export { default as Cache } from './cache'; export { default as Cache } from './cache';
export { default as Reflection } from './reflection/index'; export { default as Reflection } from './reflection/index';
export { default as PackageInstaller } from './packageinstaller';

View File

@ -0,0 +1,172 @@
import EventListener from './eventlistener';
import asar from 'asar';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import { request } from 'vendor';
import { Modals } from 'ui';
import { Utils } from 'common';
import PluginManager from './pluginmanager';
import Globals from './globals';
import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection';
import DiscordApi from './discordapi';
export default class PackageInstaller {
/**
* Handler for drag and drop package install
* @param {String} filePath Path to local file
* @param {String} channelId Current channel id
*/
static async dragAndDropHandler(filePath, channelId) {
try {
const config = JSON.parse(asar.extractFile(filePath, 'config.json').toString());
const { info, main } = config;
let icon = null;
if (info.icon && info.icon_type) {
const extractIcon = asar.extractFile(filePath, info.icon);
icon = `data:${info.icon_type};base64,${Utils.arrayBufferToBase64(extractIcon)}`;
}
const isPlugin = info.type && info.type === 'plugin' || main.endsWith('.js');
// Show install modal
const modalResult = await Modals.installModal(isPlugin ? 'plugin' : 'theme', config, filePath, icon).promise;
if (modalResult === 0) {
// Upload it instead
}
} catch (err) {
console.log(err);
}
}
/**
* Hash and verify a package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} id Package id
*/
static async verifyPackage(bytesOrPath, id) {
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
// Temporary hash to simulate response from server
const tempVerified = ['2e3532ee366816adc37b0f478bfef35e03f96e7aeee9b115f5918ef6a4e94de8', '06a2eb4e37b926354ab80cd83207db67e544c932e9beddce545967a21f8db5aa'];
const hashBytes = Security.hash('sha256', bytes, 'hex');
return tempVerified.includes(hashBytes);
}
// TODO lots of stuff
/**
* Installs or updates defined package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} name Package name
* @param {Boolean} update Does an older version already exist
*/
static async installPackage(bytesOrPath, id, update = false) {
let outputPath = null;
try {
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
const outputName = `${id}.bd`;
outputPath = path.join(Globals.getPath('plugins'), outputName);
fs.writeFileSync(outputPath, bytes);
if (!update) return PluginManager.preloadPackedContent(outputName);
const oldContent = PluginManager.getPluginById(id);
if (update && oldContent.packed && oldContent.packed.packageName !== id) {
await oldContent.unload(true);
rimraf(oldContent.packed.packagePath, err => {
if(err) console.log(err);
});
return PluginManager.preloadPackedContent(outputName);
}
if (update && !oldContent.packed) {
await oldContent.unload(true);
rimraf(oldContent.contentPath, err => {
if (err) console.log(err);
});
return PluginManager.preloadPackedContent(outputName);
}
return PluginManager.reloadContent(oldContent);
} catch (err) {
if (outputPath) {
rimraf(outputPath, err => {
if (err) console.log(err);
});
}
throw err;
}
}
/**
* Install package from remote location. Only github/bdapi is supoorted.
* @param {String} remoteLocation Remote resource location
*/
static async installRemotePackage(remoteLocation) {
try {
const { hostname } = Object.assign(document.createElement('a'), { href: remoteLocation });
if (hostname !== 'api.github.com' && hostname !== 'secretbdapi') throw 'Invalid host!';
const options = {
uri: remoteLocation,
headers: {
'User-Agent': 'BetterDiscordClient',
'Accept': 'application/octet-stream'
}
};
const response = await request.get(options);
const outputPath = path.join(Globals.getPath('tmp'), Security.hash('sha256', response, 'hex'));
fs.writeFileSync(outputPath, response);
await this.dragAndDropHandler(outputPath);
rimraf(outputPath, err => {
if (err) console.log(err);
});
} catch (err) {
throw err;
}
}
/**
* Patches Discord upload area for .bd files
*/
static async uploadAreaPatch() {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', { selector });
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = function (e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
PackageInstaller.dragAndDropHandler(e.dataTransfer.files[0].path, DiscordApi.currentChannel.id);
};
// Remove their handler, add ours, then read theirs to give ours priority to stop theirs when we get a .bd file.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
this.unpatchUploadArea = function () {
reflect.element.removeEventListener('drop', callback);
};
}
}

View File

@ -74,7 +74,7 @@ export default class extends ContentManager {
static get refreshPlugins() { return this.refreshContent } static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin } static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport) { static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport, packed = false) {
if (permissions && permissions.length > 0) { if (permissions && permissions.length > 0) {
for (const perm of permissions) { for (const perm of permissions) {
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`); Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
@ -112,11 +112,18 @@ export default class extends ContentManager {
configs, info, main, configs, info, main,
paths: { paths: {
contentPath: paths.contentPath, contentPath: paths.contentPath,
dirName: paths.dirName, dirName: packed ? packed.packageName : paths.dirName,
mainPath: paths.mainPath mainPath: paths.mainPath
} }
}); });
if (packed) instance.packed = {
pkg: packed.pkg,
packageName: packed.packageName,
packagePath: packed.packagePath,
packed: true
}; else instance.packed = false;
if (instance.enabled && this.loaded) { if (instance.enabled && this.loaded) {
instance.userConfig.enabled = false; instance.userConfig.enabled = false;
instance.start(false); instance.start(false);

View File

@ -15,6 +15,7 @@ import { Utils, Filters, ClientLogger as Logger } from 'common';
import { MonkeyPatch } from './patcher'; import { MonkeyPatch } from './patcher';
import Reflection from './reflection/index'; import Reflection from './reflection/index';
import DiscordApi from './discordapi'; import DiscordApi from './discordapi';
import PackageInstaller from './packageinstaller';
class Helpers { class Helpers {
static get plannedActions() { static get plannedActions() {
@ -501,28 +502,6 @@ export class ReactAutoPatcher {
} }
static async patchUploadArea() { static async patchUploadArea() {
const { selector } = Reflection.resolve('uploadArea'); PackageInstaller.uploadAreaPatch();
this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector});
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = function(e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
Modals.confirm('Function not ready', `You tried to install "${e.dataTransfer.files[0].path}", but installing .bd files isn't ready yet.`)
// Possibly something like Events.emit('install-file', e.dataTransfer.files[0]);
};
// Remove their handler, add ours, then readd theirs to give ours priority to stop theirs when we get a .bd file.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
this.unpatchUploadArea = function() {
reflect.element.removeEventListener('drop', callback);
};
} }
} }

View File

@ -8,3 +8,4 @@
@import './settings-modal'; @import './settings-modal';
@import './permission-modal'; @import './permission-modal';
@import './input-modal'; @import './input-modal';
@import './install-modal';

View File

@ -0,0 +1,231 @@
.bd-installModal {
.bd-modalInner {
min-width: 520px;
min-height: 200px;
@at-root .bd-err#{&} {
border: 2px solid $colerr;
}
.bd-modalBody {
flex-grow: 1;
padding: 0;
.bd-installModalBody {
display: flex;
flex-direction: column;
flex-grow: 1;
.bd-installModalTop {
padding: 0 15px;
display: flex;
height: 100px;
span {
font-weight: 700;
}
.bd-installModalIcon {
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEzMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEzMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MHtmaWxsOiNGNEY2RkM7fS5zdDF7ZmlsbDpub25lO3N0cm9rZTojMzYzOTNFO3N0cm9rZS13aWR0aDoyO30uc3Qye29wYWNpdHk6MC42O2ZpbGw6bm9uZTtzdHJva2U6IzM2MzkzRTtzdHJva2Utd2lkdGg6MjtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO308L3N0eWxlPjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xNSwxOWMwLTMuMywyLjctNiw2LTZoNDRjNC43LDAsMjAsMTUuMywyMCwyMHY3NGMwLDMuMy0yLjcsNi02LDZIMjFjLTMuMywwLTYtMi43LTYtNlYxOXoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTUsMTljMC0zLjMsMi43LTYsNi02aDQ0YzQuNywwLDIwLDE1LjMsMjAsMjB2NzRjMCwzLjMtMi43LDYtNiw2SDIxYy0zLjMsMC02LTIuNy02LTZWMTl6Ii8+PHBhdGggY2xhc3M9InN0MCIgZD0iTTY2LDE2YzAtMy4zLDEuOS00LjEsNC4yLTEuOGwxMy41LDEzLjVjMi4zLDIuMywxLjYsNC4yLTEuOCw0LjJINjljLTEuNywwLTMtMS4zLTMtM1YxNkw2NiwxNnoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNNjYsMTZjMC0zLjMsMS45LTQuMSw0LjItMS44bDEzLjUsMTMuNWMyLjMsMi4zLDEuNiw0LjItMS44LDQuMkg2OWMtMS43LDAtMy0xLjMtMy0zVjE2TDY2LDE2eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik02MSwxN0gyM2MtMi44LDAtNSwyLjItNSw1djciLz48cGF0aCBjbGFzcz0ic3QyIiBkPSJNNjksMTl2Ny41YzAsMS40LDEuMSwyLjUsMi41LDIuNUg3NCIvPjwvc3ZnPg==');
background-size: 100% 100%;
flex-shrink: 0;
width: 100px;
height: 130px;
position: fixed;
border-radius: 3px;
margin-top: -30px;
display: flex;
align-content: center;
justify-content: center;
align-items: center;
justify-items: center;
@at-root .bd-err#{&} {
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEzMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEzMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MHtmaWxsOiNGNEY2RkM7fS5zdDF7ZmlsbDpub25lO3N0cm9rZTojRDg0MDQwO3N0cm9rZS13aWR0aDoyO30uc3Qye29wYWNpdHk6MC42O2ZpbGw6bm9uZTtzdHJva2U6I0Q4NDA0MDtzdHJva2Utd2lkdGg6MjtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO308L3N0eWxlPjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xNSwxOWMwLTMuMywyLjctNiw2LTZoNDRjNC43LDAsMjAsMTUuMywyMCwyMHY3NGMwLDMuMy0yLjcsNi02LDZIMjFjLTMuMywwLTYtMi43LTYtNlYxOXoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTUsMTljMC0zLjMsMi43LTYsNi02aDQ0YzQuNywwLDIwLDE1LjMsMjAsMjB2NzRjMCwzLjMtMi43LDYtNiw2SDIxYy0zLjMsMC02LTIuNy02LTZWMTl6Ii8+PHBhdGggY2xhc3M9InN0MCIgZD0iTTY2LDE2YzAtMy4zLDEuOS00LjEsNC4yLTEuOGwxMy41LDEzLjVjMi4zLDIuMywxLjYsNC4yLTEuOCw0LjJINjljLTEuNywwLTMtMS4zLTMtM1YxNkw2NiwxNnoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNNjYsMTZjMC0zLjMsMS45LTQuMSw0LjItMS44bDEzLjUsMTMuNWMyLjMsMi4zLDEuNiw0LjItMS44LDQuMkg2OWMtMS43LDAtMy0xLjMtMy0zVjE2TDY2LDE2eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik02MSwxN0gyM2MtMi44LDAtNSwyLjItNSw1djciLz48cGF0aCBjbGFzcz0ic3QyIiBkPSJNNjksMTl2Ny41YzAsMS40LDEuMSwyLjUsMi41LDIuNUg3NCIvPjwvc3ZnPg==');
}
.bd-installModalCi {
width: 40px;
height: 40px;
}
svg {
fill: #36393e;
width: 40px;
height: 40px;
}
}
.bd-installModalInfo {
display: flex;
flex-grow: 1;
flex-direction: column;
padding: 15px 10px;
margin-left: 100px;
}
.bd-installModalDesc {
margin-top: 5px;
}
}
.bd-installModalBottom {
padding: 0 15px;
flex-grow: 1;
.bd-permScope {
&:last-child {
.bd-permInner {
border: 0;
}
}
}
}
}
}
.bd-installModalFooter {
display: flex;
justify-content: flex-end;
padding: 10px;
background: rgba(0, 0, 0, .1);
.bd-installModalStatus {
height: 36px;
line-height: 36px;
flex-grow: 1;
padding: 0 25px;
font-weight: 600;
color: #fff;
&.bd-ok {
color: $colok;
}
&.bd-err {
color: $colerr;
}
}
.bd-button {
font-size: 16px;
padding: 10px;
border-radius: 3px;
min-width: 100px;
transition: opacity .2s ease-in-out;
&.bd-ok {
background: #3ecc9c;
}
&.bd-err {
color: #fff;
}
&:hover {
opacity: .8;
}
&.bd-installModalUpload {
background: transparent;
transition: none;
&:hover {
opacity: 1;
text-decoration: underline;
}
}
}
}
}
&.bd-installModalDone {
.bd-modalInner {
min-width: 400px;
max-width: 400px;
height: 250px;
border: 2px solid $colok;
.bd-installModalBody {
display: flex;
flex-grow: 1;
align-content: center;
justify-content: center;
align-items: center;
h3 {
color: $colok;
font-size: 1.2em;
font-weight: 700;
text-align: center;
padding: 20px;
}
}
.bd-materialDesignIcon {
svg {
fill: $colok;
width: 100px;
height: 100px;
}
}
.bd-installModalFooter {
.bd-button {
margin-left: 5px;
}
}
}
}
&.bd-installModalFail {
.bd-modalInner {
min-width: 400px;
max-width: 400px;
height: 250px;
border: 2px solid $colerr;
.bd-installModalBody {
display: flex;
flex-grow: 1;
align-content: center;
justify-content: center;
align-items: center;
h3 {
color: $colerr;
font-size: 1.2em;
font-weight: 700;
text-align: center;
padding: 20px;
}
}
.bd-materialDesignIcon {
svg {
fill: $colerr;
width: 100px;
height: 100px;
}
}
.bd-installModalFooter {
&.bd-installModalErrMsg {
justify-content: flex-start;
padding: 10px;
background: rgba(0, 0, 0, .2);
font-weight: 700;
span {
color: #fff;
flex-grow: 1;
text-align: right;
}
}
}
}
}
}

View File

@ -12,6 +12,7 @@
<Card :item="plugin"> <Card :item="plugin">
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :value="plugin.enabled" @input="$emit('toggle-plugin')" /> <SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :value="plugin.enabled" @input="$emit('toggle-plugin')" />
<ButtonGroup slot="controls"> <ButtonGroup slot="controls">
<Button v-if="devmode && !plugin.packed" v-tooltip="'Package Plugin'" @click="package"><MiBoxDownload size="18"/></Button>
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button> <Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Reload'" @click="$emit('reload-plugin')"><MiRefresh size="18" /></Button> <Button v-tooltip="'Reload'" @click="$emit('reload-plugin')"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" @click="editPlugin"><MiPencil size="18" /></Button> <Button v-tooltip="'Edit'" @click="editPlugin"><MiPencil size="18" /></Button>
@ -22,18 +23,47 @@
<script> <script>
// Imports // Imports
import asar from 'asar';
import electron from 'electron';
import fs from 'fs';
import { Toasts } from 'ui';
import { Settings } from 'modules';
import { ClientLogger as Logger } from 'common'; import { ClientLogger as Logger } from 'common';
import { shell } from 'electron'; import { shell } from 'electron';
import Card from './Card.vue'; import Card from './Card.vue';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common'; import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload } from '../common';
export default { export default {
data() {
return {
devmode: Settings.getSetting('core', 'advanced', 'developer-mode').value
}
},
props: ['plugin'], props: ['plugin'],
components: { components: {
Card, Button, ButtonGroup, SettingSwitch, Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
}, },
methods: { methods: {
package() {
electron.remote.dialog.showSaveDialog({
title: 'Save Plugin Package',
defaultPath: this.plugin.name,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
asar.uncache(filepath);
asar.createPackage(this.plugin.contentPath, filepath, () => {
Toasts.success('Plugin Packaged!');
});
});
},
editPlugin() { editPlugin() {
try { try {
shell.openItem(this.plugin.contentPath); shell.openItem(this.plugin.contentPath);

View File

@ -0,0 +1,124 @@
<template>
<Modal class="bd-installModal" :headertext="modal.title" :closing="modal.closing" @close="modal.close" :noheader="true" :class="{'bd-err': !verifying && !verified, 'bd-installModalDone': installed, 'bd-installModalFail': err}">
<template v-if="!installed && !err">
<div slot="body" class="bd-installModalBody">
<div class="bd-installModalTop">
<div class="bd-installModalIcon">
<div v-if="modal.icon" class="bd-installModalCi" :style="{backgroundImage: `url(${modal.icon})`}" />
<MiExtension v-else />
</div>
<div class="bd-installModalInfo">
<span>{{modal.config.info.name}} v{{modal.config.info.version}} by {{modal.config.info.authors.map(a => a.name).join(', ')}}</span>
<div class="bd-installModalDesc">
{{modal.config.info.description}}
</div>
</div>
</div>
<div class="bd-installModalBottom">
<div v-for="(perm, i) in modal.config.permissions" :key="`perm-${i}`" class="bd-permScope">
<div class="bd-permAllow">
<div class="bd-permCheck">
<div class="bd-permCheckInner"></div>
</div>
<div class="bd-permInner">
<div class="bd-permName">{{perm.HEADER}}</div>
<div class="bd-permDesc">{{perm.BODY}}</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="verifying" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus">Verifying {{modal.contentType}}</span>
</div>
<div v-else-if="!verified" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-err">Not verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-err" @click="install" v-if="allowUnsafe">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
<div v-else-if="alreadyInstalled && upToDate" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus">Up to date version already installed!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
</div>
<div v-else slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-ok">Verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-ok" @click="install">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
</template>
<template v-else-if="err">
<div slot="body" class="bd-installModalBody">
<h3>Something went wrong :(</h3>
<MiError />
</div>
<div slot="footer" class="bd-installModalFooter bd-installModalErrMsg">
{{err.message}}
<span>Ctrl+Shift+I</span>
</div>
</template>
<template v-else>
<div slot="body" class="bd-installModalBody">
<h3>{{alreadyInstalled ? 'Succesfully Updated!' : 'Successfully Installed!'}}</h3>
<MiSuccessCircle/>
</div>
<div slot="footer" class="bd-installModalFooter">
<div class="bd-button bd-ok" v-if="installed.hasSettings" @click="showSettingsModal">Settings</div>
<div class="bd-button bd-ok" @click="modal.confirm(); modal.close();">OK</div>
</div>
</template>
</Modal>
</template>
<script>
// Imports
import { Modal, MiExtension, MiSuccessCircle, MiError } from '../../common';
import { PluginManager, ThemeManager, PackageInstaller, Settings } from 'modules';
export default {
data() {
return {
installing: false,
verifying: true,
alreadyInstalled: false,
upToDate: true,
allowUnsafe: Settings.getSetting('security', 'default', 'unsafe-content').value,
installed: false,
err: null
}
},
props: ['modal'],
components: {
Modal, MiExtension, MiSuccessCircle, MiError
},
mounted() {
const { contentType, config } = this.modal;
const alreadyInstalled = contentType === 'plugin' ? PluginManager.getPluginById(config.info.id) : ThemeManager.getContentById(config.info.id);
if (alreadyInstalled) {
this.alreadyInstalled = true;
if (config.info.version > alreadyInstalled.version) {
this.upToDate = false;
}
}
this.verify();
},
methods: {
async verify() {
const verified = await PackageInstaller.verifyPackage(this.modal.filePath);
this.verified = verified;
this.verifying = false;
},
async install() {
try {
const installed = await PackageInstaller.installPackage(this.modal.filePath, this.modal.config.info.id, this.alreadyInstalled);
this.installed = installed;
} catch (err) {
console.log(err);
this.err = err;
}
},
showSettingsModal() {
this.installed.showSettingsModal();
}
}
}
</script>

View File

@ -22,3 +22,5 @@ export { default as MiLock } from './materialicons/Lock.vue';
export { default as MiImagePlus } from './materialicons/ImagePlus.vue'; export { default as MiImagePlus } from './materialicons/ImagePlus.vue';
export { default as MiIcVpnKey } from './materialicons/IcVpnKey.vue'; export { default as MiIcVpnKey } from './materialicons/IcVpnKey.vue';
export { default as MiArrowLeft } from './materialicons/ArrowLeft.vue'; export { default as MiArrowLeft } from './materialicons/ArrowLeft.vue';
export { default as MiBoxDownload } from './materialicons/BoxDownload.vue';
export { default as MiSuccessCircle } from './materialicons/SuccessCircle.vue';

View File

@ -11,7 +11,7 @@
<template> <template>
<div :class="['bd-modal', {'bd-modalOut': closing, 'bd-modalScrolled': scrolled}]"> <div :class="['bd-modal', {'bd-modalOut': closing, 'bd-modalScrolled': scrolled}]">
<div class="bd-modalInner"> <div class="bd-modalInner">
<div class="bd-modalHeader"> <div class="bd-modalHeader" v-if="!noheader">
<slot name="header"> <slot name="header">
<div v-if="$slots.icon" class="bd-modalIcon"> <div v-if="$slots.icon" class="bd-modalIcon">
<slot name="icon" /> <slot name="icon" />
@ -41,7 +41,7 @@
import { MiClose } from './MaterialIcon'; import { MiClose } from './MaterialIcon';
export default { export default {
props: ['headertext', 'closing'], props: ['headertext', 'closing', 'noheader'],
components: { components: {
MiClose MiClose
}, },

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-materialDesignIcon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 4.9956,3L 19.0076,3L 20.7359,5.99339L 20.7304,5.99653C 20.9018,6.29149 21,6.63428 21,7L 21,19C 21,20.1046 20.1046,21 19,21L 5,21C 3.89543,21 3,20.1046 3,19L 3,7C 3,6.638 3.09618,6.29846 3.26437,6.00554L 3.26135,6.0038L 4.9956,3 Z M 5.57294,4.00001L 4.99559,5.00001L 5,5.00001L 19.0076,5.00002L 18.4303,4.00001L 5.57294,4.00001 Z M 7,12L 12,17L 17,12L 14,12L 14,10L 10,10L 10,12L 7,12 Z "/>
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-materialDesignIcon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 19.9994,11.9981C 19.9994,16.4161 16.4174,19.9981 11.9994,19.9981C 7.58139,19.9981 3.99939,16.4161 3.99939,11.9981C 3.99939,7.58007 7.58139,3.99807 11.9994,3.99807C 12.7634,3.99807 13.5004,4.11207 14.2004,4.31207L 15.7724,2.74007C 14.6074,2.26467 13.3354,1.99807 11.9994,1.99807C 6.47638,1.99807 1.99939,6.47507 1.99939,11.9981C 1.99939,17.5211 6.47638,21.9981 11.9994,21.9981C 17.5224,21.9981 21.9994,17.5211 21.9994,11.9981M 7.91339,10.0841L 6.49939,11.4981L 10.9994,15.9981L 20.9994,5.99807L 19.5854,4.58407L 10.9994,13.1701L 7.91339,10.0841 Z " />
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -17,6 +17,7 @@ import ErrorModal from './components/bd/modals/ErrorModal.vue';
import SettingsModal from './components/bd/modals/SettingsModal.vue'; import SettingsModal from './components/bd/modals/SettingsModal.vue';
import PermissionModal from './components/bd/modals/PermissionModal.vue'; import PermissionModal from './components/bd/modals/PermissionModal.vue';
import InputModal from './components/bd/modals/InputModal.vue'; import InputModal from './components/bd/modals/InputModal.vue';
import InstallModal from './components/bd/modals/InstallModal.vue';
let modals = 0; let modals = 0;
@ -190,6 +191,19 @@ export default class Modals {
return new Modal(modal, InputModal); return new Modal(modal, InputModal);
} }
static installModal(contentType, config, filePath, icon) {
return this.add(this.createInstallModal(contentType, config, filePath, icon));
}
static createInstallModal(contentType, config, filePath, icon) {
const modal = { contentType, config, filePath, icon };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = value => resolve(value);
modal.beforeClose = () => reject();
});
return new Modal(modal, InstallModal);
}
/** /**
* Creates a new permissions modal and adds it to the open stack. * Creates a new permissions modal and adds it to the open stack.
* The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user accepts the permissions or closes the modal. * The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user accepts the permissions or closes the modal.

View File

@ -32,6 +32,7 @@ module.exports = {
}, },
externals: { externals: {
electron: 'require("electron")', electron: 'require("electron")',
asar: 'require("asar")',
fs: 'require("fs")', fs: 'require("fs")',
path: 'require("path")', path: 'require("path")',
util: 'require("util")', util: 'require("util")',

775
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,11 +21,11 @@
"fs-extra": "^7.0.0", "fs-extra": "^7.0.0",
"keytar": "4.2.1", "keytar": "4.2.1",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-sass": "^4.9.2" "node-sass": "^4.9.2",
"asar": "^0.14.3"
}, },
"devDependencies": { "devDependencies": {
"aes256": "^1.0.4", "aes256": "^1.0.4",
"archiver": "^2.1.1",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-loader": "^7.1.5", "babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0", "babel-preset-env": "^1.7.0",

View File

@ -2,7 +2,7 @@
"info": { "info": {
"name": "Example Module", "name": "Example Module",
"authors": [ "Jiiks" ], "authors": [ "Jiiks" ],
"version": 1.0, "version": "1.0",
"description": "Module Example" "description": "Module Example"
}, },
"main": "index.js", "main": "index.js",

View File

@ -3,7 +3,7 @@
"id": "depend-error", "id": "depend-error",
"name": "Depend Error", "name": "Depend Error",
"authors": [ "Jiiks" ], "authors": [ "Jiiks" ],
"version": 1.0, "version": "1.0",
"description": "Depend Error Plugin Description", "description": "Depend Error Plugin Description",
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==" "icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg=="
}, },

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi" "twitter_username": "Jiiksi"
} }
], ],
"version": 1.0, "version": "1.0,"
"description": "Edit messages by double clicking them. This is a v1 fix" "description": "Edit messages by double clicking them. This is a v1 fix"
}, },
"main": "index.js" "main": "index.js"

View File

@ -4,7 +4,7 @@
"authors": [ "authors": [
"Jiiks" "Jiiks"
], ],
"version": 1.0, "version": "1.0",
"description": "Example Plugin 2 Description" "description": "Example Plugin 2 Description"
} }
} }

View File

@ -17,7 +17,7 @@
"twitter_username": "Jiiksi" "twitter_username": "Jiiksi"
} }
], ],
"version": 1.0, "version": "1.0",
"description": "A plugin for testing BetterDiscord plugin bridge." "description": "A plugin for testing BetterDiscord plugin bridge."
}, },
"main": "index.js" "main": "index.js"

View File

@ -11,7 +11,7 @@
"twitter_username": "_samuelelliott" "twitter_username": "_samuelelliott"
} }
], ],
"version": 1.0, "version": "1.0",
"description": "Plugin for testing array setting events as the first example plugin has a lot of stuff in it now." "description": "Plugin for testing array setting events as the first example plugin has a lot of stuff in it now."
}, },
"main": "index.js", "main": "index.js",

View File

@ -22,7 +22,7 @@
}, },
"Just a string" "Just a string"
], ],
"version": 1.0, "version": "1.0",
"description": "Example Plugin Description.\n\nDescriptions are preformatted (you can use newlines).", "description": "Example Plugin Description.\n\nDescriptions are preformatted (you can use newlines).",
"icon": "icon.svg", "icon": "icon.svg",
"icon_type": "image/svg+xml" "icon_type": "image/svg+xml"

Binary file not shown.

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi" "twitter_username": "Jiiksi"
} }
], ],
"version": 1.0, "version": "1.0",
"description": "Patcher Test Description" "description": "Patcher Test Description"
}, },
"main": "index.js" "main": "index.js"

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi" "twitter_username": "Jiiksi"
} }
], ],
"version": 1.0, "version": "1.0",
"description": "Permission Test Description" "description": "Permission Test Description"
}, },
"main": "index.js", "main": "index.js",

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi" "twitter_username": "Jiiksi"
} }
], ],
"version": 1.0, "version": "1.0",
"description": "Example for rendering stuff" "description": "Example for rendering stuff"
}, },
"main": "index.js" "main": "index.js"

View File

@ -2,7 +2,7 @@
"info": { "info": {
"name": "Example Theme 1", "name": "Example Theme 1",
"authors": [ "Jiiks" ], "authors": [ "Jiiks" ],
"version": 1.0, "version": "1.0",
"description": "Example Theme 1 Description", "description": "Example Theme 1 Description",
"icon": "icon.svg", "icon": "icon.svg",
"icon_type": "image/svg+xml", "icon_type": "image/svg+xml",