3.0.13
This commit is contained in:
parent
a5e57cd52c
commit
8b0fe1aaed
|
@ -38,26 +38,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {BBCodeElement} from '../chat/bbcode';
|
|
||||||
import {getKey} from '../chat/common';
|
import {getKey} from '../chat/common';
|
||||||
import {Keys} from '../keys';
|
import {Keys} from '../keys';
|
||||||
import {CoreBBCodeParser, urlRegex} from './core';
|
import {BBCodeElement, CoreBBCodeParser, urlRegex} from './core';
|
||||||
import {defaultButtons, EditorButton, EditorSelection} from './editor';
|
import {defaultButtons, EditorButton, EditorSelection} from './editor';
|
||||||
import {BBCodeParser} from './parser';
|
import {BBCodeParser} from './parser';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Editor extends Vue {
|
export default class Editor extends Vue {
|
||||||
@Prop()
|
@Prop
|
||||||
readonly extras?: EditorButton[];
|
readonly extras?: EditorButton[];
|
||||||
@Prop({default: 1000})
|
@Prop({default: 1000})
|
||||||
readonly maxlength!: number;
|
readonly maxlength!: number;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly classes?: string;
|
readonly classes?: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly value?: string;
|
readonly value?: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly placeholder?: string;
|
readonly placeholder?: string;
|
||||||
@Prop({default: true})
|
@Prop({default: true})
|
||||||
readonly hasToolbar!: boolean;
|
readonly hasToolbar!: boolean;
|
||||||
|
@ -256,7 +255,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaste(e: ClipboardEvent): void {
|
onPaste(e: ClipboardEvent): void {
|
||||||
const data = e.clipboardData.getData('text/plain');
|
const data = e.clipboardData!.getData('text/plain');
|
||||||
if(!this.isShiftPressed && urlRegex.test(data)) {
|
if(!this.isShiftPressed && urlRegex.test(data)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.applyText(`[url=${data}]`, '[/url]');
|
this.applyText(`[url=${data}]`, '[/url]');
|
||||||
|
|
|
@ -4,6 +4,8 @@ const urlFormat = '((?:https?|ftps?|irc)://[^\\s/$.?#"\']+\\.[^\\s"]+)';
|
||||||
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
|
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
|
||||||
export const urlRegex = new RegExp(`^${urlFormat}$`);
|
export const urlRegex = new RegExp(`^${urlFormat}$`);
|
||||||
|
|
||||||
|
export type BBCodeElement = HTMLElement & {cleanup?(): void};
|
||||||
|
|
||||||
function domain(url: string): string | undefined {
|
function domain(url: string): string | undefined {
|
||||||
const pieces = urlRegex.exec(url);
|
const pieces = urlRegex.exec(url);
|
||||||
if(pieces === null) return;
|
if(pieces === null) return;
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export const enum InlineDisplayMode {DISPLAY_ALL, DISPLAY_SFW, DISPLAY_NONE}
|
|
|
@ -185,9 +185,9 @@ export class BBCodeParser {
|
||||||
if(parent !== undefined)
|
if(parent !== undefined)
|
||||||
parent.appendChild(document.createTextNode(input.substring(mark, selfAllowed ? tagStart : i + 1)));
|
parent.appendChild(document.createTextNode(input.substring(mark, selfAllowed ? tagStart : i + 1)));
|
||||||
return i;
|
return i;
|
||||||
} else if(!selfAllowed)
|
}
|
||||||
return mark - 1;
|
if(!selfAllowed) return mark - 1;
|
||||||
else if(isAllowed(tagKey))
|
if(isAllowed(tagKey))
|
||||||
this.warning(`Unexpected closing ${tagKey} tag. Needed ${self} tag instead.`);
|
this.warning(`Unexpected closing ${tagKey} tag. Needed ${self} tag instead.`);
|
||||||
} else if(isAllowed(tagKey)) this.warning(`Found closing ${tagKey} tag that was never opened.`);
|
} else if(isAllowed(tagKey)) this.warning(`Found closing ${tagKey} tag that was never opened.`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import {InlineImage} from '../interfaces';
|
import {InlineDisplayMode, InlineImage} from '../interfaces';
|
||||||
|
import * as Utils from '../site/utils';
|
||||||
import {CoreBBCodeParser} from './core';
|
import {CoreBBCodeParser} from './core';
|
||||||
import {InlineDisplayMode} from './interfaces';
|
|
||||||
import {BBCodeCustomTag, BBCodeSimpleTag, BBCodeTextTag} from './parser';
|
import {BBCodeCustomTag, BBCodeSimpleTag, BBCodeTextTag} from './parser';
|
||||||
|
|
||||||
interface StandardParserSettings {
|
|
||||||
siteDomain: string
|
|
||||||
staticDomain: string
|
|
||||||
animatedIcons: boolean
|
|
||||||
inlineDisplayMode: InlineDisplayMode
|
|
||||||
}
|
|
||||||
|
|
||||||
const usernameRegex = /^[a-zA-Z0-9_\-\s]+$/;
|
const usernameRegex = /^[a-zA-Z0-9_\-\s]+$/;
|
||||||
|
|
||||||
export class StandardBBCodeParser extends CoreBBCodeParser {
|
export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
allowInlines = true;
|
|
||||||
inlines: {[key: string]: InlineImage | undefined} | undefined;
|
inlines: {[key: string]: InlineImage | undefined} | undefined;
|
||||||
|
|
||||||
createInline(inline: InlineImage): HTMLElement {
|
createInline(inline: InlineImage): HTMLElement {
|
||||||
|
@ -23,12 +15,12 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
const el = this.createElement('img');
|
const el = this.createElement('img');
|
||||||
el.className = 'inline-image';
|
el.className = 'inline-image';
|
||||||
el.title = el.alt = inline.name;
|
el.title = el.alt = inline.name;
|
||||||
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
el.src = `${Utils.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||||
outerEl.appendChild(el);
|
outerEl.appendChild(el);
|
||||||
return outerEl;
|
return outerEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public settings: StandardParserSettings) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const hrTag = new BBCodeSimpleTag('hr', 'hr', [], []);
|
const hrTag = new BBCodeSimpleTag('hr', 'hr', [], []);
|
||||||
hrTag.noClosingTag = true;
|
hrTag.noClosingTag = true;
|
||||||
|
@ -107,7 +99,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
if(!usernameRegex.test(content))
|
if(!usernameRegex.test(content))
|
||||||
return;
|
return;
|
||||||
const a = parser.createElement('a');
|
const a = parser.createElement('a');
|
||||||
a.href = `${this.settings.siteDomain}c/${content}`;
|
a.href = `${Utils.siteDomain}c/${content}`;
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
a.className = 'character-link';
|
a.className = 'character-link';
|
||||||
a.appendChild(document.createTextNode(content));
|
a.appendChild(document.createTextNode(content));
|
||||||
|
@ -120,10 +112,10 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
if(!usernameRegex.test(content))
|
if(!usernameRegex.test(content))
|
||||||
return;
|
return;
|
||||||
const a = parser.createElement('a');
|
const a = parser.createElement('a');
|
||||||
a.href = `${this.settings.siteDomain}c/${content}`;
|
a.href = `${Utils.siteDomain}c/${content}`;
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
const img = parser.createElement('img');
|
const img = parser.createElement('img');
|
||||||
img.src = `${this.settings.staticDomain}images/avatar/${content.toLowerCase()}.png`;
|
img.src = `${Utils.staticDomain}images/avatar/${content.toLowerCase()}.png`;
|
||||||
img.className = 'character-avatar icon';
|
img.className = 'character-avatar icon';
|
||||||
img.title = img.alt = content;
|
img.title = img.alt = content;
|
||||||
a.appendChild(img);
|
a.appendChild(img);
|
||||||
|
@ -137,10 +129,10 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
if(!usernameRegex.test(content))
|
if(!usernameRegex.test(content))
|
||||||
return;
|
return;
|
||||||
let extension = '.gif';
|
let extension = '.gif';
|
||||||
if(!this.settings.animatedIcons)
|
if(!Utils.settings.animateEicons)
|
||||||
extension = '.png';
|
extension = '.png';
|
||||||
const img = parser.createElement('img');
|
const img = parser.createElement('img');
|
||||||
img.src = `${this.settings.staticDomain}images/eicon/${content.toLowerCase()}${extension}`;
|
img.src = `${Utils.staticDomain}images/eicon/${content.toLowerCase()}${extension}`;
|
||||||
img.className = 'character-avatar icon';
|
img.className = 'character-avatar icon';
|
||||||
img.title = img.alt = content;
|
img.title = img.alt = content;
|
||||||
parent.appendChild(img);
|
parent.appendChild(img);
|
||||||
|
@ -148,15 +140,11 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
}));
|
}));
|
||||||
this.addTag(new BBCodeTextTag('img', (p, parent, param, content) => {
|
this.addTag(new BBCodeTextTag('img', (p, parent, param, content) => {
|
||||||
const parser = <StandardBBCodeParser>p;
|
const parser = <StandardBBCodeParser>p;
|
||||||
if(!this.allowInlines) {
|
|
||||||
parser.warning('Inline images are not allowed here.');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(typeof parser.inlines === 'undefined') {
|
if(typeof parser.inlines === 'undefined') {
|
||||||
parser.warning('This page does not support inline images.');
|
parser.warning('This page does not support inline images.');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const displayMode = this.settings.inlineDisplayMode;
|
const displayMode = Utils.settings.inlineDisplayMode;
|
||||||
if(!/^\d+$/.test(param)) {
|
if(!/^\d+$/.test(param)) {
|
||||||
parser.warning('img tag parameters must be numbers.');
|
parser.warning('img tag parameters must be numbers.');
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -188,10 +176,4 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
return element;
|
return element;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export let standardParser: StandardBBCodeParser;
|
|
||||||
|
|
||||||
export function initParser(settings: StandardParserSettings): void {
|
|
||||||
standardParser = new StandardBBCodeParser(settings);
|
|
||||||
}
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {CreateElement, FunctionalComponentOptions, RenderContext, VNode} from 'vue';
|
||||||
|
import {DefaultProps, RecordPropsDefinition} from 'vue/types/options'; //tslint:disable-line:no-submodule-imports
|
||||||
|
import {BBCodeElement} from './core';
|
||||||
|
import {BBCodeParser} from './parser';
|
||||||
|
|
||||||
|
export const BBCodeView = (parser: BBCodeParser): FunctionalComponentOptions<DefaultProps, RecordPropsDefinition<DefaultProps>> => ({
|
||||||
|
functional: true,
|
||||||
|
render(createElement: CreateElement, context: RenderContext): VNode {
|
||||||
|
/*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this
|
||||||
|
context.data.hook = {
|
||||||
|
insert(node: VNode): void {
|
||||||
|
node.elm!.appendChild(parser.parseEverything(
|
||||||
|
context.props.text !== undefined ? context.props.text : context.props.unsafeText));
|
||||||
|
if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm);
|
||||||
|
},
|
||||||
|
destroy(node: VNode): void {
|
||||||
|
const element = (<BBCodeElement>(<Element>node.elm).firstChild);
|
||||||
|
if(element.cleanup !== undefined) element.cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const vnode = createElement('span', context.data);
|
||||||
|
vnode.key = context.props.text;
|
||||||
|
return vnode;
|
||||||
|
//tslint:enable
|
||||||
|
}
|
||||||
|
});
|
|
@ -28,10 +28,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Hook} from '@f-list/vue-ts';
|
import {Component, Hook} from '@f-list/vue-ts';
|
||||||
import Axios from 'axios';
|
import Axios from 'axios';
|
||||||
|
import {BBCodeView} from '../bbcode/view';
|
||||||
import CustomDialog from '../components/custom_dialog';
|
import CustomDialog from '../components/custom_dialog';
|
||||||
import FilterableSelect from '../components/FilterableSelect.vue';
|
import FilterableSelect from '../components/FilterableSelect.vue';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import {BBCodeView} from './bbcode';
|
|
||||||
import {characterImage} from './common';
|
import {characterImage} from './common';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Character, Connection} from './interfaces';
|
import {Character, Connection} from './interfaces';
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView}
|
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView(core.bbCodeParser)}
|
||||||
})
|
})
|
||||||
export default class CharacterSearch extends CustomDialog {
|
export default class CharacterSearch extends CustomDialog {
|
||||||
l = l;
|
l = l;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {InlineDisplayMode} from '../interfaces';
|
||||||
<template>
|
<template>
|
||||||
<div style="display:flex; flex-direction: column; height:100%; justify-content: center">
|
<div style="display:flex; flex-direction: column; height:100%; justify-content: center">
|
||||||
<div class="card bg-light" style="width:400px;max-width:100%;margin:0 auto" v-if="!connected">
|
<div class="card bg-light" style="width:400px;max-width:100%;margin:0 auto" v-if="!connected">
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{l('login.selectCharacter')}}</h4>
|
<h4 class="card-title">{{l('login.selectCharacter')}}</h4>
|
||||||
<select v-model="selectedCharacter" class="form-control custom-select">
|
<select v-model="selectedCharacter" class="form-control custom-select">
|
||||||
<option v-for="character in ownCharacters" :value="character">{{character}}</option>
|
<option v-for="character in ownCharacters" :value="character">{{character.name}}</option>
|
||||||
</select>
|
</select>
|
||||||
<div style="text-align:right;margin-top:10px">
|
<div style="text-align:right;margin-top:10px">
|
||||||
<button class="btn btn-primary" @click="connect" :disabled="connecting">
|
<button class="btn btn-primary" @click="connect" :disabled="connecting">
|
||||||
|
@ -35,15 +36,14 @@
|
||||||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import Channels from '../fchat/channels';
|
import {InlineDisplayMode, SimpleCharacter} from '../interfaces';
|
||||||
import Characters from '../fchat/characters';
|
|
||||||
import {Keys} from '../keys';
|
import {Keys} from '../keys';
|
||||||
import ChatView from './ChatView.vue';
|
import ChatView from './ChatView.vue';
|
||||||
import {errorToString, getKey} from './common';
|
import {errorToString, getKey} from './common';
|
||||||
import Conversations from './conversations';
|
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import l from './localize';
|
import l from './localize';
|
||||||
import Logs from './Logs.vue';
|
import Logs from './Logs.vue';
|
||||||
|
import {init as profileApiInit} from './profile_api';
|
||||||
|
|
||||||
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string};
|
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string};
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
str = `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]${str}[/${node.bbcodeTag}]`;
|
str = `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]${str}[/${node.bbcodeTag}]`;
|
||||||
if(node.nextSibling !== null && !flags.endFound) {
|
if(node.nextSibling !== null && !flags.endFound) {
|
||||||
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
|
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
|
||||||
str += scanNode(node.nextSibling, end, range, flags);
|
str += scanNode(node.nextSibling!, end, range, flags);
|
||||||
}
|
}
|
||||||
if(node.parentElement === null) return str;
|
if(node.parentElement === null) return str;
|
||||||
return copyNode(str, node.parentNode!, end, range, flags);
|
return copyNode(str, node.parentNode!, end, range, flags);
|
||||||
|
@ -78,11 +78,12 @@
|
||||||
})
|
})
|
||||||
export default class Chat extends Vue {
|
export default class Chat extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly ownCharacters!: string[];
|
readonly ownCharacters!: SimpleCharacter[];
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly defaultCharacter!: string | undefined;
|
readonly defaultCharacter!: number;
|
||||||
selectedCharacter = this.defaultCharacter || this.ownCharacters[0]; //tslint:disable-line:strict-boolean-expressions
|
//tslint:disable-next-line:strict-boolean-expressions
|
||||||
@Prop()
|
selectedCharacter = this.ownCharacters.find((x) => x.id === this.defaultCharacter) || this.ownCharacters[0];
|
||||||
|
@Prop
|
||||||
readonly version?: string;
|
readonly version?: string;
|
||||||
error = '';
|
error = '';
|
||||||
connecting = false;
|
connecting = false;
|
||||||
|
@ -110,7 +111,7 @@
|
||||||
} else
|
} else
|
||||||
startValue = start.nodeValue!.substring(range.startOffset, start === range.endContainer ? range.endOffset : undefined);
|
startValue = start.nodeValue!.substring(range.startOffset, start === range.endContainer ? range.endOffset : undefined);
|
||||||
if(end instanceof HTMLElement && range.endOffset > 0) end = end.childNodes[range.endOffset - 1];
|
if(end instanceof HTMLElement && range.endOffset > 0) end = end.childNodes[range.endOffset - 1];
|
||||||
e.clipboardData.setData('text/plain', copyNode(startValue, start, end, range, {}));
|
e.clipboardData!.setData('text/plain', copyNode(startValue, start, end, range, {}));
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
|
@ -120,10 +121,7 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
core.register('characters', Characters(core.connection));
|
core.connection.onEvent('closed', (isReconnect) => {
|
||||||
core.register('channels', Channels(core.connection, core.characters));
|
|
||||||
core.register('conversations', Conversations());
|
|
||||||
core.connection.onEvent('closed', async(isReconnect) => {
|
|
||||||
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
|
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
|
||||||
if(this.connected) core.notifications.playSound('logout');
|
if(this.connected) core.notifications.playSound('logout');
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
|
@ -132,9 +130,13 @@
|
||||||
});
|
});
|
||||||
core.connection.onEvent('connecting', async() => {
|
core.connection.onEvent('connecting', async() => {
|
||||||
this.connecting = true;
|
this.connecting = true;
|
||||||
|
profileApiInit({
|
||||||
|
defaultCharacter: this.defaultCharacter, animateEicons: core.state.settings.animatedEicons, fuzzyDates: true,
|
||||||
|
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
|
||||||
|
}, this.ownCharacters);
|
||||||
if(core.state.settings.notifications) await core.notifications.requestPermission();
|
if(core.state.settings.notifications) await core.notifications.requestPermission();
|
||||||
});
|
});
|
||||||
core.connection.onEvent('connected', async() => {
|
core.connection.onEvent('connected', () => {
|
||||||
(<Modal>this.$refs['reconnecting']).hide();
|
(<Modal>this.$refs['reconnecting']).hide();
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
|
@ -165,7 +167,7 @@
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
this.connecting = true;
|
this.connecting = true;
|
||||||
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
|
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
|
||||||
core.connection.connect(this.selectedCharacter);
|
core.connection.connect(this.selectedCharacter.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -29,7 +29,8 @@
|
||||||
<div class="list-group conversation-nav" ref="privateConversations">
|
<div class="list-group conversation-nav" ref="privateConversations">
|
||||||
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
||||||
:class="getClasses(conversation)" :data-character="conversation.character.name" data-touch="false"
|
:class="getClasses(conversation)" :data-character="conversation.character.name" data-touch="false"
|
||||||
class="list-group-item list-group-item-action item-private" :key="conversation.key" @click.middle="conversation.close()">
|
class="list-group-item list-group-item-action item-private" :key="conversation.key"
|
||||||
|
@click.middle.prevent="conversation.close()">
|
||||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<span>{{conversation.character.name}}</span>
|
<span>{{conversation.character.name}}</span>
|
||||||
|
@ -39,8 +40,8 @@
|
||||||
:class="{'fa-comment-dots': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
:class="{'fa-comment-dots': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||||
></span>
|
></span>
|
||||||
<span style="flex:1"></span>
|
<span style="flex:1"></span>
|
||||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}"
|
||||||
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
@click="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||||
<span class="fas fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
<span class="fas fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
<div class="list-group conversation-nav" ref="channelConversations">
|
<div class="list-group conversation-nav" ref="channelConversations">
|
||||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
<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"
|
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel" :key="conversation.key"
|
||||||
@click.middle="conversation.close()">
|
@click.middle.prevent="conversation.close()">
|
||||||
<span class="name">{{conversation.name}}</span>
|
<span class="name">{{conversation.name}}</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" :aria-label="l('chat.pinTab')"
|
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" :aria-label="l('chat.pinTab')"
|
||||||
|
@ -145,6 +146,7 @@
|
||||||
this.setFontSize(core.state.settings.fontSize);
|
this.setFontSize(core.state.settings.fontSize);
|
||||||
Sortable.create(<HTMLElement>this.$refs['privateConversations'], {
|
Sortable.create(<HTMLElement>this.$refs['privateConversations'], {
|
||||||
animation: 50,
|
animation: 50,
|
||||||
|
fallbackTolerance: 5,
|
||||||
onEnd: async(e) => {
|
onEnd: async(e) => {
|
||||||
if(e.oldIndex === e.newIndex) return;
|
if(e.oldIndex === e.newIndex) return;
|
||||||
return core.conversations.privateConversations[e.oldIndex!].sort(e.newIndex!);
|
return core.conversations.privateConversations[e.oldIndex!].sort(e.newIndex!);
|
||||||
|
@ -152,6 +154,7 @@
|
||||||
});
|
});
|
||||||
Sortable.create(<HTMLElement>this.$refs['channelConversations'], {
|
Sortable.create(<HTMLElement>this.$refs['channelConversations'], {
|
||||||
animation: 50,
|
animation: 50,
|
||||||
|
fallbackTolerance: 5,
|
||||||
onEnd: async(e) => {
|
onEnd: async(e) => {
|
||||||
if(e.oldIndex === e.newIndex) return;
|
if(e.oldIndex === e.newIndex) return;
|
||||||
return core.conversations.channelConversations[e.oldIndex!].sort(e.newIndex!);
|
return core.conversations.channelConversations[e.oldIndex!].sort(e.newIndex!);
|
||||||
|
@ -256,13 +259,8 @@
|
||||||
overrideEl.id = 'overrideFontSize';
|
overrideEl.id = 'overrideFontSize';
|
||||||
document.body.appendChild(overrideEl);
|
document.body.appendChild(overrideEl);
|
||||||
const sheet = <CSSStyleSheet>overrideEl.sheet;
|
const sheet = <CSSStyleSheet>overrideEl.sheet;
|
||||||
const selectorList = ['#chatView', '.btn', '.form-control'];
|
sheet.insertRule(`#chatView, .btn, .form-control, .custom-select { font-size: ${fontSize}px; }`, sheet.cssRules.length);
|
||||||
for(const selector of selectorList)
|
sheet.insertRule(`.form-control, select.form-control { line-height: 1.428571429 }`, sheet.cssRules.length);
|
||||||
sheet.insertRule(`${selector} { font-size: ${fontSize}px; }`, sheet.cssRules.length);
|
|
||||||
|
|
||||||
const lineHeight = 1.428571429;
|
|
||||||
sheet.insertRule(`.form-control { line-height: ${lineHeight} }`, sheet.cssRules.length);
|
|
||||||
sheet.insertRule(`select.form-control { line-height: ${lineHeight} }`, sheet.cssRules.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logOut(): void {
|
logOut(): void {
|
||||||
|
@ -419,6 +417,7 @@
|
||||||
#sidebar {
|
#sidebar {
|
||||||
.body a.btn {
|
.body a.btn {
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
@media (min-width: breakpoint-min(md)) {
|
@media (min-width: breakpoint-min(md)) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div style="height:100%;display:flex;flex-direction:column;flex:1;margin:0 5px;position:relative" id="conversation">
|
<div style="height:100%;display:flex;flex-direction:column;flex:1;margin:0 5px;position:relative" id="conversation">
|
||||||
<div style="display:flex" v-if="isPrivate(conversation)" class="header">
|
<div style="display:flex" v-if="isPrivate(conversation)" class="header">
|
||||||
<img :src="characterImage" style="height:60px;width:60px;margin-right:10px" v-if="settings.showAvatars"/>
|
<img :src="characterImage" style="height:60px;width:60px;margin-right:10px" v-if="settings.showAvatars"/>
|
||||||
<div style="flex:1;position:relative;display:flex;flex-direction:column">
|
<div style="flex:1;position:relative;display:flex;flex-direction:column;user-select:text">
|
||||||
<div>
|
<div>
|
||||||
<user :character="conversation.character"></user>
|
<user :character="conversation.character"></user>
|
||||||
<a href="#" @click.prevent="showLogs()" class="btn">
|
<a href="#" @click.prevent="showLogs()" class="btn">
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
<a href="#" @click.prevent="reportDialog.report()" class="btn">
|
<a href="#" @click.prevent="reportDialog.report()" class="btn">
|
||||||
<span class="fa fa-exclamation-triangle"></span><span class="btn-text">{{l('chat.report')}}</span></a>
|
<span class="fa fa-exclamation-triangle"></span><span class="btn-text">{{l('chat.report')}}</span></a>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow:auto;max-height:50px">
|
<div style="overflow:auto;overflow-x:hidden;max-height:50px;user-select:text">
|
||||||
{{l('status.' + conversation.character.status)}}
|
{{l('status.' + conversation.character.status)}}
|
||||||
<span v-show="conversation.character.statusText"> – <bbcode :text="conversation.character.statusText"></bbcode></span>
|
<span v-show="conversation.character.statusText"> – <bbcode :text="conversation.character.statusText"></bbcode></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,9 +129,10 @@
|
||||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
||||||
|
import {BBCodeView} from '../bbcode/view';
|
||||||
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
||||||
import {Keys} from '../keys';
|
import {Keys} from '../keys';
|
||||||
import {BBCodeView, Editor} from './bbcode';
|
import {Editor} from './bbcode';
|
||||||
import CommandHelp from './CommandHelp.vue';
|
import CommandHelp from './CommandHelp.vue';
|
||||||
import {characterImage, getByteLength, getKey} from './common';
|
import {characterImage, getByteLength, getKey} from './common';
|
||||||
import ConversationSettings from './ConversationSettings.vue';
|
import ConversationSettings from './ConversationSettings.vue';
|
||||||
|
@ -148,7 +149,7 @@
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
user: UserView, 'bbcode-editor': Editor, 'manage-channel': ManageChannel, settings: ConversationSettings,
|
user: UserView, 'bbcode-editor': Editor, 'manage-channel': ManageChannel, settings: ConversationSettings,
|
||||||
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp
|
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView(core.bbCodeParser), 'command-help': CommandHelp
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class ConversationView extends Vue {
|
export default class ConversationView extends Vue {
|
||||||
|
@ -281,8 +282,13 @@
|
||||||
if(this.messageView.scrollTop < 20) {
|
if(this.messageView.scrollTop < 20) {
|
||||||
if(!this.scrolledUp) {
|
if(!this.scrolledUp) {
|
||||||
const firstMessage = this.messageView.firstElementChild;
|
const firstMessage = this.messageView.firstElementChild;
|
||||||
if(this.conversation.loadMore() && firstMessage !== null)
|
if(this.conversation.loadMore() && firstMessage !== null) {
|
||||||
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop, 0));
|
this.messageView.style.overflow = 'hidden';
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop;
|
||||||
|
this.messageView.style.overflow = 'auto';
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.scrolledUp = true;
|
this.scrolledUp = true;
|
||||||
} else this.scrolledUp = false;
|
} else this.scrolledUp = false;
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
components: {modal: Modal, 'message-view': MessageView, 'filterable-select': FilterableSelect}
|
components: {modal: Modal, 'message-view': MessageView, 'filterable-select': FilterableSelect}
|
||||||
})
|
})
|
||||||
export default class Logs extends CustomDialog {
|
export default class Logs extends CustomDialog {
|
||||||
@Prop()
|
@Prop
|
||||||
readonly conversation?: Conversation;
|
readonly conversation?: Conversation;
|
||||||
conversations: LogInterface.Conversation[] = [];
|
conversations: LogInterface.Conversation[] = [];
|
||||||
selectedConversation: LogInterface.Conversation | undefined;
|
selectedConversation: LogInterface.Conversation | undefined;
|
||||||
|
|
|
@ -16,9 +16,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Hook} from '@f-list/vue-ts';
|
import {Component, Hook} from '@f-list/vue-ts';
|
||||||
|
import {BBCodeElement} from '../bbcode/core';
|
||||||
import CustomDialog from '../components/custom_dialog';
|
import CustomDialog from '../components/custom_dialog';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import BBCodeParser, {BBCodeElement} from './bbcode';
|
import BBCodeParser from './bbcode';
|
||||||
import {errorToString, messageToString} from './common';
|
import {errorToString, messageToString} from './common';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Character, Conversation} from './interfaces';
|
import {Character, Conversation} from './interfaces';
|
||||||
|
|
|
@ -20,9 +20,9 @@
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Sidebar extends Vue {
|
export default class Sidebar extends Vue {
|
||||||
@Prop()
|
@Prop
|
||||||
readonly right?: true;
|
readonly right?: true;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly label?: string;
|
readonly label?: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly icon!: string;
|
readonly icon!: string;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<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">
|
||||||
<div class="form-group" id="statusSelector">
|
<div class="form-group" id="statusSelector">
|
||||||
<label class="control-label">{{l('chat.setStatus.status')}}</label>
|
<label class="control-label">{{l('chat.setStatus.status')}}</label>
|
||||||
<dropdown>
|
<dropdown linkClass="custom-select">
|
||||||
<span slot="title"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
<span slot="title"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
||||||
<a href="#" class="dropdown-item" v-for="item in statuses" @click.prevent="status = item">
|
<a href="#" class="dropdown-item" v-for="item in statuses" @click.prevent="status = item">
|
||||||
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
||||||
|
|
|
@ -42,8 +42,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import {BBCodeView} from '../bbcode/view';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import {BBCodeView} from './bbcode';
|
|
||||||
import {characterImage, errorToString, getByteLength, profileLink} from './common';
|
import {characterImage, errorToString, getByteLength, profileLink} from './common';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Channel, Character} from './interfaces';
|
import {Channel, Character} from './interfaces';
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
import ReportDialog from './ReportDialog.vue';
|
import ReportDialog from './ReportDialog.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {bbcode: BBCodeView, modal: Modal}
|
components: {bbcode: BBCodeView(core.bbCodeParser), modal: Modal}
|
||||||
})
|
})
|
||||||
export default class UserMenu extends Vue {
|
export default class UserMenu extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
node = node.parentElement!;
|
node = node.parentElement!;
|
||||||
}
|
}
|
||||||
if(node.dataset['touch'] === 'false' && e.type !== 'contextmenu') return;
|
if(node.dataset['touch'] === 'false' && e.type !== 'contextmenu') return;
|
||||||
if(node.character === undefined)
|
if(!node.character)
|
||||||
if(node.dataset['character'] !== undefined) node.character = core.characters.get(node.dataset['character']!);
|
if(node.dataset['character'] !== undefined) node.character = core.characters.get(node.dataset['character']!);
|
||||||
else {
|
else {
|
||||||
this.showContextMenu = false;
|
this.showContextMenu = false;
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
switch(e.type) {
|
switch(e.type) {
|
||||||
case 'click':
|
case 'click':
|
||||||
if(node.dataset['character'] === undefined)
|
if(node.dataset['character'] === undefined)
|
||||||
if(node === this.touchedElement) this.openMenu(touch, node.character, node.channel);
|
if(node === this.touchedElement) this.openMenu(touch, node.character, node.channel || undefined);
|
||||||
else this.onClick(node.character);
|
else this.onClick(node.character);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
@ -174,7 +174,7 @@
|
||||||
this.touchedElement = node;
|
this.touchedElement = node;
|
||||||
break;
|
break;
|
||||||
case 'contextmenu':
|
case 'contextmenu':
|
||||||
this.openMenu(touch, node.character, node.channel);
|
this.openMenu(touch, node.character, node.channel || undefined);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {WebSocketConnection} from '../fchat';
|
import {WebSocketConnection} from '../fchat';
|
||||||
|
|
||||||
export default class Socket implements WebSocketConnection {
|
export default class Socket implements WebSocketConnection {
|
||||||
static host = 'wss://chat.f-list.net:9799';
|
static host = 'wss://chat.f-list.net/chat2';
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private lastHandler: Promise<void> = Promise.resolve();
|
private lastHandler: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Vue, {Component, CreateElement, RenderContext, VNode} from 'vue';
|
import Vue from 'vue';
|
||||||
import {CoreBBCodeParser} from '../bbcode/core';
|
import {BBCodeElement, CoreBBCodeParser} from '../bbcode/core';
|
||||||
//tslint:disable-next-line:match-default-export-name
|
//tslint:disable-next-line:match-default-export-name
|
||||||
import BaseEditor from '../bbcode/Editor.vue';
|
import BaseEditor from '../bbcode/Editor.vue';
|
||||||
import {BBCodeTextTag} from '../bbcode/parser';
|
import {BBCodeTextTag} from '../bbcode/parser';
|
||||||
|
@ -9,34 +9,10 @@ import core from './core';
|
||||||
import {Character} from './interfaces';
|
import {Character} from './interfaces';
|
||||||
import UserView from './user_view';
|
import UserView from './user_view';
|
||||||
|
|
||||||
export const BBCodeView: Component = {
|
|
||||||
functional: true,
|
|
||||||
render(createElement: CreateElement, context: RenderContext): VNode {
|
|
||||||
/*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this
|
|
||||||
context.data.hook = {
|
|
||||||
insert(node: VNode): void {
|
|
||||||
node.elm!.appendChild(core.bbCodeParser.parseEverything(
|
|
||||||
context.props.text !== undefined ? context.props.text : context.props.unsafeText));
|
|
||||||
if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm);
|
|
||||||
},
|
|
||||||
destroy(node: VNode): void {
|
|
||||||
const element = (<BBCodeElement>(<Element>node.elm).firstChild);
|
|
||||||
if(element.cleanup !== undefined) element.cleanup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const vnode = createElement('span', context.data);
|
|
||||||
vnode.key = context.props.text;
|
|
||||||
return vnode;
|
|
||||||
//tslint:enable
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Editor extends BaseEditor {
|
export class Editor extends BaseEditor {
|
||||||
parser = core.bbCodeParser;
|
parser = core.bbCodeParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BBCodeElement = HTMLElement & {cleanup?(): void};
|
|
||||||
|
|
||||||
export default class BBCodeParser extends CoreBBCodeParser {
|
export default class BBCodeParser extends CoreBBCodeParser {
|
||||||
cleanup: Vue[] = [];
|
cleanup: Vue[] = [];
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@ abstract class Conversation implements Interfaces.Conversation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//tslint:disable-next-line:no-async-without-await
|
||||||
abstract async addMessage(message: Interfaces.Message): Promise<void>;
|
abstract async addMessage(message: Interfaces.Message): Promise<void>;
|
||||||
|
|
||||||
loadLastSent(): void {
|
loadLastSent(): void {
|
||||||
|
@ -182,7 +183,8 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
|
||||||
if(this.character.status === 'offline') {
|
if(this.character.status === 'offline') {
|
||||||
this.errorText = l('chat.errorOffline', this.character.name);
|
this.errorText = l('chat.errorOffline', this.character.name);
|
||||||
return;
|
return;
|
||||||
} else if(this.character.isIgnored) {
|
}
|
||||||
|
if(this.character.isIgnored) {
|
||||||
this.errorText = l('chat.errorIgnored', this.character.name);
|
this.errorText = l('chat.errorIgnored', this.character.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -402,6 +404,7 @@ class State implements Interfaces.State {
|
||||||
}
|
}
|
||||||
|
|
||||||
show(conversation: Conversation): void {
|
show(conversation: Conversation): void {
|
||||||
|
if(conversation === this.selectedConversation) return;
|
||||||
this.selectedConversation.onHide();
|
this.selectedConversation.onHide();
|
||||||
conversation.unread = Interfaces.UnreadState.None;
|
conversation.unread = Interfaces.UnreadState.None;
|
||||||
this.selectedConversation = conversation;
|
this.selectedConversation = conversation;
|
||||||
|
@ -419,12 +422,9 @@ class State implements Interfaces.State {
|
||||||
this.recentChannels = await core.settingsStore.get('recentChannels') || [];
|
this.recentChannels = await core.settingsStore.get('recentChannels') || [];
|
||||||
const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
|
const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
|
||||||
for(const key in settings) {
|
for(const key in settings) {
|
||||||
const settingsItem = new ConversationSettings();
|
settings[key] = Object.assign(new ConversationSettings(), settings[key]);
|
||||||
for(const itemKey in settings[key])
|
|
||||||
settingsItem[<keyof ConversationSettings>itemKey] = settings[key][<keyof ConversationSettings>itemKey];
|
|
||||||
settings[key] = settingsItem;
|
|
||||||
const conv = this.byKey(key);
|
const conv = this.byKey(key);
|
||||||
if(conv !== undefined) conv._settings = settingsItem;
|
if(conv !== undefined) conv._settings = settings[key];
|
||||||
}
|
}
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
//tslint:enable
|
//tslint:enable
|
||||||
|
@ -443,11 +443,6 @@ function isOfInterest(this: void, character: Character): boolean {
|
||||||
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
|
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOp(conv: ChannelConversation): boolean {
|
|
||||||
const ownChar = core.characters.ownCharacter;
|
|
||||||
return ownChar.isChatOp || conv.channel.members[ownChar.name]!.rank > Channel.Rank.Member;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(this: void): Interfaces.State {
|
export default function(this: void): Interfaces.State {
|
||||||
state = new State();
|
state = new State();
|
||||||
window.addEventListener('focus', () => {
|
window.addEventListener('focus', () => {
|
||||||
|
@ -523,7 +518,7 @@ export default function(this: void): Interfaces.State {
|
||||||
const char = core.characters.get(data.character);
|
const char = core.characters.get(data.character);
|
||||||
const conversation = state.channelMap[data.channel.toLowerCase()];
|
const conversation = state.channelMap[data.channel.toLowerCase()];
|
||||||
if(conversation === undefined) return core.channels.leave(data.channel);
|
if(conversation === undefined) return core.channels.leave(data.channel);
|
||||||
if(char.isIgnored && !isOp(conversation)) return;
|
if(char.isIgnored) return;
|
||||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||||
await conversation.addMessage(message);
|
await conversation.addMessage(message);
|
||||||
|
|
||||||
|
@ -552,7 +547,7 @@ export default function(this: void): Interfaces.State {
|
||||||
const char = core.characters.get(data.character);
|
const char = core.characters.get(data.character);
|
||||||
const conv = state.channelMap[data.channel.toLowerCase()];
|
const conv = state.channelMap[data.channel.toLowerCase()];
|
||||||
if(conv === undefined) return core.channels.leave(data.channel);
|
if(conv === undefined) return core.channels.leave(data.channel);
|
||||||
if((char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) && !isOp(conv)) return;
|
if(char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) return;
|
||||||
await conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));
|
await conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));
|
||||||
});
|
});
|
||||||
connection.onMessage('RLL', async(data, time) => {
|
connection.onMessage('RLL', async(data, time) => {
|
||||||
|
@ -569,7 +564,7 @@ export default function(this: void): Interfaces.State {
|
||||||
const channel = (<{channel: string}>data).channel.toLowerCase();
|
const channel = (<{channel: string}>data).channel.toLowerCase();
|
||||||
const conversation = state.channelMap[channel];
|
const conversation = state.channelMap[channel];
|
||||||
if(conversation === undefined) return core.channels.leave(channel);
|
if(conversation === undefined) return core.channels.leave(channel);
|
||||||
if(sender.isIgnored && !isOp(conversation)) return;
|
if(sender.isIgnored) return;
|
||||||
if(data.type === 'bottle' && data.target === core.connection.character) {
|
if(data.type === 'bottle' && data.target === core.connection.character) {
|
||||||
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||||
characterImage(data.character), 'attention');
|
characterImage(data.character), 'attention');
|
||||||
|
|
24
chat/core.ts
24
chat/core.ts
|
@ -1,6 +1,8 @@
|
||||||
import Vue, {WatchHandler} from 'vue';
|
import Vue, {WatchHandler} from 'vue';
|
||||||
|
import {Channels, Characters} from '../fchat';
|
||||||
import BBCodeParser from './bbcode';
|
import BBCodeParser from './bbcode';
|
||||||
import {Settings as SettingsImpl} from './common';
|
import {Settings as SettingsImpl} from './common';
|
||||||
|
import Conversations from './conversations';
|
||||||
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
|
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
|
||||||
|
|
||||||
function createBBCodeParser(): BBCodeParser {
|
function createBBCodeParser(): BBCodeParser {
|
||||||
|
@ -44,8 +46,9 @@ const vue = <Vue & VueState>new Vue({
|
||||||
state
|
state
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'state.hiddenUsers': async(newValue: string[]) => {
|
'state.hiddenUsers': async(newValue: string[], oldValue: string[]) => {
|
||||||
if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue);
|
if(data.settingsStore !== undefined && newValue !== oldValue)
|
||||||
|
await data.settingsStore.set('hiddenUsers', newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -60,20 +63,15 @@ const data = {
|
||||||
channels: <Channel.State | undefined>undefined,
|
channels: <Channel.State | undefined>undefined,
|
||||||
characters: <Character.State | undefined>undefined,
|
characters: <Character.State | undefined>undefined,
|
||||||
notifications: <Notifications | undefined>undefined,
|
notifications: <Notifications | undefined>undefined,
|
||||||
register(this: void | never, module: 'characters' | 'conversations' | 'channels',
|
register<K extends 'characters' | 'conversations' | 'channels'>(module: K, subState: VueState[K]): void {
|
||||||
subState: Channel.State | Character.State | Conversation.State): void {
|
|
||||||
Vue.set(vue, module, subState);
|
Vue.set(vue, module, subState);
|
||||||
data[module] = subState;
|
(<VueState[K]>data[module]) = subState;
|
||||||
},
|
},
|
||||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void {
|
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void {
|
||||||
vue.$watch(getter, callback);
|
vue.$watch(getter, callback);
|
||||||
},
|
},
|
||||||
async reloadSettings(): Promise<void> {
|
async reloadSettings(): Promise<void> {
|
||||||
const settings = new SettingsImpl();
|
state._settings = Object.assign(new SettingsImpl(), await core.settingsStore.get('settings'));
|
||||||
const loadedSettings = <SettingsImpl | undefined>await core.settingsStore.get('settings');
|
|
||||||
if(loadedSettings !== undefined)
|
|
||||||
for(const key in loadedSettings) settings[<keyof Settings>key] = loadedSettings[<keyof Settings>key];
|
|
||||||
state._settings = settings;
|
|
||||||
const hiddenUsers = await core.settingsStore.get('hiddenUsers');
|
const hiddenUsers = await core.settingsStore.get('hiddenUsers');
|
||||||
state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : [];
|
state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : [];
|
||||||
}
|
}
|
||||||
|
@ -85,6 +83,9 @@ export function init(this: void, connection: Connection, logsClass: new() => Log
|
||||||
data.logs = new logsClass();
|
data.logs = new logsClass();
|
||||||
data.settingsStore = new settingsClass();
|
data.settingsStore = new settingsClass();
|
||||||
data.notifications = new notificationsClass();
|
data.notifications = new notificationsClass();
|
||||||
|
data.register('characters', Characters(connection));
|
||||||
|
data.register('channels', Channels(connection, core.characters));
|
||||||
|
data.register('conversations', Conversations());
|
||||||
connection.onEvent('connecting', async() => {
|
connection.onEvent('connecting', async() => {
|
||||||
await data.reloadSettings();
|
await data.reloadSettings();
|
||||||
data.bbCodeParser = createBBCodeParser();
|
data.bbCodeParser = createBBCodeParser();
|
||||||
|
@ -101,9 +102,6 @@ export interface Core {
|
||||||
readonly channels: Channel.State
|
readonly channels: Channel.State
|
||||||
readonly bbCodeParser: BBCodeParser
|
readonly bbCodeParser: BBCodeParser
|
||||||
readonly notifications: Notifications
|
readonly notifications: Notifications
|
||||||
register(module: 'conversations', state: Conversation.State): void
|
|
||||||
register(module: 'channels', state: Channel.State): void
|
|
||||||
register(module: 'characters', state: Character.State): void
|
|
||||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -321,10 +321,6 @@ Once this process has started, do not interrupt it or your logs will get corrupt
|
||||||
'commands.roll.param0.help': 'Syntax: [1-9]d[1-100]. Addition and subtraction of rolls and fixed numbers is also possible. Example: /roll 1d6+1d20-5',
|
'commands.roll.param0.help': 'Syntax: [1-9]d[1-100]. Addition and subtraction of rolls and fixed numbers is also possible. Example: /roll 1d6+1d20-5',
|
||||||
'commands.bottle': 'Spin the bottle',
|
'commands.bottle': 'Spin the bottle',
|
||||||
'commands.bottle.help': 'Spins a bottle, randomly selecting a member of the current tab and displaying it to all.',
|
'commands.bottle.help': 'Spins a bottle, randomly selecting a member of the current tab and displaying it to all.',
|
||||||
'commands.ad': 'Post as ad',
|
|
||||||
'commands.ad.help': 'A quick way to post an ad in the current channel. You may receive an error if ads are not allowed in that channel.',
|
|
||||||
'commands.ad.param0': 'Message',
|
|
||||||
'commands.ad.param0.help': 'The message to post as an ad.',
|
|
||||||
'commands.me': 'Post as action',
|
'commands.me': 'Post as action',
|
||||||
'commands.me.help': 'This will cause your message to be formatted differently, as an action your character is performing.',
|
'commands.me.help': 'This will cause your message to be formatted differently, as an action your character is performing.',
|
||||||
'commands.me.param0': 'Message',
|
'commands.me.param0': 'Message',
|
||||||
|
@ -427,9 +423,10 @@ Any existing FChat 3.0 data for this character will be overwritten.`,
|
||||||
export default function l(key: string, ...args: (string | number)[]): string {
|
export default function l(key: string, ...args: (string | number)[]): string {
|
||||||
let i = args.length;
|
let i = args.length;
|
||||||
let str = strings[key];
|
let str = strings[key];
|
||||||
if(str === undefined)
|
if(str === undefined) {
|
||||||
if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`);
|
if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`);
|
||||||
else return '';
|
return '';
|
||||||
|
}
|
||||||
while(i-- > 0)
|
while(i-- > 0)
|
||||||
str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
|
str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
|
||||||
return str;
|
return str;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue';
|
import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue';
|
||||||
|
import {BBCodeView} from '../bbcode/view';
|
||||||
import {Channel} from '../fchat';
|
import {Channel} from '../fchat';
|
||||||
import {BBCodeView} from './bbcode';
|
|
||||||
import {formatTime} from './common';
|
import {formatTime} from './common';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Conversation} from './interfaces';
|
import {Conversation} from './interfaces';
|
||||||
|
@ -29,7 +29,8 @@ const userPostfix: {[key: number]: string | undefined} = {
|
||||||
if(message.isHighlight) classes += ' message-highlight';
|
if(message.isHighlight) classes += ' message-highlight';
|
||||||
}
|
}
|
||||||
const isAd = message.type === Conversation.Message.Type.Ad && !this.logs;
|
const isAd = message.type === Conversation.Message.Type.Ad && !this.logs;
|
||||||
children.push(createElement(BBCodeView, {props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => {
|
children.push(createElement(BBCodeView(core.bbCodeParser),
|
||||||
|
{props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
elm = elm.parentElement!;
|
elm = elm.parentElement!;
|
||||||
if(elm.scrollHeight > elm.offsetHeight) {
|
if(elm.scrollHeight > elm.offsetHeight) {
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
import Axios from 'axios';
|
import Axios from 'axios';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Editor from '../bbcode/Editor.vue';
|
import Editor from '../bbcode/Editor.vue';
|
||||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
import {StandardBBCodeParser} from '../bbcode/standard';
|
||||||
import {initParser, standardParser} from '../bbcode/standard';
|
import {BBCodeView} from '../bbcode/view';
|
||||||
import CharacterLink from '../components/character_link.vue';
|
import CharacterLink from '../components/character_link.vue';
|
||||||
import CharacterSelect from '../components/character_select.vue';
|
import CharacterSelect from '../components/character_select.vue';
|
||||||
import {setCharacters} from '../components/character_select/character_list';
|
|
||||||
import DateDisplay from '../components/date_display.vue';
|
import DateDisplay from '../components/date_display.vue';
|
||||||
import SimplePager from '../components/simple_pager.vue';
|
import SimplePager from '../components/simple_pager.vue';
|
||||||
import {
|
import {
|
||||||
Character as CharacterInfo, CharacterImage, CharacterImageOld, CharacterInfotag, CharacterSettings, KinkChoice
|
Character as CharacterInfo, CharacterImage, CharacterImageOld, CharacterInfotag, CharacterSettings, Infotag, InfotagGroup, Kink,
|
||||||
|
KinkChoice, KinkGroup, ListItem, Settings, SimpleCharacter
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import {registerMethod, Store} from '../site/character_page/data_store';
|
import {registerMethod, Store} from '../site/character_page/data_store';
|
||||||
import {
|
import {
|
||||||
Character, CharacterFriend, CharacterKink, Friend, FriendRequest, FriendsByCharacter, GuestbookState, KinkChoiceFull,
|
Character, CharacterKink, Friend, FriendRequest, FriendsByCharacter, Guestbook, GuestbookPost, KinkChoiceFull
|
||||||
SharedKinks
|
|
||||||
} from '../site/character_page/interfaces';
|
} from '../site/character_page/interfaces';
|
||||||
import '../site/directives/vue-select'; //tslint:disable-line:no-import-side-effect
|
|
||||||
import * as Utils from '../site/utils';
|
import * as Utils from '../site/utils';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
|
|
||||||
const parserSettings = {
|
|
||||||
siteDomain: 'https://www.f-list.net/',
|
|
||||||
staticDomain: 'https://static.f-list.net/',
|
|
||||||
animatedIcons: true,
|
|
||||||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
|
|
||||||
};
|
|
||||||
|
|
||||||
async function characterData(name: string | undefined): Promise<Character> {
|
async function characterData(name: string | undefined): Promise<Character> {
|
||||||
const data = await core.connection.queryApi<CharacterInfo & {
|
const data = await core.connection.queryApi<CharacterInfo & {
|
||||||
badges: string[]
|
badges: string[]
|
||||||
|
@ -47,7 +38,7 @@ async function characterData(name: string | undefined): Promise<Character> {
|
||||||
}>('character-data.php', {name});
|
}>('character-data.php', {name});
|
||||||
const newKinks: {[key: string]: KinkChoiceFull} = {};
|
const newKinks: {[key: string]: KinkChoiceFull} = {};
|
||||||
for(const key in data.kinks)
|
for(const key in data.kinks)
|
||||||
newKinks[key] = <KinkChoiceFull>(<string>data.kinks[key] === 'fave' ? 'favorite' : data.kinks[key]);
|
newKinks[key] = <KinkChoiceFull>(data.kinks[key] === 'fave' ? 'favorite' : data.kinks[key]);
|
||||||
for(const key in data.custom_kinks) {
|
for(const key in data.custom_kinks) {
|
||||||
const custom = data.custom_kinks[key];
|
const custom = data.custom_kinks[key];
|
||||||
if((<'fave'>custom.choice) === 'fave') custom.choice = 'favorite';
|
if((<'fave'>custom.choice) === 'fave') custom.choice = 'favorite';
|
||||||
|
@ -59,12 +50,12 @@ async function characterData(name: string | undefined): Promise<Character> {
|
||||||
const newInfotags: {[key: string]: CharacterInfotag} = {};
|
const newInfotags: {[key: string]: CharacterInfotag} = {};
|
||||||
for(const key in data.infotags) {
|
for(const key in data.infotags) {
|
||||||
const characterInfotag = data.infotags[key];
|
const characterInfotag = data.infotags[key];
|
||||||
const infotag = Store.kinks.infotags[key];
|
const infotag = Store.shared.infotags[key];
|
||||||
if(infotag === undefined) continue;
|
if(!infotag) continue;
|
||||||
newInfotags[key] = infotag.type === 'list' ? {list: parseInt(characterInfotag, 10)} : {string: characterInfotag};
|
newInfotags[key] = infotag.type === 'list' ? {list: parseInt(characterInfotag, 10)} : {string: characterInfotag};
|
||||||
}
|
}
|
||||||
parserSettings.inlineDisplayMode = data.current_user.inline_mode;
|
Utils.settings.inlineDisplayMode = data.current_user.inline_mode;
|
||||||
parserSettings.animatedIcons = core.state.settings.animatedEicons;
|
Utils.settings.animateEicons = core.state.settings.animatedEicons;
|
||||||
return {
|
return {
|
||||||
is_self: false,
|
is_self: false,
|
||||||
character: {
|
character: {
|
||||||
|
@ -98,13 +89,22 @@ function contactMethodIconUrl(name: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fieldsGet(): Promise<void> {
|
async function fieldsGet(): Promise<void> {
|
||||||
if(Store.kinks !== undefined) return; //tslint:disable-line:strict-type-predicates
|
if(Store.shared !== undefined) return; //tslint:disable-line:strict-type-predicates
|
||||||
try {
|
try {
|
||||||
const fields = (await (Axios.get(`${Utils.siteDomain}json/api/mapping-list.php`))).data as SharedKinks & {
|
const fields = (await (Axios.get(`${Utils.siteDomain}json/api/mapping-list.php`))).data as {
|
||||||
kinks: {[key: string]: {group_id: number}}
|
listitems: {[key: string]: ListItem}
|
||||||
infotags: {[key: string]: {list: string, group_id: string}}
|
kinks: {[key: string]: Kink & {group_id: number}}
|
||||||
|
kink_groups: {[key: string]: KinkGroup}
|
||||||
|
infotags: {[key: string]: Infotag & {list: string, group_id: string}}
|
||||||
|
infotag_groups: {[key: string]: InfotagGroup & {id: string}}
|
||||||
};
|
};
|
||||||
const kinks: SharedKinks = {kinks: {}, kink_groups: {}, infotags: {}, infotag_groups: {}, listitems: {}};
|
const kinks: {
|
||||||
|
listItems: {[key: string]: ListItem}
|
||||||
|
kinks: {[key: string]: Kink}
|
||||||
|
kinkGroups: {[key: string]: KinkGroup}
|
||||||
|
infotags: {[key: string]: Infotag}
|
||||||
|
infotagGroups: {[key: string]: InfotagGroup}
|
||||||
|
} = {kinks: {}, kinkGroups: {}, infotags: {}, infotagGroups: {}, listItems: {}};
|
||||||
for(const id in fields.kinks) {
|
for(const id in fields.kinks) {
|
||||||
const oldKink = fields.kinks[id];
|
const oldKink = fields.kinks[id];
|
||||||
kinks.kinks[oldKink.id] = {
|
kinks.kinks[oldKink.id] = {
|
||||||
|
@ -115,8 +115,8 @@ async function fieldsGet(): Promise<void> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for(const id in fields.kink_groups) {
|
for(const id in fields.kink_groups) {
|
||||||
const oldGroup = fields.kink_groups[id]!;
|
const oldGroup = fields.kink_groups[id];
|
||||||
kinks.kink_groups[oldGroup.id] = {
|
kinks.kinkGroups[oldGroup.id] = {
|
||||||
id: oldGroup.id,
|
id: oldGroup.id,
|
||||||
name: oldGroup.name,
|
name: oldGroup.name,
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -132,12 +132,12 @@ async function fieldsGet(): Promise<void> {
|
||||||
validator: oldInfotag.list,
|
validator: oldInfotag.list,
|
||||||
search_field: '',
|
search_field: '',
|
||||||
allow_legacy: true,
|
allow_legacy: true,
|
||||||
infotag_group: oldInfotag.group_id
|
infotag_group: parseInt(oldInfotag.group_id, 10)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for(const id in fields.listitems) {
|
for(const id in fields.listitems) {
|
||||||
const oldListItem = fields.listitems[id]!;
|
const oldListItem = fields.listitems[id];
|
||||||
kinks.listitems[oldListItem.id] = {
|
kinks.listItems[oldListItem.id] = {
|
||||||
id: oldListItem.id,
|
id: oldListItem.id,
|
||||||
name: oldListItem.name,
|
name: oldListItem.name,
|
||||||
value: oldListItem.value,
|
value: oldListItem.value,
|
||||||
|
@ -145,31 +145,33 @@ async function fieldsGet(): Promise<void> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for(const id in fields.infotag_groups) {
|
for(const id in fields.infotag_groups) {
|
||||||
const oldGroup = fields.infotag_groups[id]!;
|
const oldGroup = fields.infotag_groups[id];
|
||||||
kinks.infotag_groups[oldGroup.id] = {
|
kinks.infotagGroups[oldGroup.id] = {
|
||||||
id: oldGroup.id,
|
id: parseInt(oldGroup.id, 10),
|
||||||
name: oldGroup.name,
|
name: oldGroup.name,
|
||||||
description: oldGroup.description,
|
description: oldGroup.description,
|
||||||
sort_order: oldGroup.id
|
sort_order: oldGroup.id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Store.kinks = kinks;
|
Store.shared = kinks;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
Utils.ajaxError(e, 'Error loading character fields');
|
Utils.ajaxError(e, 'Error loading character fields');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function friendsGet(id: number): Promise<CharacterFriend[]> {
|
async function friendsGet(id: number): Promise<SimpleCharacter[]> {
|
||||||
return (await core.connection.queryApi<{friends: CharacterFriend[]}>('character-friends.php', {id})).friends;
|
return (await core.connection.queryApi<{friends: SimpleCharacter[]}>('character-friends.php', {id})).friends;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function imagesGet(id: number): Promise<CharacterImage[]> {
|
async function imagesGet(id: number): Promise<CharacterImage[]> {
|
||||||
return (await core.connection.queryApi<{images: CharacterImage[]}>('character-images.php', {id})).images;
|
return (await core.connection.queryApi<{images: CharacterImage[]}>('character-images.php', {id})).images;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function guestbookGet(id: number, page: number): Promise<GuestbookState> {
|
async function guestbookGet(id: number, offset: number): Promise<Guestbook> {
|
||||||
return core.connection.queryApi<GuestbookState>('character-guestbook.php', {id, page: page - 1});
|
const data = await core.connection.queryApi<{nextPage: boolean, posts: GuestbookPost[]}>('character-guestbook.php',
|
||||||
|
{id, page: offset / 10});
|
||||||
|
return {posts: data.posts, total: data.nextPage ? offset + 100 : offset};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function kinksGet(id: number): Promise<CharacterKink[]> {
|
async function kinksGet(id: number): Promise<CharacterKink[]> {
|
||||||
|
@ -180,23 +182,18 @@ async function kinksGet(id: number): Promise<CharacterKink[]> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init(characters: {[key: string]: number}): void {
|
export function init(settings: Settings, characters: SimpleCharacter[]): void {
|
||||||
Utils.setDomains(parserSettings.siteDomain, parserSettings.staticDomain);
|
Utils.setDomains('https://www.f-list.net/', 'https://static.f-list.net/');
|
||||||
initParser(parserSettings);
|
|
||||||
|
|
||||||
Vue.component('character-select', CharacterSelect);
|
Vue.component('character-select', CharacterSelect);
|
||||||
Vue.component('character-link', CharacterLink);
|
Vue.component('character-link', CharacterLink);
|
||||||
Vue.component('date-display', DateDisplay);
|
Vue.component('date-display', DateDisplay);
|
||||||
Vue.component('simple-pager', SimplePager);
|
Vue.component('simple-pager', SimplePager);
|
||||||
|
Vue.component('bbcode', BBCodeView(new StandardBBCodeParser()));
|
||||||
Vue.component('bbcode-editor', Editor);
|
Vue.component('bbcode-editor', Editor);
|
||||||
setCharacters(Object.keys(characters).map((name) => ({name, id: characters[name]})));
|
Utils.init(settings, characters);
|
||||||
core.connection.onEvent('connecting', () => {
|
core.connection.onEvent('connecting', () => {
|
||||||
Utils.Settings.defaultCharacter = characters[core.connection.character];
|
Utils.settings.defaultCharacter = characters.find((x) => x.name === core.connection.character)!.id;
|
||||||
});
|
|
||||||
Vue.directive('bbcode', (el, binding) => {
|
|
||||||
while(el.firstChild !== null)
|
|
||||||
el.removeChild(el.firstChild);
|
|
||||||
el.appendChild(standardParser.parseEverything(<string>binding.value));
|
|
||||||
});
|
});
|
||||||
registerMethod('characterData', characterData);
|
registerMethod('characterData', characterData);
|
||||||
registerMethod('contactMethodIconUrl', contactMethodIconUrl);
|
registerMethod('contactMethodIconUrl', contactMethodIconUrl);
|
||||||
|
@ -209,12 +206,10 @@ export function init(characters: {[key: string]: number}): void {
|
||||||
registerMethod('imageUrl', (image: CharacterImageOld) => image.url);
|
registerMethod('imageUrl', (image: CharacterImageOld) => image.url);
|
||||||
registerMethod('memoUpdate', async(id: number, memo: string) => {
|
registerMethod('memoUpdate', async(id: number, memo: string) => {
|
||||||
await core.connection.queryApi('character-memo-save.php', {target: id, note: memo});
|
await core.connection.queryApi('character-memo-save.php', {target: id, note: memo});
|
||||||
return {id, memo, updated_at: Date.now() / 1000};
|
|
||||||
});
|
});
|
||||||
registerMethod('imageThumbUrl', (image: CharacterImage) => `${Utils.staticDomain}images/charthumb/${image.id}.${image.extension}`);
|
registerMethod('imageThumbUrl', (image: CharacterImage) => `${Utils.staticDomain}images/charthumb/${image.id}.${image.extension}`);
|
||||||
registerMethod('bookmarkUpdate', async(id: number, state: boolean) => {
|
registerMethod('bookmarkUpdate', async(id: number, state: boolean) => {
|
||||||
await core.connection.queryApi(`bookmark-${state ? 'add' : 'remove'}.php`, {id});
|
await core.connection.queryApi(`bookmark-${state ? 'add' : 'remove'}.php`, {id});
|
||||||
return state;
|
|
||||||
});
|
});
|
||||||
registerMethod('characterFriends', async(id: number) =>
|
registerMethod('characterFriends', async(id: number) =>
|
||||||
core.connection.queryApi<FriendsByCharacter>('character-friend-list.php', {id}));
|
core.connection.queryApi<FriendsByCharacter>('character-friend-list.php', {id}));
|
||||||
|
|
|
@ -37,16 +37,18 @@ export function parse(this: void | never, input: string, context: CommandContext
|
||||||
for(let i = 0; i < command.params.length; ++i) {
|
for(let i = 0; i < command.params.length; ++i) {
|
||||||
while(args[index] === ' ') ++index;
|
while(args[index] === ' ') ++index;
|
||||||
const param = command.params[i];
|
const param = command.params[i];
|
||||||
if(index === -1)
|
if(index === -1) {
|
||||||
if(param.optional !== undefined) continue;
|
if(param.optional !== undefined) continue;
|
||||||
else return l('commands.tooFewParams');
|
return l('commands.tooFewParams');
|
||||||
|
}
|
||||||
let delimiter = param.delimiter !== undefined ? param.delimiter : defaultDelimiters[param.type];
|
let delimiter = param.delimiter !== undefined ? param.delimiter : defaultDelimiters[param.type];
|
||||||
if(delimiter === undefined) delimiter = ' ';
|
if(delimiter === undefined) delimiter = ' ';
|
||||||
const endIndex = delimiter.length > 0 ? args.indexOf(delimiter, index) : args.length;
|
const endIndex = delimiter.length > 0 ? args.indexOf(delimiter, index) : args.length;
|
||||||
const value = args.substring(index, endIndex !== -1 ? endIndex : undefined);
|
const value = args.substring(index, endIndex !== -1 ? endIndex : undefined);
|
||||||
if(value.length === 0)
|
if(value.length === 0) {
|
||||||
if(param.optional !== undefined) continue;
|
if(param.optional !== undefined) continue;
|
||||||
else return l('commands.tooFewParams');
|
return l('commands.tooFewParams');
|
||||||
|
}
|
||||||
values[i] = value;
|
values[i] = value;
|
||||||
switch(param.type) {
|
switch(param.type) {
|
||||||
case ParamType.String:
|
case ParamType.String:
|
||||||
|
@ -142,12 +144,6 @@ const commands: {readonly [key: string]: Command | undefined} = {
|
||||||
exec: (_, status: Character.Status, statusmsg: string = '') => core.connection.send('STA', {status, statusmsg}),
|
exec: (_, status: Character.Status, statusmsg: string = '') => core.connection.send('STA', {status, statusmsg}),
|
||||||
params: [{type: ParamType.Enum, options: userStatuses}, {type: ParamType.String, optional: true}]
|
params: [{type: ParamType.Enum, options: userStatuses}, {type: ParamType.String, optional: true}]
|
||||||
},
|
},
|
||||||
ad: {
|
|
||||||
exec: (conv: ChannelConversation, message: string) =>
|
|
||||||
core.connection.send('LRP', {channel: conv.channel.id, message}),
|
|
||||||
context: CommandContext.Channel,
|
|
||||||
params: [{type: ParamType.String}]
|
|
||||||
},
|
|
||||||
roll: {
|
roll: {
|
||||||
exec: (conv: ChannelConversation | PrivateConversation, dice: string) => {
|
exec: (conv: ChannelConversation | PrivateConversation, dice: string) => {
|
||||||
if(Conversation.isChannel(conv)) core.connection.send('RLL', {channel: conv.channel.id, dice});
|
if(Conversation.isChannel(conv)) core.connection.send('RLL', {channel: conv.channel.id, dice});
|
||||||
|
|
|
@ -47,6 +47,7 @@ function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic {
|
||||||
//tslint:enable
|
//tslint:enable
|
||||||
|
|
||||||
export function setupRaven(dsn: string, version: string): void {
|
export function setupRaven(dsn: string, version: string): void {
|
||||||
|
return; //TODO sentry temporarily disabled
|
||||||
Raven.config(dsn, {
|
Raven.config(dsn, {
|
||||||
release: version,
|
release: version,
|
||||||
dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
|
dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<div class="dropdown" @focusout="blur">
|
||||||
<a class="form-control custom-select" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
<a :class="linkClass" aria-haspopup="true" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen" href="#"
|
||||||
@blur="isOpen = false" style="width:100%;text-align:left;display:flex;align-items:center" role="button" tabindex="-1">
|
style="width:100%;text-align:left;align-items:center" role="button" tabindex="-1" ref="button">
|
||||||
<div style="flex:1">
|
<slot name="title">{{title}}</slot>
|
||||||
<slot name="title" style="flex:1"></slot>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" :style="open ? {display: 'block'} : undefined" @mousedown.stop.prevent @click="isOpen = false"
|
<div class="dropdown-menu" ref="menu" @mousedown.prevent.stop @click.prevent.stop="menuClick()">
|
||||||
ref="menu">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,33 +17,39 @@
|
||||||
@Component
|
@Component
|
||||||
export default class Dropdown extends Vue {
|
export default class Dropdown extends Vue {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
@Prop()
|
@Prop({default: 'btn btn-secondary dropdown-toggle'})
|
||||||
|
readonly linkClass!: string;
|
||||||
|
@Prop
|
||||||
readonly keepOpen?: boolean;
|
readonly keepOpen?: boolean;
|
||||||
|
@Prop
|
||||||
|
readonly title?: string;
|
||||||
|
|
||||||
get open(): boolean {
|
@Watch('isOpen')
|
||||||
return this.keepOpen || this.isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('open')
|
|
||||||
onToggle(): void {
|
onToggle(): void {
|
||||||
const menu = this.$refs['menu'] as HTMLElement;
|
const menu = this.$refs['menu'] as HTMLElement;
|
||||||
if(!this.isOpen) {
|
if(!this.isOpen) {
|
||||||
menu.style.cssText = '';
|
menu.style.cssText = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let element = <HTMLElement | null>this.$el;
|
menu.style.display = 'block';
|
||||||
while(element !== null) {
|
const offset = menu.getBoundingClientRect();
|
||||||
if(getComputedStyle(element).position === 'fixed') {
|
menu.style.position = 'fixed';
|
||||||
menu.style.display = 'block';
|
menu.style.left = offset.right < window.innerWidth ? `${offset.left}px` : `${window.innerWidth - offset.width}px`;
|
||||||
const offset = menu.getBoundingClientRect();
|
menu.style.top = (offset.bottom < window.innerHeight) ? `${offset.top}px` :
|
||||||
menu.style.position = 'fixed';
|
`${offset.top - offset.height - (<HTMLElement>this.$el).offsetHeight}px`;
|
||||||
menu.style.left = `${offset.left}px`;
|
}
|
||||||
menu.style.top = (offset.bottom < window.innerHeight) ? menu.style.top = `${offset.top}px` :
|
|
||||||
`${this.$el.getBoundingClientRect().top - offset.bottom + offset.top}px`;
|
blur(event: FocusEvent): void {
|
||||||
break;
|
let elm = <HTMLElement | null>event.relatedTarget;
|
||||||
}
|
while(elm) {
|
||||||
element = element.parentElement;
|
if(elm === this.$refs['menu']) return;
|
||||||
|
elm = elm.parentElement;
|
||||||
}
|
}
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuClick(): void {
|
||||||
|
if(!this.keepOpen) this.isOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,11 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<dropdown class="filterable-select" :keepOpen="keepOpen">
|
<dropdown class="filterable-select" linkClass="custom-select" :keepOpen="true">
|
||||||
<template slot="title" v-if="multiple">{{label}}</template>
|
<template slot="title" v-if="multiple">{{label}}</template>
|
||||||
<slot v-else slot="title" :option="selected">{{label}}</slot>
|
<slot v-else slot="title" :option="selected">{{label}}</slot>
|
||||||
|
|
||||||
<div style="padding:10px;">
|
<div style="padding:10px;">
|
||||||
<input v-model="filter" class="form-control" :placeholder="placeholder" @mousedown.stop @focus="keepOpen = true"
|
<input v-model="filter" class="form-control" :placeholder="placeholder" @mousedown.stop/>
|
||||||
@blur="keepOpen = false"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-items">
|
<div class="dropdown-items">
|
||||||
<template v-if="multiple">
|
<template v-if="multiple">
|
||||||
|
@ -32,21 +31,20 @@
|
||||||
components: {dropdown: Dropdown}
|
components: {dropdown: Dropdown}
|
||||||
})
|
})
|
||||||
export default class FilterableSelect extends Vue {
|
export default class FilterableSelect extends Vue {
|
||||||
@Prop()
|
@Prop
|
||||||
readonly placeholder?: string;
|
readonly placeholder?: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly options!: object[];
|
readonly options!: object[];
|
||||||
@Prop({default: () => ((filter: RegExp, value: string) => filter.test(value))})
|
@Prop({default: () => ((filter: RegExp, value: string) => filter.test(value))})
|
||||||
readonly filterFunc!: (filter: RegExp, value: object) => boolean;
|
readonly filterFunc!: (filter: RegExp, value: object) => boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly multiple?: true;
|
readonly multiple?: true;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly value?: object | object[];
|
readonly value?: object | object[];
|
||||||
@Prop()
|
@Prop
|
||||||
readonly title?: string;
|
readonly title?: string;
|
||||||
filter = '';
|
filter = '';
|
||||||
selected: object | object[] | undefined = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : undefined);
|
selected: object | object[] | undefined = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : undefined);
|
||||||
keepOpen = false;
|
|
||||||
|
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
watchValue(newValue: object | object[] | undefined): void {
|
watchValue(newValue: object | object[] | undefined): void {
|
||||||
|
@ -59,10 +57,8 @@
|
||||||
const index = selected.indexOf(item);
|
const index = selected.indexOf(item);
|
||||||
if(index === -1) selected.push(item);
|
if(index === -1) selected.push(item);
|
||||||
else selected.splice(index, 1);
|
else selected.splice(index, 1);
|
||||||
} else {
|
} else
|
||||||
this.keepOpen = false;
|
|
||||||
this.selected = item;
|
this.selected = item;
|
||||||
}
|
|
||||||
this.$emit('input', this.selected);
|
this.$emit('input', this.selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +87,7 @@
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<span v-show="isShown">
|
<span v-show="isShown">
|
||||||
<div class="modal" @click.self="hideWithCheck()" style="display:flex;justify-content:center">
|
<div class="modal" @mousedown.self="hideWithCheck()" style="display:flex;justify-content:center">
|
||||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center;margin-left:0;margin-right:0">
|
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center;margin-left:0;margin-right:0">
|
||||||
<div class="modal-content" style="max-height:100%">
|
<div class="modal-content" style="max-height:100%">
|
||||||
<div class="modal-header" style="flex-shrink:0">
|
<div class="modal-header" style="flex-shrink:0">
|
||||||
|
@ -49,17 +49,17 @@
|
||||||
export default class Modal extends Vue {
|
export default class Modal extends Vue {
|
||||||
@Prop({default: ''})
|
@Prop({default: ''})
|
||||||
readonly action!: string;
|
readonly action!: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly dialogClass?: {string: boolean};
|
readonly dialogClass?: {string: boolean};
|
||||||
@Prop({default: true})
|
@Prop({default: true})
|
||||||
readonly buttons!: boolean;
|
readonly buttons!: boolean;
|
||||||
@Prop({default: () => ({'btn-primary': true})})
|
@Prop({default: () => ({'btn-primary': true})})
|
||||||
readonly buttonClass!: {string: boolean};
|
readonly buttonClass!: {string: boolean};
|
||||||
@Prop()
|
@Prop
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
@Prop({default: true})
|
@Prop({default: true})
|
||||||
readonly showCancel!: boolean;
|
readonly showCancel!: boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly buttonText?: string;
|
readonly buttonText?: string;
|
||||||
isShown = false;
|
isShown = false;
|
||||||
keepOpen = false;
|
keepOpen = false;
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="linkClasses" v-if="character">
|
<span :class="linkClasses" v-if="character">
|
||||||
<slot v-if="deleted">[Deleted] {{ name }}</slot>
|
<slot v-if="deleted">[Deleted] {{ name }}</slot>
|
||||||
<a :href="characterUrl" class="characterLinkLink" v-else><slot>{{ name }}</slot></a>
|
<a :href="characterUrl" class="characterLinkLink" v-else :target="target"><slot>{{ name }}</slot></a>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import {SimpleCharacter} from '../interfaces';
|
||||||
import * as Utils from '../site/utils';
|
import * as Utils from '../site/utils';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class CharacterLink extends Vue {
|
export default class CharacterLink extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: {name: string, id: number, deleted: boolean} | string;
|
readonly character!: SimpleCharacter | string;
|
||||||
|
@Prop({default: '_blank'})
|
||||||
|
readonly target!: string;
|
||||||
|
|
||||||
get deleted(): boolean {
|
get deleted(): boolean {
|
||||||
return typeof(this.character) === 'string' ? false : this.character.deleted;
|
return typeof(this.character) === 'string' ? false : this.character.deleted;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<select class="form-control" :value="value" @change="emit">
|
<select :value="value" @change="emit">
|
||||||
<option v-for="o in characters" :value="o.value" v-once>{{o.text}}</option>
|
<option v-for="character in characters" :value="character.id">{{character.name}}</option>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,24 +8,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {getCharacters} from './character_select/character_list';
|
import {SimpleCharacter} from '../interfaces';
|
||||||
|
import * as Utils from '../site/utils';
|
||||||
interface SelectItem {
|
|
||||||
value: number
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class CharacterSelect extends Vue {
|
export default class CharacterSelect extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly value!: number;
|
readonly value!: number;
|
||||||
|
|
||||||
get characters(): SelectItem[] {
|
get characters(): SimpleCharacter[] {
|
||||||
const characterList = getCharacters();
|
return Utils.characters;
|
||||||
const characters: SelectItem[] = [];
|
|
||||||
for(const character of characterList)
|
|
||||||
characters.push({value: character.id, text: character.name});
|
|
||||||
return characters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(evt: Event): void {
|
emit(evt: Event): void {
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
export interface CharacterItem {
|
|
||||||
readonly name: string
|
|
||||||
readonly id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
let characterList: ReadonlyArray<CharacterItem> = [];
|
|
||||||
|
|
||||||
export function setCharacters(characters: ReadonlyArray<CharacterItem>): void {
|
|
||||||
characterList = characters;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCharacters(): ReadonlyArray<CharacterItem> {
|
|
||||||
return characterList;
|
|
||||||
}
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-header" @click="toggle()" style="cursor:pointer" :class="headerClass">
|
||||||
|
<h4>{{title}} <span class="fas" :class="'fa-chevron-' + (collapsed ? 'down' : 'up')"></span></h4>
|
||||||
|
</div>
|
||||||
|
<div :style="style" style="overflow:hidden">
|
||||||
|
<div class="card-body" ref="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Collapse extends Vue {
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly title!: string;
|
||||||
|
@Prop
|
||||||
|
readonly headerClass?: string;
|
||||||
|
collapsed = true;
|
||||||
|
timeout = 0;
|
||||||
|
style = {height: <string | undefined>'0', transition: 'height .2s'};
|
||||||
|
|
||||||
|
toggle(state?: boolean) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.collapsed = state !== undefined ? state : !this.collapsed;
|
||||||
|
this.$emit(this.collapsed ? 'close' : 'open');
|
||||||
|
if(this.collapsed) {
|
||||||
|
this.style.transition = 'initial';
|
||||||
|
this.style.height = `${(<HTMLElement>this.$refs['content']).scrollHeight}px`;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.style.transition = 'height .2s';
|
||||||
|
this.style.height = '0';
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
this.style.height = `${(<HTMLElement>this.$refs['content']).scrollHeight}px`;
|
||||||
|
this.timeout = window.setTimeout(() => this.style.height = undefined, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -2,7 +2,9 @@ import {Component} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
components: {Modal}
|
||||||
|
})
|
||||||
export default class CustomDialog extends Vue {
|
export default class CustomDialog extends Vue {
|
||||||
protected get dialog(): Modal {
|
protected get dialog(): Modal {
|
||||||
return <Modal>this.$children[0];
|
return <Modal>this.$children[0];
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||||
import {distanceInWordsToNow, format} from 'date-fns';
|
import {distanceInWordsToNow, format} from 'date-fns';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {Settings} from '../site/utils';
|
import {settings} from '../site/utils';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class DateDisplay extends Vue {
|
export default class DateDisplay extends Vue {
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
const date = isNaN(+this.time) ? new Date(`${this.time}+00:00`) : new Date(+this.time * 1000);
|
const date = isNaN(+this.time) ? new Date(`${this.time}+00:00`) : new Date(+this.time * 1000);
|
||||||
const absolute = format(date, 'YYYY-MM-DD HH:mm');
|
const absolute = format(date, 'YYYY-MM-DD HH:mm');
|
||||||
const relative = distanceInWordsToNow(date, {addSuffix: true});
|
const relative = distanceInWordsToNow(date, {addSuffix: true});
|
||||||
if(Settings.fuzzyDates) {
|
if(settings.fuzzyDates) {
|
||||||
this.primary = relative;
|
this.primary = relative;
|
||||||
this.secondary = absolute;
|
this.secondary = absolute;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
readonly field!: string;
|
readonly field!: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||||
@Prop()
|
@Prop
|
||||||
readonly label?: string;
|
readonly label?: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
@Prop({default: false})
|
@Prop({default: false})
|
||||||
readonly valid!: boolean;
|
readonly valid!: boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly helptext?: string;
|
readonly helptext?: string;
|
||||||
|
|
||||||
get hasErrors(): boolean {
|
get hasErrors(): boolean {
|
||||||
|
|
|
@ -26,13 +26,13 @@
|
||||||
readonly field!: string;
|
readonly field!: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||||
@Prop()
|
@Prop
|
||||||
readonly label?: string;
|
readonly label?: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
@Prop({default: false})
|
@Prop({default: false})
|
||||||
readonly valid!: boolean;
|
readonly valid!: boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly helptext?: string;
|
readonly helptext?: string;
|
||||||
|
|
||||||
get hasErrors(): boolean {
|
get hasErrors(): boolean {
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
readonly prev!: boolean;
|
readonly prev!: boolean;
|
||||||
@Prop({default: false})
|
@Prop({default: false})
|
||||||
readonly routed!: boolean;
|
readonly routed!: boolean;
|
||||||
@Prop({default: () => ({})})
|
@Prop({default(this: Vue & {$route: RouteParams}): RouteParams { return this.$route; }})
|
||||||
readonly route!: RouteParams;
|
readonly route!: RouteParams;
|
||||||
@Prop({default: 'page'})
|
@Prop({default: 'page'})
|
||||||
readonly paramName!: string;
|
readonly paramName!: string;
|
||||||
|
|
|
@ -22,7 +22,8 @@ const Tabs = Vue.extend({
|
||||||
return createElement('div', {staticClass: 'nav-tabs-scroll'},
|
return createElement('div', {staticClass: 'nav-tabs-scroll'},
|
||||||
[createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'},
|
[createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'},
|
||||||
[createElement('a', {
|
[createElement('a', {
|
||||||
staticClass: 'nav-link', class: {active: this._v === key}, on: { click: () => this.$emit('input', key) }
|
attrs: {href: '#'},
|
||||||
|
staticClass: 'nav-link', class: {active: this._v === key}, on: {click: () => this.$emit('input', key)}
|
||||||
}, [children[key]!])])))]);
|
}, [children[key]!])])))]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,25 +3,33 @@
|
||||||
<div v-html="styling"></div>
|
<div v-html="styling"></div>
|
||||||
<div v-if="!characters" style="display:flex; align-items:center; justify-content:center; height: 100%;">
|
<div v-if="!characters" style="display:flex; align-items:center; justify-content:center; height: 100%;">
|
||||||
<div class="card bg-light" style="width: 400px;">
|
<div class="card bg-light" style="width: 400px;">
|
||||||
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
<h3 class="card-header" style="margin-top:0;display:flex">
|
||||||
|
{{l('title')}}
|
||||||
|
<a href="#" @click.prevent="showLogs()" class="btn" style="flex:1;text-align:right">
|
||||||
|
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-danger" v-show="error">
|
<div class="alert alert-danger" v-show="error">
|
||||||
{{error}}
|
{{error}}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login()" :disabled="loggingIn"/>
|
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login()"
|
||||||
|
:disabled="loggingIn"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||||
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login()" :disabled="loggingIn"/>
|
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login()"
|
||||||
|
:disabled="loggingIn"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" v-show="showAdvanced">
|
<div class="form-group" v-show="showAdvanced">
|
||||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login()" :disabled="loggingIn"/>
|
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-outline-secondary" @click="resetHost()"><span class="fas fa-undo-alt"></span></button>
|
<button class="btn btn-outline-secondary" @click="resetHost()"><span class="fas fa-undo-alt"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +69,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
<logs ref="logsDialog"></logs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -77,18 +86,17 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Chat from '../chat/Chat.vue';
|
import Chat from '../chat/Chat.vue';
|
||||||
import {getKey, Settings} from '../chat/common';
|
import {getKey, Settings} from '../chat/common';
|
||||||
import core, {init as initCore} from '../chat/core';
|
import core from '../chat/core';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {init as profileApiInit} from '../chat/profile_api';
|
import Logs from '../chat/Logs.vue';
|
||||||
import Socket from '../chat/WebSocket';
|
import Socket from '../chat/WebSocket';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import Connection from '../fchat/connection';
|
import {SimpleCharacter} from '../interfaces';
|
||||||
import {Keys} from '../keys';
|
import {Keys} from '../keys';
|
||||||
import CharacterPage from '../site/character_page/character_page.vue';
|
import CharacterPage from '../site/character_page/character_page.vue';
|
||||||
import {defaultHost, GeneralSettings, nativeRequire} from './common';
|
import {defaultHost, GeneralSettings, nativeRequire} from './common';
|
||||||
import {fixLogs, Logs, SettingsStore} from './filesystem';
|
import {fixLogs} from './filesystem';
|
||||||
import * as SlimcatImporter from './importer';
|
import * as SlimcatImporter from './importer';
|
||||||
import Notifications from './notifications';
|
|
||||||
|
|
||||||
const webContents = electron.remote.getCurrentWebContents();
|
const webContents = electron.remote.getCurrentWebContents();
|
||||||
const parent = electron.remote.getCurrentWindow().webContents;
|
const parent = electron.remote.getCurrentWindow().webContents;
|
||||||
|
@ -106,17 +114,17 @@
|
||||||
log.info('Loaded keytar.');
|
log.info('Loaded keytar.');
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
components: {chat: Chat, modal: Modal, characterPage: CharacterPage, logs: Logs}
|
||||||
})
|
})
|
||||||
export default class Index extends Vue {
|
export default class Index extends Vue {
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
saveLogin = false;
|
saveLogin = false;
|
||||||
loggingIn = false;
|
loggingIn = false;
|
||||||
password = '';
|
password = '';
|
||||||
character: string | undefined;
|
character?: string;
|
||||||
characters: string[] | undefined;
|
characters?: SimpleCharacter[];
|
||||||
error = '';
|
error = '';
|
||||||
defaultCharacter: string | undefined;
|
defaultCharacter?: number;
|
||||||
l = l;
|
l = l;
|
||||||
settings!: GeneralSettings;
|
settings!: GeneralSettings;
|
||||||
importProgress = 0;
|
importProgress = 0;
|
||||||
|
@ -125,7 +133,7 @@
|
||||||
fixCharacter = '';
|
fixCharacter = '';
|
||||||
|
|
||||||
@Hook('created')
|
@Hook('created')
|
||||||
async created(): Promise<void> {
|
created(): void {
|
||||||
if(this.settings.account.length > 0) this.saveLogin = true;
|
if(this.settings.account.length > 0) this.saveLogin = true;
|
||||||
keyStore.getPassword(this.settings.account)
|
keyStore.getPassword(this.settings.account)
|
||||||
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
|
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
|
||||||
|
@ -140,7 +148,7 @@
|
||||||
profileViewer.show();
|
profileViewer.show();
|
||||||
});
|
});
|
||||||
electron.ipcRenderer.on('fix-logs', async() => {
|
electron.ipcRenderer.on('fix-logs', async() => {
|
||||||
this.fixCharacters = await new SettingsStore().getAvailableCharacters();
|
this.fixCharacters = await core.settingsStore.getAvailableCharacters();
|
||||||
this.fixCharacter = this.fixCharacters[0];
|
this.fixCharacter = this.fixCharacters[0];
|
||||||
(<Modal>this.$refs['fixLogsModal']).show();
|
(<Modal>this.$refs['fixLogsModal']).show();
|
||||||
});
|
});
|
||||||
|
@ -169,15 +177,14 @@
|
||||||
await keyStore.setPassword(this.settings.account, this.password);
|
await keyStore.setPassword(this.settings.account, this.password);
|
||||||
}
|
}
|
||||||
Socket.host = this.settings.host;
|
Socket.host = this.settings.host;
|
||||||
const connection = new Connection(`F-Chat 3.0 (${process.platform})`, electron.remote.app.getVersion(), Socket,
|
|
||||||
this.settings.account, this.password);
|
core.connection.onEvent('connecting', async() => {
|
||||||
connection.onEvent('connecting', async() => {
|
|
||||||
if(!electron.ipcRenderer.sendSync('connect', core.connection.character) && process.env.NODE_ENV === 'production') {
|
if(!electron.ipcRenderer.sendSync('connect', core.connection.character) && process.env.NODE_ENV === 'production') {
|
||||||
alert(l('login.alreadyLoggedIn'));
|
alert(l('login.alreadyLoggedIn'));
|
||||||
return core.connection.close();
|
return core.connection.close();
|
||||||
}
|
}
|
||||||
parent.send('connect', webContents.id, core.connection.character);
|
parent.send('connect', webContents.id, core.connection.character);
|
||||||
this.character = connection.character;
|
this.character = core.connection.character;
|
||||||
if((await core.settingsStore.get('settings')) === undefined &&
|
if((await core.settingsStore.get('settings')) === undefined &&
|
||||||
SlimcatImporter.canImportCharacter(core.connection.character)) {
|
SlimcatImporter.canImportCharacter(core.connection.character)) {
|
||||||
if(!confirm(l('importer.importGeneral'))) return core.settingsStore.set('settings', new Settings());
|
if(!confirm(l('importer.importGeneral'))) return core.settingsStore.set('settings', new Settings());
|
||||||
|
@ -186,22 +193,21 @@
|
||||||
(<Modal>this.$refs['importModal']).hide();
|
(<Modal>this.$refs['importModal']).hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connection.onEvent('connected', () => {
|
core.connection.onEvent('connected', () => {
|
||||||
core.watch(() => core.conversations.hasNew, (newValue) => parent.send('has-new', webContents.id, newValue));
|
core.watch(() => core.conversations.hasNew, (newValue) => parent.send('has-new', webContents.id, newValue));
|
||||||
Raven.setUserContext({username: core.connection.character});
|
Raven.setUserContext({username: core.connection.character});
|
||||||
});
|
});
|
||||||
connection.onEvent('closed', () => {
|
core.connection.onEvent('closed', () => {
|
||||||
if(this.character === undefined) return;
|
if(this.character === undefined) return;
|
||||||
electron.ipcRenderer.send('disconnect', this.character);
|
electron.ipcRenderer.send('disconnect', this.character);
|
||||||
this.character = undefined;
|
this.character = undefined;
|
||||||
parent.send('disconnect', webContents.id);
|
parent.send('disconnect', webContents.id);
|
||||||
Raven.setUserContext();
|
Raven.setUserContext();
|
||||||
});
|
});
|
||||||
initCore(connection, Logs, SettingsStore, Notifications);
|
core.connection.setCredentials(this.settings.account, this.password);
|
||||||
const charNames = Object.keys(data.characters);
|
this.characters = Object.keys(data.characters).map((name) => ({name, id: data.characters[name], deleted: false}))
|
||||||
this.characters = charNames.sort();
|
.sort((x, y) => x.name.localeCompare(y.name));
|
||||||
this.defaultCharacter = charNames.find((x) => data.characters[x] === data.default_character)!;
|
this.defaultCharacter = data.default_character;
|
||||||
profileApiInit(data.characters);
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.error = l('login.error');
|
this.error = l('login.error');
|
||||||
if(process.env.NODE_ENV !== 'production') throw e;
|
if(process.env.NODE_ENV !== 'production') throw e;
|
||||||
|
@ -244,8 +250,8 @@
|
||||||
preview.style.display = 'none';
|
preview.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
openProfileInBrowser(): void {
|
async openProfileInBrowser(): Promise<void> {
|
||||||
electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
|
return electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get styling(): string {
|
get styling(): string {
|
||||||
|
@ -259,6 +265,10 @@
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showLogs(): void {
|
||||||
|
(<Logs>this.$refs['logsDialog']).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
tabs: Tab[] = [];
|
tabs: Tab[] = [];
|
||||||
activeTab: Tab | undefined;
|
activeTab: Tab | undefined;
|
||||||
tabMap: {[key: number]: Tab} = {};
|
tabMap: {[key: number]: Tab} = {};
|
||||||
isMaximized = browserWindow.isMaximized();
|
isMaximized = false;
|
||||||
canOpenTab = true;
|
canOpenTab = true;
|
||||||
l = l;
|
l = l;
|
||||||
hasUpdate = false;
|
hasUpdate = false;
|
||||||
|
@ -83,8 +83,8 @@
|
||||||
lockTab = false;
|
lockTab = false;
|
||||||
|
|
||||||
@Hook('mounted')
|
@Hook('mounted')
|
||||||
mounted(): void {
|
async mounted(): Promise<void> {
|
||||||
this.addTab();
|
await this.addTab();
|
||||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
|
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
|
||||||
electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
|
electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
|
||||||
electron.ipcRenderer.on('open-tab', () => this.addTab());
|
electron.ipcRenderer.on('open-tab', () => this.addTab());
|
||||||
|
@ -153,6 +153,7 @@
|
||||||
browserWindow.hide();
|
browserWindow.hide();
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
this.isMaximized = browserWindow.isMaximized();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyAllTabs(): void {
|
destroyAllTabs(): void {
|
||||||
|
@ -186,12 +187,12 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
addTab(): void {
|
async addTab(): Promise<void> {
|
||||||
if(this.lockTab) return;
|
if(this.lockTab) return;
|
||||||
const tray = new electron.remote.Tray(trayIcon);
|
const tray = new electron.remote.Tray(trayIcon);
|
||||||
tray.setToolTip(l('title'));
|
tray.setToolTip(l('title'));
|
||||||
tray.on('click', (_) => this.trayClicked(tab));
|
tray.on('click', (_) => this.trayClicked(tab));
|
||||||
const view = new electron.remote.BrowserView();
|
const view = new electron.remote.BrowserView({webPreferences: {nodeIntegration: true}});
|
||||||
view.setAutoResize({width: true, height: true});
|
view.setAutoResize({width: true, height: true});
|
||||||
electron.ipcRenderer.send('tab-added', view.webContents.id);
|
electron.ipcRenderer.send('tab-added', view.webContents.id);
|
||||||
const tab = {active: false, view, user: undefined, hasNew: false, tray};
|
const tab = {active: false, view, user: undefined, hasNew: false, tray};
|
||||||
|
@ -200,13 +201,14 @@
|
||||||
this.tabMap[view.webContents.id] = tab;
|
this.tabMap[view.webContents.id] = tab;
|
||||||
this.show(tab);
|
this.show(tab);
|
||||||
this.lockTab = true;
|
this.lockTab = true;
|
||||||
view.webContents.loadURL(url.format({
|
await view.webContents.loadURL(url.format({
|
||||||
pathname: path.join(__dirname, 'index.html'),
|
pathname: path.join(__dirname, 'index.html'),
|
||||||
protocol: 'file:',
|
protocol: 'file:',
|
||||||
slashes: true,
|
slashes: true,
|
||||||
query: {settings: JSON.stringify(this.settings)}
|
query: {settings: JSON.stringify(this.settings)}
|
||||||
}));
|
}));
|
||||||
view.webContents.on('did-stop-loading', () => this.lockTab = false);
|
tab.view.setBounds(getWindowBounds());
|
||||||
|
this.lockTab = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(tab: Tab): void {
|
show(tab: Tab): void {
|
||||||
|
@ -259,6 +261,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-default {
|
.btn-default {
|
||||||
|
|
|
@ -35,12 +35,17 @@ import * as electron from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as qs from 'querystring';
|
import * as qs from 'querystring';
|
||||||
import {getKey} from '../chat/common';
|
import {getKey} from '../chat/common';
|
||||||
|
import {init as initCore} from '../chat/core';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {setupRaven} from '../chat/vue-raven';
|
import {setupRaven} from '../chat/vue-raven';
|
||||||
|
import Socket from '../chat/WebSocket';
|
||||||
|
import Connection from '../fchat/connection';
|
||||||
import {Keys} from '../keys';
|
import {Keys} from '../keys';
|
||||||
import {GeneralSettings, nativeRequire} from './common';
|
import {GeneralSettings, nativeRequire} from './common';
|
||||||
|
import {Logs, SettingsStore} from './filesystem';
|
||||||
import * as SlimcatImporter from './importer';
|
import * as SlimcatImporter from './importer';
|
||||||
import Index from './Index.vue';
|
import Index from './Index.vue';
|
||||||
|
import Notifications from './notifications';
|
||||||
|
|
||||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if(e.ctrlKey && e.shiftKey && getKey(e) === Keys.KeyI)
|
if(e.ctrlKey && e.shiftKey && getKey(e) === Keys.KeyI)
|
||||||
|
@ -79,7 +84,7 @@ function openIncognito(url: string): void {
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
const commands = {
|
const commands = {
|
||||||
chrome: 'chrome.exe -incognito', firefox: 'firefox.exe -private-window', vivaldi: 'vivaldi.exe -incognito',
|
chrome: 'chrome.exe -incognito', firefox: 'firefox.exe -private-window', vivaldi: 'vivaldi.exe -incognito',
|
||||||
opera: 'opera.exe -private'
|
opera: 'opera.exe -private'
|
||||||
};
|
};
|
||||||
|
@ -99,7 +104,7 @@ webContents.on('context-menu', (_, props) => {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
id: 'copy',
|
id: 'copy',
|
||||||
label: l('action.copy'),
|
label: l('action.copy'),
|
||||||
role: can('Copy') ? 'copy' : '',
|
role: can('Copy') ? 'copy' : undefined,
|
||||||
accelerator: 'CmdOrCtrl+C',
|
accelerator: 'CmdOrCtrl+C',
|
||||||
enabled: can('Copy')
|
enabled: can('Copy')
|
||||||
});
|
});
|
||||||
|
@ -107,13 +112,13 @@ webContents.on('context-menu', (_, props) => {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
id: 'cut',
|
id: 'cut',
|
||||||
label: l('action.cut'),
|
label: l('action.cut'),
|
||||||
role: can('Cut') ? 'cut' : '',
|
role: can('Cut') ? 'cut' : undefined,
|
||||||
accelerator: 'CmdOrCtrl+X',
|
accelerator: 'CmdOrCtrl+X',
|
||||||
enabled: can('Cut')
|
enabled: can('Cut')
|
||||||
}, {
|
}, {
|
||||||
id: 'paste',
|
id: 'paste',
|
||||||
label: l('action.paste'),
|
label: l('action.paste'),
|
||||||
role: props.editFlags.canPaste ? 'paste' : '',
|
role: props.editFlags.canPaste ? 'paste' : undefined,
|
||||||
accelerator: 'CmdOrCtrl+V',
|
accelerator: 'CmdOrCtrl+V',
|
||||||
enabled: props.editFlags.canPaste
|
enabled: props.editFlags.canPaste
|
||||||
});
|
});
|
||||||
|
@ -165,7 +170,7 @@ webContents.on('context-menu', (_, props) => {
|
||||||
let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||||
if(process.platform === 'win32') //get the path in DOS (8-character) format as special characters cause problems otherwise
|
if(process.platform === 'win32') //get the path in DOS (8-character) format as special characters cause problems otherwise
|
||||||
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => dictDir = stdout.trim());
|
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => dictDir = stdout.trim());
|
||||||
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)});
|
electron.webFrame.setSpellCheckProvider('', {spellCheck: (words, callback) => callback(words.filter((x) => spellchecker.isMisspelled(x)))});
|
||||||
|
|
||||||
function onSettings(s: GeneralSettings): void {
|
function onSettings(s: GeneralSettings): void {
|
||||||
settings = s;
|
settings = s;
|
||||||
|
@ -187,6 +192,10 @@ if(params['import'] !== undefined)
|
||||||
alert(l('importer.error'));
|
alert(l('importer.error'));
|
||||||
}
|
}
|
||||||
onSettings(settings);
|
onSettings(settings);
|
||||||
|
|
||||||
|
const connection = new Connection(`F-Chat 3.0 (${process.platform})`, electron.remote.app.getVersion(), Socket);
|
||||||
|
initCore(connection, Logs, SettingsStore, Notifications);
|
||||||
|
|
||||||
//tslint:disable-next-line:no-unused-expression
|
//tslint:disable-next-line:no-unused-expression
|
||||||
new Index({
|
new Index({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export const defaultHost = 'wss://chat.f-list.net:9799';
|
export const defaultHost = 'wss://chat.f-list.net/chat2';
|
||||||
|
|
||||||
export class GeneralSettings {
|
export class GeneralSettings {
|
||||||
account = '';
|
account = '';
|
||||||
|
@ -18,30 +17,6 @@ export class GeneralSettings {
|
||||||
hwAcceleration = true;
|
hwAcceleration = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkdir(dir: string): void {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(dir);
|
|
||||||
} catch(e) {
|
|
||||||
if(!(e instanceof Error)) throw e;
|
|
||||||
switch((<Error & {code: string}>e).code) {
|
|
||||||
case 'ENOENT':
|
|
||||||
const dirname = path.dirname(dir);
|
|
||||||
if(dirname === dir) throw e;
|
|
||||||
mkdir(dirname);
|
|
||||||
mkdir(dir);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(dir);
|
|
||||||
if(stat.isDirectory()) return;
|
|
||||||
} catch(e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//tslint:disable
|
//tslint:disable
|
||||||
const Module = require('module');
|
const Module = require('module');
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,9 @@ import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {mkdir} from './common';
|
|
||||||
|
|
||||||
const dictDir = path.join(electron.app.getPath('userData'), 'spellchecker');
|
const dictDir = path.join(electron.app.getPath('userData'), 'spellchecker');
|
||||||
mkdir(dictDir);
|
fs.mkdirSync(dictDir, {recursive: true});
|
||||||
const requestConfig = {responseType: 'arraybuffer'};
|
|
||||||
|
|
||||||
const downloadedPath = path.join(dictDir, 'downloaded.json');
|
const downloadedPath = path.join(dictDir, 'downloaded.json');
|
||||||
const downloadUrl = 'https://client.f-list.net/dicts/';
|
const downloadUrl = 'https://client.f-list.net/dicts/';
|
||||||
|
@ -40,7 +38,8 @@ export async function ensureDictionary(lang: string): Promise<void> {
|
||||||
const filePath = path.join(dictDir, `${lang}.${type}`);
|
const filePath = path.join(dictDir, `${lang}.${type}`);
|
||||||
const downloaded = downloadedDictionaries[file.name];
|
const downloaded = downloadedDictionaries[file.name];
|
||||||
if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) {
|
if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) {
|
||||||
await writeFile(filePath, Buffer.from((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
|
const dictionary = (await Axios.get<string>(`${downloadUrl}${file.name}`, {responseType: 'arraybuffer'})).data;
|
||||||
|
await writeFile(filePath, Buffer.from(dictionary));
|
||||||
downloadedDictionaries[file.name] = file;
|
downloadedDictionaries[file.name] = file;
|
||||||
await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
|
await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {Message as MessageImpl} from '../chat/common';
|
||||||
import core from '../chat/core';
|
import core from '../chat/core';
|
||||||
import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {GeneralSettings, mkdir} from './common';
|
import {GeneralSettings} from './common';
|
||||||
|
|
||||||
declare module '../chat/interfaces' {
|
declare module '../chat/interfaces' {
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -16,7 +16,6 @@ declare module '../chat/interfaces' {
|
||||||
|
|
||||||
const dayMs = 86400000;
|
const dayMs = 86400000;
|
||||||
const read = promisify(fs.read);
|
const read = promisify(fs.read);
|
||||||
const noAssert = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
function writeFile(p: fs.PathLike | number, data: string | object | number,
|
function writeFile(p: fs.PathLike | number, data: string | object | number,
|
||||||
options?: {encoding?: string | null; mode?: number | string; flag?: string} | string | null): void {
|
options?: {encoding?: string | null; mode?: number | string; flag?: string} | string | null): void {
|
||||||
|
@ -46,7 +45,7 @@ interface Index {
|
||||||
|
|
||||||
export function getLogDir(this: void, character: string): string {
|
export function getLogDir(this: void, character: string): string {
|
||||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'logs');
|
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'logs');
|
||||||
mkdir(dir);
|
fs.mkdirSync(dir, {recursive: true});
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +65,15 @@ export function checkIndex(this: void, index: Index, message: Message, key: stri
|
||||||
index[key] = item = {name, index: {}, offsets: []};
|
index[key] = item = {name, index: {}, offsets: []};
|
||||||
const nameLength = Buffer.byteLength(name);
|
const nameLength = Buffer.byteLength(name);
|
||||||
buffer = Buffer.allocUnsafe(nameLength + 8);
|
buffer = Buffer.allocUnsafe(nameLength + 8);
|
||||||
buffer.writeUInt8(nameLength, 0, noAssert);
|
buffer.writeUInt8(nameLength, 0);
|
||||||
buffer.write(name, 1);
|
buffer.write(name, 1);
|
||||||
offset = nameLength + 1;
|
offset = nameLength + 1;
|
||||||
}
|
}
|
||||||
const newValue = typeof size === 'function' ? size() : size;
|
const newValue = typeof size === 'function' ? size() : size;
|
||||||
item.index[date] = item.offsets.length;
|
item.index[date] = item.offsets.length;
|
||||||
item.offsets.push(newValue);
|
item.offsets.push(newValue);
|
||||||
buffer.writeUInt16LE(date, offset, noAssert);
|
buffer.writeUInt16LE(date, offset);
|
||||||
buffer.writeUIntLE(newValue, offset + 2, 5, noAssert);
|
buffer.writeUIntLE(newValue, offset + 2, 5);
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,25 +82,25 @@ export function serializeMessage(message: Message): {serialized: Buffer, size: n
|
||||||
const senderLength = Buffer.byteLength(name);
|
const senderLength = Buffer.byteLength(name);
|
||||||
const messageLength = Buffer.byteLength(message.text);
|
const messageLength = Buffer.byteLength(message.text);
|
||||||
const buffer = Buffer.allocUnsafe(senderLength + messageLength + 10);
|
const buffer = Buffer.allocUnsafe(senderLength + messageLength + 10);
|
||||||
buffer.writeUInt32LE(message.time.getTime() / 1000, 0, noAssert);
|
buffer.writeUInt32LE(message.time.getTime() / 1000, 0);
|
||||||
buffer.writeUInt8(message.type, 4, noAssert);
|
buffer.writeUInt8(message.type, 4);
|
||||||
buffer.writeUInt8(senderLength, 5, noAssert);
|
buffer.writeUInt8(senderLength, 5);
|
||||||
buffer.write(name, 6);
|
buffer.write(name, 6);
|
||||||
let offset = senderLength + 6;
|
let offset = senderLength + 6;
|
||||||
buffer.writeUInt16LE(messageLength, offset, noAssert);
|
buffer.writeUInt16LE(messageLength, offset);
|
||||||
buffer.write(message.text, offset += 2);
|
buffer.write(message.text, offset += 2);
|
||||||
buffer.writeUInt16LE(offset += messageLength, offset, noAssert);
|
buffer.writeUInt16LE(offset += messageLength, offset);
|
||||||
return {serialized: buffer, size: offset + 2};
|
return {serialized: buffer, size: offset + 2};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deserializeMessage(buffer: Buffer, offset: number = 0,
|
function deserializeMessage(buffer: Buffer, offset: number = 0,
|
||||||
characterGetter: (name: string) => Character = (name) => core.characters.get(name),
|
characterGetter: (name: string) => Character = (name) => core.characters.get(name)
|
||||||
unsafe: boolean = noAssert): {size: number, message: Conversation.Message} {
|
): {size: number, message: Conversation.Message} {
|
||||||
const time = buffer.readUInt32LE(offset, unsafe);
|
const time = buffer.readUInt32LE(offset);
|
||||||
const type = buffer.readUInt8(offset += 4, unsafe);
|
const type = buffer.readUInt8(offset += 4);
|
||||||
const senderLength = buffer.readUInt8(offset += 1, unsafe);
|
const senderLength = buffer.readUInt8(offset += 1);
|
||||||
const sender = buffer.toString('utf8', offset += 1, offset += senderLength);
|
const sender = buffer.toString('utf8', offset += 1, offset += senderLength);
|
||||||
const messageLength = buffer.readUInt16LE(offset, unsafe);
|
const messageLength = buffer.readUInt16LE(offset);
|
||||||
const text = buffer.toString('utf8', offset += 2, offset + messageLength);
|
const text = buffer.toString('utf8', offset += 2, offset + messageLength);
|
||||||
const message = new MessageImpl(type, characterGetter(sender), text, new Date(time * 1000));
|
const message = new MessageImpl(type, characterGetter(sender), text, new Date(time * 1000));
|
||||||
return {message, size: senderLength + messageLength + 10};
|
return {message, size: senderLength + messageLength + 10};
|
||||||
|
@ -126,7 +125,7 @@ export function fixLogs(character: string): void {
|
||||||
const indexFd = fs.openSync(indexPath, 'r+');
|
const indexFd = fs.openSync(indexPath, 'r+');
|
||||||
fs.readSync(indexFd, buffer, 0, 1, 0);
|
fs.readSync(indexFd, buffer, 0, 1, 0);
|
||||||
let pos = 0, lastDay = 0;
|
let pos = 0, lastDay = 0;
|
||||||
const nameEnd = buffer.readUInt8(0, noAssert) + 1;
|
const nameEnd = buffer.readUInt8(0) + 1;
|
||||||
fs.ftruncateSync(indexFd, nameEnd);
|
fs.ftruncateSync(indexFd, nameEnd);
|
||||||
fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
|
fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
|
||||||
const size = (fs.fstatSync(fd)).size;
|
const size = (fs.fstatSync(fd)).size;
|
||||||
|
@ -137,12 +136,12 @@ export function fixLogs(character: string): void {
|
||||||
const deserialized = deserializeMessage(buffer, 0, (name) => ({
|
const deserialized = deserializeMessage(buffer, 0, (name) => ({
|
||||||
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
|
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
|
||||||
isIgnored: false, name
|
isIgnored: false, name
|
||||||
}), false);
|
}));
|
||||||
const time = deserialized.message.time;
|
const time = deserialized.message.time;
|
||||||
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
|
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
|
||||||
if(day > lastDay) {
|
if(day > lastDay) {
|
||||||
buffer.writeUInt16LE(day, 0, noAssert);
|
buffer.writeUInt16LE(day, 0);
|
||||||
buffer.writeUIntLE(pos, 2, 5, noAssert);
|
buffer.writeUIntLE(pos, 2, 5);
|
||||||
fs.writeSync(indexFd, buffer, 0, 7);
|
fs.writeSync(indexFd, buffer, 0, 7);
|
||||||
lastDay = day;
|
lastDay = day;
|
||||||
}
|
}
|
||||||
|
@ -166,7 +165,7 @@ function loadIndex(name: string): Index {
|
||||||
if(file.substr(-4) === '.idx')
|
if(file.substr(-4) === '.idx')
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(path.join(dir, file));
|
const content = fs.readFileSync(path.join(dir, file));
|
||||||
let offset = content.readUInt8(0, noAssert) + 1;
|
let offset = content.readUInt8(0) + 1;
|
||||||
const item: IndexItem = {
|
const item: IndexItem = {
|
||||||
name: content.toString('utf8', 1, offset),
|
name: content.toString('utf8', 1, offset),
|
||||||
index: {},
|
index: {},
|
||||||
|
@ -175,7 +174,7 @@ function loadIndex(name: string): Index {
|
||||||
for(; offset < content.length; offset += 7) {
|
for(; offset < content.length; offset += 7) {
|
||||||
const key = content.readUInt16LE(offset);
|
const key = content.readUInt16LE(offset);
|
||||||
item.index[key] = item.offsets.length;
|
item.index[key] = item.offsets.length;
|
||||||
item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert));
|
item.offsets.push(content.readUIntLE(offset + 2, 5));
|
||||||
}
|
}
|
||||||
index[file.slice(0, -4).toLowerCase()] = item;
|
index[file.slice(0, -4).toLowerCase()] = item;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
@ -289,14 +288,14 @@ export class Logs implements Logging {
|
||||||
|
|
||||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||||
const baseDir = core.state.generalSettings!.logDirectory;
|
const baseDir = core.state.generalSettings!.logDirectory;
|
||||||
mkdir(baseDir);
|
fs.mkdirSync(baseDir, {recursive: true});
|
||||||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSettingsDir(character: string = core.connection.character): string {
|
function getSettingsDir(character: string = core.connection.character): string {
|
||||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'settings');
|
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'settings');
|
||||||
mkdir(dir);
|
fs.mkdirSync(dir, {recursive: true});
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,6 +314,7 @@ export class SettingsStore implements Settings.Store {
|
||||||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//tslint:disable-next-line:no-async-without-await
|
||||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||||
writeFile(path.join(getSettingsDir(), key), JSON.stringify(value));
|
writeFile(path.join(getSettingsDir(), key), JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,7 +231,8 @@ export async function importCharacter(ownCharacter: string, progress: (progress:
|
||||||
}
|
}
|
||||||
++index;
|
++index;
|
||||||
continue;
|
continue;
|
||||||
} else if(char === '\r' || char === '\n') {
|
}
|
||||||
|
if(char === '\r' || char === '\n') {
|
||||||
const nextLine = content.substr(index + (char === '\r' ? 2 : 1), 29);
|
const nextLine = content.substr(index + (char === '\r' ? 2 : 1), 29);
|
||||||
if(logRegex.test(nextLine) || content.length - index <= 2) {
|
if(logRegex.test(nextLine) || content.length - index <= 2) {
|
||||||
const line = content.substring(start, index);
|
const line = content.substring(start, index);
|
||||||
|
|
|
@ -35,7 +35,7 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {GeneralSettings, mkdir} from './common';
|
import {defaultHost, GeneralSettings} from './common';
|
||||||
import {ensureDictionary, getAvailableDictionaries} from './dictionaries';
|
import {ensureDictionary, getAvailableDictionaries} from './dictionaries';
|
||||||
import * as windowState from './window_state';
|
import * as windowState from './window_state';
|
||||||
import BrowserWindow = Electron.BrowserWindow;
|
import BrowserWindow = Electron.BrowserWindow;
|
||||||
|
@ -51,11 +51,11 @@ const characters: string[] = [];
|
||||||
let tabCount = 0;
|
let tabCount = 0;
|
||||||
|
|
||||||
const baseDir = app.getPath('userData');
|
const baseDir = app.getPath('userData');
|
||||||
mkdir(baseDir);
|
fs.mkdirSync(baseDir, {recursive: true});
|
||||||
let shouldImportSettings = false;
|
let shouldImportSettings = false;
|
||||||
|
|
||||||
const settingsDir = path.join(baseDir, 'data');
|
const settingsDir = path.join(baseDir, 'data');
|
||||||
mkdir(settingsDir);
|
fs.mkdirSync(settingsDir, {recursive: true});
|
||||||
const settingsFile = path.join(settingsDir, 'settings');
|
const settingsFile = path.join(settingsDir, 'settings');
|
||||||
const settings = new GeneralSettings();
|
const settings = new GeneralSettings();
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ function setUpWebContents(webContents: Electron.WebContents): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const profileMatch = linkUrl.match(/^https?:\/\/(www\.)?f-list.net\/c\/([^/#]+)\/?#?/);
|
const profileMatch = linkUrl.match(/^https?:\/\/(www\.)?f-list.net\/c\/([^/#]+)\/?#?/);
|
||||||
if(profileMatch !== null && settings.profileViewer) webContents.send('open-profile', decodeURIComponent(profileMatch[2]));
|
if(profileMatch !== null && settings.profileViewer) webContents.send('open-profile', decodeURIComponent(profileMatch[2]));
|
||||||
else electron.shell.openExternal(linkUrl);
|
else return electron.shell.openExternal(linkUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
webContents.on('will-navigate', openLinkExternally);
|
webContents.on('will-navigate', openLinkExternally);
|
||||||
|
@ -118,14 +118,14 @@ function createWindow(): Electron.BrowserWindow | undefined {
|
||||||
if(tabCount >= 3) return;
|
if(tabCount >= 3) return;
|
||||||
const lastState = windowState.getSavedWindowState();
|
const lastState = windowState.getSavedWindowState();
|
||||||
const windowProperties: Electron.BrowserWindowConstructorOptions & {maximized: boolean} = {
|
const windowProperties: Electron.BrowserWindowConstructorOptions & {maximized: boolean} = {
|
||||||
...lastState, center: lastState.x === undefined, show: false
|
...lastState, center: lastState.x === undefined, show: false, webPreferences: {nodeIntegration: true}
|
||||||
};
|
};
|
||||||
if(process.platform === 'darwin') windowProperties.titleBarStyle = 'hiddenInset';
|
if(process.platform === 'darwin') windowProperties.titleBarStyle = 'hiddenInset';
|
||||||
else windowProperties.frame = false;
|
else windowProperties.frame = false;
|
||||||
const window = new electron.BrowserWindow(windowProperties);
|
const window = new electron.BrowserWindow(windowProperties);
|
||||||
windows.push(window);
|
windows.push(window);
|
||||||
|
|
||||||
window.loadURL(url.format({
|
window.loadURL(url.format({ //tslint:disable-line:no-floating-promises
|
||||||
pathname: path.join(__dirname, 'window.html'),
|
pathname: path.join(__dirname, 'window.html'),
|
||||||
protocol: 'file:',
|
protocol: 'file:',
|
||||||
slashes: true,
|
slashes: true,
|
||||||
|
@ -145,7 +145,7 @@ function createWindow(): Electron.BrowserWindow | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPatchNotes(): void {
|
function showPatchNotes(): void {
|
||||||
electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog');
|
electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog'); //tslint:disable-line:no-floating-promises
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReady(): void {
|
function onReady(): void {
|
||||||
|
@ -160,6 +160,8 @@ function onReady(): void {
|
||||||
|
|
||||||
if(settings.version !== app.getVersion()) {
|
if(settings.version !== app.getVersion()) {
|
||||||
showPatchNotes();
|
showPatchNotes();
|
||||||
|
if(settings.host === 'wss://chat.f-list.net:9799')
|
||||||
|
settings.host = defaultHost;
|
||||||
settings.version = app.getVersion();
|
settings.version = app.getVersion();
|
||||||
setGeneralSettings(settings);
|
setGeneralSettings(settings);
|
||||||
}
|
}
|
||||||
|
@ -235,8 +237,8 @@ function onReady(): void {
|
||||||
{
|
{
|
||||||
label: l('settings.logDir'),
|
label: l('settings.logDir'),
|
||||||
click: (_, window: BrowserWindow) => {
|
click: (_, window: BrowserWindow) => {
|
||||||
const dir = <string[] | undefined>electron.dialog.showOpenDialog(
|
const dir = electron.dialog.showOpenDialog(
|
||||||
{defaultPath: new GeneralSettings().logDirectory, properties: ['openDirectory']});
|
{defaultPath: settings.logDirectory, properties: ['openDirectory']});
|
||||||
if(dir !== undefined) {
|
if(dir !== undefined) {
|
||||||
if(dir[0].startsWith(path.dirname(app.getPath('exe'))))
|
if(dir[0].startsWith(path.dirname(app.getPath('exe'))))
|
||||||
return electron.dialog.showErrorBox(l('settings.logDir'), l('settings.logDir.inAppDir'));
|
return electron.dialog.showErrorBox(l('settings.logDir'), l('settings.logDir.inAppDir'));
|
||||||
|
@ -369,7 +371,7 @@ function onReady(): void {
|
||||||
});
|
});
|
||||||
electron.ipcMain.on('connect', (e: Event & {sender: Electron.WebContents}, character: string) => {
|
electron.ipcMain.on('connect', (e: Event & {sender: Electron.WebContents}, character: string) => {
|
||||||
if(characters.indexOf(character) !== -1) return e.returnValue = false;
|
if(characters.indexOf(character) !== -1) return e.returnValue = false;
|
||||||
else characters.push(character);
|
characters.push(character);
|
||||||
e.returnValue = true;
|
e.returnValue = true;
|
||||||
});
|
});
|
||||||
electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
|
electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
|
||||||
|
|
|
@ -4,36 +4,12 @@ const pkg = require(path.join(__dirname, 'package.json'));
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const child_process = require('child_process');
|
const child_process = require('child_process');
|
||||||
|
|
||||||
function mkdir(dir) {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(dir);
|
|
||||||
} catch(e) {
|
|
||||||
if(!(e instanceof Error)) throw e;
|
|
||||||
switch(e.code) {
|
|
||||||
case 'ENOENT':
|
|
||||||
const dirname = path.dirname(dir);
|
|
||||||
if(dirname === dir) throw e;
|
|
||||||
mkdir(dirname);
|
|
||||||
mkdir(dir);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(dir);
|
|
||||||
if(stat.isDirectory()) return;
|
|
||||||
} catch(e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const distDir = path.join(__dirname, 'dist');
|
const distDir = path.join(__dirname, 'dist');
|
||||||
const isBeta = pkg.version.indexOf('beta') !== -1;
|
const isBeta = pkg.version.indexOf('beta') !== -1;
|
||||||
const spellcheckerPath = 'spellchecker/build/Release/spellchecker.node', keytarPath = 'keytar/build/Release/keytar.node';
|
const spellcheckerPath = 'spellchecker/build/Release/spellchecker.node', keytarPath = 'keytar/build/Release/keytar.node';
|
||||||
const modules = path.join(__dirname, 'app', 'node_modules');
|
const modules = path.join(__dirname, 'app', 'node_modules');
|
||||||
mkdir(path.dirname(path.join(modules, spellcheckerPath)));
|
fs.mkdirSync(path.dirname(path.join(modules, spellcheckerPath)), {recursive: true});
|
||||||
mkdir(path.dirname(path.join(modules, keytarPath)));
|
fs.mkdirSync(path.dirname(path.join(modules, keytarPath)), {recursive: true});
|
||||||
fs.copyFileSync(require.resolve(spellcheckerPath), path.join(modules, spellcheckerPath));
|
fs.copyFileSync(require.resolve(spellcheckerPath), path.join(modules, spellcheckerPath));
|
||||||
fs.copyFileSync(require.resolve(keytarPath), path.join(modules, keytarPath));
|
fs.copyFileSync(require.resolve(keytarPath), path.join(modules, keytarPath));
|
||||||
|
|
||||||
|
@ -69,7 +45,7 @@ require('electron-packager')({
|
||||||
setupExe: setupName,
|
setupExe: setupName,
|
||||||
remoteReleases: 'https://client.f-list.net/win32/' + (isBeta ? '?channel=beta' : ''),
|
remoteReleases: 'https://client.f-list.net/win32/' + (isBeta ? '?channel=beta' : ''),
|
||||||
signWithParams: process.argv.length > 3 ? `/a /f ${process.argv[2]} /p ${process.argv[3]} /fd sha256 /tr http://timestamp.digicert.com /td sha256` : undefined
|
signWithParams: process.argv.length > 3 ? `/a /f ${process.argv[2]} /p ${process.argv[3]} /fd sha256 /tr http://timestamp.digicert.com /td sha256` : undefined
|
||||||
}).catch((e) => console.log(`Error while creating installer: ${e.message}`));
|
}).catch((e) => console.error(`Error while creating installer: ${e.message}`));
|
||||||
} else if(process.platform === 'darwin') {
|
} else if(process.platform === 'darwin') {
|
||||||
console.log('Creating Mac DMG');
|
console.log('Creating Mac DMG');
|
||||||
const target = path.join(distDir, `F-Chat.dmg`);
|
const target = path.join(distDir, `F-Chat.dmg`);
|
||||||
|
@ -104,7 +80,7 @@ require('electron-packager')({
|
||||||
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));
|
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));
|
||||||
fs.copyFileSync(path.join(__dirname, 'build', 'icon.png'), path.join(appPaths[0], 'icon.png'));
|
fs.copyFileSync(path.join(__dirname, 'build', 'icon.png'), path.join(appPaths[0], 'icon.png'));
|
||||||
const libDir = path.join(appPaths[0], 'usr', 'lib'), libSource = path.join(__dirname, 'build', 'linux-libs');
|
const libDir = path.join(appPaths[0], 'usr', 'lib'), libSource = path.join(__dirname, 'build', 'linux-libs');
|
||||||
mkdir(libDir);
|
fs.mkdirSync(libDir, {recursive: true});
|
||||||
for(const file of fs.readdirSync(libSource))
|
for(const file of fs.readdirSync(libSource))
|
||||||
fs.copyFileSync(path.join(libSource, file), path.join(libDir, file));
|
fs.copyFileSync(path.join(libSource, file), path.join(libDir, file));
|
||||||
fs.symlinkSync(path.join(appPaths[0], 'icon.png'), path.join(appPaths[0], '.DirIcon'));
|
fs.symlinkSync(path.join(appPaths[0], 'icon.png'), path.join(appPaths[0], '.DirIcon'));
|
||||||
|
@ -114,13 +90,13 @@ require('electron-packager')({
|
||||||
const stream = fs.createWriteStream(downloaded);
|
const stream = fs.createWriteStream(downloaded);
|
||||||
res.data.pipe(stream);
|
res.data.pipe(stream);
|
||||||
stream.on('close', () => {
|
stream.on('close', () => {
|
||||||
const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|httpos://client.f-list.net/fchat.AppImage.zsync'];
|
const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|https://client.f-list.net/fchat.AppImage.zsync'];
|
||||||
if(process.argv.length > 2) args.push('-s', '--sign-key', process.argv[2]);
|
if(process.argv.length > 2) args.push('-s', '--sign-key', process.argv[2]);
|
||||||
else console.warn('Warning: Creating unsigned AppImage');
|
else console.warn('Warning: Creating unsigned AppImage');
|
||||||
if(process.argv.length > 3) args.push('--sign-args', `--passphrase=${process.argv[3]}`);
|
if(process.argv.length > 3) args.push('--sign-args', `--no-tty --passphrase=${process.argv[3]}`);
|
||||||
fs.chmodSync(downloaded, 0o755);
|
fs.chmodSync(downloaded, 0o755);
|
||||||
child_process.spawn(downloaded, ['--appimage-extract'], {cwd: distDir}).on('close', () => {
|
child_process.spawn(downloaded, ['--appimage-extract'], {cwd: distDir}).on('close', () => {
|
||||||
const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir});
|
const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir, env: {ARCH: 'x86_64'}});
|
||||||
child.stdout.on('data', (data) => console.log(data.toString()));
|
child.stdout.on('data', (data) => console.log(data.toString()));
|
||||||
child.stderr.on('data', (data) => console.error(data.toString()));
|
child.stderr.on('data', (data) => console.error(data.toString()));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "fchat",
|
"name": "fchat",
|
||||||
"version": "3.0.10",
|
"version": "3.0.13",
|
||||||
"author": "The F-List Team",
|
"author": "The F-List Team",
|
||||||
"description": "F-List.net Chat Client",
|
"description": "F-List.net Chat Client",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noEmitHelpers": true,
|
"noEmitHelpers": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true
|
"noUnusedParameters": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,7 +33,6 @@ const mainConfig = {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForkTsCheckerWebpackPlugin({
|
new ForkTsCheckerWebpackPlugin({
|
||||||
workers: 2,
|
|
||||||
async: false,
|
async: false,
|
||||||
tslint: path.join(__dirname, '../tslint.json'),
|
tslint: path.join(__dirname, '../tslint.json'),
|
||||||
tsconfig: './tsconfig-main.json'
|
tsconfig: './tsconfig-main.json'
|
||||||
|
@ -90,7 +89,6 @@ const mainConfig = {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForkTsCheckerWebpackPlugin({
|
new ForkTsCheckerWebpackPlugin({
|
||||||
workers: 2,
|
|
||||||
async: false,
|
async: false,
|
||||||
tslint: path.join(__dirname, '../tslint.json'),
|
tslint: path.join(__dirname, '../tslint.json'),
|
||||||
tsconfig: './tsconfig-renderer.json',
|
tsconfig: './tsconfig-renderer.json',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
||||||
import * as qs from 'qs';
|
import * as qs from 'qs';
|
||||||
import {Connection as Interfaces, WebSocketConnection} from './interfaces';
|
import {Connection as Interfaces, WebSocketConnection} from './interfaces';
|
||||||
|
import ReadyState = WebSocketConnection.ReadyState;
|
||||||
|
|
||||||
const fatalErrors = [2, 3, 4, 9, 30, 31, 33, 39, 40, 62, -4];
|
const fatalErrors = [2, 3, 4, 9, 30, 31, 33, 39, 40, 62, -4];
|
||||||
const dieErrors = [9, 30, 31, 39, 40];
|
const dieErrors = [9, 30, 31, 39, 40];
|
||||||
|
@ -11,25 +12,32 @@ async function queryApi(this: void, endpoint: string, data: object): Promise<Axi
|
||||||
|
|
||||||
export default class Connection implements Interfaces.Connection {
|
export default class Connection implements Interfaces.Connection {
|
||||||
character = '';
|
character = '';
|
||||||
vars: Interfaces.Vars & {[key: string]: string} = <any>{}; //tslint:disable-line:no-any
|
vars: Interfaces.Vars = <any>{}; //tslint:disable-line:no-any
|
||||||
protected socket: WebSocketConnection | undefined = undefined;
|
protected socket: WebSocketConnection | undefined = undefined;
|
||||||
private messageHandlers: {[key in keyof Interfaces.ServerCommands]?: Interfaces.CommandHandler<key>[]} = {};
|
//tslint:disable-next-line:no-object-literal-type-assertion
|
||||||
private connectionHandlers: {[key in Interfaces.EventType]?: Interfaces.EventHandler[]} = {};
|
private messageHandlers = <{ [key in keyof Interfaces.ServerCommands]: Interfaces.CommandHandler<key>[] }>{};
|
||||||
|
private connectionHandlers: { [key in Interfaces.EventType]?: Interfaces.EventHandler[] } = {};
|
||||||
private errorHandlers: ((error: Error) => void)[] = [];
|
private errorHandlers: ((error: Error) => void)[] = [];
|
||||||
private ticket = '';
|
private ticket = '';
|
||||||
private cleanClose = false;
|
private cleanClose = false;
|
||||||
private reconnectTimer: NodeJS.Timer | undefined;
|
private reconnectTimer: NodeJS.Timer | undefined;
|
||||||
private readonly ticketProvider: Interfaces.TicketProvider;
|
private account = '';
|
||||||
|
private ticketProvider?: Interfaces.TicketProvider;
|
||||||
private reconnectDelay = 0;
|
private reconnectDelay = 0;
|
||||||
private isReconnect = false;
|
private isReconnect = false;
|
||||||
|
private pinTimeout?: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(private readonly clientName: string, private readonly version: string,
|
constructor(private readonly clientName: string, private readonly version: string,
|
||||||
private readonly socketProvider: new() => WebSocketConnection, private readonly account: string,
|
private readonly socketProvider: new() => WebSocketConnection) {
|
||||||
ticketProvider: Interfaces.TicketProvider | string) {
|
}
|
||||||
|
|
||||||
|
setCredentials(account: string, ticketProvider: Interfaces.TicketProvider | string): void {
|
||||||
|
this.account = account;
|
||||||
this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider;
|
this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(character: string): Promise<void> {
|
async connect(character: string): Promise<void> {
|
||||||
|
if(!this.ticketProvider) throw new Error('No credentials set!');
|
||||||
this.cleanClose = false;
|
this.cleanClose = false;
|
||||||
if(this.character !== character) this.isReconnect = false;
|
if(this.character !== character) this.isReconnect = false;
|
||||||
this.character = character;
|
this.character = character;
|
||||||
|
@ -67,6 +75,7 @@ export default class Connection implements Interfaces.Connection {
|
||||||
method: 'ticket',
|
method: 'ticket',
|
||||||
ticket: this.ticket
|
ticket: this.ticket
|
||||||
});
|
});
|
||||||
|
this.resetPinTimeout();
|
||||||
});
|
});
|
||||||
this.socket.onMessage(async(msg: string) => {
|
this.socket.onMessage(async(msg: string) => {
|
||||||
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
|
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
|
||||||
|
@ -74,6 +83,7 @@ export default class Connection implements Interfaces.Connection {
|
||||||
return this.handleMessage(type, data);
|
return this.handleMessage(type, data);
|
||||||
});
|
});
|
||||||
this.socket.onClose(async() => {
|
this.socket.onClose(async() => {
|
||||||
|
if(this.pinTimeout) clearTimeout(this.pinTimeout);
|
||||||
if(!this.cleanClose) this.reconnect();
|
if(!this.cleanClose) this.reconnect();
|
||||||
this.socket = undefined;
|
this.socket = undefined;
|
||||||
await this.invokeHandlers('closed', !this.cleanClose);
|
await this.invokeHandlers('closed', !this.cleanClose);
|
||||||
|
@ -95,10 +105,11 @@ export default class Connection implements Interfaces.Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return this.socket !== undefined;
|
return this.socket !== undefined && this.socket.readyState === ReadyState.OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryApi<T = object>(endpoint: string, data?: {account?: string, ticket?: string}): Promise<T> {
|
async queryApi<T = object>(endpoint: string, data?: {account?: string, ticket?: string}): Promise<T> {
|
||||||
|
if(!this.ticketProvider) throw new Error('No credentials set!');
|
||||||
if(data === undefined) data = {};
|
if(data === undefined) data = {};
|
||||||
data.account = this.account;
|
data.account = this.account;
|
||||||
data.ticket = this.ticket;
|
data.ticket = this.ticket;
|
||||||
|
@ -156,10 +167,11 @@ export default class Connection implements Interfaces.Connection {
|
||||||
for(const handler of handlers) await handler(data, time);
|
for(const handler of handlers) await handler(data, time);
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'VAR':
|
case 'VAR':
|
||||||
this.vars[data.variable] = data.value;
|
this.vars[<keyof Interfaces.Vars>data.variable] = data.value;
|
||||||
break;
|
break;
|
||||||
case 'PIN':
|
case 'PIN':
|
||||||
this.send('PIN');
|
this.send('PIN');
|
||||||
|
this.resetPinTimeout();
|
||||||
break;
|
break;
|
||||||
case 'ERR':
|
case 'ERR':
|
||||||
if(fatalErrors.indexOf(data.number) !== -1) {
|
if(fatalErrors.indexOf(data.number) !== -1) {
|
||||||
|
@ -198,4 +210,9 @@ export default class Connection implements Interfaces.Connection {
|
||||||
if(request) (<Error & {request: true}>error).request = true;
|
if(request) (<Error & {request: true}>error).request = true;
|
||||||
for(const handler of this.errorHandlers) handler(error);
|
for(const handler of this.errorHandlers) handler(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resetPinTimeout(): void {
|
||||||
|
if(this.pinTimeout) clearTimeout(this.pinTimeout);
|
||||||
|
this.pinTimeout = setTimeout(() => this.socket!.close(), 90000);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -122,21 +122,22 @@ export namespace Connection {
|
||||||
export type EventHandler = (isReconnect: boolean) => Promise<void> | void;
|
export type EventHandler = (isReconnect: boolean) => Promise<void> | void;
|
||||||
|
|
||||||
export interface Vars {
|
export interface Vars {
|
||||||
readonly chat_max: number
|
chat_max: number
|
||||||
readonly priv_max: number
|
priv_max: number
|
||||||
readonly lfrp_max: number
|
lfrp_max: number
|
||||||
readonly cds_max: number
|
cds_max: number
|
||||||
readonly lfrp_flood: number
|
lfrp_flood: number
|
||||||
readonly msg_flood: number
|
msg_flood: number
|
||||||
readonly sta_flood: number
|
sta_flood: number
|
||||||
readonly permissions: number
|
permissions: number
|
||||||
readonly icon_blacklist: ReadonlyArray<string>
|
icon_blacklist: ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Connection {
|
export interface Connection {
|
||||||
readonly character: string
|
readonly character: string
|
||||||
readonly vars: Vars
|
readonly vars: Readonly<Vars>
|
||||||
readonly isOpen: boolean
|
readonly isOpen: boolean
|
||||||
|
setCredentials(account: string, ticketProvider: TicketProvider | string): void
|
||||||
connect(character: string): void
|
connect(character: string): void
|
||||||
close(keepState?: boolean): void
|
close(keepState?: boolean): void
|
||||||
onMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
onMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||||
|
|
|
@ -46,10 +46,9 @@ export interface Infotag {
|
||||||
name: string
|
name: string
|
||||||
type: InfotagType
|
type: InfotagType
|
||||||
search_field: string
|
search_field: string
|
||||||
validator: string
|
validator?: string
|
||||||
allow_legacy: boolean
|
allow_legacy: boolean
|
||||||
infotag_group: string
|
infotag_group: number
|
||||||
list?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Character extends SimpleCharacter {
|
export interface Character extends SimpleCharacter {
|
||||||
|
@ -57,7 +56,7 @@ export interface Character extends SimpleCharacter {
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
kinks: {[key: string]: KinkChoice | number | undefined}
|
kinks: {[key: number]: KinkChoice | number | undefined}
|
||||||
inlines: {[key: string]: InlineImage}
|
inlines: {[key: string]: InlineImage}
|
||||||
customs: {[key: string]: CustomKink | undefined}
|
customs: {[key: string]: CustomKink | undefined}
|
||||||
infotags: {[key: number]: CharacterInfotag | undefined}
|
infotags: {[key: number]: CharacterInfotag | undefined}
|
||||||
|
@ -96,4 +95,42 @@ export interface CustomKink {
|
||||||
name: string
|
name: string
|
||||||
choice: KinkChoice
|
choice: KinkChoice
|
||||||
description: string
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KinkGroup {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
sort_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InfotagGroup {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
sort_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
sort_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum InlineDisplayMode {DISPLAY_ALL, DISPLAY_SFW, DISPLAY_NONE}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
animateEicons: boolean
|
||||||
|
inlineDisplayMode: InlineDisplayMode
|
||||||
|
defaultCharacter: number
|
||||||
|
fuzzyDates: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharedDefinitions {
|
||||||
|
readonly listItems: {readonly [key: string]: Readonly<ListItem>}
|
||||||
|
readonly kinks: {readonly [key: string]: Readonly<Kink>}
|
||||||
|
readonly kinkGroups: {readonly [key: string]: Readonly<KinkGroup>}
|
||||||
|
readonly infotags: {readonly [key: string]: Readonly<Infotag>}
|
||||||
|
readonly infotagGroups: {readonly [key: string]: Readonly<InfotagGroup>}
|
||||||
}
|
}
|
|
@ -63,15 +63,13 @@
|
||||||
import * as Raven from 'raven-js';
|
import * as Raven from 'raven-js';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Chat from '../chat/Chat.vue';
|
import Chat from '../chat/Chat.vue';
|
||||||
import core, {init as initCore} from '../chat/core';
|
import core from '../chat/core';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {init as profileApiInit} from '../chat/profile_api';
|
|
||||||
import Socket from '../chat/WebSocket';
|
import Socket from '../chat/WebSocket';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import Connection from '../fchat/connection';
|
import {SimpleCharacter} from '../interfaces';
|
||||||
import CharacterPage from '../site/character_page/character_page.vue';
|
import CharacterPage from '../site/character_page/character_page.vue';
|
||||||
import {appVersion, GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
import {appVersion, GeneralSettings, getGeneralSettings, setGeneralSettings, SettingsStore} from './filesystem';
|
||||||
import Notifications from './notifications';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -97,9 +95,9 @@
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
saveLogin = false;
|
saveLogin = false;
|
||||||
loggingIn = false;
|
loggingIn = false;
|
||||||
characters?: ReadonlyArray<string>;
|
characters?: ReadonlyArray<SimpleCharacter>;
|
||||||
error = '';
|
error = '';
|
||||||
defaultCharacter?: string;
|
defaultCharacter?: number;
|
||||||
settingsStore = new SettingsStore();
|
settingsStore = new SettingsStore();
|
||||||
l = l;
|
l = l;
|
||||||
settings!: GeneralSettings;
|
settings!: GeneralSettings;
|
||||||
|
@ -147,27 +145,20 @@
|
||||||
}
|
}
|
||||||
if(this.saveLogin) await setGeneralSettings(this.settings);
|
if(this.saveLogin) await setGeneralSettings(this.settings);
|
||||||
Socket.host = this.settings.host;
|
Socket.host = this.settings.host;
|
||||||
const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket,
|
core.connection.setCredentials(this.settings.account, this.settings.password);
|
||||||
this.settings.account, this.settings.password);
|
core.connection.onEvent('connected', () => {
|
||||||
connection.onEvent('connected', () => {
|
|
||||||
Raven.setUserContext({username: core.connection.character});
|
Raven.setUserContext({username: core.connection.character});
|
||||||
document.addEventListener('backbutton', confirmBack);
|
document.addEventListener('backbutton', confirmBack);
|
||||||
NativeBackground.start();
|
NativeBackground.start();
|
||||||
});
|
});
|
||||||
connection.onEvent('closed', () => {
|
core.connection.onEvent('closed', () => {
|
||||||
Raven.setUserContext();
|
Raven.setUserContext();
|
||||||
document.removeEventListener('backbutton', confirmBack);
|
document.removeEventListener('backbutton', confirmBack);
|
||||||
NativeBackground.stop();
|
NativeBackground.stop();
|
||||||
});
|
});
|
||||||
initCore(connection, Logs, SettingsStore, Notifications);
|
this.characters = Object.keys(data.characters).map((name) => ({name, id: data.characters[name], deleted: false}))
|
||||||
const charNames = Object.keys(data.characters);
|
.sort((x, y) => x.name.localeCompare(y.name));
|
||||||
this.characters = charNames.sort();
|
this.defaultCharacter = data.default_character;
|
||||||
for(const character of charNames)
|
|
||||||
if(data.characters[character] === data.default_character) {
|
|
||||||
this.defaultCharacter = character;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
profileApiInit(data.characters);
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.error = l('login.error');
|
this.error = l('login.error');
|
||||||
if(process.env.NODE_ENV !== 'production') throw e;
|
if(process.env.NODE_ENV !== 'production') throw e;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
|
@ -8,8 +8,8 @@ android {
|
||||||
applicationId "net.f_list.fchat"
|
applicationId "net.f_list.fchat"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 21
|
versionCode 25
|
||||||
versionName "3.0.10"
|
versionName "3.0.12"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
|
|
@ -2,19 +2,24 @@ package net.f_list.fchat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.PowerManager
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
|
|
||||||
class Background(private val ctx: Context) {
|
class Background(private val ctx: Context) {
|
||||||
private val serviceIntent: Intent by lazy { Intent(ctx, BackgroundService::class.java) }
|
private val serviceIntent: Intent by lazy { Intent(ctx, BackgroundService::class.java) }
|
||||||
|
private val powerManager: PowerManager by lazy { ctx.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun start() {
|
fun start() {
|
||||||
|
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "fchat")
|
||||||
|
wakeLock!!.acquire()
|
||||||
ctx.startService(serviceIntent)
|
ctx.startService(serviceIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
if(wakeLock != null && wakeLock!!.isHeld) wakeLock!!.release()
|
||||||
ctx.stopService(serviceIntent)
|
ctx.stopService(serviceIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.JsResult
|
import android.webkit.JsResult
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
|
@ -96,7 +97,7 @@ class MainActivity : Activity() {
|
||||||
"n.getCharacters=function(){return JSON.parse(n.getCharactersN())}})(NativeLogs)", null)
|
"n.getCharacters=function(){return JSON.parse(n.getCharactersN())}})(NativeLogs)", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debugHandler.postDelayed(keepAlive, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addFolder(folder: java.io.File, out: ZipOutputStream, path: String) {
|
private fun addFolder(folder: java.io.File, out: ZipOutputStream, path: String) {
|
||||||
|
@ -109,6 +110,13 @@ class MainActivity : Activity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keepAlive = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
webView.dispatchWindowVisibilityChanged(View.VISIBLE);
|
||||||
|
debugHandler.postDelayed(this, 10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val debug = Runnable {
|
val debug = Runnable {
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val permission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
val permission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
|
|
@ -30,8 +30,13 @@
|
||||||
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
||||||
*/
|
*/
|
||||||
import Axios from 'axios';
|
import Axios from 'axios';
|
||||||
|
import {init as initCore} from '../chat/core';
|
||||||
import {setupRaven} from '../chat/vue-raven';
|
import {setupRaven} from '../chat/vue-raven';
|
||||||
|
import Socket from '../chat/WebSocket';
|
||||||
|
import Connection from '../fchat/connection';
|
||||||
|
import {appVersion, Logs, SettingsStore} from './filesystem';
|
||||||
import Index from './Index.vue';
|
import Index from './Index.vue';
|
||||||
|
import Notifications from './notifications';
|
||||||
|
|
||||||
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
||||||
(<any>window)['setupPlatform'] = (platform: string) => { //tslint:disable-line:no-any
|
(<any>window)['setupPlatform'] = (platform: string) => { //tslint:disable-line:no-any
|
||||||
|
@ -41,6 +46,9 @@ const version = (<{version: string}>require('./package.json')).version; //tslint
|
||||||
if(process.env.NODE_ENV === 'production')
|
if(process.env.NODE_ENV === 'production')
|
||||||
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
|
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
|
||||||
|
|
||||||
|
const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket);
|
||||||
|
initCore(connection, Logs, SettingsStore, Notifications);
|
||||||
|
|
||||||
new Index({ //tslint:disable-line:no-unused-expression
|
new Index({ //tslint:disable-line:no-unused-expression
|
||||||
el: '#app'
|
el: '#app'
|
||||||
});
|
});
|
|
@ -31,7 +31,7 @@ export const appVersion = (<{version: string}>require('./package.json')).version
|
||||||
export class GeneralSettings {
|
export class GeneralSettings {
|
||||||
account = '';
|
account = '';
|
||||||
password = '';
|
password = '';
|
||||||
host = 'wss://chat.f-list.net:9799';
|
host = 'wss://chat.f-list.net/chat2';
|
||||||
theme = 'default';
|
theme = 'default';
|
||||||
version = appVersion;
|
version = appVersion;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,9 @@ export class Logs implements Logging {
|
||||||
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
||||||
const file = await NativeFile.read('!settings');
|
const file = await NativeFile.read('!settings');
|
||||||
if(file === undefined) return undefined;
|
if(file === undefined) return undefined;
|
||||||
return <GeneralSettings>JSON.parse(file);
|
const settings = <GeneralSettings>JSON.parse(file);
|
||||||
|
if(settings.host === 'wss://chat.f-list.net:9799') settings.host = 'wss://chat.f-list.net/chat2';
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
||||||
|
|
|
@ -121,7 +121,7 @@ class Logs: NSObject, WKScriptMessageHandler {
|
||||||
indexItem = IndexItem(conversation as String)
|
indexItem = IndexItem(conversation as String)
|
||||||
index![key] = indexItem
|
index![key] = indexItem
|
||||||
let cstring = conversation.utf8String
|
let cstring = conversation.utf8String
|
||||||
var length = strlen(cstring)
|
var length = strlen(cstring!)
|
||||||
write(indexFd.fileDescriptor, &length, 1)
|
write(indexFd.fileDescriptor, &length, 1)
|
||||||
write(indexFd.fileDescriptor, cstring, length)
|
write(indexFd.fileDescriptor, cstring, length)
|
||||||
}
|
}
|
||||||
|
@ -135,11 +135,11 @@ class Logs: NSObject, WKScriptMessageHandler {
|
||||||
write(fd.fileDescriptor, &time, 4)
|
write(fd.fileDescriptor, &time, 4)
|
||||||
write(fd.fileDescriptor, &type, 1)
|
write(fd.fileDescriptor, &type, 1)
|
||||||
var cstring = sender.utf8String
|
var cstring = sender.utf8String
|
||||||
var length = strlen(cstring)
|
var length = strlen(cstring!)
|
||||||
write(fd.fileDescriptor, &length, 1)
|
write(fd.fileDescriptor, &length, 1)
|
||||||
write(fd.fileDescriptor, cstring, length)
|
write(fd.fileDescriptor, cstring, length)
|
||||||
cstring = text.utf8String
|
cstring = text.utf8String
|
||||||
length = strlen(cstring)
|
length = strlen(cstring!)
|
||||||
write(fd.fileDescriptor, &length, 2)
|
write(fd.fileDescriptor, &length, 2)
|
||||||
write(fd.fileDescriptor, cstring, length)
|
write(fd.fileDescriptor, cstring, length)
|
||||||
var size = fd.offsetInFile - start
|
var size = fd.offsetInFile - start
|
||||||
|
|
|
@ -20,7 +20,11 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
|
||||||
controller.add(Logs(), name: "Logs")
|
controller.add(Logs(), name: "Logs")
|
||||||
config.userContentController = controller
|
config.userContentController = controller
|
||||||
config.mediaTypesRequiringUserActionForPlayback = [.video]
|
config.mediaTypesRequiringUserActionForPlayback = [.video]
|
||||||
config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority")
|
if #available(iOS 12.2, *) {
|
||||||
|
config.setValue(true, forKey: "alwaysRunsAtForegroundPriority")
|
||||||
|
} else {
|
||||||
|
config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority")
|
||||||
|
}
|
||||||
webView = WKWebView(frame: UIApplication.shared.windows[0].frame, configuration: config)
|
webView = WKWebView(frame: UIApplication.shared.windows[0].frame, configuration: config)
|
||||||
webView.uiDelegate = self
|
webView.uiDelegate = self
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
|
|
|
@ -22,6 +22,6 @@ export default class Notifications extends BaseNotifications {
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestPermission(): Promise<void> {
|
async requestPermission(): Promise<void> {
|
||||||
NativeNotification.requestPermission();
|
return NativeNotification.requestPermission();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "net.f_list.fchat",
|
"name": "net.f_list.fchat",
|
||||||
"version": "3.0.10",
|
"version": "3.0.12",
|
||||||
"displayName": "F-Chat",
|
"displayName": "F-Chat",
|
||||||
"author": "The F-List Team",
|
"author": "The F-List Team",
|
||||||
"description": "F-List.net Chat Client",
|
"description": "F-List.net Chat Client",
|
||||||
|
|
|
@ -45,7 +45,7 @@ const config = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
new ForkTsCheckerWebpackPlugin({async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
||||||
new VueLoaderPlugin()
|
new VueLoaderPlugin()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
56
package.json
56
package.json
|
@ -5,48 +5,48 @@
|
||||||
"description": "F-List Exported",
|
"description": "F-List Exported",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@f-list/fork-ts-checker-webpack-plugin": "^0.5.2",
|
"@f-list/fork-ts-checker-webpack-plugin": "^1.3.7",
|
||||||
"@f-list/vue-ts": "^1.0.2",
|
"@f-list/vue-ts": "^1.0.3",
|
||||||
"@fortawesome/fontawesome-free": "^5.6.1",
|
"@fortawesome/fontawesome-free": "^5.9.0",
|
||||||
"@types/lodash": "^4.14.119",
|
"@types/lodash": "^4.14.134",
|
||||||
"@types/sortablejs": "^1.7.0",
|
"@types/sortablejs": "^1.7.2",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.19.0",
|
||||||
"bootstrap": "^4.1.3",
|
"bootstrap": "^4.3.1",
|
||||||
"css-loader": "^2.0.1",
|
"css-loader": "^3.0.0",
|
||||||
"date-fns": "^1.30.1",
|
"date-fns": "^1.30.1",
|
||||||
"electron": "3.0.13",
|
"electron": "^5.0.4",
|
||||||
"electron-log": "^2.2.17",
|
"electron-log": "^3.0.1",
|
||||||
"electron-packager": "^13.0.1",
|
"electron-packager": "^14.0.0",
|
||||||
"electron-rebuild": "^1.8.2",
|
"electron-rebuild": "^1.8.4",
|
||||||
"extract-loader": "^3.1.0",
|
"extract-loader": "^3.1.0",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^4.0.0",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.11.0",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||||
"qs": "^6.6.0",
|
"qs": "^6.6.0",
|
||||||
"raven-js": "^3.27.0",
|
"raven-js": "^3.27.2",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"sortablejs": "^1.8.0-rc1",
|
"sortablejs": "~1.9.0",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"ts-loader": "^5.3.1",
|
"ts-loader": "^6.0.3",
|
||||||
"tslib": "^1.9.3",
|
"tslib": "^1.10.0",
|
||||||
"tslint": "^5.12.0",
|
"tslint": "^5.17.0",
|
||||||
"typescript": "^3.2.2",
|
"typescript": "^3.5.2",
|
||||||
"vue": "^2.5.21",
|
"vue": "^2.6.8",
|
||||||
"vue-loader": "^15.4.2",
|
"vue-loader": "^15.7.0",
|
||||||
"vue-template-compiler": "^2.5.21",
|
"vue-template-compiler": "^2.6.8",
|
||||||
"webpack": "^4.27.1"
|
"webpack": "^4.35.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"keytar": "^4.3.0",
|
"keytar": "^4.10.0",
|
||||||
"spellchecker": "^3.5.0"
|
"spellchecker": "^3.6.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.5.2",
|
"appdmg": "^0.6.0",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"electron-winstaller": "^2.7.0"
|
"electron-winstaller": "^3.0.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-rebuild -o spellchecker,keytar"
|
"postinstall": "electron-rebuild -fo spellchecker,keytar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ div.indentText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.styledText, .bbcode {
|
.bbcode {
|
||||||
@include force-word-wrapping;
|
@include force-word-wrapping;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
border-color: theme-color("secondary");
|
border-color: theme-color("secondary");
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
@media (min-width: breakpoint-min(sm)) {
|
@media (min-width: breakpoint-min(sm)) {
|
||||||
.name {
|
.name {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -54,5 +54,6 @@
|
||||||
|
|
||||||
* {
|
* {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
|
@ -59,4 +59,12 @@ select {
|
||||||
@extend .custom-select;
|
@extend .custom-select;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn {
|
||||||
|
color: $link-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $link-hover-color;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
.kink-editor-panel {
|
|
||||||
position: fixed;
|
|
||||||
width: 55%;
|
|
||||||
}
|
|
|
@ -1,24 +1,15 @@
|
||||||
.note-folder {
|
.note-folder {
|
||||||
clear: both;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.note-folder-total, .note-folder-unread {
|
.note-folder-total, .note-folder-unread {
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
display: inline-block;
|
|
||||||
float: left;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.note-folder-unread {
|
.note-folder-unread {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
.note-folder-name {
|
.note-folder-name {
|
||||||
width: auto;
|
flex: 1;
|
||||||
display: inline-block;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.note-folder-icon {
|
|
||||||
display: inline-block;
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"description": "F-Chat Themes",
|
"description": "F-Chat Themes",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.3",
|
"@fortawesome/fontawesome-free": "^5.6.3",
|
||||||
"bootstrap": "^4.0.0",
|
"bootstrap": "^4.2.1",
|
||||||
"node-sass": "^4.7.2"
|
"node-sass": "^4.11.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node-sass --importer=./importer --output css"
|
"build": "node-sass --importer=./importer --output css"
|
||||||
|
|
|
@ -9,6 +9,5 @@
|
||||||
@import "../tickets";
|
@import "../tickets";
|
||||||
@import "../notes";
|
@import "../notes";
|
||||||
@import "../threads";
|
@import "../threads";
|
||||||
@import "../kink_editor";
|
|
||||||
@import "../flist_overrides";
|
@import "../flist_overrides";
|
||||||
@import "../tag_input";
|
@import "../tag_input";
|
||||||
|
|
|
@ -30,6 +30,7 @@ $body-bg: $gray-100;
|
||||||
$link-color: $gray-800;
|
$link-color: $gray-800;
|
||||||
$link-hover-color: lighten($link-color, 15%);
|
$link-hover-color: lighten($link-color, 15%);
|
||||||
$dropdown-bg: $gray-200;
|
$dropdown-bg: $gray-200;
|
||||||
|
$dropdown-divider-bg: $gray-300;
|
||||||
|
|
||||||
// Modal Dialogs
|
// Modal Dialogs
|
||||||
$modal-backdrop-bg: $white;
|
$modal-backdrop-bg: $white;
|
||||||
|
|
471
scss/yarn.lock
471
scss/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
||||||
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character">
|
<div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character">
|
||||||
<sidebar :character="character" @memo="memo" @bookmarked="bookmarked" :oldApi="oldApi"></sidebar>
|
<sidebar :character="character" @memo="memo" :oldApi="oldApi"></sidebar>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character">
|
<div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character">
|
||||||
<div id="characterView">
|
<div id="characterView">
|
||||||
|
@ -34,28 +34,26 @@
|
||||||
</tabs>
|
</tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-content">
|
<div role="tabpanel" v-show="tab === '0'">
|
||||||
<div role="tabpanel" class="tab-pane" :class="{active: tab === '0'}" id="overview">
|
<div style="margin-bottom:10px">
|
||||||
<div v-bbcode="character.character.description" style="margin-bottom: 10px"></div>
|
<bbcode :text="character.character.description"></bbcode>
|
||||||
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
|
|
||||||
</div>
|
|
||||||
<div role="tabpanel" class="tab-pane" :class="{active: tab === '1'}" id="infotags">
|
|
||||||
<character-infotags :character="character" ref="tab1"></character-infotags>
|
|
||||||
</div>
|
|
||||||
<div role="tabpanel" class="tab-pane" id="groups" :class="{active: tab === '2'}" v-if="!oldApi">
|
|
||||||
<character-groups :character="character" ref="tab2"></character-groups>
|
|
||||||
</div>
|
|
||||||
<div role="tabpanel" class="tab-pane" id="images" :class="{active: tab === '3'}">
|
|
||||||
<character-images :character="character" ref="tab3" :use-preview="imagePreview"></character-images>
|
|
||||||
</div>
|
|
||||||
<div v-if="character.settings.guestbook" role="tabpanel" class="tab-pane" :class="{active: tab === '4'}"
|
|
||||||
id="guestbook">
|
|
||||||
<character-guestbook :character="character" :oldApi="oldApi" ref="tab4"></character-guestbook>
|
|
||||||
</div>
|
|
||||||
<div v-if="character.is_self || character.settings.show_friends" role="tabpanel" class="tab-pane"
|
|
||||||
:class="{active: tab === '5'}" id="friends">
|
|
||||||
<character-friends :character="character" ref="tab5"></character-friends>
|
|
||||||
</div>
|
</div>
|
||||||
|
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" v-show="tab === '1'">
|
||||||
|
<character-infotags :character="character" ref="tab1"></character-infotags>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" v-show="tab === '2'" v-if="!oldApi">
|
||||||
|
<character-groups :character="character" ref="tab2"></character-groups>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" v-show="tab === '3'">
|
||||||
|
<character-images :character="character" ref="tab3" :use-preview="imagePreview"></character-images>
|
||||||
|
</div>
|
||||||
|
<div v-if="character.settings.guestbook" role="tabpanel" v-show="tab === '4'">
|
||||||
|
<character-guestbook :character="character" :oldApi="oldApi" ref="tab4"></character-guestbook>
|
||||||
|
</div>
|
||||||
|
<div v-if="character.is_self || character.settings.show_friends" role="tabpanel" v-show="tab === '5'">
|
||||||
|
<character-friends :character="character" ref="tab5"></character-friends>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,18 +66,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {standardParser} from '../../bbcode/standard';
|
import {StandardBBCodeParser} from '../../bbcode/standard';
|
||||||
import * as Utils from '../utils';
|
import {BBCodeView} from '../../bbcode/view';
|
||||||
import {methods, Store} from './data_store';
|
|
||||||
import {Character, SharedStore} from './interfaces';
|
|
||||||
|
|
||||||
import DateDisplay from '../../components/date_display.vue';
|
import DateDisplay from '../../components/date_display.vue';
|
||||||
import Tabs from '../../components/tabs';
|
import Tabs from '../../components/tabs';
|
||||||
|
import * as Utils from '../utils';
|
||||||
|
import {methods, Store} from './data_store';
|
||||||
import FriendsView from './friends.vue';
|
import FriendsView from './friends.vue';
|
||||||
import GroupsView from './groups.vue';
|
import GroupsView from './groups.vue';
|
||||||
import GuestbookView from './guestbook.vue';
|
import GuestbookView from './guestbook.vue';
|
||||||
import ImagesView from './images.vue';
|
import ImagesView from './images.vue';
|
||||||
import InfotagsView from './infotags.vue';
|
import InfotagsView from './infotags.vue';
|
||||||
|
import {Character, SharedStore} from './interfaces';
|
||||||
import CharacterKinksView from './kinks.vue';
|
import CharacterKinksView from './kinks.vue';
|
||||||
import Sidebar from './sidebar.vue';
|
import Sidebar from './sidebar.vue';
|
||||||
|
|
||||||
|
@ -87,28 +85,25 @@
|
||||||
show?(): void
|
show?(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const standardParser = new StandardBBCodeParser();
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
sidebar: Sidebar,
|
sidebar: Sidebar, date: DateDisplay, 'character-friends': FriendsView, 'character-guestbook': GuestbookView,
|
||||||
date: DateDisplay, tabs: Tabs,
|
'character-groups': GroupsView, 'character-infotags': InfotagsView, 'character-images': ImagesView, tabs: Tabs,
|
||||||
'character-friends': FriendsView,
|
'character-kinks': CharacterKinksView, bbcode: BBCodeView(standardParser)
|
||||||
'character-guestbook': GuestbookView,
|
|
||||||
'character-groups': GroupsView,
|
|
||||||
'character-infotags': InfotagsView,
|
|
||||||
'character-images': ImagesView,
|
|
||||||
'character-kinks': CharacterKinksView
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class CharacterPage extends Vue {
|
export default class CharacterPage extends Vue {
|
||||||
@Prop()
|
@Prop
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly characterid?: number;
|
readonly id?: number;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly authenticated!: boolean;
|
readonly authenticated!: boolean;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly oldApi?: true;
|
readonly oldApi?: true;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly imagePreview?: true;
|
readonly imagePreview?: true;
|
||||||
shared: SharedStore = Store;
|
shared: SharedStore = Store;
|
||||||
character: Character | undefined;
|
character: Character | undefined;
|
||||||
|
@ -143,20 +138,15 @@
|
||||||
Vue.set(this.character!, 'memo', memo);
|
Vue.set(this.character!, 'memo', memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarked(state: boolean): void {
|
|
||||||
Vue.set(this.character!, 'bookmarked', state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getCharacter(): Promise<void> {
|
private async _getCharacter(): Promise<void> {
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.character = undefined;
|
this.character = undefined;
|
||||||
if(this.name === undefined || this.name.length === 0)
|
if((this.name === undefined || this.name.length === 0) && !this.id)
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
await methods.fieldsGet();
|
await methods.fieldsGet();
|
||||||
this.character = await methods.characterData(this.name, this.characterid);
|
this.character = await methods.characterData(this.name, this.id);
|
||||||
standardParser.allowInlines = true;
|
|
||||||
standardParser.inlines = this.character.character.inlines;
|
standardParser.inlines = this.character.character.inlines;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.error = Utils.isJSONError(e) ? <string>e.response.data.error : (<Error>e).message;
|
this.error = Utils.isJSONError(e) ? <string>e.response.data.error : (<Error>e).message;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="contact-method" :title="altText">
|
<div class="contact-method" :title="infotag.name">
|
||||||
<span v-if="contactLink" class="contact-link">
|
<span v-if="contactLink" class="contact-link">
|
||||||
<a :href="contactLink" target="_blank" rel="nofollow noreferrer noopener">
|
<a :href="contactLink" target="_blank" rel="nofollow noreferrer noopener">
|
||||||
<img :src="iconUrl"><span class="contact-value">{{value}}</span>
|
<img :src="iconUrl"><span class="contact-value">{{value}}</span>
|
||||||
|
@ -14,39 +14,27 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import {CharacterInfotag, Infotag} from '../../interfaces';
|
||||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||||
import {methods, Store} from './data_store';
|
import {methods} from './data_store';
|
||||||
|
|
||||||
interface DisplayContactMethod {
|
|
||||||
id: number
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ContactMethodView extends Vue {
|
export default class ContactMethodView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
private readonly method!: DisplayContactMethod;
|
readonly infotag!: Infotag;
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly data!: CharacterInfotag;
|
||||||
|
|
||||||
get iconUrl(): string {
|
get iconUrl(): string {
|
||||||
const infotag = Store.kinks.infotags[this.method.id];
|
return methods.contactMethodIconUrl(this.infotag.name);
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return 'Unknown Infotag';
|
|
||||||
return methods.contactMethodIconUrl(infotag.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value(): string {
|
get value(): string {
|
||||||
return formatContactValue(this.method.id, this.method.value);
|
return formatContactValue(this.infotag, this.data.string!);
|
||||||
}
|
|
||||||
|
|
||||||
get altText(): string {
|
|
||||||
const infotag = Store.kinks.infotags[this.method.id];
|
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return '';
|
|
||||||
return infotag.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get contactLink(): string | undefined {
|
get contactLink(): string | undefined {
|
||||||
return formatContactLink(this.method.id, this.method.value);
|
return formatContactLink(this.infotag, this.data.string!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
import {urlRegex as websitePattern} from '../../bbcode/core';
|
import {urlRegex as websitePattern} from '../../bbcode/core';
|
||||||
import {Store} from './data_store';
|
import {Infotag} from '../../interfaces';
|
||||||
|
|
||||||
const daUsernamePattern = /^([a-z0-9_\-]+)$/i;
|
const daUsernamePattern = /^([a-z0-9_\-]+)$/i;
|
||||||
const daSitePattern = /^https?:\/\/([a-z0-9_\-]+)\.deviantart\.com\//i;
|
const daSitePattern = /^https?:\/\/([a-z0-9_\-]+)\.deviantart\.com\//i;
|
||||||
|
@ -30,10 +30,7 @@ function normalizeSiteUsernamePair(site: RegExp, username: RegExp): (value: stri
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatContactValue(id: number, value: string): string {
|
export function formatContactValue(infotag: Infotag, value: string): string {
|
||||||
const infotag = Store.kinks.infotags[id];
|
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return value;
|
|
||||||
const methodName = infotag.name.toLowerCase();
|
const methodName = infotag.name.toLowerCase();
|
||||||
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
||||||
deviantart(): string | undefined {
|
deviantart(): string | undefined {
|
||||||
|
@ -56,10 +53,7 @@ export function formatContactValue(id: number, value: string): string {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatContactLink(id: number, value: string): string | undefined {
|
export function formatContactLink(infotag: Infotag, value: string): string | undefined {
|
||||||
const infotag = Store.kinks.infotags[id];
|
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return;
|
|
||||||
const methodName = infotag.name.toLowerCase();
|
const methodName = infotag.name.toLowerCase();
|
||||||
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
||||||
deviantart(): string | undefined {
|
deviantart(): string | undefined {
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
name = '';
|
name = '';
|
||||||
description = '';
|
description = '';
|
||||||
choice: KinkChoice = 'favorite';
|
choice: KinkChoice = 'favorite';
|
||||||
target = Utils.Settings.defaultCharacter;
|
target = Utils.settings.defaultCharacter;
|
||||||
formErrors = {};
|
formErrors = {};
|
||||||
submitting = false;
|
submitting = false;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Component} from 'vue';
|
||||||
import {SharedStore, StoreMethods} from './interfaces';
|
import {SharedStore, StoreMethods} from './interfaces';
|
||||||
|
|
||||||
export let Store: SharedStore = {
|
export let Store: SharedStore = {
|
||||||
kinks: <any>undefined, //tslint:disable-line:no-any
|
shared: undefined!,
|
||||||
authenticated: false
|
authenticated: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal id="deleteDialog" :action="'Delete character' + name" :disabled="deleting" @submit.prevent="deleteCharacter()">
|
<modal id="deleteDialog" :action="'Delete character ' + name" :disabled="deleting" @submit.prevent="deleteCharacter()">
|
||||||
Are you sure you want to permanently delete {{ name }}?<br/>
|
Are you sure you want to permanently delete {{ name }}?<br/>
|
||||||
Character deletion cannot be undone for any reason.
|
Character deletion cannot be undone for any reason.
|
||||||
</modal>
|
</modal>
|
||||||
|
|
|
@ -46,8 +46,8 @@
|
||||||
async checkName(): Promise<boolean> {
|
async checkName(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this.checking = true;
|
this.checking = true;
|
||||||
const result = await methods.characterNameCheck(this.newName);
|
await methods.characterNameCheck(this.newName);
|
||||||
this.valid = result.valid;
|
this.valid = true;
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
return true;
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
@ -64,9 +64,8 @@
|
||||||
async duplicate(): Promise<void> {
|
async duplicate(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.duplicating = true;
|
this.duplicating = true;
|
||||||
const result = await methods.characterDuplicate(this.character.character.id, this.newName);
|
await methods.characterDuplicate(this.character.character.id, this.newName);
|
||||||
this.hide();
|
this.hide();
|
||||||
window.location.assign(result.next);
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
Utils.ajaxError(e, 'Unable to duplicate character');
|
Utils.ajaxError(e, 'Unable to duplicate character');
|
||||||
this.valid = false;
|
this.valid = false;
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: Character;
|
readonly character!: Character;
|
||||||
|
|
||||||
ourCharacter = Utils.Settings.defaultCharacter;
|
ourCharacter = Utils.settings.defaultCharacter;
|
||||||
|
|
||||||
incoming: FriendRequest[] = [];
|
incoming: FriendRequest[] = [];
|
||||||
pending: FriendRequest[] = [];
|
pending: FriendRequest[] = [];
|
||||||
|
@ -113,7 +113,12 @@
|
||||||
try {
|
try {
|
||||||
this.requesting = true;
|
this.requesting = true;
|
||||||
const newRequest = await methods.friendRequest(this.character.character.id, this.ourCharacter);
|
const newRequest = await methods.friendRequest(this.character.character.id, this.ourCharacter);
|
||||||
this.pending.push(newRequest);
|
if(typeof newRequest === 'number')
|
||||||
|
this.pending.push({
|
||||||
|
id: newRequest, source: Utils.characters.find((x) => x.id === this.ourCharacter)!, target: this.character.character,
|
||||||
|
createdAt: Date.now() / 1000
|
||||||
|
});
|
||||||
|
else this.existing.push(newRequest);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if(Utils.isJSONError(e))
|
if(Utils.isJSONError(e))
|
||||||
this.error = <string>e.response.data.error;
|
this.error = <string>e.response.data.error;
|
||||||
|
|
|
@ -15,14 +15,15 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import * as Utils from '../utils';
|
import * as Utils from '../utils';
|
||||||
import {methods} from './data_store';
|
import {methods} from './data_store';
|
||||||
import {Character, CharacterFriend} from './interfaces';
|
import {Character} from './interfaces';
|
||||||
|
import {SimpleCharacter} from '../../interfaces';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class FriendsView extends Vue {
|
export default class FriendsView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
private readonly character!: Character;
|
private readonly character!: Character;
|
||||||
private shown = false;
|
private shown = false;
|
||||||
friends: CharacterFriend[] = [];
|
friends: SimpleCharacter[] = [];
|
||||||
loading = true;
|
loading = true;
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
<label v-show="canEdit" class="control-label">Unapproved only:
|
<label v-show="canEdit" class="control-label">Unapproved only:
|
||||||
<input type="checkbox" v-model="unapprovedOnly"/>
|
<input type="checkbox" v-model="unapprovedOnly"/>
|
||||||
</label>
|
</label>
|
||||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="nextPage" @prev="previousPage"></simple-pager>
|
<simple-pager :next="hasNextPage" :prev="page > 1" @next="++page" @prev="--page"></simple-pager>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<div class="alert alert-info" v-show="posts.length === 0">No guestbook posts.</div>
|
<div class="alert alert-info" v-show="posts.length === 0">No guestbook posts.</div>
|
||||||
<guestbook-post :post="post" :can-edit="canEdit" v-for="post in posts" :key="post.id" @reload="getPage"></guestbook-post>
|
<guestbook-post :character="character" :post="post" :can-edit="canEdit" v-for="post in posts" :key="post.id"
|
||||||
|
@reload="getPage"></guestbook-post>
|
||||||
<div v-if="authenticated && !oldApi" class="form-horizontal">
|
<div v-if="authenticated && !oldApi" class="form-horizontal">
|
||||||
<bbcode-editor v-model="newPost.message" :maxlength="5000" classes="form-control"></bbcode-editor>
|
<bbcode-editor v-model="newPost.message" :maxlength="5000" classes="form-control"></bbcode-editor>
|
||||||
<input type="checkbox" id="guestbookPostPrivate" v-model="newPost.privatePost"/>
|
<input type="checkbox" id="guestbookPostPrivate" v-model="newPost.privatePost"/>
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="guestbook-controls">
|
<div class="guestbook-controls">
|
||||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="nextPage" @prev="previousPage"></simple-pager>
|
<simple-pager :next="hasNextPage" :prev="page > 1" @next="++page" @prev="--page"></simple-pager>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -40,14 +41,12 @@
|
||||||
export default class GuestbookView extends Vue {
|
export default class GuestbookView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: Character;
|
readonly character!: Character;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly oldApi?: true;
|
readonly oldApi?: true;
|
||||||
loading = true;
|
loading = true;
|
||||||
error = '';
|
error = '';
|
||||||
authenticated = Store.authenticated;
|
authenticated = Store.authenticated;
|
||||||
|
|
||||||
posts: GuestbookPost[] = [];
|
posts: GuestbookPost[] = [];
|
||||||
|
|
||||||
unapprovedOnly = false;
|
unapprovedOnly = false;
|
||||||
page = 1;
|
page = 1;
|
||||||
hasNextPage = false;
|
hasNextPage = false;
|
||||||
|
@ -55,28 +54,18 @@
|
||||||
newPost = {
|
newPost = {
|
||||||
posting: false,
|
posting: false,
|
||||||
privatePost: false,
|
privatePost: false,
|
||||||
character: Utils.Settings.defaultCharacter,
|
character: Utils.settings.defaultCharacter,
|
||||||
message: ''
|
message: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
async nextPage(): Promise<void> {
|
|
||||||
this.page += 1;
|
|
||||||
return this.getPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
async previousPage(): Promise<void> {
|
|
||||||
this.page -= 1;
|
|
||||||
return this.getPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('unapprovedOnly')
|
@Watch('unapprovedOnly')
|
||||||
|
@Watch('page')
|
||||||
async getPage(): Promise<void> {
|
async getPage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const guestbookState = await methods.guestbookPageGet(this.character.character.id, this.page, this.unapprovedOnly);
|
const guestbook = await methods.guestbookPageGet(this.character.character.id, (this.page - 1) * 10, 10, this.unapprovedOnly);
|
||||||
this.posts = guestbookState.posts;
|
this.posts = guestbook.posts;
|
||||||
this.hasNextPage = guestbookState.nextPage;
|
this.hasNextPage = (this.page + 1) * 10 < guestbook.total;
|
||||||
this.canEdit = guestbookState.canEdit;
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.posts = [];
|
this.posts = [];
|
||||||
this.hasNextPage = false;
|
this.hasNextPage = false;
|
||||||
|
|
|
@ -19,16 +19,15 @@
|
||||||
{{ (post.approved) ? 'Unapprove' : 'Approve' }}
|
{{ (post.approved) ? 'Unapprove' : 'Approve' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-danger" v-show="!post.deleted && (canEdit || post.canEdit)"
|
<!-- TODO proper permission handling -->
|
||||||
@click="deletePost" :disabled="deleting">Delete
|
<button class="btn btn-danger" v-show="!post.deleted && canEdit" @click="deletePost" :disabled="deleting">Delete</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="bbcode guestbook-message" v-bbcode="post.message"></div>
|
<bbcode class="bbcode guestbook-message" :text="post.message"></bbcode>
|
||||||
<div v-if="post.reply && !replyBox" class="guestbook-reply">
|
<div v-if="post.reply && !replyBox" class="guestbook-reply">
|
||||||
<date-display v-if="post.repliedAt" :time="post.repliedAt"></date-display>
|
<date-display v-if="post.repliedAt" :time="post.repliedAt"></date-display>
|
||||||
<div class="reply-message" v-bbcode="post.reply"></div>
|
<bbcode class="reply-message" :text="post.reply"></bbcode>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,12 +53,14 @@
|
||||||
import DateDisplay from '../../components/date_display.vue';
|
import DateDisplay from '../../components/date_display.vue';
|
||||||
import * as Utils from '../utils';
|
import * as Utils from '../utils';
|
||||||
import {methods} from './data_store';
|
import {methods} from './data_store';
|
||||||
import {GuestbookPost} from './interfaces';
|
import {Character, GuestbookPost} from './interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {'date-display': DateDisplay, 'character-link': CharacterLink}
|
components: {'date-display': DateDisplay, 'character-link': CharacterLink}
|
||||||
})
|
})
|
||||||
export default class GuestbookPostView extends Vue {
|
export default class GuestbookPostView extends Vue {
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly character!: Character;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly post!: GuestbookPost;
|
readonly post!: GuestbookPost;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
async deletePost(): Promise<void> {
|
async deletePost(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.deleting = true;
|
this.deleting = true;
|
||||||
await methods.guestbookPostDelete(this.post.id);
|
await methods.guestbookPostDelete(this.character.character.id, this.post.id);
|
||||||
Vue.set(this.post, 'deleted', true);
|
Vue.set(this.post, 'deleted', true);
|
||||||
this.$emit('reload');
|
this.$emit('reload');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
async approve(): Promise<void> {
|
async approve(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.approving = true;
|
this.approving = true;
|
||||||
await methods.guestbookPostApprove(this.post.id, !this.post.approved);
|
await methods.guestbookPostApprove(this.character.character.id, this.post.id, !this.post.approved);
|
||||||
this.post.approved = !this.post.approved;
|
this.post.approved = !this.post.approved;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
Utils.ajaxError(e, 'Unable to change post approval.');
|
Utils.ajaxError(e, 'Unable to change post approval.');
|
||||||
|
@ -104,9 +105,9 @@
|
||||||
async postReply(): Promise<void> {
|
async postReply(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.replying = true;
|
this.replying = true;
|
||||||
const replyData = await methods.guestbookPostReply(this.post.id, this.replyMessage);
|
await methods.guestbookPostReply(this.character.character.id, this.post.id, this.replyMessage);
|
||||||
this.post.reply = replyData.reply;
|
this.post.reply = this.replyMessage;
|
||||||
this.post.repliedAt = replyData.repliedAt;
|
this.post.repliedAt = Date.now() / 1000;
|
||||||
this.replyBox = false;
|
this.replyBox = false;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
Utils.ajaxError(e, 'Unable to post guestbook reply.');
|
Utils.ajaxError(e, 'Unable to post guestbook reply.');
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
export default class ImagesView extends Vue {
|
export default class ImagesView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
private readonly character!: Character;
|
private readonly character!: Character;
|
||||||
@Prop()
|
@Prop
|
||||||
private readonly usePreview?: boolean;
|
private readonly usePreview?: boolean;
|
||||||
private shown = false;
|
private shown = false;
|
||||||
previewImage = '';
|
previewImage = '';
|
||||||
|
|
|
@ -1,50 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="infotag">
|
<div class="infotag">
|
||||||
<span class="infotag-label">{{label}}: </span>
|
<span class="infotag-label">{{infotag.name}}: </span>
|
||||||
<span v-if="!contactLink" class="infotag-value">{{value}}</span>
|
<span v-if="infotag.infotag_group !== contactGroupId" class="infotag-value">{{value}}</span>
|
||||||
<span v-if="contactLink" class="infotag-value"><a :href="contactLink">{{value}}</a></span>
|
<span v-else class="infotag-value"><a :href="contactLink">{{contactValue}}</a></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import {CharacterInfotag, Infotag, ListItem} from '../../interfaces';
|
||||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||||
import {Store} from './data_store';
|
import {Store} from './data_store';
|
||||||
import {DisplayInfotag} from './interfaces';
|
import {CONTACT_GROUP_ID} from './interfaces';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class InfotagView extends Vue {
|
export default class InfotagView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
private readonly infotag!: DisplayInfotag;
|
readonly infotag!: Infotag;
|
||||||
|
@Prop({required: true})
|
||||||
get label(): string {
|
readonly data!: CharacterInfotag;
|
||||||
const infotag = Store.kinks.infotags[this.infotag.id];
|
readonly contactGroupId = CONTACT_GROUP_ID;
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return 'Unknown Infotag';
|
|
||||||
return infotag.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get contactLink(): string | undefined {
|
get contactLink(): string | undefined {
|
||||||
if(this.infotag.isContact)
|
return formatContactLink(this.infotag, this.data.string!);
|
||||||
return formatContactLink(this.infotag.id, this.infotag.string!);
|
}
|
||||||
|
|
||||||
|
get contactValue(): string {
|
||||||
|
return formatContactValue(this.infotag, this.data.string!);
|
||||||
}
|
}
|
||||||
|
|
||||||
get value(): string {
|
get value(): string {
|
||||||
const infotag = Store.kinks.infotags[this.infotag.id];
|
switch(this.infotag.type) {
|
||||||
if(typeof infotag === 'undefined')
|
|
||||||
return '';
|
|
||||||
if(this.infotag.isContact)
|
|
||||||
return formatContactValue(this.infotag.id, this.infotag.string!);
|
|
||||||
switch(infotag.type) {
|
|
||||||
case 'text':
|
case 'text':
|
||||||
return this.infotag.string!;
|
return this.data.string!;
|
||||||
case 'number':
|
case 'number':
|
||||||
if(infotag.allow_legacy && this.infotag.number === null)
|
if(this.infotag.allow_legacy && !this.data.number)
|
||||||
return this.infotag.string !== undefined ? this.infotag.string : '';
|
return this.data.string !== undefined ? this.data.string : '';
|
||||||
return this.infotag.number!.toPrecision();
|
return this.data.number!.toPrecision();
|
||||||
}
|
}
|
||||||
const listitem = Store.kinks.listitems[this.infotag.list!];
|
const listitem = <ListItem | undefined>Store.shared.listItems[this.data.list!];
|
||||||
if(typeof listitem === 'undefined')
|
if(typeof listitem === 'undefined')
|
||||||
return '';
|
return '';
|
||||||
return listitem.value;
|
return listitem.value;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="infotags row">
|
<div class="infotags row">
|
||||||
<div class="infotag-group col-sm-3" v-for="group in groupedInfotags" :key="group.id" style="margin-top:5px">
|
<div class="infotag-group col-sm-3" v-for="group in groups" :key="group.id" style="margin-top:5px">
|
||||||
<div class="infotag-title">{{group.name}}</div>
|
<div class="infotag-title">{{group.name}}</div>
|
||||||
<hr>
|
<hr>
|
||||||
<infotag :infotag="infotag" v-for="infotag in group.infotags" :key="infotag.id"></infotag>
|
<infotag v-for="infotag in getInfotags(group.id)" :key="infotag.id" :infotag="infotag"
|
||||||
|
:data="character.character.infotags[infotag.id]"></infotag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -11,66 +12,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop} from '@f-list/vue-ts';
|
import {Component, Prop} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import * as Utils from '../utils';
|
import {Infotag, InfotagGroup} from '../../interfaces';
|
||||||
import {Store} from './data_store';
|
import {Store} from './data_store';
|
||||||
import {Character, CONTACT_GROUP_ID, DisplayInfotag} from './interfaces';
|
|
||||||
|
|
||||||
import InfotagView from './infotag.vue';
|
import InfotagView from './infotag.vue';
|
||||||
|
import {Character} from './interfaces';
|
||||||
interface DisplayInfotagGroup {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
sortOrder: number
|
|
||||||
infotags: DisplayInfotag[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {infotag: InfotagView}
|
components: {infotag: InfotagView}
|
||||||
})
|
})
|
||||||
export default class InfotagsView extends Vue {
|
export default class InfotagsView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
private readonly character!: Character;
|
readonly character!: Character;
|
||||||
|
|
||||||
get groupedInfotags(): DisplayInfotagGroup[] {
|
get groups(): {readonly [key: string]: Readonly<InfotagGroup>} {
|
||||||
const groups = Store.kinks.infotag_groups;
|
return Store.shared.infotagGroups;
|
||||||
const infotags = Store.kinks.infotags;
|
}
|
||||||
const characterTags = this.character.character.infotags;
|
|
||||||
const outputGroups: DisplayInfotagGroup[] = [];
|
|
||||||
const groupedTags = Utils.groupObjectBy(infotags, 'infotag_group');
|
|
||||||
for(const groupId in groups) {
|
|
||||||
const group = groups[groupId]!;
|
|
||||||
const groupedInfotags = groupedTags[groupId];
|
|
||||||
if(groupedInfotags === undefined) continue;
|
|
||||||
const collectedTags: DisplayInfotag[] = [];
|
|
||||||
for(const infotag of groupedInfotags) {
|
|
||||||
const characterInfotag = characterTags[infotag.id];
|
|
||||||
if(typeof characterInfotag === 'undefined')
|
|
||||||
continue;
|
|
||||||
const newInfotag: DisplayInfotag = {
|
|
||||||
id: infotag.id,
|
|
||||||
isContact: infotag.infotag_group === CONTACT_GROUP_ID,
|
|
||||||
string: characterInfotag.string,
|
|
||||||
number: characterInfotag.number,
|
|
||||||
list: characterInfotag.list
|
|
||||||
};
|
|
||||||
collectedTags.push(newInfotag);
|
|
||||||
}
|
|
||||||
collectedTags.sort((a, b): number => {
|
|
||||||
const infotagA = infotags[a.id]!;
|
|
||||||
const infotagB = infotags[b.id]!;
|
|
||||||
return infotagA.name < infotagB.name ? -1 : 1;
|
|
||||||
});
|
|
||||||
outputGroups.push({
|
|
||||||
id: group.id,
|
|
||||||
name: group.name,
|
|
||||||
sortOrder: group.sort_order,
|
|
||||||
infotags: collectedTags
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
outputGroups.sort((a, b) => a.sortOrder < b.sortOrder ? -1 : 1);
|
getInfotags(group: number): Infotag[] {
|
||||||
|
return Object.keys(Store.shared.infotags).map((x) => Store.shared.infotags[x])
|
||||||
return outputGroups.filter((a) => a.infotags.length > 0);
|
.filter((x) => x.infotag_group === group && this.character.character.infotags[x.id] !== undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,33 +1,23 @@
|
||||||
import {Character as CharacterInfo, CharacterImage, CharacterSettings, Infotag, Kink, KinkChoice} from '../../interfaces';
|
import {
|
||||||
|
Character as CharacterInfo, CharacterImage, CharacterSettings, KinkChoice, SharedDefinitions, SimpleCharacter
|
||||||
export interface CharacterMenuItem {
|
} from '../../interfaces';
|
||||||
label: string
|
|
||||||
permission: string
|
|
||||||
link(character: Character): string
|
|
||||||
handleClick?(evt: MouseEvent): void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectItem {
|
|
||||||
text: string
|
|
||||||
value: string | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SharedStore {
|
export interface SharedStore {
|
||||||
kinks: SharedKinks
|
shared: SharedDefinitions
|
||||||
authenticated: boolean
|
authenticated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreMethods {
|
export interface StoreMethods {
|
||||||
bookmarkUpdate(id: number, state: boolean): Promise<boolean>
|
bookmarkUpdate(id: number, state: boolean): Promise<void>
|
||||||
|
|
||||||
characterBlock?(id: number, block: boolean, reason?: string): Promise<void>
|
characterBlock?(id: number, block: boolean, reason?: string): Promise<void>
|
||||||
characterCustomKinkAdd(id: number, name: string, description: string, choice: KinkChoice): Promise<void>
|
characterCustomKinkAdd(id: number, name: string, description: string, choice: KinkChoice): Promise<void>
|
||||||
characterData(name: string | undefined, id: number | undefined): Promise<Character>
|
characterData(name: string | undefined, id: number | undefined): Promise<Character>
|
||||||
characterDelete(id: number): Promise<void>
|
characterDelete(id: number): Promise<void>
|
||||||
characterDuplicate(id: number, name: string): Promise<DuplicateResult>
|
characterDuplicate(id: number, name: string): Promise<void>
|
||||||
characterFriends(id: number): Promise<FriendsByCharacter>
|
characterFriends(id: number): Promise<FriendsByCharacter>
|
||||||
characterNameCheck(name: string): Promise<CharacterNameCheckResult>
|
characterNameCheck(name: string): Promise<void>
|
||||||
characterRename?(id: number, name: string, renamedFor?: string): Promise<RenameResult>
|
characterRename?(id: number, name: string, renamedFor?: string): Promise<void>
|
||||||
characterReport(reportData: CharacterReportData): Promise<void>
|
characterReport(reportData: CharacterReportData): Promise<void>
|
||||||
|
|
||||||
contactMethodIconUrl(name: string): string
|
contactMethodIconUrl(name: string): string
|
||||||
|
@ -36,20 +26,20 @@ export interface StoreMethods {
|
||||||
fieldsGet(): Promise<void>
|
fieldsGet(): Promise<void>
|
||||||
|
|
||||||
friendDissolve(friend: Friend): Promise<void>
|
friendDissolve(friend: Friend): Promise<void>
|
||||||
friendRequest(target: number, source: number): Promise<FriendRequest>
|
friendRequest(target: number, source: number): Promise<Friend | number>
|
||||||
friendRequestAccept(request: FriendRequest): Promise<Friend>
|
friendRequestAccept(request: FriendRequest): Promise<Friend>
|
||||||
friendRequestIgnore(request: FriendRequest): Promise<void>
|
friendRequestIgnore(request: FriendRequest): Promise<void>
|
||||||
friendRequestCancel(request: FriendRequest): Promise<void>
|
friendRequestCancel(request: FriendRequest): Promise<void>
|
||||||
|
|
||||||
friendsGet(id: number): Promise<CharacterFriend[]>
|
friendsGet(id: number): Promise<SimpleCharacter[]>
|
||||||
|
|
||||||
groupsGet(id: number): Promise<CharacterGroup[]>
|
groupsGet(id: number): Promise<CharacterGroup[]>
|
||||||
|
|
||||||
guestbookPageGet(id: number, page: number, unapproved: boolean): Promise<GuestbookState>
|
guestbookPageGet(character: number, offset?: number, limit?: number, unapproved_only?: boolean): Promise<Guestbook>
|
||||||
guestbookPostApprove(id: number, approval: boolean): Promise<void>
|
guestbookPostApprove(character: number, id: number, approval: boolean): Promise<void>
|
||||||
guestbookPostDelete(id: number): Promise<void>
|
guestbookPostDelete(character: number, id: number): Promise<void>
|
||||||
guestbookPostPost(target: number, source: number, message: string, privatePost: boolean): Promise<void>
|
guestbookPostPost(target: number, source: number, message: string, privatePost: boolean): Promise<void>
|
||||||
guestbookPostReply(id: number, reply: string | null): Promise<GuestbookReply>
|
guestbookPostReply(character: number, id: number, reply: string | null): Promise<void>
|
||||||
|
|
||||||
hasPermission?(permission: string): boolean
|
hasPermission?(permission: string): boolean
|
||||||
|
|
||||||
|
@ -59,20 +49,12 @@ export interface StoreMethods {
|
||||||
|
|
||||||
kinksGet(id: number): Promise<CharacterKink[]>
|
kinksGet(id: number): Promise<CharacterKink[]>
|
||||||
|
|
||||||
memoUpdate(id: number, memo: string): Promise<MemoReply>
|
memoUpdate(id: number, memo: string): Promise<void>
|
||||||
}
|
|
||||||
|
|
||||||
export interface SharedKinks {
|
|
||||||
kinks: {[key: string]: Kink | undefined}
|
|
||||||
kink_groups: {[key: string]: KinkGroup | undefined}
|
|
||||||
infotags: {[key: string]: Infotag | undefined}
|
|
||||||
infotag_groups: {[key: string]: InfotagGroup | undefined}
|
|
||||||
listitems: {[key: string]: ListItem | undefined}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SiteDate = number | string | null;
|
export type SiteDate = number | string | null;
|
||||||
export type KinkChoiceFull = KinkChoice | number;
|
export type KinkChoiceFull = KinkChoice | number;
|
||||||
export const CONTACT_GROUP_ID = '1';
|
export const CONTACT_GROUP_ID = 1;
|
||||||
|
|
||||||
export interface DisplayKink {
|
export interface DisplayKink {
|
||||||
id: number
|
id: number
|
||||||
|
@ -84,40 +66,7 @@ export interface DisplayKink {
|
||||||
hasSubkinks: boolean
|
hasSubkinks: boolean
|
||||||
subkinks: DisplayKink[]
|
subkinks: DisplayKink[]
|
||||||
ignore: boolean
|
ignore: boolean
|
||||||
}
|
key: string
|
||||||
|
|
||||||
export interface DisplayInfotag {
|
|
||||||
id: number
|
|
||||||
isContact: boolean
|
|
||||||
string?: string
|
|
||||||
number?: number | null
|
|
||||||
list?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KinkGroup {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
sort_order: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InfotagGroup {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
sort_order: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListItem {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
sort_order: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CharacterFriend {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CharacterKink {
|
export interface CharacterKink {
|
||||||
|
@ -125,14 +74,6 @@ export interface CharacterKink {
|
||||||
choice: KinkChoice
|
choice: KinkChoice
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterName = string | CharacterNameDetails;
|
|
||||||
|
|
||||||
export interface CharacterNameDetails {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
deleted: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ThreadOrderMode = 'post' | 'explicit';
|
export type ThreadOrderMode = 'post' | 'explicit';
|
||||||
|
|
||||||
export interface GroupPermissions {
|
export interface GroupPermissions {
|
||||||
|
@ -151,7 +92,7 @@ export interface CharacterGroup {
|
||||||
orderMode: ThreadOrderMode
|
orderMode: ThreadOrderMode
|
||||||
createdAt: SiteDate
|
createdAt: SiteDate
|
||||||
myPermissions: GroupPermissions
|
myPermissions: GroupPermissions
|
||||||
character: CharacterName
|
character: SimpleCharacter
|
||||||
owner: boolean
|
owner: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +119,7 @@ export interface Character {
|
||||||
|
|
||||||
export interface GuestbookPost {
|
export interface GuestbookPost {
|
||||||
readonly id: number
|
readonly id: number
|
||||||
readonly character: CharacterNameDetails
|
readonly character: SimpleCharacter
|
||||||
approved: boolean
|
approved: boolean
|
||||||
readonly private: boolean
|
readonly private: boolean
|
||||||
postedAt: SiteDate
|
postedAt: SiteDate
|
||||||
|
@ -189,33 +130,9 @@ export interface GuestbookPost {
|
||||||
deleted?: boolean
|
deleted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuestbookReply {
|
export interface Guestbook {
|
||||||
readonly reply: string
|
readonly posts: GuestbookPost[]
|
||||||
readonly postId: number
|
readonly total: number
|
||||||
readonly repliedAt: SiteDate
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuestbookState {
|
|
||||||
posts: GuestbookPost[]
|
|
||||||
readonly nextPage: boolean
|
|
||||||
readonly canEdit: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemoReply {
|
|
||||||
readonly id: number
|
|
||||||
readonly memo: string
|
|
||||||
readonly updated_at: SiteDate
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DuplicateResult {
|
|
||||||
// Url to redirect user to when duplication is complete.
|
|
||||||
readonly next: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RenameResult = DuplicateResult;
|
|
||||||
|
|
||||||
export interface CharacterNameCheckResult {
|
|
||||||
valid: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CharacterReportData {
|
export interface CharacterReportData {
|
||||||
|
@ -229,8 +146,8 @@ export interface CharacterReportData {
|
||||||
|
|
||||||
export interface Friend {
|
export interface Friend {
|
||||||
id: number
|
id: number
|
||||||
source: CharacterNameDetails
|
source: SimpleCharacter
|
||||||
target: CharacterNameDetails
|
target: SimpleCharacter
|
||||||
createdAt: SiteDate
|
createdAt: SiteDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="character-kink" :class="kinkClasses" :id="kinkId" @click="toggleSubkinks" :data-custom="customId"
|
<div class="character-kink" :class="kinkClasses" :id="kink.key" @click="toggleSubkinks"
|
||||||
@mouseover.stop="showTooltip = true" @mouseout.stop="showTooltip = false">
|
@mouseover.stop="showTooltip = true" @mouseout.stop="showTooltip = false">
|
||||||
<i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i>
|
<i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i>
|
||||||
<i v-show="!kink.hasSubkinks && kink.isCustom" class="far fa-dot-circle custom-kink-icon"></i>
|
<i v-show="!kink.hasSubkinks && kink.isCustom" class="far fa-dot-circle custom-kink-icon"></i>
|
||||||
|
@ -42,10 +42,6 @@
|
||||||
this.listClosed = !this.listClosed;
|
this.listClosed = !this.listClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get kinkId(): number {
|
|
||||||
return this.kink.isCustom ? -this.kink.id : this.kink.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
get kinkClasses(): {[key: string]: boolean} {
|
get kinkClasses(): {[key: string]: boolean} {
|
||||||
const classes: {[key: string]: boolean} = {
|
const classes: {[key: string]: boolean} = {
|
||||||
'stock-kink': !this.kink.isCustom,
|
'stock-kink': !this.kink.isCustom,
|
||||||
|
@ -53,7 +49,7 @@
|
||||||
highlighted: !this.kink.isCustom && this.highlights[this.kink.id],
|
highlighted: !this.kink.isCustom && this.highlights[this.kink.id],
|
||||||
subkink: this.kink.hasSubkinks
|
subkink: this.kink.hasSubkinks
|
||||||
};
|
};
|
||||||
classes[`kink-id-${this.kinkId}`] = true;
|
classes[`kink-id-${this.kink.key}`] = true;
|
||||||
classes[`kink-group-${this.kink.group}`] = true;
|
classes[`kink-group-${this.kink.group}`] = true;
|
||||||
if(!this.kink.isCustom && typeof this.comparisons[this.kink.id] !== 'undefined')
|
if(!this.kink.isCustom && typeof this.comparisons[this.kink.id] !== 'undefined')
|
||||||
classes[`comparison-${this.comparisons[this.kink.id]}`] = true;
|
classes[`comparison-${this.comparisons[this.kink.id]}`] = true;
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<h4>Favorites</h4>
|
<h4>Favorites</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||||
:comparisons="comparison"></kink>
|
:comparisons="comparison"></kink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<h4>Yes</h4>
|
<h4>Yes</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||||
:comparisons="comparison"></kink>
|
:comparisons="comparison"></kink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<h4>Maybe</h4>
|
<h4>Maybe</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||||
:comparisons="comparison"></kink>
|
:comparisons="comparison"></kink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<h4>No</h4>
|
<h4>No</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||||
:comparisons="comparison"></kink>
|
:comparisons="comparison"></kink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,11 +67,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {Kink, KinkChoice} from '../../interfaces';
|
import {Kink, KinkChoice, KinkGroup} from '../../interfaces';
|
||||||
import * as Utils from '../utils';
|
import * as Utils from '../utils';
|
||||||
import CopyCustomMenu from './copy_custom_menu.vue';
|
import CopyCustomMenu from './copy_custom_menu.vue';
|
||||||
import {methods, Store} from './data_store';
|
import {methods, Store} from './data_store';
|
||||||
import {Character, DisplayKink, KinkGroup} from './interfaces';
|
import {Character, DisplayKink} from './interfaces';
|
||||||
import KinkView from './kink.vue';
|
import KinkView from './kink.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -80,10 +80,10 @@
|
||||||
export default class CharacterKinksView extends Vue {
|
export default class CharacterKinksView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: Character;
|
readonly character!: Character;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly oldApi?: true;
|
readonly oldApi?: true;
|
||||||
shared = Store;
|
shared = Store;
|
||||||
characterToCompare = Utils.Settings.defaultCharacter;
|
characterToCompare = Utils.settings.defaultCharacter;
|
||||||
highlightGroup: number | undefined;
|
highlightGroup: number | undefined;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
@ -120,8 +120,8 @@
|
||||||
this.highlighting = {};
|
this.highlighting = {};
|
||||||
if(group === null) return;
|
if(group === null) return;
|
||||||
const toAssign: {[key: string]: boolean} = {};
|
const toAssign: {[key: string]: boolean} = {};
|
||||||
for(const kinkId in this.shared.kinks.kinks) {
|
for(const kinkId in Store.shared.kinks) {
|
||||||
const kink = this.shared.kinks.kinks[kinkId]!;
|
const kink = Store.shared.kinks[kinkId];
|
||||||
if(kink.kink_group === group)
|
if(kink.kink_group === group)
|
||||||
toAssign[kinkId] = true;
|
toAssign[kinkId] = true;
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
get kinkGroups(): {[key: string]: KinkGroup | undefined} {
|
get kinkGroups(): {[key: string]: KinkGroup | undefined} {
|
||||||
return this.shared.kinks.kink_groups;
|
return Store.shared.kinkGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
get compareButtonText(): string {
|
get compareButtonText(): string {
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
get groupedKinks(): {[key in KinkChoice]: DisplayKink[]} {
|
get groupedKinks(): {[key in KinkChoice]: DisplayKink[]} {
|
||||||
const kinks = this.shared.kinks.kinks;
|
const kinks = Store.shared.kinks;
|
||||||
const characterKinks = this.character.character.kinks;
|
const characterKinks = this.character.character.kinks;
|
||||||
const characterCustoms = this.character.character.customs;
|
const characterCustoms = this.character.character.customs;
|
||||||
const displayCustoms: {[key: string]: DisplayKink | undefined} = {};
|
const displayCustoms: {[key: string]: DisplayKink | undefined} = {};
|
||||||
|
@ -152,7 +152,8 @@
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
hasSubkinks: false,
|
hasSubkinks: false,
|
||||||
ignore: false,
|
ignore: false,
|
||||||
subkinks: []
|
subkinks: [],
|
||||||
|
key: kink.id.toString()
|
||||||
});
|
});
|
||||||
const kinkSorter = (a: DisplayKink, b: DisplayKink) => {
|
const kinkSorter = (a: DisplayKink, b: DisplayKink) => {
|
||||||
if(this.character.settings.customs_first && a.isCustom !== b.isCustom)
|
if(this.character.settings.customs_first && a.isCustom !== b.isCustom)
|
||||||
|
@ -174,13 +175,14 @@
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
hasSubkinks: false,
|
hasSubkinks: false,
|
||||||
ignore: false,
|
ignore: false,
|
||||||
subkinks: []
|
subkinks: [],
|
||||||
|
key: `c${custom.id}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const kinkId in characterKinks) {
|
for(const kinkId in characterKinks) {
|
||||||
const kinkChoice = characterKinks[kinkId]!;
|
const kinkChoice = characterKinks[kinkId]!;
|
||||||
const kink = kinks[kinkId];
|
const kink = <Kink | undefined>kinks[kinkId];
|
||||||
if(kink === undefined) continue;
|
if(kink === undefined) continue;
|
||||||
const newKink = makeKink(kink);
|
const newKink = makeKink(kink);
|
||||||
if(typeof kinkChoice === 'number' && typeof displayCustoms[kinkChoice] !== 'undefined') {
|
if(typeof kinkChoice === 'number' && typeof displayCustoms[kinkChoice] !== 'undefined') {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
export default class MemoDialog extends CustomDialog {
|
export default class MemoDialog extends CustomDialog {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: {id: number, name: string};
|
readonly character!: {id: number, name: string};
|
||||||
@Prop()
|
@Prop
|
||||||
readonly memo?: Memo;
|
readonly memo?: Memo;
|
||||||
message = '';
|
message = '';
|
||||||
editing = false;
|
editing = false;
|
||||||
|
|
|
@ -2,12 +2,27 @@
|
||||||
<modal id="reportDialog" :action="'Report character' + name" :disabled="!dataValid || submitting" @submit.prevent="submitReport()">
|
<modal id="reportDialog" :action="'Report character' + name" :disabled="!dataValid || submitting" @submit.prevent="submitReport()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
<select v-select="validTypes" v-model="type" class="form-control"></select>
|
<select v-model="type" class="form-control">
|
||||||
|
<option>None</option>
|
||||||
|
<option value="profile">Profile Violation</option>
|
||||||
|
<option value="name_request">Name Request</option>
|
||||||
|
<option value="takedown">Art Takedown</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="type !== 'takedown'">
|
<div v-if="type !== 'takedown'">
|
||||||
<div class="form-group" v-if="type === 'profile'">
|
<div class="form-group" v-if="type === 'profile'">
|
||||||
<label>Violation Type</label>
|
<label>Violation Type</label>
|
||||||
<select v-select="violationTypes" v-model="violation" class="form-control"></select>
|
<select v-model="violation" class="form-control">
|
||||||
|
<option>Real life images on underage character</option>
|
||||||
|
<option>Real life animal images on sexual character</option>
|
||||||
|
<option>Amateur/farmed real life images</option>
|
||||||
|
<option>Defamation</option>
|
||||||
|
<option>OOC Kinks</option>
|
||||||
|
<option>Real life contact information</option>
|
||||||
|
<option>Solicitation for real life contact</option>
|
||||||
|
<option>Other</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Your Character</label>
|
<label>Your Character</label>
|
||||||
|
@ -30,7 +45,7 @@
|
||||||
import Modal from '../../components/Modal.vue';
|
import Modal from '../../components/Modal.vue';
|
||||||
import * as Utils from '../utils';
|
import * as Utils from '../utils';
|
||||||
import {methods} from './data_store';
|
import {methods} from './data_store';
|
||||||
import {Character, SelectItem} from './interfaces';
|
import {Character} from './interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {modal: Modal}
|
components: {modal: Modal}
|
||||||
|
@ -38,34 +53,13 @@
|
||||||
export default class ReportDialog extends CustomDialog {
|
export default class ReportDialog extends CustomDialog {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: Character;
|
readonly character!: Character;
|
||||||
|
ourCharacter = Utils.settings.defaultCharacter;
|
||||||
ourCharacter = Utils.Settings.defaultCharacter;
|
|
||||||
type = '';
|
type = '';
|
||||||
violation = '';
|
violation = '';
|
||||||
message = '';
|
message = '';
|
||||||
|
|
||||||
submitting = false;
|
submitting = false;
|
||||||
|
|
||||||
ticketUrl = `${Utils.siteDomain}tickets/new`;
|
ticketUrl = `${Utils.siteDomain}tickets/new`;
|
||||||
|
|
||||||
validTypes: ReadonlyArray<SelectItem> = [
|
|
||||||
{text: 'None', value: ''},
|
|
||||||
{text: 'Profile Violation', value: 'profile'},
|
|
||||||
{text: 'Name Request', value: 'name_request'},
|
|
||||||
{text: 'Art Takedown', value: 'takedown'},
|
|
||||||
{text: 'Other', value: 'other'}
|
|
||||||
];
|
|
||||||
violationTypes: ReadonlyArray<string> = [
|
|
||||||
'Real life images on underage character',
|
|
||||||
'Real life animal images on sexual character',
|
|
||||||
'Amateur/farmed real life images',
|
|
||||||
'Defamation',
|
|
||||||
'OOC Kinks',
|
|
||||||
'Real life contact information',
|
|
||||||
'Solicitation for real life contact',
|
|
||||||
'Other'
|
|
||||||
];
|
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.character.character.name;
|
return this.character.character.name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="character.self_staff || character.settings.block_bookmarks !== true">
|
<span v-if="character.self_staff || character.settings.block_bookmarks !== true">
|
||||||
<a @click.prevent="toggleBookmark()" :class="{bookmarked: character.bookmarked, unbookmarked: !character.bookmarked}"
|
<a @click.prevent="toggleBookmark()" href="#" class="btn"
|
||||||
href="#" class="btn">
|
:class="{bookmarked: character.bookmarked, unbookmarked: !character.bookmarked}">
|
||||||
<i class="fa fa-fw" :class="{'fa-minus': character.bookmarked, 'fa-plus': !character.bookmarked}"></i>Bookmark
|
<i class="fa fa-fw" :class="{'fa-minus': character.bookmarked, 'fa-plus': !character.bookmarked}"></i>Bookmark
|
||||||
</a>
|
</a>
|
||||||
<span v-if="character.settings.block_bookmarks" class="prevents-bookmarks">!</span>
|
<span v-if="character.settings.block_bookmarks" class="prevents-bookmarks">!</span>
|
||||||
|
@ -39,11 +39,13 @@
|
||||||
<div v-if="character.character.online_chat" @click="showInChat()" class="character-page-online-chat">Online In Chat</div>
|
<div v-if="character.character.online_chat" @click="showInChat()" class="character-page-online-chat">Online In Chat</div>
|
||||||
|
|
||||||
<div class="contact-block">
|
<div class="contact-block">
|
||||||
<contact-method v-for="method in contactMethods" :method="method" :key="method.id"></contact-method>
|
<contact-method v-for="method in contactMethods" :infotag="method" :key="method.id"
|
||||||
|
:data="character.character.infotags[method.id]"></contact-method>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quick-info-block">
|
<div class="quick-info-block">
|
||||||
<infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id"></infotag-item>
|
<infotag-item v-for="id in quickInfoIds" v-if="character.character.infotags[id]" :infotag="getInfotag(id)"
|
||||||
|
:data="character.character.infotags[id]" :key="id"></infotag-item>
|
||||||
<div class="quick-info">
|
<div class="quick-info">
|
||||||
<span class="quick-info-label">Created: </span>
|
<span class="quick-info-label">Created: </span>
|
||||||
<span class="quick-info-value"><date :time="character.character.created_at"></date></span>
|
<span class="quick-info-value"><date :time="character.character.created_at"></date></span>
|
||||||
|
@ -99,7 +101,7 @@
|
||||||
import DuplicateDialog from './duplicate_dialog.vue';
|
import DuplicateDialog from './duplicate_dialog.vue';
|
||||||
import FriendDialog from './friend_dialog.vue';
|
import FriendDialog from './friend_dialog.vue';
|
||||||
import InfotagView from './infotag.vue';
|
import InfotagView from './infotag.vue';
|
||||||
import {Character, CONTACT_GROUP_ID, SharedStore} from './interfaces';
|
import {Character, CONTACT_GROUP_ID} from './interfaces';
|
||||||
import MemoDialog from './memo_dialog.vue';
|
import MemoDialog from './memo_dialog.vue';
|
||||||
import ReportDialog from './report_dialog.vue';
|
import ReportDialog from './report_dialog.vue';
|
||||||
|
|
||||||
|
@ -139,9 +141,8 @@
|
||||||
export default class Sidebar extends Vue {
|
export default class Sidebar extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly character!: Character;
|
readonly character!: Character;
|
||||||
@Prop()
|
@Prop
|
||||||
readonly oldApi?: true;
|
readonly oldApi?: true;
|
||||||
readonly shared: SharedStore = Store;
|
|
||||||
readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these.
|
readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these.
|
||||||
readonly avatarUrl = Utils.avatarURL;
|
readonly avatarUrl = Utils.avatarURL;
|
||||||
|
|
||||||
|
@ -209,20 +210,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleBookmark(): Promise<void> {
|
async toggleBookmark(): Promise<void> {
|
||||||
const previousState = this.character.bookmarked;
|
|
||||||
try {
|
try {
|
||||||
const state = !this.character.bookmarked;
|
await methods.bookmarkUpdate(this.character.character.id, !this.character.bookmarked);
|
||||||
this.$emit('bookmarked', state);
|
this.character.bookmarked = !this.character.bookmarked;
|
||||||
const actualState = await methods.bookmarkUpdate(this.character.character.id, state);
|
|
||||||
this.$emit('bookmarked', actualState);
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.$emit('bookmarked', previousState);
|
|
||||||
Utils.ajaxError(e, 'Unable to change bookmark state.');
|
Utils.ajaxError(e, 'Unable to change bookmark state.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get editUrl(): string {
|
get editUrl(): string {
|
||||||
return `${Utils.siteDomain}character/${this.character.character.id}/`;
|
return `${Utils.siteDomain}character/${this.character.character.id}/edit`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get noteUrl(): string {
|
get noteUrl(): string {
|
||||||
|
@ -230,33 +227,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
get contactMethods(): {id: number, value?: string}[] {
|
get contactMethods(): {id: number, value?: string}[] {
|
||||||
const contactInfotags = Utils.groupObjectBy(Store.kinks.infotags, 'infotag_group');
|
return Object.keys(Store.shared.infotags).map((x) => Store.shared.infotags[x])
|
||||||
contactInfotags[CONTACT_GROUP_ID]!.sort((a: Infotag, b: Infotag) => a.name < b.name ? -1 : 1);
|
.filter((x) => x.infotag_group === CONTACT_GROUP_ID && this.character.character.infotags[x.id] !== undefined)
|
||||||
const contactMethods = [];
|
.sort((a, b) => a.name < b.name ? -1 : 1);
|
||||||
for(const infotag of contactInfotags[CONTACT_GROUP_ID]!) {
|
|
||||||
const charTag = this.character.character.infotags[infotag.id];
|
|
||||||
if(charTag === undefined) continue;
|
|
||||||
contactMethods.push({
|
|
||||||
id: infotag.id,
|
|
||||||
value: charTag.string
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return contactMethods;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get quickInfoItems(): {id: number, string?: string, list?: number, number?: number}[] {
|
getInfotag(id: number): Infotag {
|
||||||
const quickItems = [];
|
return Store.shared.infotags[id];
|
||||||
for(const id of this.quickInfoIds) {
|
|
||||||
const infotag = this.character.character.infotags[id];
|
|
||||||
if(infotag === undefined) continue;
|
|
||||||
quickItems.push({
|
|
||||||
id,
|
|
||||||
string: infotag.string,
|
|
||||||
list: infotag.list,
|
|
||||||
number: infotag.number
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return quickItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get authenticated(): boolean {
|
get authenticated(): boolean {
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import Vue, {VNodeDirective} from 'vue';
|
|
||||||
//tslint:disable:strict-boolean-expressions
|
|
||||||
type Option = { value: string | null, disabled: boolean, text: string, label: string, options: Option[]} | string | number;
|
|
||||||
|
|
||||||
function rebuild(e: HTMLElement, binding: VNodeDirective): void {
|
|
||||||
const el = <HTMLSelectElement>e;
|
|
||||||
if(binding.oldValue === binding.value) return;
|
|
||||||
if(!binding.value) console.error('Must provide a value');
|
|
||||||
const value = <Option[]>binding.value;
|
|
||||||
|
|
||||||
function _isObject(val: any): val is object { //tslint:disable-line:no-any
|
|
||||||
return val !== null && typeof val === 'object';
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearOptions(): void {
|
|
||||||
let i = el.options.length;
|
|
||||||
while(i--) {
|
|
||||||
const opt = el.options[i];
|
|
||||||
const parent = opt.parentNode!;
|
|
||||||
if(parent === el) parent.removeChild(opt);
|
|
||||||
else {
|
|
||||||
el.removeChild(parent);
|
|
||||||
i = el.options.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildOptions(parent: HTMLElement, options: Option[]): void {
|
|
||||||
let newEl: (HTMLOptionElement & {'_value'?: string | null});
|
|
||||||
for(let i = 0, l = options.length; i < l; i++) {
|
|
||||||
const op = options[i];
|
|
||||||
if(!_isObject(op) || !op.options) {
|
|
||||||
newEl = document.createElement('option');
|
|
||||||
if(typeof op === 'string' || typeof op === 'number')
|
|
||||||
newEl.text = newEl.value = op as string;
|
|
||||||
else {
|
|
||||||
if(op.value !== null && !_isObject(op.value))
|
|
||||||
newEl.value = op.value;
|
|
||||||
newEl['_value'] = op.value;
|
|
||||||
newEl.text = op.text || '';
|
|
||||||
if(op.disabled)
|
|
||||||
newEl.disabled = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newEl = <any>document.createElement('optgroup'); //tslint:disable-line:no-any
|
|
||||||
newEl.label = op.label;
|
|
||||||
buildOptions(newEl, op.options);
|
|
||||||
}
|
|
||||||
parent.appendChild(newEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearOptions();
|
|
||||||
buildOptions(el, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Vue.directive('select', {
|
|
||||||
inserted: rebuild,
|
|
||||||
update: rebuild
|
|
||||||
});
|
|
|
@ -1,18 +1,14 @@
|
||||||
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
||||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
import {InlineDisplayMode, Settings, SimpleCharacter} from '../interfaces';
|
||||||
|
|
||||||
interface Dictionary<T> {
|
type FlashMessageType = 'info' | 'success' | 'warning' | 'danger';
|
||||||
[key: string]: T | undefined;
|
type FlashMessageImpl = (type: FlashMessageType, message: string) => void;
|
||||||
}
|
|
||||||
|
|
||||||
type flashMessageType = 'info' | 'success' | 'warning' | 'danger';
|
let flashImpl: FlashMessageImpl = (type: FlashMessageType, message: string) => {
|
||||||
type flashMessageImpl = (type: flashMessageType, message: string) => void;
|
|
||||||
|
|
||||||
let flashImpl: flashMessageImpl = (type: flashMessageType, message: string) => {
|
|
||||||
console.log(`${type}: ${message}`);
|
console.log(`${type}: ${message}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setFlashMessageImplementation(impl: flashMessageImpl): void {
|
export function setFlashMessageImplementation(impl: FlashMessageImpl): void {
|
||||||
flashImpl = impl;
|
flashImpl = impl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,32 +24,6 @@ export function characterURL(name: string): string {
|
||||||
return `${siteDomain}c/${name}`;
|
return `${siteDomain}c/${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupObjectBy<K extends string, T extends {[k in K]: string}>(obj: Dictionary<T>, key: K): Dictionary<T[]> {
|
|
||||||
const newObject: Dictionary<T[]> = {};
|
|
||||||
for(const objkey in obj) {
|
|
||||||
if(!(objkey in obj)) continue;
|
|
||||||
const realItem = <T>obj[objkey];
|
|
||||||
const newKey = realItem[key];
|
|
||||||
if(newObject[<string>newKey] === undefined) newObject[newKey] = [];
|
|
||||||
newObject[<string>newKey]!.push(realItem);
|
|
||||||
}
|
|
||||||
return newObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function groupArrayBy<K extends string, T extends {[k in K]: string}>(arr: T[], key: K): Dictionary<T[]> {
|
|
||||||
const newObject: Dictionary<T[]> = {};
|
|
||||||
arr.map((item) => {
|
|
||||||
const newKey = item[key];
|
|
||||||
if(newObject[<string>newKey] === undefined) newObject[newKey] = [];
|
|
||||||
newObject[<string>newKey]!.push(item);
|
|
||||||
});
|
|
||||||
return newObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterOut<K extends string, V, T extends {[key in K]: V}>(arr: ReadonlyArray<T>, field: K, value: V): T[] {
|
|
||||||
return arr.filter((item) => item[field] !== value);
|
|
||||||
}
|
|
||||||
|
|
||||||
//tslint:disable-next-line:no-any
|
//tslint:disable-next-line:no-any
|
||||||
export function isJSONError(error: any): error is Error & {response: AxiosResponse<{[key: string]: object | string | number}>} {
|
export function isJSONError(error: any): error is Error & {response: AxiosResponse<{[key: string]: object | string | number}>} {
|
||||||
return (<AxiosError>error).response !== undefined && typeof (<AxiosError>error).response!.data === 'object';
|
return (<AxiosError>error).response !== undefined && typeof (<AxiosError>error).response!.data === 'object';
|
||||||
|
@ -75,6 +45,7 @@ export function ajaxError(error: any, prefix: string, showFlashMessage: boolean
|
||||||
message = (<Error & {response?: AxiosResponse}>error).response !== undefined ?
|
message = (<Error & {response?: AxiosResponse}>error).response !== undefined ?
|
||||||
(<Error & {response: AxiosResponse}>error).response.statusText : error.name;
|
(<Error & {response: AxiosResponse}>error).response.statusText : error.name;
|
||||||
} else message = <string>error;
|
} else message = <string>error;
|
||||||
|
console.error(error);
|
||||||
if(showFlashMessage) flashError(`[ERROR] ${prefix}: ${message}`);
|
if(showFlashMessage) flashError(`[ERROR] ${prefix}: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,35 +57,28 @@ export function flashSuccess(message: string): void {
|
||||||
flashMessage('success', message);
|
flashMessage('success', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flashMessage(type: flashMessageType, message: string): void {
|
export function flashMessage(type: FlashMessageType, message: string): void {
|
||||||
flashImpl(type, message);
|
flashImpl(type, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export let siteDomain = '';
|
export let siteDomain = '';
|
||||||
export let staticDomain = '';
|
export let staticDomain = '';
|
||||||
|
|
||||||
interface Settings {
|
export let settings: Settings = {
|
||||||
animatedIcons: boolean
|
animateEicons: true,
|
||||||
inlineDisplayMode: InlineDisplayMode
|
|
||||||
defaultCharacter: number
|
|
||||||
fuzzyDates: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export let Settings: Settings = {
|
|
||||||
animatedIcons: false,
|
|
||||||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL,
|
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL,
|
||||||
defaultCharacter: -1,
|
defaultCharacter: -1,
|
||||||
fuzzyDates: true
|
fuzzyDates: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let characters: SimpleCharacter[] = [];
|
||||||
|
|
||||||
export function setDomains(site: string, stat: string): void {
|
export function setDomains(site: string, stat: string): void {
|
||||||
siteDomain = site;
|
siteDomain = site;
|
||||||
staticDomain = stat;
|
staticDomain = stat;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copySettings(settings: Settings): void {
|
export function init(s: Settings, c: SimpleCharacter[]): void {
|
||||||
Settings.animatedIcons = settings.animatedIcons;
|
settings = s;
|
||||||
Settings.inlineDisplayMode = settings.inlineDisplayMode;
|
characters = c;
|
||||||
Settings.defaultCharacter = settings.defaultCharacter;
|
|
||||||
Settings.fuzzyDates = settings.fuzzyDates;
|
|
||||||
}
|
}
|
|
@ -119,6 +119,7 @@
|
||||||
true,
|
true,
|
||||||
"allow-declarations"
|
"allow-declarations"
|
||||||
],
|
],
|
||||||
|
"ordered-imports": true,
|
||||||
"prefer-function-over-method": [
|
"prefer-function-over-method": [
|
||||||
true,
|
true,
|
||||||
"allow-public"
|
"allow-public"
|
||||||
|
@ -138,7 +139,8 @@
|
||||||
true,
|
true,
|
||||||
"never"
|
"never"
|
||||||
],
|
],
|
||||||
"strict-boolean-expressions": [true, "allow-boolean-or-undefined"],
|
"strict-boolean-expressions": false,
|
||||||
|
"strict-comparisons": false,
|
||||||
"switch-default": false,
|
"switch-default": false,
|
||||||
"trailing-comma": [
|
"trailing-comma": [
|
||||||
true,
|
true,
|
||||||
|
|
|
@ -36,6 +36,7 @@ import l from '../chat/localize';
|
||||||
import {setupRaven} from '../chat/vue-raven';
|
import {setupRaven} from '../chat/vue-raven';
|
||||||
import Socket from '../chat/WebSocket';
|
import Socket from '../chat/WebSocket';
|
||||||
import Connection from '../fchat/connection';
|
import Connection from '../fchat/connection';
|
||||||
|
import {SimpleCharacter} from '../interfaces';
|
||||||
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
|
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
|
||||||
import {Logs, SettingsStore} from './logs';
|
import {Logs, SettingsStore} from './logs';
|
||||||
import Notifications from './notifications';
|
import Notifications from './notifications';
|
||||||
|
@ -49,7 +50,7 @@ Axios.defaults.params = { __fchat: `web/${version}` };
|
||||||
if(process.env.NODE_ENV === 'production')
|
if(process.env.NODE_ENV === 'production')
|
||||||
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
|
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
|
||||||
|
|
||||||
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<string>, defaultCharacter: string | null};
|
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<SimpleCharacter>, defaultCharacter: number | null};
|
||||||
|
|
||||||
const ticketProvider = async() => {
|
const ticketProvider = async() => {
|
||||||
const data = (await Axios.post<{ticket?: string, error: string}>(
|
const data = (await Axios.post<{ticket?: string, error: string}>(
|
||||||
|
@ -58,7 +59,8 @@ const ticketProvider = async() => {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const connection = new Connection('F-Chat 3.0 (Web)', version, Socket, chatSettings.account, ticketProvider);
|
const connection = new Connection('F-Chat 3.0 (Web)', version, Socket);
|
||||||
|
connection.setCredentials(chatSettings.account, ticketProvider);
|
||||||
initCore(connection, Logs, SettingsStore, Notifications);
|
initCore(connection, Logs, SettingsStore, Notifications);
|
||||||
|
|
||||||
window.addEventListener('beforeunload', (e) => {
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
|
|
@ -120,7 +120,7 @@ export class Logs implements Logging {
|
||||||
if(character === this.loadedCharacter) return this.loadedIndex!;
|
if(character === this.loadedCharacter) return this.loadedIndex!;
|
||||||
if(character === core.connection.character) {
|
if(character === core.connection.character) {
|
||||||
this.loadedDb = this.db;
|
this.loadedDb = this.db;
|
||||||
this.loadedIndex = this.index !== undefined ? this.index : {};
|
this.loadedIndex = this.index;
|
||||||
} else
|
} else
|
||||||
try {
|
try {
|
||||||
this.loadedDb = await openDatabase(character);
|
this.loadedDb = await openDatabase(character);
|
||||||
|
@ -130,7 +130,7 @@ export class Logs implements Logging {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
this.loadedCharacter = character;
|
this.loadedCharacter = character;
|
||||||
return this.loadedIndex;
|
return this.loadedIndex!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversations(character: string): Promise<ReadonlyArray<{key: string, name: string}>> {
|
async getConversations(character: string): Promise<ReadonlyArray<{key: string, name: string}>> {
|
||||||
|
@ -179,7 +179,7 @@ export class SettingsStore implements Settings.Store {
|
||||||
const pinned: Settings.Keys['pinned'] = {channels: [], private: []};
|
const pinned: Settings.Keys['pinned'] = {channels: [], private: []};
|
||||||
pinned.channels = tabs.filter((x) => x.type === 'channel').map((x) => x.id.toLowerCase());
|
pinned.channels = tabs.filter((x) => x.type === 'channel').map((x) => x.id.toLowerCase());
|
||||||
pinned.private = tabs.filter((x) => x.type === 'user').map((x) => x.title);
|
pinned.private = tabs.filter((x) => x.type === 'user').map((x) => x.title);
|
||||||
return pinned;
|
return pinned as Settings.Keys[K];
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ export class SettingsStore implements Settings.Store {
|
||||||
settings.playSound = old.html5Audio;
|
settings.playSound = old.html5Audio;
|
||||||
settings.joinMessages = old.joinLeaveAlerts;
|
settings.joinMessages = old.joinLeaveAlerts;
|
||||||
settings.clickOpensMessage = !old.leftClickOpensFlist;
|
settings.clickOpensMessage = !old.leftClickOpensFlist;
|
||||||
return settings;
|
return settings as unknown as Settings.Keys[K];
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue