emotes + addon list controls

This commit is contained in:
Zack Rauen 2019-06-29 00:47:56 -04:00
parent b2eee5cf4c
commit 84c4db7e99
13 changed files with 365 additions and 53 deletions

View File

@ -123,17 +123,6 @@
min-height: 20px;
}
.bd-search {
margin: 0 0 10px 0px;
padding: 5px;
border-radius: 3px;
outline: none;
border: 0;
background-color: #202225;
color: #fff;
}
.bd-server-card {
position: relative;
border-width: 1px;
@ -299,6 +288,126 @@
.bd-addon-controls {
display: flex;
align-items: center;
justify-content: space-between;
}
.bd-addon-controls .bd-search {
font-size: 13px;
margin: 0;
width: 200px;
}
.bd-addon-dropdowns {
display: flex;
}
.bd-select-wrap + .bd-select-wrap {
margin-left: 10px;
}
.bd-select-wrap {
color: #f6f6f7;
font-size: 13px;
display: flex;
align-items: center;
}
.bd-select-wrap label {
opacity: .3;
margin-right: 5px;
}
.bd-select {
position: relative;
cursor: pointer;
}
.bd-select-controls {
background-color: rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 3px;
padding: 5px;
}
.bd-select-transparent .bd-select-controls {
background: none;
border: none;
padding: 0;
}
.bd-select-arrow, .bd-select-controls svg {
margin-left: 10px;
fill: #FFFFFF;
}
.bd-select .bd-select-options {
position: absolute;
background: #2F3136;
border-radius: 0 0 3px 3px;
max-height: 300px;
min-width: 100%;
overflow-y: auto;
box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 5px 0px;
border: 1px solid rgba(0, 0, 0, 0.3);
border-top: 0;
margin-top: -1px;
z-index: 1;
}
.bd-select-transparent .bd-select-options {
border: 1px solid rgba(0, 0, 0, 0.3);
margin-top: 3px;
margin-left: -1px;
}
.bd-select .bd-select-option {
padding: 8px 12px;
cursor: pointer;
white-space: pre;
}
.bd-select .bd-select-option:hover,
.bd-select .bd-select-option.selected {
background: #26272B;
}
.bd-search-wrapper {
padding: 3px;
border-radius: 3px;
outline: none;
border: 0;
background-color: #202225;
color: #fff;
display: flex;
align-items: center;
}
.bd-search {
padding: 2px 3px;
background: none;
border: 0;
color: #fff;
flex: 1;
}
.bd-search-wrapper > svg {
margin-right: 2px;
}
/* BEGIN EMOTE STYLING */
/* =================== */
#emote-container {
@ -1219,11 +1328,11 @@ body .ace_closeButton:active {
display: flex;
}
.bd-slist {
.bd-addon-list {
user-select: text;
}
.bd-slist li {
.bd-addon-list .bd-addon-card {
max-height: 175px;
margin-bottom: 20px;
padding: 5px 8px;
@ -1231,23 +1340,23 @@ body .ace_closeButton:active {
border-radius: 5px;
overflow: hidden;
}
.theme-dark .bd-slist li {
.theme-dark .bd-addon-list .bd-addon-card {
background-color: rgba(32,34,37,.6);
color: #f6f6f7;
border-color: #202225;
}
.theme-light .bd-slist li {
.theme-light .bd-addon-list .bd-addon-card {
background-color: #f8f9f9;
color: #4f545c;
border-color: #dcddde;
}
.bd-slist li.settings-open {
.bd-addon-list .bd-addon-card.settings-open {
max-height: 800px;
overflow-y: auto;
}
.bd-slist .bd-header {
.bd-addon-list .bd-header {
font-size: 12px;
font-weight: 700;
display: flex;
@ -1257,36 +1366,36 @@ body .ace_closeButton:active {
border-bottom: 1px solid transparent;
overflow: hidden;
}
.theme-dark .bd-slist .bd-header {
.theme-dark .bd-addon-list .bd-header {
color: #f6f6f7;
border-bottom-color: rgba(114,118,125,.3);
}
.theme-light .bd-slist .bd-header {
.theme-light .bd-addon-list .bd-header {
color: #4f545c;
border-bottom-color: rgba(185,187,190,.3);
}
.bd-slist .bd-description {
.bd-addon-list .bd-description {
word-break: break-word;
max-height: 100px;
margin: 5px 0;
padding: 5px 0;
overflow-y: auto;
}
.theme-dark .bd-slist .bd-description {
.theme-dark .bd-addon-list .bd-description {
color: #b9bbbe;
}
.theme-light .bd-slist .bd-description {
.theme-light .bd-addon-list .bd-description {
color: #72767d;
}
.bd-slist .scroller::-webkit-scrollbar-track-piece,
.bd-slist .scroller::-webkit-scrollbar-thumb {
.bd-addon-list .scroller::-webkit-scrollbar-track-piece,
.bd-addon-list .scroller::-webkit-scrollbar-thumb {
border-radius:0 !important;
border-color:transparent;
}
.bd-slist .bd-footer {
.bd-addon-list .bd-footer {
font-size: 12px;
font-weight: 700;
display: flex;
@ -1296,14 +1405,14 @@ body .ace_closeButton:active {
border-top: 1px solid transparent;
overflow: hidden;
}
.theme-dark .bd-slist .bd-footer {
.theme-dark .bd-addon-list .bd-footer {
border-top-color: rgba(114,118,125,.3);
}
.theme-light .bd-slist .bd-footer {
.theme-light .bd-addon-list .bd-footer {
border-top-color: rgba(185,187,190,.3);
}
.bd-slist .bd-footer button {
.bd-addon-list .bd-footer button {
background: #7289da;
color: #FFF;
border-radius: 5px;
@ -1313,15 +1422,15 @@ body .ace_closeButton:active {
transition: opacity 250ms ease;
}
.bd-slist .bd-footer button:disabled {
.bd-addon-list .bd-footer button:disabled {
opacity: 0.4;
}
.bd-slist .bd-footer a {
.bd-addon-list .bd-footer a {
color: #7289da;
}
.bd-slist .bd-footer a:hover {
.bd-addon-list .bd-footer a:hover {
text-decoration: underline;
}
/* ======================= */

File diff suppressed because one or more lines are too long

View File

@ -206,6 +206,8 @@ export default new class EmoteModule extends Builtin {
async loadEmoteData(categories) {
if (!categories) categories = this.categories;
if (!Array.isArray(categories)) categories = [categories];
const all = Object.keys(Emotes);
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
Toasts.show(Strings.Emotes.loading, {type: "info"});
this.emotesLoaded = false;
@ -233,6 +235,8 @@ export default new class EmoteModule extends Builtin {
unloadEmoteData(categories) {
if (!categories) categories = this.categories;
if (!Array.isArray(categories)) categories = [categories];
const all = Object.keys(Emotes);
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
for (const category of categories) {
delete Emotes[category];
Emotes[category] = {};
@ -242,7 +246,7 @@ export default new class EmoteModule extends Builtin {
downloadEmotes(category) {
const url = this.getRemoteFile(category);
this.log(`Downloading ${category} from ${url}`);
const options = {url: url, timeout: 8000, json: true};
const options = {url: url, timeout: 10000, json: true};
return new Promise(resolve => {
request.get(options, (error, response, parsedData) => {
if (error || response.statusCode != 200) {

View File

@ -20,7 +20,7 @@ export default [
name: "Categories",
collapsible: true,
settings: [
{type: "switch", id: "twitch", value: true},
{type: "switch", id: "twitchglobal", value: true},
{type: "switch", id: "twitchsubscriber", value: false},
{type: "switch", id: "frankerfacez", value: true},
{type: "switch", id: "bttv", value: true}

View File

@ -149,15 +149,15 @@ export default {
},
categories: {
name: "Categories",
twitch: {
name: "Twitch",
twitchglobal: {
name: "Twitch Globals",
note: "Show Twitch global emotes"
},
twitchsubscriber: {
name: "Twitch",
name: "Twitch Subscribers",
note: "Show Twitch subscriber emotes"
},
ffz: {
frankerfacez: {
name: "FrankerFaceZ",
note: "Show emotes from FFZ"
},

View File

@ -148,9 +148,13 @@ export default class AddonManager {
if (!fs.existsSync(possiblePath) || filename !== fs.realpathSync(possiblePath)) return Reflect.apply(originalRequire, this, arguments);
let fileContent = fs.readFileSync(filename, "utf8");
fileContent = stripBOM(fileContent);
const stats = fs.statSync(filename);
const meta = self.extractMeta(fileContent);
meta.id = meta.name;
meta.filename = path.basename(filename);
meta.added = stats.atimeMs;
meta.modified = stats.mtimeMs;
meta.size = stats.size;
fileContent = self.getFileModification(module, fileContent, meta);
module._compile(fileContent, filename);
};

View File

@ -0,0 +1,10 @@
import {React} from "modules";
export default class DownArrow extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
</svg>;
}
}

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

@ -0,0 +1,11 @@
import {React} from "modules";
export default class Search extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>;
}
}

View File

@ -2,6 +2,7 @@ import {React, WebpackModules, Strings} from "modules";
import SettingsTitle from "../settings/title";
import ServerCard from "./card";
import Connection from "../../structs/psconnection";
import Search from "../settings/components/search";
const SettingsView = WebpackModules.getByDisplayName("SettingsView");
@ -87,7 +88,8 @@ export default class PublicServers extends React.Component {
}
get searchBox() {
return React.createElement("input", {onKeyDown: this.searchKeyDown, type: "text", className: "bd-search", placeholder: `${Strings.PublicServers.search}...`, maxLength: "50"});
return <Search onKeyDown={this.searchKeyDown} placeholder={`${Strings.PublicServers.search}...`} />;
// return React.createElement("input", {onKeyDown: this.searchKeyDown, type: "text", className: "bd-search", placeholder: `${Strings.PublicServers.search}...`, maxLength: "50"});
}
get title() {

View File

@ -84,10 +84,10 @@ export default class AddonCard extends React.Component {
const props = {id: `${name}-settings`, className: "addon-settings", ref: this.panelRef};
if (typeof(settingsPanel) == "string") props.dangerouslySetInnerHTML = this.settingsPanel;
return <li className="settings-open bd-switch-item">
return <div className="bd-addon-card settings-open bd-switch-item">
<div className="bd-close" onClick={this.closeSettings}><CloseButton /></div>
<div {...props}>{this.settingsPanel instanceof React.Component ? this.settingsPanel : null}</div>
</li>;
</div>;
}
buildLink(which) {
@ -115,7 +115,7 @@ export default class AddonCard extends React.Component {
const description = this.getString(addon.description);
const version = this.getString(addon.version);
return <li dataName={name} dataVersion={version} className="settings-closed bd-switch-item">
return <div dataName={name} dataVersion={version} className="bd-addon-card settings-closed bd-switch-item">
<div className="bd-header">
<span className="bd-header-title">{this.buildTitle(name, version, author)}</span>
<div className="bd-controls">
@ -128,6 +128,6 @@ export default class AddonCard extends React.Component {
</div>
<div className="bd-description-wrap scroller-wrap fade"><div className="bd-description scroller">{description}</div></div>
{this.footer}
</li>;
</div>;
}
}

View File

@ -3,27 +3,84 @@ import {React, Settings, Strings} from "modules";
import SettingsTitle from "./title";
import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard";
import Select from "./components/select";
import Search from "./components/search";
const sortOptions = [
{label: "Name", value: "name"},
{label: "Author", value: "author"},
{label: "Version", value: "version"},
{label: "Date Added", value: "added"},
{label: "Date Modified", value: "modified"}
];
const directionOptions = [
{label: "Ascending", value: "true"},
{label: "Descending", value: "false"}
];
export default class AddonList extends React.Component {
constructor(props) {
super(props);
this.state = {sort: "name", ascending: true, query: ""};
this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this);
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
reverse(value) {
this.setState({ascending: value == "true"});
}
sort(value) {
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const showReloadIcon = !Settings.get("settings", "addons", "autoReload");
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: () => {require("electron").shell.openItem(folder);}} : null;
const sortedAddons = addonList.sort((a, b) => {
const first = a[this.state.sort];
const second = b[this.state.sort];
if (typeof(first) == "string") return first.toLocaleLowerCase().localeCompare(second.toLocaleLowerCase());
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!this.state.ascending) sortedAddons.reverse();
return [
<SettingsTitle key="title" text={title} button={button} otherChildren={showReloadIcon && <ReloadIcon className="bd-reload" onClick={this.reload.bind(this)} />} />,
<ul key="addonList" className={"bd-slist"}>
{addonList.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(addon => {
<div className="bd-controls bd-addon-controls">
<Search onChange={this.search} placeholder={`Search...`} />
<div className="bd-addon-dropdowns">
<Select options={sortOptions} label="Sort By:" onChange={this.sort} style="transparent" />
<Select options={directionOptions} label="Order:" onChange={this.reverse} style="transparent" />
</div>
</div>,
<div key="addonList" className={"bd-addon-list"}>
{sortedAddons.map(addon => {
if (this.state.query) {
let matches = addon.name.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.author.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.description.toLocaleLowerCase().includes(this.state.query);
if (!matches) return null;
}
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} />;
})}
</ul>
</div>
];
}
}

View File

@ -0,0 +1,11 @@
import {React} from "modules";
import SearchIcon from "../../icons/search";
export default class Search extends React.Component {
render() {
return <div className="bd-search-wrapper">
<input onChange={this.props.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-search" placeholder={this.props.placeholder} maxLength="50" />
<SearchIcon />
</div>;
}
}

View File

@ -0,0 +1,56 @@
import {React} from "modules";
import Arrow from "../../icons/downarrow";
export default class Select extends React.Component {
constructor(props) {
super(props);
this.state = {open: false, value: this.props.value || this.props.options[0].value};
this.onChange = this.onChange.bind(this);
this.showMenu = this.showMenu.bind(this);
this.hideMenu = this.hideMenu.bind(this);
}
showMenu(event) {
event.preventDefault();
this.setState({open: true}, () => {
document.addEventListener("click", this.hideMenu);
});
}
hideMenu() {
this.setState({open: false}, () => {
document.removeEventListener("click", this.hideMenu);
});
}
onChange(value) {
this.setState({value});
if (this.props.onChange) this.props.onChange(value);
}
get selected() {return this.props.options.find(o => o.value == this.state.value);}
get options() {
const selected = this.selected;
return <div className="bd-select-options">
{this.props.options.map(opt =>
<div className={`bd-select-option${selected.value == opt.value ? " selected" : ""}`} onClick={this.onChange.bind(this, opt.value)}>{opt.label}</div>
)}
</div>;
}
render() {
const style = this.props.style == "transparent" ? " bd-select-transparent" : "";
const isOpen = this.state.open ? " menu-open" : "";
return <div className="bd-select-wrap">
<label className="bd-label">{this.props.label}</label>
<div className={`bd-select${style}${isOpen}`} onClick={this.showMenu}>
<div className="bd-select-controls">
<div className="bd-select-value">{this.selected.label}</div>
<Arrow className="bd-select-arrow" />
</div>
{this.state.open && this.options}
</div>
</div>;
}
}