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;
}
.floating-window-buttons {
display: flex;
}
.floating-window-buttons .button {
cursor: pointer;
padding: 0 2px;
}
.floating-window-buttons .close-button svg {
.floating-window-buttons .button svg {
fill: #dcddde;
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 {
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 */
/* =================== */
#emote-container {
@ -1153,6 +1179,13 @@ color: #f6f6f7;
max-width: 750px;
}
.floating-addon-window {
min-width: 535px;
min-height: 605px;
max-height: 90%;
max-width: 90%;
}
/* Ace Editor Settings */
#ace_settingsmenu_container {
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",
id: "addons",
collapsible: true,
shown: false,
settings: [
{type: "switch", id: "addonErrors", 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",
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: {
name: "Custom CSS",
@ -191,7 +199,11 @@ export default {
version: "Version",
added: "Date Added",
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: {
loading: "Loading emotes in the background do not reload.",
@ -225,9 +237,10 @@ export default {
query: "for {{query}}"
},
Modals: {
confirmClose: "Are You Sure?",
confirmAction: "Are You Sure?",
okay: "Okay",
cancel: "Cancel",
close: "Close",
name: "Name",
message: "Message",
error: "Error",

View File

@ -6,13 +6,20 @@ import DataStore from "./datastore";
import AddonError from "../structs/addonerror";
import MetaError from "../structs/metaerror";
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 fs = require("fs");
const Module = require("module").Module;
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 stripBOM = function(fileContent) {
@ -28,6 +35,7 @@ export default class AddonManager {
get moduleExtension() {return "";}
get extension() {return "";}
get addonFolder() {return "";}
get language() {return "";}
get prefix() {return "addon";}
get collection() {return "settings";}
get category() {return "addons";}
@ -258,4 +266,60 @@ export default class AddonManager {
if (Settings.get(this.collection, this.category, this.id)) this.watchAddons();
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 addonFolder() {return path.resolve(Config.dataPath, "plugins");}
get prefix() {return "plugin";}
get language() {return "javascript";}
constructor() {
super();
@ -37,7 +38,11 @@ export default new class PluginManager extends AddonManager {
folder: this.addonFolder,
onChange: this.togglePlugin.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;
}

View File

@ -16,6 +16,7 @@ export default new class ThemeManager extends AddonManager {
get extension() {return ".theme.css";}
get addonFolder() {return path.resolve(Config.dataPath, "themes");}
get prefix() {return "theme";}
get language() {return "css";}
initialize() {
const errors = super.initialize();
@ -23,7 +24,11 @@ export default new class ThemeManager extends AddonManager {
folder: this.addonFolder,
onChange: this.toggleTheme.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;
}

View File

@ -2,6 +2,7 @@ import {React, Strings} from "modules";
import Screen from "../../structs/screen";
import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals";
export default class FloatingWindow extends React.Component {
@ -18,6 +19,7 @@ export default class FloatingWindow extends React.Component {
this.window = React.createRef();
this.close = this.close.bind(this);
this.maximize = this.maximize.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onDragStop = this.onDragStop.bind(this);
@ -55,7 +57,6 @@ export default class FloatingWindow extends React.Component {
onDrag(e) {
const div = this.window.current;
div.style.position = "fixed";
div.style.top = (e.clientY - this.offY) + "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}>
<span className="title">{this.props.title}</span>
<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}>
<CloseButton />
</div>
@ -97,11 +101,19 @@ export default class FloatingWindow extends React.Component {
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() {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmClose, this.props.confirmationText, {
Modals.showConfirmationModal(Strings.Modals.confirmAction, this.props.confirmationText, {
danger: true,
confirmText: "Close",
confirmText: Strings.Modals.close,
onConfirm: () => {resolve(true);},
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 {
render() {
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="M0 0h24v24H0z" fill="none" />
</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 CloseButton from "../icons/close";
import ReloadIcon from "../icons/reload";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";
import Switch from "./components/switch";
export default class AddonCard extends React.Component {
@ -119,7 +121,9 @@ export default class AddonCard extends React.Component {
<div className="bd-addon-header">
<span className="bd-title">{this.buildTitle(name, version, author)}</span>
<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} />
</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 ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard";
@ -14,6 +15,21 @@ export default class AddonList extends React.Component {
this.sort = this.sort.bind(this);
this.reverse = this.reverse.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() {
@ -89,9 +105,31 @@ export default class AddonList extends React.Component {
}
const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function";
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>
];
}
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);}
});
});
}
}