eicon picker

This commit is contained in:
Mr. Stallion 2023-05-28 21:13:16 -07:00
parent cf1f23a19e
commit 2e807adae2
8 changed files with 367 additions and 25 deletions

View File

@ -1,13 +1,14 @@
# Changelog
## 1.23.0
## Canary
* Cleaned up top menu
* Profile Helper now only shows up if you have anything to fix; otherwise the profile helper can be found in the Settings menu
* Post Ads and Ad Editor have been merged together
## 1.23.0
* Improved text editor
* eicon picker (courtesy of @Xariah Dailstone)
* color picker
* unicode emoji picker
* Added [privacy statement](PRIVACY.md)
## 1.22.0

237
bbcode/EIconSelector.vue Normal file
View File

@ -0,0 +1,237 @@
<template>
<div class="eicon-selector-ui">
<div v-if="!store || refreshing" class="d-flex align-items-center loading">
<strong>Loading...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
<div v-else tabindex="0">
<div>
<div class="search-bar">
<input type="text" class="form-control search" id="search" v-model="search" ref="search" placeholder="Search eicons..." @click.prevent.stop="setFocus()" @mousedown.prevent.stop @mouseup.prevent.stop />
<div class="btn-group search-buttons">
<div class="btn expressions" @click.prevent.stop="runSearch('category:expressions')" aria-label="Expressions">
<i class="fas fa-theater-masks"></i>
</div>
<div class="btn sexual" @click.prevent.stop="runSearch('category:sexual')" aria-label="Sexual">
<i class="fas fa-heart"></i>
</div>
<div class="btn bubbles" @click.prevent.stop="runSearch('category:bubbles')" aria-label="Speech bubbles">
<i class="fas fa-comment"></i>
</div>
<div class="btn actions" @click.prevent.stop="runSearch('category:symbols')" aria-label="Symbols">
<i class="fas fa-icons"></i>
</div>
<div class="btn memes" @click.prevent.stop="runSearch('category:memes')" aria-label="Memes">
<i class="fas fa-poo"></i>
</div>
<div class="btn random" @click.prevent.stop="runSearch('category:random')" aria-label="Random">
<i class="fas fa-random"></i>
</div>
<div class="btn refresh" @click.prevent.stop="refreshIcons()" aria-label="Refresh eicons">
<i class="fas fa-sync"></i>
</div>
</div>
</div>
<div class="courtesy">
Courtesy of <a href="https://xariah.net/eicons">xariah.net</a>
</div>
<div class="upload">
<a href="https://www.f-list.net/icons.php">Upload eicons</a>
</div>
</div>
<div class="carousel slide w-100 results">
<div class="carousel-inner w-100" role="listbox">
<div class="carousel-item" v-for="eicon in results" role="img" :aria-label="eicon">
<img class="eicon" :alt="eicon" :src="'https://static.f-list.net/images/eicon/' + eicon + '.gif'" :title="eicon" @click.prevent.stop="selectIcon(eicon)">
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import _ from 'lodash';
import { Component, Hook, Prop, Watch } from '@f-list/vue-ts';
import Vue from 'vue';
import { EIconStore } from '../learn/eicon/store';
@Component
export default class EIconSelector extends Vue {
@Prop
readonly onSelect?: (eicon: string) => void;
store: EIconStore | undefined;
results: string[] = [];
search: string = '';
refreshing = false;
searchUpdateDebounce = _.debounce(() => this.runSearch(), 200, { maxWait: 1000 });
@Hook('mounted')
async mounted(): Promise<void> {
this.store = await EIconStore.getSharedStore();
this.runSearch('');
}
@Watch('search')
searchUpdate() {
this.searchUpdateDebounce();
}
runSearch(search?: string) {
if (search) {
this.search = search;
}
const s = this.search.toLowerCase().trim();
if (s.substring(0, 9) === 'category:') {
const category = s.substring(9).trim();
if (category === 'random') {
this.results = _.map(this.store?.random(100), (e) => e.eicon);
} else {
this.results = this.getCategoryResults(category);
}
} else {
if (s.length === 0) {
this.results = _.map(this.store?.random(100), (e) => e.eicon);
} else {
this.results = _.map(_.take(this.store?.search(s), 100), (e) => e.eicon);
}
}
}
getCategoryResults(category: string): string[] {
switch(category) {
case 'expressions':
return ['coolemoji', 'coughing emoji', 'flushedemoji', 'eyerollemoji', 'laughingemoji', 'grinning emoji', 'party emoji', 'pensiveemoji', 'lipbite emoji', 'nauseous emoji', 'angryemoji', 'facialemoji', 'clapemoji', 'heart eyes', 'kissing heart', 'cowboy emoji', 'cowemoji', 'eggplantemoji', 'peachemoji', 'melting emoji', 'poopemoji', 'thinkingemoji', 'triumphemoji', 'uwuemoji', 'voremoji', 'skullemoji', 'smugemoji', 'heartflooshed', 'fluttersorry', 'snake emoji', 'horseeyes', 'thehorse', 'catblob', 'catblobangery', 'splashemoji', 'tonguemoji', 'blobhugs', 'lickscreen', 'eyes emoji', 'nerdmeme', 'horsepls', 'e62pog', 'thirstytwi', 'bangfingerbang', 'chefs kiss', 'excuse me', 'psychopath', 'ashemote3', 'whentheohitsright', 'caradrinkreact', 'lip_bite', 'twittersob'];
case 'symbols':
return ['loveslove', 'pimpdcash', 'pls stop', 'gender-female', 'gender-male', 'gendershemale', 'gender-cuntboy', 'gender-mherm', 'gender-transgender', 'usflag', 'europeflag', 'lgbt', 'transflag', 'yaoilove', 'sunnyuhsuperlove', 'discovered', 'thbun', 'cuckquean', 'goldendicegmgolddicegif', 'pentagramo', 'sexsymbol', 'idnd1', 'instagram', 'twitterlogo', 'snapchaticon', 'tiktok', 'uber', 'google', 'suitclubs', 'suitdiamonds', 'suithearts', 'suitspades'];
case 'bubbles':
return ['takemetohornyjail', 'notcashmoney', 'lickme', 'iacs', 'imahugeslut', 'fuckyouasshole', 'bubblecute', 'pat my head', 'chorse', 'knotslutbubble', 'toofuckinghot', 'pbmr', 'imabimbo', 'dicefuck', 'ciaig', 'horseslut', 'fatdick', 'tomboypussy', 'breakthesubs', 'fuckingnya', 'iltclion', 'suckfuckobey', 'shemale', 'breedmaster', 'imastepfordwife', 'prier ahegao', 'buttslutbb', 'notgayoranything', 'onlyfans', 'horsecockneed', 'crimes', 'breed143', 'nagagross', 'willrim', 'muskslut', '4lewdbubble', 'thatslewd', 'hypnosiss', 'imahypnoslut', 'sheepsass2', 'imahugeslut', 'ratedmilf', 'notahealslut', 'ratedstud', 'ratedslut', 'xarcuminme', '5lewdbubble', 'xarcumonme', 'choke me', 'iamgoingtopunchyou', 'snapmychoker', 'rude1', 'fuckbun', 'iamindanger', 'fuckingelves', 'slutmug', 'helpicantstopsuckingcocks', 'talkpooltoy', 'thatskindahot', 'simpbait',];
case 'sexual':
return ['kissspink', 'paytonkiss', 'coralbutt4', 'slavefidget', 'capstrip', 'pinkundress', 'jhab1', 'caninelover', 'pole', 'rorobutt2', 'fingerlick', 'lapgrind', 'jackthighs', 'a condom', 'wolf abs', 'musclefuck2', 'verobutt3', 'bumsqueeze', 'realahegao4', 'influencerhater', 'assfucker', 'gagged2', 'ballsack3', 'fingering wolf', 'sloppy01', 'sybian', 'femboibate1', 'floppyhorsecock', 'blackshem1', 'fingersucc', 'vullylick', 'freyasuckfingers', 'cmontakeit', 'jessi flash', 'poju-butt', 'cheegrope2', 'patr1', 'ahega01 2', 'handjob1nuke', 'harmanfingers', 'rorysheath2', 'hermione1', '2buttw1', 'dropsqueeze', 'lixlove', 'bbctitjob6', 'appreciativetease', 'bimbolick', 'subj3', 'salivashare', 'ballsworship3', 'wolfsknot2', 'gaykiss', 'slurpkiss', 'absbulge', 'cockiss', 'horsedick11', 'knot1', 'g4ebulge', 'blackadamrough', 'flaunt', 'cummiefj', 'lovetosuck', 'worship', 'hopelessly in love', 'knotts', 'cockloveeee', 'donglove', 'woowyknotjob', 'cummz', 'every drop', 'edgyoops', 'orccummies2', 'oralcreampie100px', 'horseoral9a', 'swallowit', 'sinahegao', 'gayicon2', 'slut4', 'hossspurties2', 'cumringgag', 'jillbimbogiffell2', 'artistry01'];
case 'memes':
return ['guncock', 'michaelguns', 'wegotabadass', 'gonnabang', 'flirting101', 'monkeymeme', 'monkeymeme2', 'horsenoises', 'nyancat', 'gayb', 'fortasshole', 'dickletsign', 'sausageface', 'siren0', 'apologize to god', 'jabbalick', 'zeldawink', 'whatislove', 'surprisemothafucka', 'females', 'thanksihateit'];
}
return [];
}
selectIcon(eicon: string): void {
if (this.onSelect) {
this.onSelect(eicon);
}
}
async refreshIcons(): Promise<void> {
this.refreshing = true;
await this.store?.update();
await this.runSearch();
this.refreshing = false;
}
setFocus(): void {
(this.$refs['search'] as any).focus();
(this.$refs['search'] as any).select();
}
}
</script>
<style lang="scss">
.eicon-selector-ui {
.loading {
}
.search-bar {
display: flex;
.search {
flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.search-buttons {
margin-left: -1px;
.btn {
border-bottom: 1px solid var(--secondary);
}
.expressions {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.refresh {
}
}
}
.courtesy {
position: absolute;
bottom: 5px;
font-size: 9px;
right: 10px;
opacity: 50%;
}
.upload {
position: absolute;
bottom: 5px;
font-size: 9px;
left: 10px;
}
.results {
max-height: 200px;
overflow: hidden;
margin-top: 5px;
.carousel-inner {
overflow-x: scroll;
overflow-y: hidden;
.carousel-item {
display: table-cell;
border: solid 1px transparent !important;
&:hover {
background-color: var(--secondary) !important;
border: solid 1px var(--gray-dark) !important;
}
img {
width: auto;
height: auto;
max-height: 75px;
}
}
}
}
}
</style>

View File

@ -16,6 +16,12 @@
</div>
</div>
<div class="popover popover-top eicon-selector" v-show="eiconPopupVisible" v-on-clickaway="dismissEIconSelector">
<div class="popover-body">
<EIconSelector :onSelect="onSelectEIcon" ref="eIconSelector"></EIconSelector>
</div>
</div>
<div class="btn-group toolbar-buttons" style="flex-wrap:wrap">
<div v-if="!!characterName" class="character-btn">
<icon :character="characterName"></icon>
@ -59,10 +65,12 @@
import {defaultButtons, EditorButton, EditorSelection} from './editor';
import {BBCodeParser} from './parser';
import {default as IconView} from './IconView.vue';
import {default as EIconSelector} from './EIconSelector.vue';
@Component({
components: {
'icon': IconView
'icon': IconView,
'EIconSelector': EIconSelector
},
mixins: [ clickaway ]
})
@ -96,6 +104,7 @@
buttonColors = ['red', 'orange', 'yellow', 'green', 'cyan', 'purple', 'blue', 'pink', 'black', 'brown', 'white', 'gray'];
colorPopupVisible = false;
eiconPopupVisible = false;
preview = false;
previewWarnings: ReadonlyArray<string> = [];
@ -176,7 +185,7 @@
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')!;
const colorButtonIndex = _.findIndex(buttons, (b) => b.tag === 'color');
if (this.colorPopupVisible) {
const colorButton = _.cloneDeep(buttons[colorButtonIndex]);
@ -185,11 +194,26 @@
buttons[colorButtonIndex] = colorButton;
}
const eiconButtonIndex = _.findIndex(buttons, (b) => b.tag === 'eicon');
if (this.eiconPopupVisible) {
const eiconButton = _.cloneDeep(buttons[eiconButtonIndex]);
eiconButton.outerClass = 'toggled';
buttons[eiconButtonIndex] = eiconButton;
}
return buttons;
}
getColorButton(): EditorButton {
return _.find(this.buttons, (b) => b.icon === 'fa-eye-dropper')!;
getButtonByTag(tag: string): EditorButton {
const btn = _.find(this.buttons, (b) => b.tag === tag);
if (!btn) {
throw new Error('Unknown button');
}
return btn;
}
@Watch('value')
@ -228,16 +252,16 @@
this.element.setSelectionRange(start, end);
}
applyText(startText: string, endText: string): void {
applyText(startText: string, endText: string, withInject?: string): void {
const selection = this.getSelection();
if(selection.length > 0) {
const replacement = startText + selection.text + endText;
const replacement = startText + (withInject || selection.text) + endText;
this.text = this.replaceSelection(replacement);
this.setSelection(selection.start, selection.start + replacement.length);
} else {
const start = this.text.substr(0, selection.start) + startText;
const end = endText + this.text.substr(selection.start);
this.text = start + end;
this.text = start + (withInject || '') + end;
this.$nextTick(() => this.setSelection(start.length));
}
this.$emit('input', this.text);
@ -248,27 +272,47 @@
}
colorApply(btnColor: string): void {
const button = this.getColorButton();
const button = this.getButtonByTag('color');
this.applyButtonEffect(button, btnColor);
this.colorPopupVisible = false;
}
dismissEIconSelector(): void {
this.eiconPopupVisible = false;
}
onSelectEIcon(eiconId: string): void {
this.eiconApply(eiconId);
}
eiconApply(eiconId: string): void {
const button = this.getButtonByTag('eicon');
this.applyButtonEffect(button, undefined, eiconId);
this.eiconPopupVisible = false;
}
apply(button: EditorButton): void {
if (button.tag === 'color') {
this.colorPopupVisible = !this.colorPopupVisible;
return;
} else if (button.tag === 'eicon') {
this.eiconPopupVisible = !this.eiconPopupVisible;
} else if (button.tag === 'emoji') {
if (this.eiconPopupVisible) {
setTimeout(() => (this.$refs.eIconSelector as any).setFocus(), 100);
}
return;
}
this.applyButtonEffect(button);
}
applyButtonEffect(button: EditorButton, withArgument?: string): void {
applyButtonEffect(button: EditorButton, withArgument?: string, withInject?: string): void {
// Allow emitted variations for custom buttons.
this.$once('insert', (startText: string, endText: string) => this.applyText(startText, endText));
// noinspection TypeScriptValidateTypes
@ -285,7 +329,7 @@
const sbl = button.startText ? button.startText.length : 0;
if(this.text.length + sbl + ebl > this.maxlength) return;
this.applyText(button.startText || '', button.endText || '');
this.applyText(button.startText || '', button.endText || '', withInject);
this.lastInput = Date.now();
}
@ -402,6 +446,17 @@
}
}
.eicon-selector {
width: 550px;
max-width: 550px;
top: -169px;
left: 0;
line-height: 1;
z-index: 1000;
background-color: var(--input-bg);
min-height: 170px;
}
.color-selector {
max-width: 145px;
top: -57px;

View File

@ -65,7 +65,7 @@
</div>
</a>
<a href="#" @click.prevent="showAddPmPartner()" class="new-conversation" :class="{ glowing: conversations.privateConversations.length === 0 }">Open Conversation</a>
<a href="#" @click.prevent="showAddPmPartner()" class="new-conversation" :class="{ glowing: conversations.privateConversations.length === 0 && privateCanGlow }">Open Conversation</a>
</div>
<a href="#" @click.prevent="showChannels()" class="btn"><span class="fas fa-list"></span>
@ -86,7 +86,7 @@
</span>
</a>
<a href="#" @click.prevent="showChannels()" class="join-channel" :class="{ glowing: conversations.channelConversations.length === 0 }">Join Channel</a>
<a href="#" @click.prevent="showChannels()" class="join-channel" :class="{ glowing: conversations.channelConversations.length === 0 && channelCanGlow }">Join Channel</a>
</div>
</sidebar>
<div style="display:flex;flex-direction:column;flex:1;min-width:0">
@ -136,7 +136,7 @@
</template>/me
<script lang="ts">
import {Component, Hook} from '@f-list/vue-ts';
import { Component, Hook, Watch } from '@f-list/vue-ts';
import Sortable from 'sortablejs';
@ -200,6 +200,26 @@
focusListener!: () => void;
blurListener!: () => void;
channelConversations = core.conversations.channelConversations
privateConversations = core.conversations.privateConversations
privateCanGlow = !this.channelConversations?.length;
channelCanGlow = !this.privateConversations?.length;
@Watch('conversations.channelConversations')
channelConversationsChange() {
if (this.conversations.channelConversations?.length) {
this.channelCanGlow = false;
}
}
@Watch('conversations.privateConversations')
privateConversationsChange() {
if (this.conversations.privateConversations?.length) {
this.privateCanGlow = false;
}
}
@Hook('mounted')
mounted(): void {
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);

View File

@ -110,4 +110,8 @@
.statusEditor .bbcode-toolbar .color-selector {
left: 58px !important;
}
.statusEditor .bbcode-toolbar .eicon-selector {
top: 30px !important;
}
</style>

View File

@ -93,6 +93,11 @@
<style lang="scss">
.ad-list .bbcode-toolbar .color-selector {
left: 58px !important;
top: 30px !important;
}
.ad-list .bbcode-toolbar .eicon-selector {
top: 30px !important;
}
.w-100 {

View File

@ -1,7 +1,5 @@
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';
@ -41,9 +39,11 @@ export class EIconStore {
this.asOfTimestamp = data?.asOfTimestamp || 0;
this.lookup = _.fromPairs(_.map(this.records, (r) => [r.eicon, r]));
this.resortList();
log.info('eicons.loaded.local', { records: this.records.length, asOfTimestamp: this.asOfTimestamp });
log.info('eicons.loaded', { records: this.records.length, asOfTimestamp: this.asOfTimestamp });
await this.update();
log.info('eicons.loaded.update.remote', { records: this.records.length, asOfTimestamp: this.asOfTimestamp });
} catch (err) {
try {
await this.downloadAll();
@ -54,7 +54,7 @@ export class EIconStore {
}
protected getStoreFilename(): string {
const baseDir = app.getPath('userData');
const baseDir = remote.app.getPath('userData');
const settingsDir = path.join(baseDir, 'data');
return path.join(settingsDir, 'eicons.json');
@ -78,7 +78,7 @@ export class EIconStore {
}
async update(): Promise<void> {
log.info('eicons.update');
log.info('eicons.update', { asOf: this.asOfTimestamp });
const changes = await this.updater.fetchUpdates(this.asOfTimestamp);
@ -92,6 +92,8 @@ export class EIconStore {
this.asOfTimestamp = changes.asOfTimestamp;
log.info('eicons.update.processed', { removals: removals.length, additions: additions.length, asOf: this.asOfTimestamp });
if (changes.recordUpdates.length > 0) {
await this.save();
}
@ -131,7 +133,7 @@ export class EIconStore {
}
search(searchString: string): EIconRecord[] {
const lcSearch = searchString.toLowerCase();
const lcSearch = searchString.trim().toLowerCase();
const found = _.filter(this.records, (r) => r.eicon.indexOf(lcSearch) >= 0);
return found.sort((a, b) => {
@ -146,4 +148,22 @@ export class EIconStore {
return a.eicon.localeCompare(b.eicon);
});
}
random(count: number): EIconRecord[] {
return _.sampleSize(this.records, count);
}
private static sharedStore: EIconStore | undefined;
static async getSharedStore(): Promise<EIconStore> {
if (!EIconStore.sharedStore) {
EIconStore.sharedStore = new EIconStore();
await EIconStore.sharedStore.load();
setInterval(() => EIconStore.sharedStore!.update(), 60 * 60 * 1000);
}
return EIconStore.sharedStore;
}
}

View File

@ -19,7 +19,7 @@ export class EIconUpdater {
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 records = _.map(_.filter(lines, (line) => (line.trim().substr(0, 1) !== '#' && line.trim() !== '')), (line) => {
const [eicon, timestamp] = _.split(line, '\t', 2);
return { eicon: eicon.toLowerCase(), timestamp: parseInt(timestamp, 10) };
});
@ -34,7 +34,7 @@ export class EIconUpdater {
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 recordUpdates = _.map(_.filter(lines, (line) => (line.trim().substr(0, 1) !== '#' && line.trim() !== '')), (line) => {
const [action, eicon, timestamp] = _.split(line, '\t', 3);
return { action: action as '+' | '-', eicon: eicon.toLowerCase(), timestamp: parseInt(timestamp, 10) };
});