color picker

This commit is contained in:
Mr. Stallion 2023-05-27 20:00:51 -07:00
parent afb8510ea5
commit cf1f23a19e
9 changed files with 391 additions and 10 deletions

View File

@ -6,13 +6,22 @@
<i class="fa fa-code"></i>
</a>
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :disabled="disabled" :style="showToolbar ? {display: 'flex'} : undefined" @mousedown.stop.prevent
v-if="hasToolbar" style="flex:1 51%">
<div class="btn-group" style="flex-wrap:wrap">
v-if="hasToolbar" style="flex:1 51%; position: relative">
<div class="popover popover-top color-selector" v-show="colorPopupVisible" v-on-clickaway="dismissColorSelector">
<div class="popover-body">
<div class="btn-group" role="group" aria-label="Color">
<button v-for="btnCol in buttonColors" type="button" class="btn text-color" :class="btnCol" :title="btnCol" @click.prevent.stop="colorApply(btnCol)"></button>
</div>
</div>
</div>
<div class="btn-group toolbar-buttons" style="flex-wrap:wrap">
<div v-if="!!characterName" class="character-btn">
<icon :character="characterName"></icon>
</div>
<div class="btn btn-light btn-sm" v-for="button in buttons" :title="button.title" @click.prevent.stop="apply(button)">
<div class="btn btn-light btn-sm" v-for="button in buttons" :class="button.outerClass" :title="button.title" @click.prevent.stop="apply(button)">
<i :class="(button.class ? button.class : 'fa ') + button.icon"></i>
</div>
<div @click="previewBBCode" class="btn btn-light btn-sm" :class="preview ? 'active' : ''"
@ -41,7 +50,9 @@
<script lang="ts">
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
import _ from 'lodash';
import Vue from 'vue';
import { mixin as clickaway } from 'vue-clickaway';
import {getKey} from '../chat/common';
import {Keys} from '../keys';
import {BBCodeElement, CoreBBCodeParser, urlRegex} from './core';
@ -52,7 +63,8 @@
@Component({
components: {
'icon': IconView
}
},
mixins: [ clickaway ]
})
export default class Editor extends Vue {
@Prop
@ -82,6 +94,9 @@
@Prop({default: null})
readonly characterName: string | null = null;
buttonColors = ['red', 'orange', 'yellow', 'green', 'cyan', 'purple', 'blue', 'pink', 'black', 'brown', 'white', 'gray'];
colorPopupVisible = false;
preview = false;
previewWarnings: ReadonlyArray<string> = [];
previewResult = '';
@ -156,12 +171,27 @@
get buttons(): EditorButton[] {
const buttons = this.defaultButtons.slice();
if(this.extras !== undefined)
for(let i = 0, l = this.extras.length; i < l; i++)
buttons.push(this.extras[i]);
const colorButtonIndex = _.findIndex(buttons, (b) => b.icon === 'fa-eye-dropper')!;
if (this.colorPopupVisible) {
const colorButton = _.cloneDeep(buttons[colorButtonIndex]);
colorButton.outerClass = 'toggled';
buttons[colorButtonIndex] = colorButton;
}
return buttons;
}
getColorButton(): EditorButton {
return _.find(this.buttons, (b) => b.icon === 'fa-eye-dropper')!;
}
@Watch('value')
watchValue(newValue: string): void {
this.$nextTick(() => this.resize());
@ -213,7 +243,32 @@
this.$emit('input', this.text);
}
dismissColorSelector(): void {
this.colorPopupVisible = false;
}
colorApply(btnColor: string): void {
const button = this.getColorButton();
this.applyButtonEffect(button, btnColor);
this.colorPopupVisible = false;
}
apply(button: EditorButton): void {
if (button.tag === 'color') {
this.colorPopupVisible = !this.colorPopupVisible;
return;
} else if (button.tag === 'eicon') {
} else if (button.tag === 'emoji') {
}
this.applyButtonEffect(button);
}
applyButtonEffect(button: EditorButton, withArgument?: string): void {
// Allow emitted variations for custom buttons.
this.$once('insert', (startText: string, endText: string) => this.applyText(startText, endText));
// noinspection TypeScriptValidateTypes
@ -221,8 +276,8 @@
// tslint:ignore-next-line:no-any
return button.handler.call(this as any, this);
}
if(button.startText === undefined)
button.startText = `[${button.tag}]`;
if(button.startText === undefined || withArgument)
button.startText = `[${button.tag}${withArgument ? '=' + withArgument : ''}]`;
if(button.endText === undefined)
button.endText = `[/${button.tag}]`;
@ -266,7 +321,7 @@
if(button.key === key) {
e.stopPropagation();
e.preventDefault();
this.apply(button);
this.applyButtonEffect(button);
break;
}
} else if(e.shiftKey) this.isShiftPressed = true;
@ -339,4 +394,94 @@
}
}
}
.bbcode-toolbar {
.toolbar-buttons {
.btn.toggled {
background-color: var(--secondary) !important;
}
}
.color-selector {
max-width: 145px;
top: -57px;
left: 94px;
line-height: 1;
z-index: 1000;
background-color: var(--input-bg);
.btn-group {
display: block;
}
.btn {
&.text-color {
border-radius: 0 !important;
margin: 0 !important;
padding: 0 !important;
margin-right: -1px !important;
margin-bottom: -1px !important;
border: 1px solid var(--secondary);
width: 1.3rem;
height: 1.3rem;
&::before {
display: none !important;
}
&:hover {
border-color: var(--gray-dark) !important;
}
&.red {
background-color: var(--textRedColor);
}
&.orange {
background-color: var(--textOrangeColor);
}
&.yellow {
background-color: var(--textYellowColor);
}
&.green {
background-color: var(--textGreenColor);
}
&.cyan {
background-color: var(--textCyanColor);
}
&.purple {
background-color: var(--textPurpleColor);
}
&.blue {
background-color: var(--textBlueColor);
}
&.pink {
background-color: var(--textPinkColor);
}
&.black {
background-color: var(--textBlackColor);
}
&.brown {
background-color: var(--textBrownColor);
}
&.white {
background-color: var(--textWhiteColor);
}
&.gray {
background-color: var(--textGrayColor);
}
}
}
}
}
</style>

View File

@ -7,6 +7,7 @@ export interface EditorButton {
icon: string;
key?: Keys;
class?: string;
outerClass?: string;
startText?: string;
endText?: string;
handler?(vm: Vue): void;

View File

@ -1,5 +1,5 @@
<template>
<modal :action="l('chat.setStatus')" @submit="setStatus" @close="reset" dialogClass="w-100 modal-lg">
<modal :action="l('chat.setStatus')" @submit="setStatus" @close="reset" dialogClass="w-100 modal-lg statusEditor">
<div class="form-group" id="statusSelector">
<label class="control-label">{{l('chat.setStatus.status')}}</label>
<dropdown linkClass="custom-select">
@ -105,3 +105,9 @@
}
}
</script>
<style lang="scss">
.statusEditor .bbcode-toolbar .color-selector {
left: 58px !important;
}
</style>

View File

@ -91,6 +91,10 @@
</script>
<style lang="scss">
.ad-list .bbcode-toolbar .color-selector {
left: 58px !important;
}
.w-100 {
min-width: 70%;
}

149
learn/eicon/store.ts Normal file
View File

@ -0,0 +1,149 @@
import _ from 'lodash';
import * as electron from 'electron';
const app = electron.app;
import * as remote from '@electron/remote';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import * as fs from 'fs';
import * as path from 'path';
import { EIconRecord, EIconUpdater } from './updater';
export class EIconStore {
protected records: EIconRecord[] = [];
protected lookup: Record<string, EIconRecord> = {};
protected asOfTimestamp = 0;
protected updater = new EIconUpdater();
async save(): Promise<void> {
const fn = this.getStoreFilename();
log.info('eicons.save', { records: this.records.length, asOfTimestamp: this.asOfTimestamp, fn });
fs.writeFileSync(fn, JSON.stringify({
asOfTimestamp: this.asOfTimestamp,
records: this.records
}));
remote.ipcMain.emit('eicons.reload', { asOfTimestamp: this.asOfTimestamp });
}
async load(): Promise<void> {
const fn = this.getStoreFilename();
log.info('eicons.load', { fn });
try {
const data = JSON.parse(fs.readFileSync(fn, 'utf-8'));
this.records = data?.records || [];
this.asOfTimestamp = data?.asOfTimestamp || 0;
this.lookup = _.fromPairs(_.map(this.records, (r) => [r.eicon, r]));
this.resortList();
log.info('eicons.loaded', { records: this.records.length, asOfTimestamp: this.asOfTimestamp });
} catch (err) {
try {
await this.downloadAll();
} catch (err2) {
log.error('eicons.load.failure', { err: err2 });
}
}
}
protected getStoreFilename(): string {
const baseDir = app.getPath('userData');
const settingsDir = path.join(baseDir, 'data');
return path.join(settingsDir, 'eicons.json');
}
async downloadAll(): Promise<void> {
log.info('eicons.downloadAll');
const eicons = await this.updater.fetchAll();
this.records = eicons.records;
this.lookup = _.fromPairs(_.map(this.records, (r) => [r.eicon, r]));
_.each(eicons.records, (changeRecord) => this.addIcon(changeRecord));
this.resortList();
this.asOfTimestamp = eicons.asOfTimestamp;
await this.save();
}
async update(): Promise<void> {
log.info('eicons.update');
const changes = await this.updater.fetchUpdates(this.asOfTimestamp);
const removals = _.filter(changes.recordUpdates, (changeRecord) => changeRecord.action === '-');
const additions = _.filter(changes.recordUpdates, (changeRecord) => changeRecord.action === '+');
_.each(removals, (changeRecord) => this.removeIcon(changeRecord));
_.each(additions, (changeRecord) => this.addIcon(changeRecord));
this.resortList();
this.asOfTimestamp = changes.asOfTimestamp;
if (changes.recordUpdates.length > 0) {
await this.save();
}
}
protected resortList(): void {
_.sortBy(this.records, 'eicon');
}
protected addIcon(record: EIconRecord): void {
if (record.eicon in this.lookup) {
this.lookup[record.eicon].timestamp = record.timestamp;
return;
}
const r = {
eicon: record.eicon,
timestamp: record.timestamp
};
this.records.push(r);
this.lookup[record.eicon] = r;
}
protected removeIcon(record: EIconRecord): void {
if (!(record.eicon in this.lookup)) {
return;
}
delete this.lookup[record.eicon];
const idx = this.records.findIndex((r) => (r.eicon === record.eicon));
if (idx >= 0) {
this.records.splice(idx, 1);
}
}
search(searchString: string): EIconRecord[] {
const lcSearch = searchString.toLowerCase();
const found = _.filter(this.records, (r) => r.eicon.indexOf(lcSearch) >= 0);
return found.sort((a, b) => {
if ((a.eicon.substr(0, lcSearch.length) === lcSearch) && (b.eicon.substr(0, lcSearch.length) !== lcSearch)) {
return -1;
}
if ((b.eicon.substr(0, lcSearch.length) === lcSearch) && (a.eicon.substr(0, lcSearch.length) !== lcSearch)) {
return 1;
}
return a.eicon.localeCompare(b.eicon);
});
}
}

47
learn/eicon/updater.ts Normal file
View File

@ -0,0 +1,47 @@
import Axios from 'axios';
import _ from 'lodash';
export interface EIconRecord {
eicon: string;
timestamp: number;
}
export interface EIconRecordUpdate extends EIconRecord {
action: '+' | '-';
}
export class EIconUpdater {
static readonly FULL_DATA_URL = 'https://xariah.net/eicons/Home/EiconsDataBase/base.doc';
static readonly DATA_UPDATE_URL = 'https://xariah.net/eicons/Home/EiconsDataDeltaSince';
async fetchAll(): Promise<{ records: EIconRecord[], asOfTimestamp: number }> {
const result = await Axios.get(EIconUpdater.FULL_DATA_URL);
const lines = _.split(result.data, '\n');
const records = _.map(_.filter(lines, (line) => (line.trim().substr(0, 1) !== '#')), (line) => {
const [eicon, timestamp] = _.split(line, '\t', 2);
return { eicon: eicon.toLowerCase(), timestamp: parseInt(timestamp, 10) };
});
const asOfLine = _.first(_.filter(lines, (line: string) => line.substring(0, 9) === '# As Of: '));
const asOfTimestamp = asOfLine ? parseInt(asOfLine.substring(9), 10) : 0;
return { records, asOfTimestamp };
}
async fetchUpdates(fromTimestampInSecs: number): Promise<{ recordUpdates: EIconRecordUpdate[], asOfTimestamp: number }> {
const result = await Axios.get(`${EIconUpdater.DATA_UPDATE_URL}/${fromTimestampInSecs}`);
const lines = _.split(result.data, '\n');
const recordUpdates = _.map(_.filter(lines, (line) => (line.trim().substr(0, 1) !== '#')), (line) => {
const [action, eicon, timestamp] = _.split(line, '\t', 3);
return { action: action as '+' | '-', eicon: eicon.toLowerCase(), timestamp: parseInt(timestamp, 10) };
});
const asOfLine = _.first(_.filter(lines, (line: string) => line.substring(0, 9) === '# As Of: '));
const asOfTimestamp = asOfLine ? parseInt(asOfLine.substring(9), 10) : 0;
return { recordUpdates, asOfTimestamp };
}
}

View File

@ -13,6 +13,7 @@
"@types/lodash": "4.14.162",
"@types/node": "16.18.32",
"@types/node-fetch": "2.6.4",
"@types/vue-clickaway": "2.2.0",
"@types/qs": "^6.9.5",
"@types/request-promise": "^4.1.46",
"@types/sortablejs": "^1.10.6",
@ -48,6 +49,7 @@
"tslint": "^6.1.3",
"typescript": "^3.9.7",
"vue": "2.6.12",
"vue-clickaway": "2.2.2",
"vue-input-tag": "^2.0.7",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.12",

View File

@ -60,4 +60,17 @@ $risingVariables: (
input-bg: #{$input-bg},
input-color: #{$input-color},
textRedColor: #{$red-color},
textBlueColor: #{$blue-color},
textYellowColor: #{$yellow-color},
textGreenColor: #{$green-color},
textCyanColor: #{$cyan-color},
textPurpleColor: #{$purple-color},
textWhiteColor: #{$white-color},
textBlackColor: #{$black-color},
textBrownColor: #{$brown-color},
textPinkColor: #{$pink-color},
textGrayColor: #{$gray-color},
textOrangeColor: #{$orange-color},
);

View File

@ -518,6 +518,13 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/vue-clickaway@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/vue-clickaway/-/vue-clickaway-2.2.0.tgz#a560c5280dac0dd0906b5b78249be0c859acffc6"
integrity sha512-bPRisDZhtscJ+2PmGIAyEBB7+ep6j/FFrPGr5kpWzAFylRxCcZrw70tlXUwslgZtSYZEU1e6QJkSOPHFw21XXQ==
dependencies:
vue "^2.0.0"
"@types/yauzl@^2.9.1":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
@ -4698,7 +4705,7 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
loose-envify@^1.0.0:
loose-envify@^1.0.0, loose-envify@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -7680,6 +7687,13 @@ vue-class-component@6.3.2:
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-6.3.2.tgz#e6037e84d1df2af3bde4f455e50ca1b9eec02be6"
integrity sha512-cH208IoM+jgZyEf/g7mnFyofwPDJTM/QvBNhYMjqGB8fCsRyTf68rH2ISw/G20tJv+5mIThQ3upKwoL4jLTr1A==
vue-clickaway@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/vue-clickaway/-/vue-clickaway-2.2.2.tgz#cecf6839575e8b2afc5d3edb3efb616d293dbb44"
integrity sha512-25SpjXKetL06GLYoLoC8pqAV6Cur9cQ//2g35GRFBV4FgoljbZZjTINR8g2NuVXXDMLSUXaKx5dutgO4PaDE7A==
dependencies:
loose-envify "^1.2.0"
vue-hot-reload-api@^2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
@ -7724,7 +7738,7 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@2.6.12, vue@^2.5.17, vue@^2.5.20:
vue@2.6.12, vue@^2.0.0, vue@^2.5.17, vue@^2.5.20:
version "2.6.12"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==