add edit/delete and improve window

This commit is contained in:
Zack Rauen 2019-06-30 01:32:14 -04:00
parent 0ea223c8f3
commit 3e933923e8
14 changed files with 322 additions and 23 deletions

View File

@ -232,15 +232,28 @@
padding: 2px 0; padding: 2px 0;
} }
.floating-window-buttons {
display: flex;
}
.floating-window-buttons .button { .floating-window-buttons .button {
cursor: pointer; cursor: pointer;
padding: 0 2px; padding: 0 2px;
} }
.floating-window-buttons .close-button svg { .floating-window-buttons .button svg {
fill: #dcddde;
margin-top: 1.5px; margin-top: 1.5px;
} }
.floating-window-buttons .button:hover svg {
fill: white;
}
.floating-window-buttons .button:hover {
background-color: #36393F;
}
.floating-window-buttons .close-button:hover { .floating-window-buttons .close-button:hover {
background-color: #f04747; background-color: #f04747;
} }
@ -537,6 +550,19 @@ background-color: #000;
} }
.bd-addon-button {
cursor: pointer;
}
.bd-addon-button + .bd-addon-button {
margin-left: 5px;
}
/* BEGIN EMOTE STYLING */ /* BEGIN EMOTE STYLING */
/* =================== */ /* =================== */
#emote-container { #emote-container {
@ -1153,6 +1179,13 @@ color: #f6f6f7;
max-width: 750px; max-width: 750px;
} }
.floating-addon-window {
min-width: 535px;
min-height: 605px;
max-height: 90%;
max-width: 90%;
}
/* Ace Editor Settings */ /* Ace Editor Settings */
#ace_settingsmenu_container { #ace_settingsmenu_container {
background: rgba(0,0,0, 0.7)!important; background: rgba(0,0,0, 0.7)!important;

File diff suppressed because one or more lines are too long

View File

@ -28,10 +28,12 @@ export default [
type: "category", type: "category",
id: "addons", id: "addons",
collapsible: true, collapsible: true,
shown: false,
settings: [ settings: [
{type: "switch", id: "addonErrors", value: true}, {type: "switch", id: "addonErrors", value: true},
{type: "switch", id: "autoScroll", value: true}, {type: "switch", id: "autoScroll", value: true},
{type: "switch", id: "autoReload", value: true} {type: "switch", id: "autoReload", value: true},
{type: "dropdown", id: "editAction", value: "detached", options: [{value: "detached"}, {value: "system"}]}
] ]
}, },
{ {

View File

@ -71,6 +71,14 @@ export default {
name: "Automatic Loading", name: "Automatic Loading",
note: "Automatically loads, reloads, and unloads plugins and themes" note: "Automatically loads, reloads, and unloads plugins and themes"
}, },
editAction: {
name: "Edit Action",
note: "Where plugins & themes appear when editing",
options: {
detached: "Detached Window",
system: "System Editor"
}
}
}, },
customcss: { customcss: {
name: "Custom CSS", name: "Custom CSS",
@ -191,7 +199,11 @@ export default {
version: "Version", version: "Version",
added: "Date Added", added: "Date Added",
modified: "Date Modified", modified: "Date Modified",
search: "Search {{type}}" search: "Search {{type}}",
editAddon: "Edit",
deleteAddon: "Delete",
confirmDelete: "Are you sure you want to delete {{name}}?",
confirmationText: "You have unsaved changes to {{name}}. Closing this window will lose all those changes.",
}, },
Emotes: { Emotes: {
loading: "Loading emotes in the background do not reload.", loading: "Loading emotes in the background do not reload.",
@ -225,9 +237,10 @@ export default {
query: "for {{query}}" query: "for {{query}}"
}, },
Modals: { Modals: {
confirmClose: "Are You Sure?", confirmAction: "Are You Sure?",
okay: "Okay", okay: "Okay",
cancel: "Cancel", cancel: "Cancel",
close: "Close",
name: "Name", name: "Name",
message: "Message", message: "Message",
error: "Error", error: "Error",

View File

@ -6,13 +6,20 @@ import DataStore from "./datastore";
import AddonError from "../structs/addonerror"; import AddonError from "../structs/addonerror";
import MetaError from "../structs/metaerror"; import MetaError from "../structs/metaerror";
import Toasts from "../ui/toasts"; import Toasts from "../ui/toasts";
import DiscordModules from "./discordmodules";
import Strings from "./strings";
import AddonEditor from "../ui/misc/addoneditor";
import FloatingWindowContainer from "../ui/floating/container";
const React = DiscordModules.React;
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const Module = require("module").Module; const Module = require("module").Module;
Module.globalPaths.push(path.resolve(require("electron").remote.app.getAppPath(), "node_modules")); Module.globalPaths.push(path.resolve(require("electron").remote.app.getAppPath(), "node_modules"));
const splitRegex = /[^\S\r\n]*?\n[^\S\r\n]*?\*[^\S\r\n]?/; const splitRegex = /[^\S\r\n]*?\r?\n[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/; const escapedAtRegex = /^\\@/;
const stripBOM = function(fileContent) { const stripBOM = function(fileContent) {
@ -28,6 +35,7 @@ export default class AddonManager {
get moduleExtension() {return "";} get moduleExtension() {return "";}
get extension() {return "";} get extension() {return "";}
get addonFolder() {return "";} get addonFolder() {return "";}
get language() {return "";}
get prefix() {return "addon";} get prefix() {return "addon";}
get collection() {return "settings";} get collection() {return "settings";}
get category() {return "addons";} get category() {return "addons";}
@ -258,4 +266,60 @@ export default class AddonManager {
if (Settings.get(this.collection, this.category, this.id)) this.watchAddons(); if (Settings.get(this.collection, this.category, this.id)) this.watchAddons();
return errors; return errors;
} }
deleteAddon(idOrFileOrAddon) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
return fs.unlinkSync(path.resolve(this.addonFolder, addon.filename));
}
saveAddon(idOrFileOrAddon, content) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
return fs.writeFileSync(path.resolve(this.addonFolder, addon.filename), content);
}
editAddon(idOrFileOrAddon, system) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
const fullPath = path.resolve(this.addonFolder, addon.filename);
if (typeof(system) == "undefined") system = Settings.get("settings", "addons", "editAction") == "system";
if (system) return require("electron").shell.openItem(`${fullPath}`);
return this.openDetached(addon);
}
openDetached(addon) {
const fullPath = path.resolve(this.addonFolder, addon.filename);
const content = fs.readFileSync(fullPath).toString();
const editorRef = React.createRef();
const editor = React.createElement(AddonEditor, {
id: "bd-floating-editor-" + addon.name,
ref: editorRef,
content: content,
save: this.saveAddon.bind(this, addon),
openNative: this.editAddon.bind(this, addon, true),
language: this.language
});
FloatingWindowContainer.open({
onClose: () => {
this.isDetached = false;
},
onResize: () => {
if (!editorRef || !editorRef.current || !editorRef.current.resize) return;
editorRef.current.resize();
},
title: addon.name,
id: content.id,
className: "floating-addon-window",
height: 470,
width: 410,
center: true,
resizable: true,
children: editor,
confirmClose: () => {
if (!editorRef || !editorRef.current) return false;
return editorRef.current.hasUnsavedChanges;
},
confirmationText: Strings.Addons.confirmationText.format({name: addon.name})
});
}
} }

View File

@ -19,6 +19,7 @@ export default new class PluginManager extends AddonManager {
get extension() {return ".plugin.js";} get extension() {return ".plugin.js";}
get addonFolder() {return path.resolve(Config.dataPath, "plugins");} get addonFolder() {return path.resolve(Config.dataPath, "plugins");}
get prefix() {return "plugin";} get prefix() {return "plugin";}
get language() {return "javascript";}
constructor() { constructor() {
super(); super();
@ -37,7 +38,11 @@ export default new class PluginManager extends AddonManager {
folder: this.addonFolder, folder: this.addonFolder,
onChange: this.togglePlugin.bind(this), onChange: this.togglePlugin.bind(this),
reload: this.reloadPlugin.bind(this), reload: this.reloadPlugin.bind(this),
refreshList: this.updatePluginList.bind(this) refreshList: this.updatePluginList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})}); })});
return errors; return errors;
} }

View File

@ -16,6 +16,7 @@ export default new class ThemeManager extends AddonManager {
get extension() {return ".theme.css";} get extension() {return ".theme.css";}
get addonFolder() {return path.resolve(Config.dataPath, "themes");} get addonFolder() {return path.resolve(Config.dataPath, "themes");}
get prefix() {return "theme";} get prefix() {return "theme";}
get language() {return "css";}
initialize() { initialize() {
const errors = super.initialize(); const errors = super.initialize();
@ -23,7 +24,11 @@ export default new class ThemeManager extends AddonManager {
folder: this.addonFolder, folder: this.addonFolder,
onChange: this.toggleTheme.bind(this), onChange: this.toggleTheme.bind(this),
reload: this.reloadTheme.bind(this), reload: this.reloadTheme.bind(this),
refreshList: this.updateThemeList.bind(this) refreshList: this.updateThemeList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})}); })});
return errors; return errors;
} }

View File

@ -2,6 +2,7 @@ import {React, Strings} from "modules";
import Screen from "../../structs/screen"; import Screen from "../../structs/screen";
import CloseButton from "../icons/close"; import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals"; import Modals from "../modals";
export default class FloatingWindow extends React.Component { export default class FloatingWindow extends React.Component {
@ -18,6 +19,7 @@ export default class FloatingWindow extends React.Component {
this.window = React.createRef(); this.window = React.createRef();
this.close = this.close.bind(this); this.close = this.close.bind(this);
this.maximize = this.maximize.bind(this);
this.onDrag = this.onDrag.bind(this); this.onDrag = this.onDrag.bind(this);
this.onDragStart = this.onDragStart.bind(this); this.onDragStart = this.onDragStart.bind(this);
this.onDragStop = this.onDragStop.bind(this); this.onDragStop = this.onDragStop.bind(this);
@ -55,7 +57,6 @@ export default class FloatingWindow extends React.Component {
onDrag(e) { onDrag(e) {
const div = this.window.current; const div = this.window.current;
div.style.position = "fixed";
div.style.top = (e.clientY - this.offY) + "px"; div.style.top = (e.clientY - this.offY) + "px";
div.style.left = (e.clientX - this.offX) + "px"; div.style.left = (e.clientX - this.offX) + "px";
} }
@ -75,6 +76,9 @@ export default class FloatingWindow extends React.Component {
<div className="floating-window-titlebar" ref={this.titlebar}> <div className="floating-window-titlebar" ref={this.titlebar}>
<span className="title">{this.props.title}</span> <span className="title">{this.props.title}</span>
<div className="floating-window-buttons"> <div className="floating-window-buttons">
<div className="button maximize-button" onClick={this.maximize}>
<MaximizeIcon size="18px" />
</div>
<div className="button close-button" onClick={this.close}> <div className="button close-button" onClick={this.close}>
<CloseButton /> <CloseButton />
</div> </div>
@ -97,11 +101,19 @@ export default class FloatingWindow extends React.Component {
if (this.props.close && shouldClose) this.props.close(); if (this.props.close && shouldClose) this.props.close();
} }
maximize() {
this.window.current.style.width = "100%";
this.window.current.style.height = "100%";
this.window.current.style.top = "20px";
this.window.current.style.left = "0px";
if (this.props.onResize) this.props.onResize();
}
confirmClose() { confirmClose() {
return new Promise(resolve => { return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmClose, this.props.confirmationText, { Modals.showConfirmationModal(Strings.Modals.confirmAction, this.props.confirmationText, {
danger: true, danger: true,
confirmText: "Close", confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);}, onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);} onCancel: () => {resolve(false);}
}); });

11
src/ui/icons/delete.jsx Normal file
View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class Delete extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>;
}
}

View File

@ -3,7 +3,7 @@ import {React} from "modules";
export default class Edit extends React.Component { export default class Edit extends React.Component {
render() { render() {
const size = this.props.size || "24px"; const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}> return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" /> <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
<path d="M0 0h24v24H0z" fill="none" /> <path d="M0 0h24v24H0z" fill="none" />
</svg>; </svg>;

View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class FullScreen extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}
}

View File

@ -0,0 +1,65 @@
import {React, Strings} from "modules";
import Editor from "../customcss/editor";
import Save from "../icons/save";
import Edit from "../icons/edit";
import Cog from "../icons/cog";
export default class AddonEditor extends React.Component {
constructor(props) {
super(props);
this.hasUnsavedChanges = false;
this.onChange = this.onChange.bind(this);
this.save = this.save.bind(this);
this.openNative = this.openNative.bind(this);
this.update = this.update.bind(this);
this.controls = [
{label: React.createElement(Save, {size: "18px"}), tooltip: Strings.CustomCSS.save, onClick: this.save},
{label: React.createElement(Edit, {size: "18px"}), tooltip: Strings.CustomCSS.openNative, onClick: this.openNative},
{label: React.createElement(Cog, {size: "18px"}), tooltip: Strings.CustomCSS.settings, onClick: "showSettings"}
];
}
update() {
this.forceUpdate();
}
updateEditor(newCSS) {
if (!this.editor) return;
this.editor.value = newCSS;
}
get value() {return this.editor.session.getValue();}
set value(newValue) {
this.editor.setValue(newValue);
}
showSettings() {return this.editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(this.editor);}
resize() {return this.editor.resize();}
setEditorRef(editor) {
this.editor = editor;
if (this.props.editorRef && typeof(this.props.editorRef.current) !== "undefined") this.props.editorRef.current = editor;
else if (this.props.editorRef) this.props.editorRef = editor;
}
render() {
return <Editor ref={this.setEditorRef.bind(this)} language={this.props.language} id={this.props.id || "bd-addon-editor"} controls={this.controls} value={this.props.content} onChange={this.onChange} />;
}
onChange() {
this.hasUnsavedChanges = true;
}
save(event, content) {
this.hasUnsavedChanges = false;
if (this.props.save) this.props.save(content);
}
openNative() {
if (this.props.openNative) this.props.openNative();
}
}

View File

@ -1,6 +1,8 @@
import {React, Logger, Strings} from "modules"; import {React, Logger, Strings} from "modules";
import CloseButton from "../icons/close"; import CloseButton from "../icons/close";
import ReloadIcon from "../icons/reload"; import ReloadIcon from "../icons/reload";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";
import Switch from "./components/switch"; import Switch from "./components/switch";
export default class AddonCard extends React.Component { export default class AddonCard extends React.Component {
@ -119,7 +121,9 @@ export default class AddonCard extends React.Component {
<div className="bd-addon-header"> <div className="bd-addon-header">
<span className="bd-title">{this.buildTitle(name, version, author)}</span> <span className="bd-title">{this.buildTitle(name, version, author)}</span>
<div className="bd-controls"> <div className="bd-controls">
{this.props.showReloadIcon && <ReloadIcon className="bd-reload bd-reload-card" onClick={this.reload} />} {this.props.editAddon && <div className="bd-addon-button" onClick={this.props.editAddon}><EditIcon /></div>}
{this.props.deleteAddon && <div className="bd-addon-button" onClick={this.props.deleteAddon}><DeleteIcon /></div>}
{this.props.showReloadIcon && <div className="bd-addon-button" onClick={this.reload}><ReloadIcon className="bd-reload bd-reload-card" /></div>}
<Switch checked={this.props.enabled} onChange={this.onChange} /> <Switch checked={this.props.enabled} onChange={this.onChange} />
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import {React, Settings, Strings} from "modules"; import {React, Settings, Strings, Events} from "modules";
import Modals from "../modals";
import SettingsTitle from "./title"; import SettingsTitle from "./title";
import ReloadIcon from "../icons/reload"; import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard"; import AddonCard from "./addoncard";
@ -14,6 +15,21 @@ export default class AddonList extends React.Component {
this.sort = this.sort.bind(this); this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this); this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this); this.search = this.search.bind(this);
this.update = this.update.bind(this);
}
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`${this.props.prefix}-loaded`, this.update);
Events.off(`${this.props.prefix}-unloaded`, this.update);
}
update() {
this.forceUpdate();
} }
reload() { reload() {
@ -89,9 +105,31 @@ export default class AddonList extends React.Component {
} }
const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function"; const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.plugin.getSettingsPanel.bind(addon.plugin); const getSettings = hasSettings && addon.plugin.getSettingsPanel.bind(addon.plugin);
return <AddonCard showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; return <AddonCard editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />;
})} })}
</div> </div>
]; ];
} }
editAddon(id) {
if (this.props.editAddon) this.props.editAddon(id);
}
async deleteAddon(id) {
const addon = this.props.addonList.find(a => a.id == id);
const shouldDelete = await this.confirmDelete(addon);
if (!shouldDelete) return;
if (this.props.deleteAddon) this.props.deleteAddon(addon);
}
confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
} }