ad editor / post ads

This commit is contained in:
Mr. Stallion 2022-12-24 21:44:55 -08:00
parent 4a56ef254a
commit 5b0e8e2e68
20 changed files with 642 additions and 19 deletions

View File

@ -1,5 +1,22 @@
# Changelog
## Todo
* Settings allow selecting which match comparisons you care about
* Import/Export ads
## 1.20.0
* Kink scoring is skipped if characters have only a few shared kinks
* Kink scoring gives more weight to 'favorite' and 'no' categories
* Fixed auto-responder not responding to previously unknown characters
* Fixed re-order tabs
* Added Ad Editor:
* Central ad editor for all ads
* Button near 'Console' in the Sidebar
* Added Post Ads:
* Select ads based on your mood, preference, etc.
* Launch ads on multiple channels
* Button near 'Console' in the Sidebar
## 1.19.3
* Added option to have character portrait displayed next to text input
* Fixed asexual orientation ID

View File

@ -1,8 +1,8 @@
# Download
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-linux.AppImage) (82 MB)
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-linux.AppImage) (82 MB)
# F-Chat Rising

View File

@ -5,7 +5,7 @@
style="border-bottom-left-radius:0;border-bottom-right-radius:0" v-if="hasToolbar">
<i class="fa fa-code"></i>
</a>
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? {display: 'flex'} : undefined" @mousedown.stop.prevent
<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">
<div v-if="!!characterName" class="character-btn">

View File

@ -19,6 +19,18 @@
{{l('settings.open')}}</a></div>
<div><a href="#" @click.prevent="showRecent()" class="btn"><span class="fas fa-history"></span>
{{l('chat.recentConversations')}}</a></div>
<div><a href="#" @click.prevent="showAdCenter()" class="btn"><span class="fas fa-ad"></span>
Ad Editor</a></div>
<div><a href="#" @click.prevent="showAdLauncher()" class="btn"><span class="fas fa-play"></span>
Post Ads</a>
<span v-show="adsAreRunning()" class="adControls">
<span aria-label="Stop All Ads" class="fas fa-stop" @click.prevent="stopAllAds()"></span>
</span>
</div>
<div class="list-group conversation-nav">
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
class="list-group-item list-group-item-action">
@ -51,6 +63,7 @@
</div>
<a href="#" @click.prevent="showChannels()" class="btn"><span class="fas fa-list"></span>
{{l('chat.channels')}}</a>
<div class="list-group conversation-nav" ref="channelConversations">
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel" :key="conversation.key"
@ -92,6 +105,8 @@
<channels ref="channelsDialog"></channels>
<status-switcher ref="statusDialog"></status-switcher>
<character-search ref="searchDialog"></character-search>
<adLauncher ref="adLauncher"></adLauncher>
<adCenter ref="adCenter"></adCenter>
<settings ref="settingsDialog"></settings>
<report-dialog ref="reportDialog"></report-dialog>
<user-menu ref="userMenu" :reportDialog="$refs['reportDialog']"></user-menu>
@ -131,6 +146,8 @@
import NoteStatus from '../site/NoteStatus.vue';
import { Dialog } from '../helpers/dialog';
// import { EventBus } from './preview/event-bus';
import AdCenterDialog from './ads/AdCenter.vue';
import AdLauncherDialog from './ads/AdLauncher.vue';
const unreadClasses = {
[Conversation.UnreadState.None]: '',
@ -145,7 +162,9 @@
'user-menu': UserMenu, 'recent-conversations': RecentConversations,
'image-preview': ImagePreview,
'add-pm-partner': PmPartnerAdder,
'note-status': NoteStatus
'note-status': NoteStatus,
adCenter: AdCenterDialog,
adLauncher: AdLauncherDialog
}
})
export default class ChatView extends Vue {
@ -216,6 +235,8 @@
}, (value) => {
this.setFontSize(value);
});
void core.adCenter.load();
}
@Hook('destroyed')
@ -341,6 +362,14 @@
(<StatusSwitcher>this.$refs['statusDialog']).show();
}
showAdCenter(): void {
(<AdCenterDialog>this.$refs['adCenter']).show();
}
showAdLauncher(): void {
(<AdLauncherDialog>this.$refs['adLauncher']).show();
}
showAddPmPartner(): void {
(<PmPartnerAdder>this.$refs['addPmPartnerDialog']).show();
}
@ -372,6 +401,14 @@
getImagePreview(): ImagePreview | undefined {
return this.$refs['imagePreview'] as ImagePreview;
}
adsAreRunning(): boolean {
return core.adCenter.adsAreRunning();
}
stopAllAds(): void {
core.adCenter.stopAllAds();
}
}
</script>
@ -543,5 +580,20 @@
display: none;
}
}
.adControls {
float: right;
margin-right: 0.25rem;
margin-top: 3px;
span {
color: var(--danger);
cursor: pointer;
&:hover {
color: var(--red);
}
}
}
}
</style>

162
chat/ads/AdCenter.vue Normal file
View File

@ -0,0 +1,162 @@
<template>
<modal :action="'Ad Editor'" @submit="submit" ref="dialog" @open="load" dialogClass="w-100"
:buttonText="'Save'">
<div class="form-group ad-list" v-for="(ad, index) in ads">
<label :for="'adm-content-' + index" class="control-label">Ad #{{(index + 1)}}
<a v-if="(index > 0)" @click="moveAdUp(index)" title="Move Up"><i class="fa fa-arrow-up"></i></a>
<a v-if="(index < ads.length - 1)" @click="moveAdDown(index)" title="Move Down"><i class="fa fa-arrow-down"></i></a>
<a @click="removeAd(index)" title="Remove Ad"><i class="fas fa-times-circle"></i></a>
</label>
<editor :id="'adm-content-' + index" v-model="ad.content" :hasToolbar="true" class="form-control" :maxlength="core.connection.vars.lfrp_max" :disabled="ad.disabled">
</editor>
<tagEditor :id="'adm-tags-' + index" v-model="ad.tags" placeholder="Enter one or more tags, e.g. 'romantic'" :add-tag-on-keys="[13, 188, 9, 32]" class="form-control" :disabled="ad.disabled" :add-tag-on-blur="true"></tagEditor>
<label class="control-label disable" :for="'adm-disabled-' + index">
<input type="checkbox" :id="'adm-disabled-' + index" v-model='ad.disabled' />
Disable
</label>
</div>
<button class="btn btn-outline-secondary" @click="addAd()">Add Another</button>
</modal>
</template>
<script lang="ts">
import {Component} from '@f-list/vue-ts';
import CustomDialog from '../../components/custom_dialog';
import Modal from '../../components/Modal.vue';
import {Conversation} from '../interfaces';
import l from '../localize';
import {Editor} from '../bbcode';
import core from '../core';
import { Dialog } from '../../helpers/dialog';
import InputTag from 'vue-input-tag';
import { Ad } from './ad-center';
import _ from 'lodash';
@Component({
components: {modal: Modal, editor: Editor, tagEditor: InputTag},
})
export default class AdCenterDialog extends CustomDialog {
l = l;
setting = Conversation.Setting;
ads!: Ad[];
core = core;
load(): void {
this.ads = _.cloneDeep(core.adCenter.get());
if (this.ads.length === 0) {
this.addAd();
}
}
async submit(): Promise<void> {
await core.adCenter.set(this.ads);
}
addAd(): void {
this.ads.push({
content: '',
disabled: false,
tags: []
});
}
removeAd(index: number): void {
// if (confirm('Are you sure you wish to remove this ad?')) {
if (Dialog.confirmDialog('Are you sure you wish to remove this ad?')) {
this.ads.splice(index, 1);
}
}
moveAdUp(index: number): void {
const ad = this.ads.splice(index, 1);
this.ads.splice(index - 1, 0, ad[0]);
}
moveAdDown(index: number): void {
const ad = this.ads.splice(index, 1);
this.ads.splice(index + 1, 0, ad[0]);
}
}
</script>
<style lang="scss">
.w-100 {
min-width: 70%;
}
.form-group.ad-list {
label {
font-size: 140%;
a {
padding-right: 0.3rem;
opacity:0.3;
font-size: 70%;
&:hover {
opacity:0.6
}
}
}
.bbcode-preview {
margin-top: 0;
border: 1px solid;
padding: 5px;
border-radius: 0 5px 5px 5px;
background: var(--input-bg);
border-color: var(--secondary);
}
.bbcode-editor {
border: none;
background: none;
height: auto;
textarea {
width: 100%;
color: var(--input-color);
background-color: var(--input-bg);
border-radius: 0 5px 5px 5px;
}
}
.vue-input-tag-wrapper[disabled=disabled],
textarea[disabled=disabled],
div.bbcode-toolbar[disabled=disabled] {
opacity: 0.5;
pointer-events: none;
}
.vue-input-tag-wrapper {
margin: 0.375rem 0.75rem;
width: auto;
height: auto;
padding-bottom: 0;
input {
width: 260px;
color: var(--gray-dark);
}
}
label.disable {
color: var(--gray-dark);
margin: 0.375rem 0.75rem;
font-size: 100%;
}
}
</style>

197
chat/ads/AdLauncher.vue Normal file
View File

@ -0,0 +1,197 @@
<template>
<modal action="Post Ads" @submit="submit" ref="dialog" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
<div v-if="hasAds()">
<h4>Ad Tags</h4>
<div class="form-group">
<p>Serve ads that match any one of these tags:</p>
<label class="control-label" :for="`adr-tag-${index}`" v-for="(tag,index) in tags">
<input type="checkbox" v-model="tag.value" :id="`adr-tag-${index}`" />
{{ tag.title }}
</label>
</div>
<h4>Target Channels</h4>
<div class="form-group">
<p>Serve ads on these channels:</p>
<label class="control-label" :for="`adr-channel-${index}`" v-for="(channel,index) in channels">
<input type="checkbox" v-model="channel.value" :id="`adr-channel-${index}`" />
{{ channel.title }}
</label>
</div>
<h4>Post Order</h4>
<div class="form-group">
<label class="control-label" for="adOrderRandom">
<input type="radio" v-model="adOrder" value="random" id="adOrderRandom" />
Random order
</label>
<label class="control-label" for="adOrderAdCenter">
<input type="radio" v-model="adOrder" value="ad-center" id="adOrderAdCenter" />
Follow order in Ad Center
</label>
</div>
<h4>Campaign</h4>
<div class="form-group">
<label class="control-label" for="timeoutMinutes">
Timeout
</label>
<select class="form-control" v-model="timeoutMinutes" id="timeoutMinutes">
<option v-for="timeout in timeoutOptions" :value="timeout.value">{{timeout.title}}</option>
</select>
</div>
<p class="matches">
<b>{{matchCount}}</b> ads will be used.
</p>
</div>
<div v-else>
<h4>No Ads to Post!</h4>
<p>Use the <button class="btn btn-outline-secondary" @click="openAdEditor()">Ad Editor</button> to create some ads first, then return here to post them.</p>
</div>
</modal>
</template>
<script lang="ts">
import {Component, Watch} from '@f-list/vue-ts';
import CustomDialog from '../../components/custom_dialog';
import Modal from '../../components/Modal.vue';
import core from '../core';
import _ from 'lodash';
import AdCenterDialog from './AdCenter.vue';
@Component({
components: {modal: Modal}
})
export default class AdLauncherDialog extends CustomDialog {
adOrder: 'random' | 'ad-center' = 'random';
matchCount = 0;
timeoutMinutes = 180;
tags: { value: boolean, title: string }[] = [];
channels: { value: boolean, title: string, id: string }[] = [];
timeoutOptions = [
{ value: 30, title: '30 minutes' },
{ value: 60, title: '1 hour' },
{ value: 120, title: '2 hours' },
{ value: 180, title: '3 hours' }
]
load() {
this.channels = _.map(_.filter(core.channels.joinedChannels, (c) => (c.mode === 'ads' || c.mode === 'both')),
(c) => ({ value: false, title: c.name, id: c.id }));
this.tags = _.map(core.adCenter.getActiveTags(), (t) => ({ value: false, title: t }));
this.checkCanSubmit();
}
hasAds(): boolean {
return core.adCenter.getActiveAds().length > 0;
}
@Watch('tags', { deep: true })
updateTags(): void {
this.matchCount = core.adCenter.getMatchingAds(this.getWantedTags()).length;
this.checkCanSubmit();
}
@Watch('channels', { deep: true })
updateChannels(): void {
this.checkCanSubmit();
}
checkCanSubmit() {
const channelCount = _.filter(this.channels, (channel) => channel.value).length;
const tagCount = _.filter(this.tags, (tag) => tag.value).length;
this.dialog.forceDisabled(tagCount === 0 || channelCount === 0);
}
getWantedTags(): string[] {
return _.map(_.filter(this.tags, (t) => t.value), (t) => t.title);
}
getWantedChannels(): string[] {
return _.map(_.filter(this.channels, (t) => t.value), (t) => t.id);
}
openAdEditor(): void {
this.hide();
(<AdCenterDialog>this.$parent.$refs['adCenter']).show();
}
submit(e: Event) {
const tags = this.getWantedTags();
const channelIds = this.getWantedChannels();
if (tags.length === 0) {
e.preventDefault();
alert('Select at least one tag to post');
return;
}
if (channelIds.length === 0) {
e.preventDefault();
alert('Select at least one channel to post in');
return;
}
if (!_.every(channelIds, (channelId) => {
if (core.adCenter.isSafeToOverride(channelId)) {
return true;
}
const chan = core.channels.getChannel(channelId);
if (!chan) {
return true;
}
return confirm(`Warning: This action will overwrite ads on channel ${chan.name}. Ads that are not stored in the Ad Center will be lost. Are you sure you wish to continue?`);
})) {
e.preventDefault();
return;
}
core.adCenter.schedule(
tags,
channelIds,
this.adOrder,
this.timeoutMinutes
);
this.hide();
}
}
</script>
<style lang="scss">
.adLauncher {
label {
display: block;
margin-left: 0.75rem;
color: var(--gray-dark);
}
select {
margin-left: 0.75rem;
width: auto;
padding-right: 1.5rem;
}
.matches {
margin: 0;
margin-top: 2rem;
color: var(--gray);
}
}
</style>

View File

@ -2,6 +2,24 @@
<modal :action="`Ads for ${conversation.name}`" @submit="submit" ref="dialog" @open="load()" dialogClass="w-100"
:buttonText="l('conversationSettings.save')">
<div class="phased-out-warning">
<h4>Prepare to Move</h4>
<p>
Channel-specific ads are being phased out.
Use <button class="btn btn-outline-secondary" @click="openAdEditor()">Ad Editor</button>
and
<button class="btn btn-outline-secondary" @click="openPostAds()">Post Ads</button>
instead,
always available on the left sidebar.
</p>
<p>
<button class="btn btn-outline-secondary" @click="copyAds()">Copy Channel Ads to Ad Editor</button>
</p>
</div>
<div class="form-group">
<label class="control-label" for="randomOrder">
<input type="checkbox" v-model="randomOrder" id="randomOrder" />
@ -33,6 +51,8 @@
import {Editor} from '../bbcode';
import core from '../core';
import { Dialog } from '../../helpers/dialog';
import AdCenterDialog from './AdCenter.vue';
import _ from 'lodash';
@Component({
components: {modal: Modal, editor: Editor}
@ -94,6 +114,28 @@
this.ads.splice(index + 1, 0, ad[0]);
}
openAdEditor() {
this.hide();
(<AdCenterDialog>this.$parent.$parent.$refs['adCenter']).show();
}
openPostAds() {
this.hide();
(<AdCenterDialog>this.$parent.$parent.$refs['adLauncher']).show();
}
async copyAds(): Promise<void> {
await Promise.all(_.map(
this.ads,
async (ad) => {
if (core.adCenter.isMissingFromAdCenter(ad)) {
await core.adCenter.add(ad);
}
}
));
this.openAdEditor();
}
}
</script>
@ -139,5 +181,11 @@
}
}
}
.phased-out-warning {
border: 1px solid orange;
padding: 15px;
margin-bottom: 2rem;
}
</style>

118
chat/ads/ad-center.ts Normal file
View File

@ -0,0 +1,118 @@
import _ from 'lodash';
import core from '../core';
import { Conversation } from '../interfaces';
export interface Ad {
disabled: boolean;
tags: string[];
content: string;
}
export class AdCenter {
private ads: Ad[] = [];
async load(): Promise<void> {
this.ads = (await core.settingsStore.get('ads')) || [];
}
get(): Ad[] {
return this.ads;
}
async set(ads: Ad[]): Promise<void> {
const cleanedAds = _.map(
_.filter(ads, (ad) => (ad.content.trim().length > 0)),
(ad): Ad => {
const filteredTags = _.map(_.filter(ad.tags, (tag) => tag.trim().length > 0), (tag) => tag.trim());
return {
...ad,
content: ad.content.trim(),
tags: filteredTags.length > 0 ? filteredTags : ['default']
};
}
);
this.ads = cleanedAds;
await core.settingsStore.set('ads', cleanedAds);
}
async add(content: string, tags: string[] = ['default']): Promise<void> {
this.ads.push({ content, tags, disabled: false });
await this.set(this.ads);
}
getTags(ads: Ad[] = this.ads): string[] {
return _.uniq(_.flatten(_.map(ads, (ad) => ad.tags)));
}
getActiveTags(): string[] {
return this.getTags(this.getActiveAds());
}
getActiveAds(): Ad[] {
return _.filter(this.ads, (ad) => !ad.disabled);
}
getMatchingAds(tags: string[]): Ad[] {
return _.filter(this.ads, (ad) => !ad.disabled && _.intersection(ad.tags, tags).length > 0);
}
schedule(tags: string[], channelIds: string[], order: 'random' | 'ad-center', timeoutMinutes: number): void {
const ads = this.getMatchingAds(tags);
_.each(channelIds, (channelId) => this.scheduleForChannel(channelId, ads, order, timeoutMinutes));
}
adsAreRunning(): boolean {
return !_.every(core.conversations.channelConversations, (conv) => !conv.isSendingAutomatedAds());
}
stopAllAds(): void {
_.each(core.conversations.channelConversations, (conv) => conv.adManager.stop());
}
protected getConversation(channelId: string): Conversation.ChannelConversation | undefined {
return core.conversations.channelConversations.find((c) => c.channel.id === channelId);
}
isMissingFromAdCenter(adContentToTest: string): boolean {
const cleaned = adContentToTest.trim().toLowerCase();
return _.every(this.ads, (ad) => ad.content.trim().toLowerCase() !== cleaned);
}
isSafeToOverride(channelId: string): boolean {
const conv = this.getConversation(channelId);
if (!conv) {
return true;
}
return _.every(conv.settings.adSettings.ads, (adContent) => !this.isMissingFromAdCenter(adContent));
}
// tslint:disable-next-line:prefer-function-over-method
protected scheduleForChannel(channelId: string, ads: Ad[], order: 'random' | 'ad-center', timeoutMinutes: number): void {
const conv = this.getConversation(channelId);
if (!conv) {
return;
}
conv.settings = {
...conv.settings,
adSettings: {
...conv.settings.adSettings,
ads: _.map(_.filter(ads, (ad) => !ad.disabled && ad.content.trim().length > 0), (ad) => ad.content.trim()),
randomOrder: order === 'random'
}
};
conv.adManager.stop();
conv.adManager.start(timeoutMinutes * 60 * 1000);
}
}

View File

@ -163,7 +163,7 @@ export class AdManager {
return this.firstPost;
}
start(): void {
start(timeoutMs = AdManager.POSTING_PERIOD): void {
const chanConv = (<Conversation.ChannelConversation>this.conversation);
const initialWait = Math.max(
@ -180,7 +180,7 @@ export class AdManager {
(this.conversation.settings.adSettings.lastAdTimestamp || 0) + (core.connection.vars.lfrp_flood * 1000)
));
this.expireDue = new Date(Date.now() + AdManager.POSTING_PERIOD);
this.expireDue = new Date(Date.now() + timeoutMs);
this.adMap = this.generateAdMap();
// tslint:disable-next-line: no-unnecessary-type-assertion

View File

@ -6,6 +6,7 @@ import {Settings as SettingsImpl} from './common';
import Conversations from './conversations';
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
import { AdCoordinatorGuest } from './ads/ad-coordinator-guest';
import { AdCenter } from './ads/ad-center';
import { GeneralSettings } from '../electron/common';
import { SiteSession } from '../site/site-session';
import _ from 'lodash';
@ -64,6 +65,7 @@ const data = {
notifications: <Notifications | undefined>undefined,
cache: <CacheManager | undefined>undefined,
adCoordinator: <AdCoordinatorGuest | undefined>undefined,
adCenter: <AdCenter | undefined>undefined,
siteSession: <SiteSession | undefined>undefined,
register<K extends 'characters' | 'conversations' | 'channels'>(module: K, subState: VueState[K]): void {
@ -96,6 +98,7 @@ export function init(
data.notifications = new notificationsClass();
data.cache = new CacheManager();
data.adCoordinator = new AdCoordinatorGuest();
data.adCenter = new AdCenter();
data.siteSession = new SiteSession();
(data.state as any).generalSettings = settings;
@ -126,6 +129,7 @@ export interface Core {
readonly notifications: Notifications
readonly cache: CacheManager
readonly adCoordinator: AdCoordinatorGuest;
readonly adCenter: AdCenter;
readonly siteSession: SiteSession;
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void

View File

@ -7,6 +7,7 @@ import { SmartFilterSettings } from '../learn/filter/types';
export {Connection, Channel, Character} from '../fchat/interfaces';
export const userStatuses: ReadonlyArray<Character.Status> = ['online', 'looking', 'away', 'busy', 'dnd'];
export const channelModes: ReadonlyArray<Channel.Mode> = ['chat', 'ads', 'both'];
import { Ad } from './ads/ad-center';
export namespace Conversation {
interface BaseMessage {
@ -185,6 +186,7 @@ export namespace Settings {
searchHistory: (ExtendedSearchData | SearchData)[]
hideNonMatchingAds: boolean
hideProfileComparisonSummary: boolean
ads: Ad[]
};
export interface Store {

View File

@ -14,7 +14,7 @@
</div>
<div class="modal-footer" v-if="buttons">
<button type="button" class="btn btn-secondary" @click="hideWithCheck" v-if="showCancel">Cancel</button>
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="disabled">
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="shouldBeDisabled()">
{{submitText}}
</button>
</div>
@ -62,12 +62,22 @@
@Prop
readonly buttonText?: string;
isShown = false;
keepOpen = false;
forcedDisabled = false;
get submitText(): string {
return this.buttonText !== undefined ? this.buttonText : this.action;
}
forceDisabled(disabled: boolean): void {
this.forcedDisabled = disabled;
}
shouldBeDisabled(): boolean {
return this.disabled || this.forcedDisabled;
}
submit(e: Event): void {
this.$emit('submit', e);
if(!e.defaultPrevented) this.hideWithCheck();

View File

@ -10,8 +10,8 @@ export default class CustomDialog extends Vue {
return <Modal>this.$children[0];
}
show(): void {
this.dialog.show();
show(keepOpen?: boolean): void {
this.dialog.show(keepOpen);
}
hide(): void {

View File

@ -50,7 +50,7 @@ theme: jekyll-theme-slate
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
download:
version: 1.19.3
version: 1.20.0
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%

View File

@ -305,6 +305,7 @@ export class SettingsStore implements Settings.Store {
const file = path.join(getSettingsDir(character), key);
return <Settings.Keys[K]>JSON.parse(fs.readFileSync(file, 'utf8'));
} catch(e) {
console.error('READ KEY FAILURE', e, key, character);
return undefined;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "1.19.3",
"version": "1.20.0",
"author": "The F-List Team and Mister Stallion (Esq.)",
"description": "F-List.net Chat Client",
"main": "main.js",

4
index.d.ts vendored
View File

@ -6,3 +6,7 @@ declare module "!!raw-loader!*" {
declare module "any-ascii" {
export default function anyAscii(s: string): string;
}
declare module 'vue-input-tag' {
}

View File

@ -730,8 +730,8 @@ export const kinkMatchScoreMap = {
favorite: {
favorite: 1,
yes: 0.35,
maybe: -0.35,
no: -1
maybe: -0.5,
no: -1.5
},
yes: {
@ -749,8 +749,8 @@ export const kinkMatchScoreMap = {
},
no: {
favorite: -1,
yes: -0.35,
favorite: -1.5,
yes: -0.5,
maybe: 0,
no: 0
}

View File

@ -1,6 +1,6 @@
{
"name": "f-list-rising",
"version": "1.19.3",
"version": "1.20.0",
"author": "The F-List Team and and Mister Stallion (Esq.)",
"description": "A heavily modded F-Chat 3.0 client for F-List",
"license": "MIT",
@ -48,6 +48,7 @@
"tslint": "^6.1.3",
"typescript": "^3.9.7",
"vue": "2.6.12",
"vue-input-tag": "^2.0.7",
"vue-loader": "^15.9.8",
"vue-template-compiler": "2.6.12",
"webpack": "5.8.0"

View File

@ -7504,6 +7504,13 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
vue-input-tag@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/vue-input-tag/-/vue-input-tag-2.0.7.tgz#daea4db750df34a8c87605f88455dc5f64e1052b"
integrity sha512-MiTVXg/OQ0SEPmQRPBw77akCrANAn/MUvo8U5fXLEwwtzGAASpGSru2uBvCMakDB5zF0ZV3ynkX/V8tOfSU6qA==
dependencies:
vue "^2.5.17"
vue-loader@15.9.8, vue-loader@^15.9.8:
version "15.9.8"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.8.tgz#4b0f602afaf66a996be1e534fb9609dc4ab10e61"
@ -7536,7 +7543,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.20:
vue@2.6.12, 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==