0.2.17 - Webpack 4, Bootstrap 4, remove jquery
This commit is contained in:
parent
690ae19404
commit
04ab2f96da
|
@ -1,27 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bbcodeEditorContainer">
|
<div class="bbcode-editor">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<a tabindex="0" class="btn bbcodeEditorButton bbcode-btn" role="button" @click="showToolbar = true" @blur="showToolbar = false">
|
<a tabindex="0" class="btn btn-secondary bbcode-btn btn-sm" role="button" @click="showToolbar = true" @blur="showToolbar = false"
|
||||||
<span class="fa fa-code"></span></a>
|
style="border-bottom-left-radius:0;border-bottom-right-radius:0">
|
||||||
<div class="bbcode-toolbar" role="toolbar" :style="showToolbar ? 'display:block' : ''" @mousedown.stop.prevent>
|
<i class="fa fa-code"></i>
|
||||||
|
</a>
|
||||||
|
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? 'display:flex' : ''" @mousedown.stop.prevent>
|
||||||
|
<div class="btn-group" style="flex-wrap:wrap">
|
||||||
|
<div class="btn btn-secondary btn-sm" v-for="button in buttons" :title="button.title" @click.prevent.stop="apply(button)">
|
||||||
|
<i :class="(button.class ? button.class : 'fa ') + button.icon"></i>
|
||||||
|
</div>
|
||||||
|
<div @click="previewBBCode" class="btn btn-secondary btn-sm" :class="preview ? 'active' : ''"
|
||||||
|
:title="preview ? 'Close Preview' : 'Preview'">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" class="close" aria-label="Close" style="margin-left:10px" @click="showToolbar = false">×</button>
|
<button type="button" class="close" aria-label="Close" style="margin-left:10px" @click="showToolbar = false">×</button>
|
||||||
<div class="bbcodeEditorButton btn" v-for="button in buttons" :title="button.title" @click.prevent.stop="apply(button)">
|
|
||||||
<span :class="'fa ' + button.icon"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="previewBBCode" class="bbcodeEditorButton btn" :class="preview ? 'active' : ''"
|
|
||||||
:title="preview ? 'Close Preview' : 'Preview'">
|
|
||||||
<span class="fa fa-eye"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bbcodeEditorTextarea">
|
<div class="bbcode-editor-text-area">
|
||||||
<textarea ref="input" v-model="text" @input="onInput" v-show="!preview" :maxlength="maxlength"
|
<textarea ref="input" v-model="text" @input="onInput" v-show="!preview" :maxlength="maxlength"
|
||||||
:class="'bbcodeTextAreaTextArea ' + classes" @keyup="onKeyUp" :disabled="disabled" @paste="onPaste"
|
:class="finalClasses" @keyup="onKeyUp" :disabled="disabled" @paste="onPaste" style="border-top-left-radius:0"
|
||||||
:placeholder="placeholder" @keypress="$emit('keypress', $event)" @keydown="onKeyDown"></textarea>
|
:placeholder="placeholder" @keypress="$emit('keypress', $event)" @keydown="onKeyDown"></textarea>
|
||||||
<div class="bbcodePreviewArea" v-show="preview">
|
<div ref="sizer"></div>
|
||||||
<div class="bbcodePreviewHeader">
|
<div class="bbcode-preview" v-show="preview">
|
||||||
<ul class="bbcodePreviewWarnings" v-show="previewWarnings.length">
|
<div class="bbcode-preview-warnings">
|
||||||
|
<div class="alert alert-danger" v-show="previewWarnings.length">
|
||||||
<li v-for="warning in previewWarnings">{{warning}}</li>
|
<li v-for="warning in previewWarnings">{{warning}}</li>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bbcode" ref="preview-element"></div>
|
<div class="bbcode" ref="preview-element"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +40,7 @@
|
||||||
import {Prop, Watch} from 'vue-property-decorator';
|
import {Prop, Watch} from 'vue-property-decorator';
|
||||||
import {BBCodeElement} from '../chat/bbcode';
|
import {BBCodeElement} from '../chat/bbcode';
|
||||||
import {getKey} from '../chat/common';
|
import {getKey} from '../chat/common';
|
||||||
|
import {Keys} from '../keys';
|
||||||
import {CoreBBCodeParser, urlRegex} from './core';
|
import {CoreBBCodeParser, urlRegex} from './core';
|
||||||
import {defaultButtons, EditorButton, EditorSelection} from './editor';
|
import {defaultButtons, EditorButton, EditorSelection} from './editor';
|
||||||
import {BBCodeParser} from './parser';
|
import {BBCodeParser} from './parser';
|
||||||
|
@ -44,7 +50,7 @@
|
||||||
@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()
|
||||||
|
@ -53,15 +59,18 @@
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
@Prop()
|
@Prop()
|
||||||
readonly placeholder?: string;
|
readonly placeholder?: string;
|
||||||
|
@Prop({default: false, type: Boolean})
|
||||||
|
readonly invalid!: boolean;
|
||||||
preview = false;
|
preview = false;
|
||||||
previewWarnings: ReadonlyArray<string> = [];
|
previewWarnings: ReadonlyArray<string> = [];
|
||||||
previewResult = '';
|
previewResult = '';
|
||||||
text = this.value !== undefined ? this.value : '';
|
text = this.value !== undefined ? this.value : '';
|
||||||
element: HTMLTextAreaElement;
|
element!: HTMLTextAreaElement;
|
||||||
maxHeight: number;
|
sizer!: HTMLElement;
|
||||||
minHeight: number;
|
maxHeight!: number;
|
||||||
|
minHeight!: number;
|
||||||
showToolbar = false;
|
showToolbar = false;
|
||||||
protected parser: BBCodeParser;
|
protected parser!: BBCodeParser;
|
||||||
protected defaultButtons = defaultButtons;
|
protected defaultButtons = defaultButtons;
|
||||||
private isShiftPressed = false;
|
private isShiftPressed = false;
|
||||||
private undoStack: string[] = [];
|
private undoStack: string[] = [];
|
||||||
|
@ -74,16 +83,31 @@
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.element = <HTMLTextAreaElement>this.$refs['input'];
|
this.element = <HTMLTextAreaElement>this.$refs['input'];
|
||||||
const $element = $(this.element);
|
const styles = getComputedStyle(this.element);
|
||||||
this.maxHeight = parseInt($element.css('max-height'), 10);
|
this.maxHeight = parseInt(styles.maxHeight! , 10);
|
||||||
//tslint:disable-next-line:strict-boolean-expressions
|
//tslint:disable-next-line:strict-boolean-expressions
|
||||||
this.minHeight = parseInt($element.css('min-height'), 10) || $element.outerHeight() || 50;
|
this.minHeight = parseInt(styles.minHeight!, 10) || 50;
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if(Date.now() - this.lastInput >= 500 && this.text !== this.undoStack[0] && this.undoIndex === 0) {
|
if(Date.now() - this.lastInput >= 500 && this.text !== this.undoStack[0] && this.undoIndex === 0) {
|
||||||
if(this.undoStack.length >= 30) this.undoStack.pop();
|
if(this.undoStack.length >= 30) this.undoStack.pop();
|
||||||
this.undoStack.unshift(this.text);
|
this.undoStack.unshift(this.text);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
this.sizer = <HTMLElement>this.$refs['sizer'];
|
||||||
|
this.sizer.style.cssText = styles.cssText;
|
||||||
|
this.sizer.style.height = '0';
|
||||||
|
this.sizer.style.overflow = 'hidden';
|
||||||
|
this.sizer.style.position = 'absolute';
|
||||||
|
this.sizer.style.top = '0';
|
||||||
|
this.sizer.style.visibility = 'hidden';
|
||||||
|
this.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
get finalClasses(): string | undefined {
|
||||||
|
let classes = this.classes;
|
||||||
|
if(this.invalid)
|
||||||
|
classes += ' is-invalid';
|
||||||
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
get buttons(): EditorButton[] {
|
get buttons(): EditorButton[] {
|
||||||
|
@ -169,15 +193,15 @@
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent): void {
|
onKeyDown(e: KeyboardEvent): void {
|
||||||
const key = getKey(e);
|
const key = getKey(e);
|
||||||
if((e.metaKey || e.ctrlKey) && !e.shiftKey && key !== 'control' && key !== 'meta') {
|
if((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
||||||
if(key === 'z') {
|
if(key === Keys.KeyZ) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if(this.undoIndex === 0 && this.undoStack[0] !== this.text) this.undoStack.unshift(this.text);
|
if(this.undoIndex === 0 && this.undoStack[0] !== this.text) this.undoStack.unshift(this.text);
|
||||||
if(this.undoStack.length > this.undoIndex + 1) {
|
if(this.undoStack.length > this.undoIndex + 1) {
|
||||||
this.text = this.undoStack[++this.undoIndex];
|
this.text = this.undoStack[++this.undoIndex];
|
||||||
this.lastInput = Date.now();
|
this.lastInput = Date.now();
|
||||||
}
|
}
|
||||||
} else if(key === 'y') {
|
} else if(key === Keys.KeyY) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if(this.undoIndex > 0) {
|
if(this.undoIndex > 0) {
|
||||||
this.text = this.undoStack[--this.undoIndex];
|
this.text = this.undoStack[--this.undoIndex];
|
||||||
|
@ -191,20 +215,20 @@
|
||||||
this.apply(button);
|
this.apply(button);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if(key === 'shift') this.isShiftPressed = true;
|
} else if(e.shiftKey) this.isShiftPressed = true;
|
||||||
this.$emit('keydown', e);
|
this.$emit('keydown', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyUp(e: KeyboardEvent): void {
|
onKeyUp(e: KeyboardEvent): void {
|
||||||
if(getKey(e) === 'shift') this.isShiftPressed = false;
|
if(!e.shiftKey) this.isShiftPressed = false;
|
||||||
this.$emit('keyup', e);
|
this.$emit('keyup', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(): void {
|
resize(): void {
|
||||||
if(this.maxHeight > 0) {
|
this.sizer.style.fontSize = this.element.style.fontSize;
|
||||||
this.element.style.height = 'auto';
|
this.sizer.style.lineHeight = this.element.style.lineHeight;
|
||||||
this.element.style.height = `${Math.max(Math.min(this.element.scrollHeight + 5, this.maxHeight), this.minHeight)}px`;
|
this.sizer.textContent = this.element.value;
|
||||||
}
|
this.element.style.height = `${Math.max(Math.min(this.sizer.scrollHeight, this.maxHeight), this.minHeight)}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaste(e: ClipboardEvent): void {
|
onPaste(e: ClipboardEvent): void {
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class CoreBBCodeParser extends BBCodeParser {
|
||||||
} else if(content.length > 0) url = content;
|
} else if(content.length > 0) url = content;
|
||||||
else {
|
else {
|
||||||
parser.warning('url tag contains no url.');
|
parser.warning('url tag contains no url.');
|
||||||
element.textContent = ''; //Dafuq!?
|
element.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ export class CoreBBCodeParser extends BBCodeParser {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = 'link-domain';
|
span.className = 'link-domain';
|
||||||
span.textContent = ` [${domain(url)}]`;
|
span.textContent = ` [${domain(url)}]`;
|
||||||
|
(<HTMLElement & {bbcodeHide: true}>span).bbcodeHide = true;
|
||||||
element.appendChild(span);
|
element.appendChild(span);
|
||||||
}, []));
|
}, []));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import {Keys} from '../keys';
|
||||||
|
|
||||||
export interface EditorButton {
|
export interface EditorButton {
|
||||||
title: string;
|
title: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
key?: string;
|
key?: Keys;
|
||||||
class?: string;
|
class?: string;
|
||||||
startText?: string;
|
startText?: string;
|
||||||
endText?: string;
|
endText?: string;
|
||||||
|
@ -23,74 +24,75 @@ export let defaultButtons: ReadonlyArray<EditorButton> = [
|
||||||
title: 'Bold (Ctrl+B)\n\nMakes text appear with a bold weight.',
|
title: 'Bold (Ctrl+B)\n\nMakes text appear with a bold weight.',
|
||||||
tag: 'b',
|
tag: 'b',
|
||||||
icon: 'fa-bold',
|
icon: 'fa-bold',
|
||||||
key: 'b'
|
key: Keys.KeyB
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Italic (Ctrl+I)\n\nMakes text appear with an italic style.',
|
title: 'Italic (Ctrl+I)\n\nMakes text appear with an italic style.',
|
||||||
tag: 'i',
|
tag: 'i',
|
||||||
icon: 'fa-italic',
|
icon: 'fa-italic',
|
||||||
key: 'i'
|
key: Keys.KeyI
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Underline (Ctrl+U)\n\nMakes text appear with an underline beneath it.',
|
title: 'Underline (Ctrl+U)\n\nMakes text appear with an underline beneath it.',
|
||||||
tag: 'u',
|
tag: 'u',
|
||||||
icon: 'fa-underline',
|
icon: 'fa-underline',
|
||||||
key: 'u'
|
key: Keys.KeyU
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Strikethrough (Ctrl+S)\n\nPlaces a horizontal line through the text. Usually used to signify a correction or redaction without omitting the text.',
|
title: 'Strikethrough (Ctrl+S)\n\nPlaces a horizontal line through the text. Usually used to signify a correction or redaction without omitting the text.',
|
||||||
tag: 's',
|
tag: 's',
|
||||||
icon: 'fa-strikethrough',
|
icon: 'fa-strikethrough',
|
||||||
key: 's'
|
key: Keys.KeyS
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
|
title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
|
||||||
tag: 'color',
|
tag: 'color',
|
||||||
startText: '[color=]',
|
startText: '[color=]',
|
||||||
icon: 'fa-eyedropper',
|
icon: 'fa-eye-dropper',
|
||||||
key: 'd'
|
key: Keys.KeyD
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Superscript (Ctrl+↑)\n\nLifts text above the text baseline. Makes text slightly smaller. Cannot be nested.',
|
title: 'Superscript (Ctrl+↑)\n\nLifts text above the text baseline. Makes text slightly smaller. Cannot be nested.',
|
||||||
tag: 'sup',
|
tag: 'sup',
|
||||||
icon: 'fa-superscript',
|
icon: 'fa-superscript',
|
||||||
key: 'arrowup'
|
key: Keys.ArrowUp
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Subscript (Ctrl+↓)\n\nPushes text below the text baseline. Makes text slightly smaller. Cannot be nested.',
|
title: 'Subscript (Ctrl+↓)\n\nPushes text below the text baseline. Makes text slightly smaller. Cannot be nested.',
|
||||||
tag: 'sub',
|
tag: 'sub',
|
||||||
icon: 'fa-subscript',
|
icon: 'fa-subscript',
|
||||||
key: 'arrowdown'
|
key: Keys.ArrowDown
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'URL (Ctrl+L)\n\nCreates a clickable link to another page of your choosing.',
|
title: 'URL (Ctrl+L)\n\nCreates a clickable link to another page of your choosing.',
|
||||||
tag: 'url',
|
tag: 'url',
|
||||||
startText: '[url=]',
|
startText: '[url=]',
|
||||||
icon: 'fa-link',
|
icon: 'fa-link',
|
||||||
key: 'l'
|
key: Keys.KeyL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'User (Ctrl+R)\n\nLinks to a character\'s profile.',
|
title: 'User (Ctrl+R)\n\nLinks to a character\'s profile.',
|
||||||
tag: 'user',
|
tag: 'user',
|
||||||
icon: 'fa-user',
|
icon: 'fa-user',
|
||||||
key: 'r'
|
key: Keys.KeyR
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Icon (Ctrl+O)\n\nShows a character\'s profile image, linking to their profile.',
|
title: 'Icon (Ctrl+O)\n\nShows a character\'s profile image, linking to their profile.',
|
||||||
tag: 'icon',
|
tag: 'icon',
|
||||||
icon: 'fa-user-circle',
|
icon: 'fa-user-circle',
|
||||||
key: 'o'
|
key: Keys.KeyO
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'EIcon (Ctrl+E)\n\nShows a previously uploaded eicon. If the icon is a gif, it will be shown as animated unless a user has turned this off.',
|
title: 'EIcon (Ctrl+E)\n\nShows a previously uploaded eicon. If the icon is a gif, it will be shown as animated unless a user has turned this off.',
|
||||||
tag: 'eicon',
|
tag: 'eicon',
|
||||||
icon: 'fa-smile-o',
|
class: 'far ',
|
||||||
key: 'e'
|
icon: 'fa-smile',
|
||||||
|
key: Keys.KeyE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Noparse (Ctrl+N)\n\nAll BBCode placed within this tag will be ignored and treated as text. Great for sharing structure without it being rendered.',
|
title: 'Noparse (Ctrl+N)\n\nAll BBCode placed within this tag will be ignored and treated as text. Great for sharing structure without it being rendered.',
|
||||||
tag: 'noparse',
|
tag: 'noparse',
|
||||||
icon: 'fa-ban',
|
icon: 'fa-ban',
|
||||||
key: 'n'
|
key: Keys.KeyN
|
||||||
}
|
}
|
||||||
];
|
];
|
|
@ -26,7 +26,7 @@ export abstract class BBCodeTag {
|
||||||
|
|
||||||
export class BBCodeSimpleTag extends BBCodeTag {
|
export class BBCodeSimpleTag extends BBCodeTag {
|
||||||
|
|
||||||
constructor(tag: string, private elementName: keyof ElementTagNameMap, private classes?: string[], tagList?: string[]) {
|
constructor(tag: string, private elementName: keyof HTMLElementTagNameMap, private classes?: string[], tagList?: string[]) {
|
||||||
super(tag, tagList);
|
super(tag, tagList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,9 +81,9 @@ class ParserTag {
|
||||||
export class BBCodeParser {
|
export class BBCodeParser {
|
||||||
private _warnings: string[] = [];
|
private _warnings: string[] = [];
|
||||||
private _tags: {[tag: string]: BBCodeTag | undefined} = {};
|
private _tags: {[tag: string]: BBCodeTag | undefined} = {};
|
||||||
private _line: number;
|
private _line = -1;
|
||||||
private _column: number;
|
private _column = -1;
|
||||||
private _currentTag: ParserTag;
|
private _currentTag!: ParserTag;
|
||||||
private _storeWarnings = false;
|
private _storeWarnings = false;
|
||||||
|
|
||||||
parseEverything(input: string): HTMLElement {
|
parseEverything(input: string): HTMLElement {
|
||||||
|
@ -103,7 +103,7 @@ export class BBCodeParser {
|
||||||
return stack[0].element;
|
return stack[0].element;
|
||||||
}
|
}
|
||||||
|
|
||||||
createElement<K extends keyof HTMLElementTagNameMap>(tag: K | keyof ElementTagNameMap): HTMLElementTagNameMap[K] {
|
createElement<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K] {
|
||||||
return document.createElement(tag);
|
return document.createElement(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +218,8 @@ export class BBCodeParser {
|
||||||
quickReset(i);
|
quickReset(i);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
(<HTMLElement & {bbcodeTag: string}>el).bbcodeTag = tagKey;
|
||||||
|
if(param.length > 0) (<HTMLElement & {bbcodeParam: string}>el).bbcodeParam = param;
|
||||||
if(!this._tags[tagKey]!.noClosingTag)
|
if(!this._tags[tagKey]!.noClosingTag)
|
||||||
stack.push(new ParserTag(tagKey, param, el, parent, this._line, this._column));
|
stack.push(new ParserTag(tagKey, param, el, parent, this._line, this._column));
|
||||||
} else if(ignoreClosing[tagKey] > 0) {
|
} else if(ignoreClosing[tagKey] > 0) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import * as $ from 'jquery';
|
|
||||||
import {CoreBBCodeParser} from './core';
|
import {CoreBBCodeParser} from './core';
|
||||||
import {InlineDisplayMode} from './interfaces';
|
import {InlineDisplayMode} from './interfaces';
|
||||||
import {BBCodeCustomTag, BBCodeSimpleTag} from './parser';
|
import {BBCodeCustomTag, BBCodeSimpleTag} from './parser';
|
||||||
|
@ -8,6 +7,7 @@ interface InlineImage {
|
||||||
hash: string
|
hash: string
|
||||||
extension: string
|
extension: string
|
||||||
nsfw: boolean
|
nsfw: boolean
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StandardParserSettings {
|
interface StandardParserSettings {
|
||||||
|
@ -23,6 +23,18 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
allowInlines = true;
|
allowInlines = true;
|
||||||
inlines: {[key: string]: InlineImage | undefined} | undefined;
|
inlines: {[key: string]: InlineImage | undefined} | undefined;
|
||||||
|
|
||||||
|
createInline(inline: InlineImage): HTMLElement {
|
||||||
|
const p1 = inline.hash.substr(0, 2);
|
||||||
|
const p2 = inline.hash.substr(2, 2);
|
||||||
|
const outerEl = this.createElement('div');
|
||||||
|
const el = this.createElement('img');
|
||||||
|
el.className = 'inline-image';
|
||||||
|
el.title = el.alt = inline.name!;
|
||||||
|
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||||
|
outerEl.appendChild(el);
|
||||||
|
return outerEl;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public settings: StandardParserSettings) {
|
constructor(public settings: StandardParserSettings) {
|
||||||
super();
|
super();
|
||||||
const hrTag = new BBCodeSimpleTag('hr', 'hr', [], []);
|
const hrTag = new BBCodeSimpleTag('hr', 'hr', [], []);
|
||||||
|
@ -54,16 +66,24 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
//return null;
|
//return null;
|
||||||
}
|
}
|
||||||
const outer = parser.createElement('div');
|
const outer = parser.createElement('div');
|
||||||
outer.className = 'collapseHeader';
|
outer.className = 'card bg-light bbcode-collapse';
|
||||||
const headerText = parser.createElement('div');
|
const headerText = parser.createElement('div');
|
||||||
headerText.className = 'collapseHeaderText';
|
headerText.className = 'card-header bbcode-collapse-header';
|
||||||
|
const icon = parser.createElement('i');
|
||||||
|
icon.className = 'fas fa-chevron-down';
|
||||||
|
icon.style.marginRight = '10px';
|
||||||
|
headerText.appendChild(icon);
|
||||||
|
headerText.appendChild(document.createTextNode(param));
|
||||||
outer.appendChild(headerText);
|
outer.appendChild(headerText);
|
||||||
const innerText = parser.createElement('span');
|
|
||||||
innerText.appendChild(document.createTextNode(param));
|
|
||||||
headerText.appendChild(innerText);
|
|
||||||
const body = parser.createElement('div');
|
const body = parser.createElement('div');
|
||||||
body.className = 'collapseBlock';
|
body.className = 'card-body bbcode-collapse-body closed';
|
||||||
|
body.style.height = '0';
|
||||||
outer.appendChild(body);
|
outer.appendChild(body);
|
||||||
|
headerText.addEventListener('click', () => {
|
||||||
|
const isCollapsed = parseInt(body.style.height!, 10) === 0;
|
||||||
|
body.style.height = isCollapsed ? `${body.scrollHeight}px` : '0';
|
||||||
|
icon.className = `fas fa-chevron-${isCollapsed ? 'up' : 'down'}`;
|
||||||
|
});
|
||||||
parent.appendChild(outer);
|
parent.appendChild(outer);
|
||||||
return body;
|
return body;
|
||||||
}));
|
}));
|
||||||
|
@ -122,7 +142,12 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
img.className = 'character-avatar icon';
|
img.className = 'character-avatar icon';
|
||||||
parent.replaceChild(img, element);
|
parent.replaceChild(img, element);
|
||||||
}, []));
|
}, []));
|
||||||
this.addTag('img', new BBCodeCustomTag('img', (p, parent, param) => {
|
this.addTag('img', new BBCodeCustomTag('img', (parser, parent) => {
|
||||||
|
const el = parser.createElement('span');
|
||||||
|
parent.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}, (p, element, parent, param) => {
|
||||||
|
const content = element.textContent!;
|
||||||
const parser = <StandardBBCodeParser>p;
|
const parser = <StandardBBCodeParser>p;
|
||||||
if(!this.allowInlines) {
|
if(!this.allowInlines) {
|
||||||
parser.warning('Inline images are not allowed here.');
|
parser.warning('Inline images are not allowed here.');
|
||||||
|
@ -132,82 +157,38 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||||
parser.warning('This page does not support inline images.');
|
parser.warning('This page does not support inline images.');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let p1: string, p2: string, inline;
|
|
||||||
const displayMode = this.settings.inlineDisplayMode;
|
const displayMode = this.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;
|
||||||
}
|
}
|
||||||
if(typeof parser.inlines[param] !== 'object') {
|
const inline = parser.inlines[param];
|
||||||
|
if(typeof inline !== 'object') {
|
||||||
parser.warning(`Could not find an inline image with id ${param} It will not be visible.`);
|
parser.warning(`Could not find an inline image with id ${param} It will not be visible.`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
inline = parser.inlines[param]!;
|
inline.name = content;
|
||||||
p1 = inline.hash.substr(0, 2);
|
|
||||||
p2 = inline.hash.substr(2, 2);
|
|
||||||
|
|
||||||
if(displayMode === InlineDisplayMode.DISPLAY_NONE || (displayMode === InlineDisplayMode.DISPLAY_SFW && inline.nsfw)) {
|
if(displayMode === InlineDisplayMode.DISPLAY_NONE || (displayMode === InlineDisplayMode.DISPLAY_SFW && inline.nsfw)) {
|
||||||
const el = parser.createElement('a');
|
const el = parser.createElement('a');
|
||||||
el.className = 'unloadedInline';
|
el.className = 'unloadedInline';
|
||||||
el.href = '#';
|
el.href = '#';
|
||||||
el.dataset.inlineId = param;
|
el.dataset.inlineId = param;
|
||||||
el.onclick = () => {
|
el.onclick = () => {
|
||||||
$('.unloadedInline').each((_, element) => {
|
Array.prototype.forEach.call(document.getElementsByClassName('unloadedInline'), ((e: HTMLElement) => {
|
||||||
const inlineId = $(element).data('inline-id');
|
const showInline = parser.inlines![e.dataset.inlineId!];
|
||||||
if(typeof parser.inlines![inlineId] !== 'object')
|
if(typeof showInline !== 'object') return;
|
||||||
return;
|
e.parentElement!.replaceChild(parser.createInline(showInline), e);
|
||||||
const showInline = parser.inlines![inlineId]!;
|
}));
|
||||||
const showP1 = showInline.hash.substr(0, 2);
|
|
||||||
const showP2 = showInline.hash.substr(2, 2);
|
|
||||||
//tslint:disable-next-line:max-line-length
|
|
||||||
$(element).replaceWith(`<div><img class="inline-image" src="${this.settings.staticDomain}images/charinline/${showP1}/${showP2}/${showInline.hash}.${showInline.extension}"/></div>`);
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const prefix = inline.nsfw ? '[NSFW Inline] ' : '[Inline] ';
|
const prefix = inline.nsfw ? '[NSFW Inline] ' : '[Inline] ';
|
||||||
el.appendChild(document.createTextNode(prefix));
|
el.appendChild(document.createTextNode(prefix));
|
||||||
parent.appendChild(el);
|
parent.replaceChild(el, element);
|
||||||
return el;
|
} else parent.replaceChild(parser.createInline(inline), element);
|
||||||
} else {
|
|
||||||
const outerEl = parser.createElement('div');
|
|
||||||
const el = parser.createElement('img');
|
|
||||||
el.className = 'inline-image';
|
|
||||||
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
|
||||||
outerEl.appendChild(el);
|
|
||||||
parent.appendChild(outerEl);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
}, (_, element, __, ___) => {
|
|
||||||
// Need to remove any appended contents, because this is a total hack job.
|
|
||||||
if(element.className !== 'inline-image')
|
|
||||||
return;
|
|
||||||
while(element.firstChild !== null)
|
|
||||||
element.removeChild(element.firstChild);
|
|
||||||
}, []));
|
}, []));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initCollapse(): void {
|
|
||||||
$('.collapseHeader[data-bound!=true]').each((_, element) => {
|
|
||||||
const $element = $(element);
|
|
||||||
const $body = $element.children('.collapseBlock');
|
|
||||||
$element.children('.collapseHeaderText').on('click', () => {
|
|
||||||
if($element.hasClass('expandedHeader')) {
|
|
||||||
$body.css('max-height', '0');
|
|
||||||
$element.removeClass('expandedHeader');
|
|
||||||
} else {
|
|
||||||
$body.css('max-height', 'none');
|
|
||||||
const height = $body.outerHeight();
|
|
||||||
$body.css('max-height', '0');
|
|
||||||
$element.addClass('expandedHeader');
|
|
||||||
setTimeout(() => $body.css('max-height', height!), 1);
|
|
||||||
setTimeout(() => $body.css('max-height', 'none'), 250);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
$('.collapseHeader').attr('data-bound', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
export let standardParser: StandardBBCodeParser;
|
export let standardParser: StandardBBCodeParser;
|
||||||
|
|
||||||
export function initParser(settings: StandardParserSettings): void {
|
export function initParser(settings: StandardParserSettings): void {
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :buttons="false" :action="l('chat.channels')" @close="closed">
|
<modal :buttons="false" :action="l('chat.channels')" @close="closed" dialog-class="w-100 channel-list">
|
||||||
<div style="display: flex; flex-direction: column;">
|
<div style="display:flex;flex-direction:column">
|
||||||
<ul class="nav nav-tabs" style="flex-shrink:0">
|
<tabs style="flex-shrink:0" :tabs="[l('channelList.public'), l('channelList.private')]" v-model="tab"></tabs>
|
||||||
<li role="presentation" :class="{active: !privateTabShown}">
|
|
||||||
<a href="#" @click.prevent="privateTabShown = false">{{l('channelList.public')}}</a>
|
|
||||||
</li>
|
|
||||||
<li role="presentation" :class="{active: privateTabShown}">
|
|
||||||
<a href="#" @click.prevent="privateTabShown = true">{{l('channelList.private')}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
<div style="display:flex; padding: 10px 0; flex-shrink: 0;">
|
<div style="display:flex; padding: 10px 0; flex-shrink: 0;">
|
||||||
<input class="form-control" style="flex:1; margin-right:10px;" v-model="filter" :placeholder="l('filter')"/>
|
<input class="form-control" style="flex:1; margin-right:10px;" v-model="filter" :placeholder="l('filter')"/>
|
||||||
<a href="#" @click.prevent="sortCount = !sortCount">
|
<a href="#" @click.prevent="sortCount = !sortCount">
|
||||||
<span class="fa fa-2x" :class="{'fa-sort-amount-desc': sortCount, 'fa-sort-alpha-asc': !sortCount}"></span>
|
<span class="fa fa-2x" :class="{'fa-sort-amount-down': sortCount, 'fa-sort-alpha-down': !sortCount}"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow: auto;" v-show="!privateTabShown">
|
<div style="overflow: auto;" v-show="tab == 0">
|
||||||
<div v-for="channel in officialChannels" :key="channel.id">
|
<div v-for="channel in officialChannels" :key="channel.id">
|
||||||
<label :for="channel.id">
|
<label :for="channel.id">
|
||||||
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
||||||
|
@ -24,7 +17,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow: auto;" v-show="privateTabShown">
|
<div style="overflow: auto;" v-show="tab == 1">
|
||||||
<div v-for="channel in openRooms" :key="channel.id">
|
<div v-for="channel in openRooms" :key="channel.id">
|
||||||
<label :for="channel.id">
|
<label :for="channel.id">
|
||||||
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
||||||
|
@ -46,13 +39,13 @@
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
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 Tabs from '../components/tabs';
|
||||||
|
import {Channel} from '../fchat';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Channel} from './interfaces';
|
|
||||||
import l from './localize';
|
import l from './localize';
|
||||||
import ListItem = Channel.ListItem;
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {modal: Modal}
|
components: {modal: Modal, tabs: Tabs}
|
||||||
})
|
})
|
||||||
export default class ChannelList extends CustomDialog {
|
export default class ChannelList extends CustomDialog {
|
||||||
privateTabShown = false;
|
privateTabShown = false;
|
||||||
|
@ -60,6 +53,7 @@
|
||||||
sortCount = true;
|
sortCount = true;
|
||||||
filter = '';
|
filter = '';
|
||||||
createName = '';
|
createName = '';
|
||||||
|
tab = '0';
|
||||||
|
|
||||||
get openRooms(): ReadonlyArray<Channel.ListItem> {
|
get openRooms(): ReadonlyArray<Channel.ListItem> {
|
||||||
return this.applyFilter(core.channels.openRooms);
|
return this.applyFilter(core.channels.openRooms);
|
||||||
|
@ -92,8 +86,15 @@
|
||||||
this.createName = '';
|
this.createName = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
setJoined(channel: ListItem): void {
|
setJoined(channel: Channel.ListItem): void {
|
||||||
channel.isJoined ? core.channels.leave(channel.id) : core.channels.join(channel.id);
|
channel.isJoined ? core.channels.leave(channel.id) : core.channels.join(channel.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.channel-list .modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,9 +12,9 @@
|
||||||
@Component
|
@Component
|
||||||
export default class ChannelView extends Vue {
|
export default class ChannelView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly id: string;
|
readonly id!: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly text: string;
|
readonly text!: string;
|
||||||
|
|
||||||
joinChannel(): void {
|
joinChannel(): void {
|
||||||
if(this.channel === undefined || !this.channel.isJoined)
|
if(this.channel === undefined || !this.channel.isJoined)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :action="l('characterSearch.action')" @submit.prevent="submit"
|
<modal :action="l('characterSearch.action')" @submit.prevent="submit" dialogClass="w-100"
|
||||||
:buttonText="results ? l('characterSearch.again') : undefined" class="character-search">
|
:buttonText="results ? l('characterSearch.again') : undefined" class="character-search">
|
||||||
<div v-if="options && !results">
|
<div v-if="options && !results">
|
||||||
<div v-show="error" class="alert alert-danger">{{error}}</div>
|
<div v-show="error" class="alert alert-danger">{{error}}</div>
|
||||||
|
@ -113,10 +113,9 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
core.connection.onMessage('FKS', (data) => {
|
core.connection.onMessage('FKS', (data) => {
|
||||||
this.results = data.characters.filter((x) => core.state.hiddenUsers.indexOf(x) === -1)
|
this.results = data.characters.map((x) => core.characters.get(x))
|
||||||
.map((x) => core.characters.get(x)).sort(sort);
|
.filter((x) => core.state.hiddenUsers.indexOf(x.name) === -1 && !x.isIgnored).sort(sort);
|
||||||
});
|
});
|
||||||
(<Modal>this.$children[0]).fixDropdowns();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterKink(filter: RegExp, kink: Kink): boolean {
|
filterKink(filter: RegExp, kink: Kink): boolean {
|
||||||
|
@ -144,7 +143,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
.character-search {
|
.character-search {
|
||||||
.dropdown {
|
.dropdown {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<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="well" 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">
|
||||||
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
||||||
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
||||||
<div class="card-block">
|
<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">
|
<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}}</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">
|
||||||
{{l(connecting ? 'login.connecting' : 'login.connect')}}
|
{{l(connecting ? 'login.connecting' : 'login.connect')}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -37,14 +37,44 @@
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import l from './localize';
|
import l from './localize';
|
||||||
|
|
||||||
|
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string, bbcodeHide?: boolean};
|
||||||
|
|
||||||
|
function copyNode(str: string, node: BBCodeNode, range: Range, flags: {endFound?: true, rootFound?: true}): string {
|
||||||
|
if(node.bbcodeTag !== undefined)
|
||||||
|
str = `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]${str}[/${node.bbcodeTag}]`;
|
||||||
|
if(node.nextSibling !== null && !flags.endFound) {
|
||||||
|
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\n';
|
||||||
|
str += scanNode(node.nextSibling!, range, flags);
|
||||||
|
}
|
||||||
|
if(node.parentElement === null) flags.rootFound = true;
|
||||||
|
if(flags.rootFound && flags.endFound) return str;
|
||||||
|
return copyNode(str, node.parentNode!, range, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanNode(node: BBCodeNode, range: Range, flags: {endFound?: true}): string {
|
||||||
|
if(node.bbcodeHide) return '';
|
||||||
|
if(node === range.endContainer) {
|
||||||
|
flags.endFound = true;
|
||||||
|
return node.nodeValue!.substr(0, range.endOffset);
|
||||||
|
}
|
||||||
|
let str = '';
|
||||||
|
if(node.bbcodeTag !== undefined) str += `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]`;
|
||||||
|
if(node instanceof Text) str += node.nodeValue;
|
||||||
|
if(node.firstChild !== null) str += scanNode(node.firstChild, range, flags);
|
||||||
|
if(node.bbcodeTag !== undefined) str += `[/${node.bbcodeTag}]`;
|
||||||
|
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\n';
|
||||||
|
if(node.nextSibling !== null && !flags.endFound) str += scanNode(node.nextSibling, range, flags);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {chat: ChatView, modal: Modal}
|
components: {chat: ChatView, modal: Modal}
|
||||||
})
|
})
|
||||||
export default class Chat extends Vue {
|
export default class Chat extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly ownCharacters: string[];
|
readonly ownCharacters!: string[];
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly defaultCharacter: string | undefined;
|
readonly defaultCharacter!: string | undefined;
|
||||||
selectedCharacter = this.defaultCharacter || this.ownCharacters[0]; //tslint:disable-line:strict-boolean-expressions
|
selectedCharacter = this.defaultCharacter || this.ownCharacters[0]; //tslint:disable-line:strict-boolean-expressions
|
||||||
error = '';
|
error = '';
|
||||||
connecting = false;
|
connecting = false;
|
||||||
|
@ -52,6 +82,14 @@
|
||||||
l = l;
|
l = l;
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
|
document.addEventListener('copy', ((e: ClipboardEvent) => {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
if(selection.isCollapsed) return;
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
e.clipboardData.setData('text/plain', copyNode(range.startContainer.nodeValue!.substr(range.startOffset),
|
||||||
|
range.startContainer, range, {}));
|
||||||
|
e.preventDefault();
|
||||||
|
}) as EventListener);
|
||||||
core.register('characters', Characters(core.connection));
|
core.register('characters', Characters(core.connection));
|
||||||
core.register('channels', Channels(core.connection, core.characters));
|
core.register('channels', Channels(core.connection, core.characters));
|
||||||
core.register('conversations', Conversations());
|
core.register('conversations', Conversations());
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
|
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
|
||||||
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;width:60px"/>
|
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;width:60px"/>
|
||||||
{{ownCharacter.name}}
|
{{ownCharacter.name}}
|
||||||
<a href="#" @click.prevent="logOut" class="btn"><span class="fa fa-sign-out"></span>{{l('chat.logout')}}</a><br/>
|
<a href="#" @click.prevent="logOut" class="btn"><i class="fa fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
|
||||||
<div>
|
<div>
|
||||||
{{l('chat.status')}}
|
{{l('chat.status')}}
|
||||||
<a href="#" @click.prevent="$refs['statusDialog'].show()" class="btn">
|
<a href="#" @click.prevent="$refs['statusDialog'].show()" class="btn">
|
||||||
|
@ -35,9 +35,10 @@
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<span>{{conversation.character.name}}</span>
|
<span>{{conversation.character.name}}</span>
|
||||||
<div style="text-align:right;line-height:0">
|
<div style="text-align:right;line-height:0">
|
||||||
<span class="fa"
|
<span class="fas"
|
||||||
:class="{'fa-commenting': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
:class="{'fa-comment-alt': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||||
></span><span class="pin fa fa-thumb-tack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
></span><span class="fa fa-reply" v-show="needsReply(conversation)"></span>
|
||||||
|
<span class="pin fa fa-thumbtack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
||||||
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||||
<span class="fa fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
<span class="fa fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +50,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"
|
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel"
|
||||||
:key="conversation.key"><span class="name">{{conversation.name}}</span><span><span class="pin fa fa-thumb-tack"
|
:key="conversation.key"><span class="name">{{conversation.name}}</span><span><span class="pin fa fa-thumbtack"
|
||||||
:class="{'active': conversation.isPinned}" @click.stop="conversation.isPinned = !conversation.isPinned"
|
:class="{'active': conversation.isPinned}" @click.stop="conversation.isPinned = !conversation.isPinned"
|
||||||
:aria-label="l('chat.pinTab')" @mousedown.prevent></span><span class="fa fa-times leave"
|
:aria-label="l('chat.pinTab')" @mousedown.prevent></span><span class="fa fa-times leave"
|
||||||
@click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span></span>
|
@click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span></span>
|
||||||
|
@ -66,7 +67,7 @@
|
||||||
<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)" class="list-group-item list-group-item-action" :key="conversation.key">
|
:class="getClasses(conversation)" class="list-group-item list-group-item-action" :key="conversation.key">
|
||||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||||
<span class="fa fa-user-circle-o conversation-icon" v-else></span>
|
<span class="far fa-user-circle conversation-icon" v-else></span>
|
||||||
<div class="name">{{conversation.character.name}}</div>
|
<div class="name">{{conversation.character.name}}</div>
|
||||||
</a>
|
</a>
|
||||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
import Sortable = require('sortablejs');
|
import Sortable = require('sortablejs');
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import {Keys} from '../keys';
|
||||||
import ChannelList from './ChannelList.vue';
|
import ChannelList from './ChannelList.vue';
|
||||||
import CharacterSearch from './CharacterSearch.vue';
|
import CharacterSearch from './CharacterSearch.vue';
|
||||||
import {characterImage, getKey} from './common';
|
import {characterImage, getKey} from './common';
|
||||||
|
@ -128,7 +130,7 @@
|
||||||
characterImage = characterImage;
|
characterImage = characterImage;
|
||||||
conversations = core.conversations;
|
conversations = core.conversations;
|
||||||
getStatusIcon = getStatusIcon;
|
getStatusIcon = getStatusIcon;
|
||||||
keydownListener: (e: KeyboardEvent) => void;
|
keydownListener!: (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);
|
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);
|
||||||
|
@ -190,12 +192,22 @@
|
||||||
window.removeEventListener('keydown', this.keydownListener);
|
window.removeEventListener('keydown', this.keydownListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
needsReply(conversation: Conversation): boolean {
|
||||||
|
if(!core.state.settings.showNeedsReply) return false;
|
||||||
|
for(let i = conversation.messages.length - 1; i >= 0; --i) {
|
||||||
|
const sender = conversation.messages[i].sender;
|
||||||
|
if(sender !== undefined)
|
||||||
|
return sender !== core.characters.ownCharacter;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent): void {
|
onKeyDown(e: KeyboardEvent): void {
|
||||||
const selected = this.conversations.selectedConversation;
|
const selected = this.conversations.selectedConversation;
|
||||||
const pms = this.conversations.privateConversations;
|
const pms = this.conversations.privateConversations;
|
||||||
const channels = this.conversations.channelConversations;
|
const channels = this.conversations.channelConversations;
|
||||||
const console = this.conversations.consoleTab;
|
const console = this.conversations.consoleTab;
|
||||||
if(getKey(e) === 'arrowup' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
if(getKey(e) === Keys.ArrowUp && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
||||||
if(selected === console) { //tslint:disable-line:curly
|
if(selected === console) { //tslint:disable-line:curly
|
||||||
if(channels.length > 0) channels[channels.length - 1].show();
|
if(channels.length > 0) channels[channels.length - 1].show();
|
||||||
else if(pms.length > 0) pms[pms.length - 1].show();
|
else if(pms.length > 0) pms[pms.length - 1].show();
|
||||||
|
@ -210,7 +222,7 @@
|
||||||
else console.show();
|
else console.show();
|
||||||
else channels[index - 1].show();
|
else channels[index - 1].show();
|
||||||
}
|
}
|
||||||
else if(getKey(e) === 'arrowdown' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
else if(getKey(e) === Keys.ArrowDown && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
||||||
if(selected === console) { //tslint:disable-line:curly - false positive
|
if(selected === console) { //tslint:disable-line:curly - false positive
|
||||||
if(pms.length > 0) pms[0].show();
|
if(pms.length > 0) pms[0].show();
|
||||||
else if(channels.length > 0) channels[0].show();
|
else if(channels.length > 0) channels[0].show();
|
||||||
|
@ -263,8 +275,18 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
@import "../less/flist_variables.less";
|
@import "~bootstrap/scss/functions";
|
||||||
|
@import "~bootstrap/scss/variables";
|
||||||
|
@import "~bootstrap/scss/mixins/breakpoints";
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bbcode, .message, .profile-viewer {
|
||||||
|
user-select: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.list-group.conversation-nav {
|
.list-group.conversation-nav {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -317,8 +339,10 @@
|
||||||
margin: 0 45px 5px;
|
margin: 0 45px 5px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: none;
|
display: none;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
@media (max-width: @screen-xs-max) {
|
@media (max-width: breakpoint-max(xs)) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +387,7 @@
|
||||||
.body a.btn {
|
.body a.btn {
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
@media (min-width: @screen-sm-min) {
|
@media (min-width: breakpoint-min(sm)) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -78,7 +78,8 @@
|
||||||
name: `/${key} - ${l(`commands.${key}`)}`,
|
name: `/${key} - ${l(`commands.${key}`)}`,
|
||||||
help: l(`commands.${key}.help`),
|
help: l(`commands.${key}.help`),
|
||||||
context,
|
context,
|
||||||
permission: command.permission !== undefined ? l(`commands.help.permission${Permission[command.permission]}`) : undefined,
|
permission: command.permission !== undefined ?
|
||||||
|
l(`commands.help.permission${Permission[command.permission]}`) : undefined,
|
||||||
params,
|
params,
|
||||||
syntax
|
syntax
|
||||||
});
|
});
|
||||||
|
@ -87,7 +88,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
#command-help {
|
#command-help {
|
||||||
h4 {
|
h4 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -50,14 +50,14 @@
|
||||||
})
|
})
|
||||||
export default class ConversationSettings extends CustomDialog {
|
export default class ConversationSettings extends CustomDialog {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly conversation: Conversation;
|
readonly conversation!: Conversation;
|
||||||
l = l;
|
l = l;
|
||||||
setting = Conversation.Setting;
|
setting = Conversation.Setting;
|
||||||
notify: Conversation.Setting;
|
notify!: Conversation.Setting;
|
||||||
highlight: Conversation.Setting;
|
highlight!: Conversation.Setting;
|
||||||
highlightWords: string;
|
highlightWords!: string;
|
||||||
joinMessages: Conversation.Setting;
|
joinMessages!: Conversation.Setting;
|
||||||
defaultHighlights: boolean;
|
defaultHighlights!: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
<span v-show="conversation.channel.id.substr(0, 4) !== 'adh-'" class="fa fa-star" :title="l('channel.official')"
|
<span v-show="conversation.channel.id.substr(0, 4) !== 'adh-'" class="fa fa-star" :title="l('channel.official')"
|
||||||
style="margin-right:5px;"></span>
|
style="margin-right:5px;vertical-align:sub"></span>
|
||||||
<h4 style="margin: 0; display:inline; vertical-align: middle;">{{conversation.name}}</h4>
|
<h5 style="margin:0;display:inline;vertical-align:middle">{{conversation.name}}</h5>
|
||||||
<a @click="descriptionExpanded = !descriptionExpanded" class="btn">
|
<a @click="descriptionExpanded = !descriptionExpanded" class="btn">
|
||||||
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
|
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
|
||||||
<span class="btn-text">{{l('channel.description')}}</span>
|
<span class="btn-text">{{l('channel.description')}}</span>
|
||||||
|
@ -37,8 +37,9 @@
|
||||||
<span class="btn-text">{{l('chat.report')}}</span></a>
|
<span class="btn-text">{{l('chat.report')}}</span></a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-pills mode-switcher">
|
<ul class="nav nav-pills mode-switcher">
|
||||||
<li v-for="mode in modes" :class="{active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'}">
|
<li v-for="mode in modes" class="nav-item">
|
||||||
<a href="#" @click.prevent="setMode(mode)">{{l('channel.mode.' + mode)}}</a>
|
<a :class="{active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'}"
|
||||||
|
class="nav-link" href="#" @click.prevent="setMode(mode)">{{l('channel.mode.' + mode)}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,9 +52,15 @@
|
||||||
<h4>{{l('chat.consoleTab')}}</h4>
|
<h4>{{l('chat.consoleTab')}}</h4>
|
||||||
<logs :conversation="conversation"></logs>
|
<logs :conversation="conversation"></logs>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="search" v-show="showSearch" style="position:relative">
|
||||||
|
<input v-model="searchInput" @keydown.esc="showSearch = false; searchInput = ''" @keypress="lastSearchInput = Date.now()"
|
||||||
|
:placeholder="l('chat.search')" ref="searchField" class="form-control"/>
|
||||||
|
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0"
|
||||||
|
@click="showSearch = false"><i class="fas fa-times"></i></a>
|
||||||
|
</div>
|
||||||
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
|
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
|
||||||
ref="messages" @scroll="onMessagesScroll">
|
ref="messages" @scroll="onMessagesScroll">
|
||||||
<template v-for="message in conversation.messages">
|
<template v-for="message in messages">
|
||||||
<message-view :message="message" :channel="conversation.channel" :key="message.id"
|
<message-view :message="message" :channel="conversation.channel" :key="message.id"
|
||||||
:classes="message == conversation.lastRead ? 'last-read' : ''">
|
:classes="message == conversation.lastRead ? 'last-read' : ''">
|
||||||
</message-view>
|
</message-view>
|
||||||
|
@ -80,20 +87,22 @@
|
||||||
<span class="redText" style="flex:1;margin-left:5px">{{conversation.errorText}}</span>
|
<span class="redText" style="flex:1;margin-left:5px">{{conversation.errorText}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="position:relative; margin-top:5px;">
|
<div style="position:relative; margin-top:5px;">
|
||||||
<div class="overlay-disable" v-show="adCountdown">{{adCountdown}}</div>
|
<bbcode-editor v-model="conversation.enteredText" @keydown="onKeyDown" :extras="extraButtons" @input="keepScroll"
|
||||||
<bbcode-editor v-model="conversation.enteredText" @keydown="onKeyDown" :extras="extraButtons" @input="onInput"
|
:classes="'form-control chat-text-box' + (conversation.isSendingAds ? ' ads-text-box' : '')"
|
||||||
:classes="'form-control chat-text-box' + (conversation.isSendingAds ? ' ads-text-box' : '')" :disabled="adCountdown"
|
|
||||||
ref="textBox" style="position:relative" :maxlength="conversation.maxMessageLength">
|
ref="textBox" style="position:relative" :maxlength="conversation.maxMessageLength">
|
||||||
<div style="float:right;text-align:right;display:flex;align-items:center">
|
<div style="float:right;text-align:right;display:flex;align-items:center">
|
||||||
<div v-show="conversation.maxMessageLength" style="margin-right: 5px;">
|
<div v-show="conversation.maxMessageLength" style="margin-right:5px">
|
||||||
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-pills send-ads-switcher" v-if="conversation.channel" style="position:relative;z-index:10">
|
<ul class="nav nav-pills send-ads-switcher" v-if="conversation.channel" style="position:relative;z-index:10">
|
||||||
<li :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}">
|
<li class="nav-item">
|
||||||
<a href="#" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
|
<a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||||
|
class="nav-link" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}">
|
<li class="nav-item">
|
||||||
<a href="#" @click.prevent="setSendingAds(true)">{{l('channel.mode.ads')}}</a>
|
<a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||||
|
class="nav-link" @click.prevent="setSendingAds(true)">
|
||||||
|
{{l('channel.mode.ads' + (adCountdown ? '.countdown' : ''), adCountdown)}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,6 +122,7 @@
|
||||||
import {Prop, Watch} from 'vue-property-decorator';
|
import {Prop, Watch} from 'vue-property-decorator';
|
||||||
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
|
import {Keys} from '../keys';
|
||||||
import {BBCodeView, Editor} from './bbcode';
|
import {BBCodeView, 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';
|
||||||
|
@ -135,16 +145,29 @@
|
||||||
})
|
})
|
||||||
export default class ConversationView extends Vue {
|
export default class ConversationView extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly reportDialog: ReportDialog;
|
readonly reportDialog!: ReportDialog;
|
||||||
modes = channelModes;
|
modes = channelModes;
|
||||||
descriptionExpanded = false;
|
descriptionExpanded = false;
|
||||||
l = l;
|
l = l;
|
||||||
extraButtons: EditorButton[] = [];
|
extraButtons: EditorButton[] = [];
|
||||||
getByteLength = getByteLength;
|
getByteLength = getByteLength;
|
||||||
tabOptions: string[] | undefined;
|
tabOptions: string[] | undefined;
|
||||||
tabOptionsIndex: number;
|
tabOptionsIndex!: number;
|
||||||
tabOptionSelection: EditorSelection;
|
tabOptionSelection!: EditorSelection;
|
||||||
|
showSearch = false;
|
||||||
|
searchInput = '';
|
||||||
|
search = '';
|
||||||
|
lastSearchInput = 0;
|
||||||
messageCount = 0;
|
messageCount = 0;
|
||||||
|
searchTimer = 0;
|
||||||
|
windowHeight = window.innerHeight;
|
||||||
|
resizeHandler = () => {
|
||||||
|
const messageView = <HTMLElement>this.$refs['messages'];
|
||||||
|
if(this.windowHeight - window.innerHeight + messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15)
|
||||||
|
messageView.scrollTop = messageView.scrollHeight - messageView.offsetHeight;
|
||||||
|
this.windowHeight = window.innerHeight;
|
||||||
|
}
|
||||||
|
keydownHandler!: EventListener;
|
||||||
|
|
||||||
created(): void {
|
created(): void {
|
||||||
this.extraButtons = [{
|
this.extraButtons = [{
|
||||||
|
@ -153,12 +176,34 @@
|
||||||
icon: 'fa-question',
|
icon: 'fa-question',
|
||||||
handler: () => (<Modal>this.$refs['helpDialog']).show()
|
handler: () => (<Modal>this.$refs['helpDialog']).show()
|
||||||
}];
|
}];
|
||||||
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
|
window.addEventListener('keydown', this.keydownHandler = ((e: KeyboardEvent) => {
|
||||||
|
if(getKey(e) === Keys.KeyF && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
|
||||||
|
this.showSearch = true;
|
||||||
|
this.$nextTick(() => (<HTMLElement>this.$refs['searchField']).focus());
|
||||||
|
}
|
||||||
|
}) as EventListener);
|
||||||
|
this.searchTimer = window.setInterval(() => {
|
||||||
|
if(Date.now() - this.lastSearchInput > 500 && this.search !== this.searchInput)
|
||||||
|
this.search = this.searchInput;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyed(): void {
|
||||||
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
window.removeEventListener('keydown', this.keydownHandler);
|
||||||
|
clearInterval(this.searchTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
get conversation(): Conversation {
|
get conversation(): Conversation {
|
||||||
return core.conversations.selectedConversation;
|
return core.conversations.selectedConversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get messages(): ReadonlyArray<Conversation.Message> {
|
||||||
|
return this.search !== '' ? this.conversation.messages.filter((x) => x.text.indexOf(this.search) !== -1)
|
||||||
|
: this.conversation.messages;
|
||||||
|
}
|
||||||
|
|
||||||
@Watch('conversation')
|
@Watch('conversation')
|
||||||
conversationChanged(): void {
|
conversationChanged(): void {
|
||||||
(<Editor>this.$refs['textBox']).focus();
|
(<Editor>this.$refs['textBox']).focus();
|
||||||
|
@ -168,14 +213,14 @@
|
||||||
messageAdded(newValue: Conversation.Message[]): void {
|
messageAdded(newValue: Conversation.Message[]): void {
|
||||||
const messageView = <HTMLElement>this.$refs['messages'];
|
const messageView = <HTMLElement>this.$refs['messages'];
|
||||||
if(!this.keepScroll() && newValue.length === this.messageCount)
|
if(!this.keepScroll() && newValue.length === this.messageCount)
|
||||||
this.$nextTick(() => messageView.scrollTop -= (<HTMLElement>messageView.lastElementChild).clientHeight);
|
messageView.scrollTop -= (<HTMLElement>messageView.firstElementChild).clientHeight;
|
||||||
this.messageCount = newValue.length;
|
this.messageCount = newValue.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
keepScroll(): boolean {
|
keepScroll(): boolean {
|
||||||
const messageView = <HTMLElement>this.$refs['messages'];
|
const messageView = <HTMLElement>this.$refs['messages'];
|
||||||
if(messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15) {
|
if(messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15) {
|
||||||
setTimeout(() => messageView.scrollTop = messageView.scrollHeight - messageView.offsetHeight, 0);
|
setImmediate(() => messageView.scrollTop = messageView.scrollHeight);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -197,18 +242,9 @@
|
||||||
if(oldValue === 'clear') this.keepScroll();
|
if(oldValue === 'clear') this.keepScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
onInput(): void {
|
|
||||||
const messageView = <HTMLElement>this.$refs['messages'];
|
|
||||||
const oldHeight = messageView.offsetHeight;
|
|
||||||
if(messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15)
|
|
||||||
setTimeout(() => {
|
|
||||||
if(oldHeight > messageView.offsetHeight) messageView.scrollTop += oldHeight - messageView.offsetHeight;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onKeyDown(e: KeyboardEvent): Promise<void> {
|
async onKeyDown(e: KeyboardEvent): Promise<void> {
|
||||||
const editor = <Editor>this.$refs['textBox'];
|
const editor = <Editor>this.$refs['textBox'];
|
||||||
if(getKey(e) === 'tab') {
|
if(getKey(e) === Keys.Tab) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if(this.conversation.enteredText.length === 0 || this.isConsoleTab) return;
|
if(this.conversation.enteredText.length === 0 || this.isConsoleTab) return;
|
||||||
if(this.tabOptions === undefined) {
|
if(this.tabOptions === undefined) {
|
||||||
|
@ -242,10 +278,10 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(this.tabOptions !== undefined) this.tabOptions = undefined;
|
if(this.tabOptions !== undefined) this.tabOptions = undefined;
|
||||||
if(getKey(e) === 'arrowup' && this.conversation.enteredText.length === 0
|
if(getKey(e) === Keys.ArrowUp && this.conversation.enteredText.length === 0
|
||||||
&& !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey)
|
&& !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey)
|
||||||
this.conversation.loadLastSent();
|
this.conversation.loadLastSent();
|
||||||
else if(getKey(e) === 'enter') {
|
else if(getKey(e) === Keys.Enter) {
|
||||||
if(e.shiftKey) return;
|
if(e.shiftKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await this.conversation.send();
|
await this.conversation.send();
|
||||||
|
@ -270,14 +306,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get showAdCountdown(): boolean {
|
|
||||||
return Conversation.isChannel(this.conversation) && this.conversation.adCountdown > 0 && this.conversation.isSendingAds;
|
|
||||||
}
|
|
||||||
|
|
||||||
get adCountdown(): string | undefined {
|
get adCountdown(): string | undefined {
|
||||||
if(!this.showAdCountdown) return;
|
if(!Conversation.isChannel(this.conversation) || this.conversation.adCountdown <= 0) return;
|
||||||
const conv = (<Conversation.ChannelConversation>this.conversation);
|
return l('chat.adCountdown',
|
||||||
return l('chat.adCountdown', Math.floor(conv.adCountdown / 60).toString(), (conv.adCountdown % 60).toString());
|
Math.floor(this.conversation.adCountdown / 60).toString(), (this.conversation.adCountdown % 60).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
get characterImage(): string {
|
get characterImage(): string {
|
||||||
|
@ -301,11 +333,14 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
@import "../less/flist_variables.less";
|
@import "~bootstrap/scss/functions";
|
||||||
|
@import "~bootstrap/scss/variables";
|
||||||
|
@import "~bootstrap/scss/mixins/breakpoints";
|
||||||
|
|
||||||
#conversation {
|
#conversation {
|
||||||
.header {
|
.header {
|
||||||
@media (min-width: @screen-sm-min) {
|
@media (min-width: breakpoint-min(sm)) {
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
}
|
}
|
||||||
a.btn {
|
a.btn {
|
||||||
|
@ -317,7 +352,7 @@
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: @screen-xs-max) {
|
@media (max-width: breakpoint-max(xs)) {
|
||||||
.mode-switcher a {
|
.mode-switcher a {
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<a href="#" @click.prevent="showLogs" class="btn">
|
<a href="#" @click.prevent="showLogs" class="btn">
|
||||||
<span class="fa" :class="isPersistent ? 'fa-file-text-o' : 'fa-download'"></span>
|
<span :class="isPersistent ? 'fa fa-file-alt' : 'fa fa-download'"></span>
|
||||||
<span class="btn-text">{{l('logs.title')}}</span>
|
<span class="btn-text">{{l('logs.title')}}</span>
|
||||||
</a>
|
</a>
|
||||||
<modal v-if="isPersistent" :buttons="false" ref="dialog" id="logs-dialog" :action="l('logs.title')" dialogClass="modal-lg"
|
<modal v-if="isPersistent" :buttons="false" ref="dialog" id="logs-dialog" :action="l('logs.title')"
|
||||||
@open="onOpen" class="form-horizontal">
|
dialogClass="modal-lg w-100 modal-dialog-centered" @open="onOpen">
|
||||||
<div class="form-group">
|
<div class="form-group row" style="flex-shrink:0">
|
||||||
<label class="col-sm-2">{{l('logs.conversation')}}</label>
|
<label class="col-2 col-form-label">{{l('logs.conversation')}}</label>
|
||||||
<div class="col-sm-10">
|
<filterable-select v-model="selectedConversation" :options="conversations" :filterFunc="filterConversation"
|
||||||
<filterable-select v-model="selectedConversation" :options="conversations" :filterFunc="filterConversation"
|
:placeholder="l('filter')" @input="loadMessages" class="form-control col-10">
|
||||||
buttonClass="form-control" :placeholder="l('filter')" @input="loadMessages">
|
<template slot-scope="s">{{s.option && ((s.option.id[0] == '#' ? '#' : '') + s.option.name)}}</template>
|
||||||
<template slot-scope="s">{{s.option && ((s.option.id[0] == '#' ? '#' : '') + s.option.name)}}</template>
|
</filterable-select>
|
||||||
</filterable-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group row" style="flex-shrink:0">
|
||||||
<label for="date" class="col-sm-2">{{l('logs.date')}}</label>
|
<label for="date" class="col-2 col-form-label">{{l('logs.date')}}</label>
|
||||||
<div class="col-sm-10" style="display:flex">
|
<div class="col-8">
|
||||||
<select class="form-control" v-model="selectedDate" id="date" @change="loadMessages">
|
<select class="form-control" v-model="selectedDate" id="date" @change="loadMessages">
|
||||||
|
<option>{{l('logs.selectDate')}}</option>
|
||||||
<option v-for="date in dates" :value="date.getTime()">{{formatDate(date)}}</option>
|
<option v-for="date in dates" :value="date.getTime()">{{formatDate(date)}}</option>
|
||||||
</select>
|
</select>
|
||||||
<button @click="downloadDay" class="btn btn-default" :disabled="!selectedDate"><span class="fa fa-download"></span></button>
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<button @click="downloadDay" class="btn btn-secondary form-control" :disabled="!selectedDate"><span
|
||||||
|
class="fa fa-download"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages-both" style="overflow: auto">
|
<div class="messages-both" style="overflow: auto">
|
||||||
<message-view v-for="message in filteredMessages" :message="message" :key="message.id"></message-view>
|
<message-view v-for="message in filteredMessages" :message="message" :key="message.id"></message-view>
|
||||||
</div>
|
</div>
|
||||||
<input class="form-control" v-model="filter" :placeholder="l('filter')" v-show="messages"/>
|
<input class="form-control" v-model="filter" :placeholder="l('filter')" v-show="messages" type="text"/>
|
||||||
</modal>
|
</modal>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -59,7 +61,7 @@
|
||||||
export default class Logs extends Vue {
|
export default class Logs extends Vue {
|
||||||
//tslint:disable:no-null-keyword
|
//tslint:disable:no-null-keyword
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly conversation: Conversation;
|
readonly conversation!: Conversation;
|
||||||
selectedConversation: {id: string, name: string} | null = null;
|
selectedConversation: {id: string, name: string} | null = null;
|
||||||
selectedDate: string | null = null;
|
selectedDate: string | null = null;
|
||||||
isPersistent = LogInterfaces.isPersistent(core.logs);
|
isPersistent = LogInterfaces.isPersistent(core.logs);
|
||||||
|
@ -77,7 +79,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
(<Modal>this.$refs['dialog']).fixDropdowns();
|
|
||||||
this.conversationChanged();
|
this.conversationChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<a href="#" @click.prevent="openDialog" class="btn">
|
<a href="#" @click.prevent="openDialog" class="btn">
|
||||||
<span class="fa fa-edit"></span> <span class="btn-text">{{l('manageChannel.open')}}</span>
|
<span class="fa fa-edit"></span> <span class="btn-text">{{l('manageChannel.open')}}</span>
|
||||||
</a>
|
</a>
|
||||||
<modal ref="dialog" :action="l('manageChannel.action', channel.name)" :buttonText="l('manageChannel.submit')" @submit="submit">
|
<modal ref="dialog" :action="l('manageChannel.action', channel.name)" :buttonText="l('manageChannel.submit')" @submit="submit"
|
||||||
|
dialogClass="w-100 modal-lg">
|
||||||
<div class="form-group" v-show="channel.id.substr(0, 4) === 'adh-'">
|
<div class="form-group" v-show="channel.id.substr(0, 4) === 'adh-'">
|
||||||
<label class="control-label" for="isPublic">
|
<label class="control-label" for="isPublic">
|
||||||
<input type="checkbox" id="isPublic" v-model="isPublic"/>
|
<input type="checkbox" id="isPublic" v-model="isPublic"/>
|
||||||
|
@ -27,13 +28,14 @@
|
||||||
<div v-if="isChannelOwner">
|
<div v-if="isChannelOwner">
|
||||||
<h4>{{l('manageChannel.mods')}}</h4>
|
<h4>{{l('manageChannel.mods')}}</h4>
|
||||||
<div v-for="(mod, index) in opList">
|
<div v-for="(mod, index) in opList">
|
||||||
<a href="#" @click.prevent="opList.splice(index, 1)" class="btn fa fa-times"
|
<a href="#" @click.prevent="opList.splice(index, 1)" class="btn" style="padding:0;vertical-align:baseline">
|
||||||
style="padding:0;vertical-align:baseline"></a>
|
<i class="fas fa-times"></i>
|
||||||
|
</a>
|
||||||
{{mod}}
|
{{mod}}
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;margin-top:5px">
|
<div style="display:flex;margin-top:5px">
|
||||||
<input :placeholder="l('manageChannel.modAddName')" v-model="modAddName" class="form-control"/>
|
<input :placeholder="l('manageChannel.modAddName')" v-model="modAddName" class="form-control"/>
|
||||||
<button class="btn btn-default" @click="modAdd" :disabled="!modAddName">{{l('manageChannel.modAdd')}}</button>
|
<button class="btn btn-secondary" @click="modAdd" :disabled="!modAddName">{{l('manageChannel.modAdd')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -56,7 +58,7 @@
|
||||||
})
|
})
|
||||||
export default class ManageChannel extends Vue {
|
export default class ManageChannel extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly channel: Channel;
|
readonly channel!: Channel;
|
||||||
modes = channelModes;
|
modes = channelModes;
|
||||||
isPublic = this.channelIsPublic;
|
isPublic = this.channelIsPublic;
|
||||||
mode = this.channel.mode;
|
mode = this.channel.mode;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :buttons="false" :action="l('chat.recentConversations')">
|
<modal :buttons="false" :action="l('chat.recentConversations')" dialogClass="w-100">
|
||||||
<div style="display:flex; flex-direction:column; max-height:500px; flex-wrap:wrap;">
|
<div style="display:flex; flex-direction:column; max-height:500px; flex-wrap:wrap;">
|
||||||
<div v-for="recent in recentConversations" style="margin: 3px;">
|
<div v-for="recent in recentConversations" style="margin: 3px;">
|
||||||
<user-view v-if="recent.character" :character="getCharacter(recent.character)"></user-view>
|
<user-view v-if="recent.character" :character="getCharacter(recent.character)"></user-view>
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :action="l('settings.action')" @submit="submit" @close="init()" id="settings">
|
<modal :action="l('settings.action')" @submit="submit" @close="init()" id="settings" dialogClass="w-100">
|
||||||
<ul class="nav nav-tabs" style="flex-shrink:0;margin-bottom:10px">
|
<tabs style="flex-shrink:0;margin-bottom:10px" :tabs="tabs" v-model="selectedTab"></tabs>
|
||||||
<li role="presentation" v-for="tab in tabs" :class="{active: tab == selectedTab}">
|
|
||||||
<a href="#" @click.prevent="selectedTab = tab">{{l('settings.tabs.' + tab)}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div v-show="selectedTab == 'general'">
|
<div v-show="selectedTab == 'general'">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
|
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
|
||||||
|
@ -96,13 +92,19 @@
|
||||||
{{l('settings.alwaysNotify')}}
|
{{l('settings.alwaysNotify')}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="showNeedsReply">
|
||||||
|
<input type="checkbox" id="showNeedsReply" v-model="showNeedsReply"/>
|
||||||
|
{{l('settings.showNeedsReply')}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="selectedTab == 'import'" style="display:flex;padding-top:10px">
|
<div v-show="selectedTab == 'import'" style="display:flex;padding-top:10px">
|
||||||
<select id="import" class="form-control" v-model="importCharacter" style="flex:1;">
|
<select id="import" class="form-control" v-model="importCharacter" style="flex:1;">
|
||||||
<option value="">{{l('settings.import.selectCharacter')}}</option>
|
<option value="">{{l('settings.import.selectCharacter')}}</option>
|
||||||
<option v-for="character in availableImports" :value="character">{{character}}</option>
|
<option v-for="character in availableImports" :value="character">{{character}}</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-default" @click="doImport" :disabled="!importCharacter">{{l('settings.import')}}</button>
|
<button class="btn btn-secondary" @click="doImport" :disabled="!importCharacter">{{l('settings.import')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -111,34 +113,36 @@
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
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 Tabs from '../components/tabs';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Settings as SettingsInterface} from './interfaces';
|
import {Settings as SettingsInterface} from './interfaces';
|
||||||
import l from './localize';
|
import l from './localize';
|
||||||
|
|
||||||
@Component(
|
@Component(
|
||||||
{components: {modal: Modal}}
|
{components: {modal: Modal, tabs: Tabs}}
|
||||||
)
|
)
|
||||||
export default class SettingsView extends CustomDialog {
|
export default class SettingsView extends CustomDialog {
|
||||||
l = l;
|
l = l;
|
||||||
availableImports: ReadonlyArray<string> = [];
|
availableImports: ReadonlyArray<string> = [];
|
||||||
selectedTab = 'general';
|
selectedTab = 'general';
|
||||||
importCharacter = '';
|
importCharacter = '';
|
||||||
playSound: boolean;
|
playSound!: boolean;
|
||||||
clickOpensMessage: boolean;
|
clickOpensMessage!: boolean;
|
||||||
disallowedTags: string;
|
disallowedTags!: string;
|
||||||
notifications: boolean;
|
notifications!: boolean;
|
||||||
highlight: boolean;
|
highlight!: boolean;
|
||||||
highlightWords: string;
|
highlightWords!: string;
|
||||||
showAvatars: boolean;
|
showAvatars!: boolean;
|
||||||
animatedEicons: boolean;
|
animatedEicons!: boolean;
|
||||||
idleTimer: string;
|
idleTimer!: string;
|
||||||
messageSeparators: boolean;
|
messageSeparators!: boolean;
|
||||||
eventMessages: boolean;
|
eventMessages!: boolean;
|
||||||
joinMessages: boolean;
|
joinMessages!: boolean;
|
||||||
alwaysNotify: boolean;
|
alwaysNotify!: boolean;
|
||||||
logMessages: boolean;
|
logMessages!: boolean;
|
||||||
logAds: boolean;
|
logAds!: boolean;
|
||||||
fontSize: number;
|
fontSize!: number;
|
||||||
|
showNeedsReply!: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -168,6 +172,7 @@
|
||||||
this.logMessages = settings.logMessages;
|
this.logMessages = settings.logMessages;
|
||||||
this.logAds = settings.logAds;
|
this.logAds = settings.logAds;
|
||||||
this.fontSize = settings.fontSize;
|
this.fontSize = settings.fontSize;
|
||||||
|
this.showNeedsReply = settings.showNeedsReply;
|
||||||
};
|
};
|
||||||
|
|
||||||
async doImport(): Promise<void> {
|
async doImport(): Promise<void> {
|
||||||
|
@ -178,14 +183,18 @@
|
||||||
};
|
};
|
||||||
await importKey('settings');
|
await importKey('settings');
|
||||||
await importKey('pinned');
|
await importKey('pinned');
|
||||||
|
await importKey('modes');
|
||||||
await importKey('conversationSettings');
|
await importKey('conversationSettings');
|
||||||
this.init();
|
this.init();
|
||||||
core.reloadSettings();
|
core.reloadSettings();
|
||||||
core.conversations.reloadSettings();
|
core.conversations.reloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
get tabs(): ReadonlyArray<string> {
|
get tabs(): {readonly [key: string]: string} {
|
||||||
return this.availableImports.length > 0 ? ['general', 'notifications', 'import'] : ['general', 'notifications'];
|
const tabs: {[key: string]: string} = {};
|
||||||
|
(this.availableImports.length > 0 ? ['general', 'notifications', 'import'] : ['general', 'notifications'])
|
||||||
|
.forEach((item) => tabs[item] = l(`settings.tabs.${item}`));
|
||||||
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(): Promise<void> {
|
async submit(): Promise<void> {
|
||||||
|
@ -205,7 +214,8 @@
|
||||||
alwaysNotify: this.alwaysNotify,
|
alwaysNotify: this.alwaysNotify,
|
||||||
logMessages: this.logMessages,
|
logMessages: this.logMessages,
|
||||||
logAds: this.logAds,
|
logAds: this.logAds,
|
||||||
fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize
|
fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize,
|
||||||
|
showNeedsReply: this.showNeedsReply
|
||||||
};
|
};
|
||||||
if(this.notifications) await core.notifications.requestPermission();
|
if(this.notifications) await core.notifications.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-wrapper" :class="{open: expanded}">
|
<div class="sidebar-wrapper" :class="{open: expanded}">
|
||||||
<div :class="'sidebar sidebar-' + (right ? 'right' : 'left')">
|
<div :class="'sidebar sidebar-' + (right ? 'right' : 'left')">
|
||||||
<button @click="expanded = !expanded" class="btn btn-default btn-xs expander" :aria-label="label">
|
<button @click="expanded = !expanded" class="btn btn-secondary btn-xs expander" :aria-label="label">
|
||||||
<span :class="'fa fa-rotate-270 ' + icon" style="vertical-align: middle" v-if="right"></span>
|
<span :class="'fa fa-fw fa-rotate-270 ' + icon" v-if="right"></span>
|
||||||
<span class="fa" :class="{'fa-chevron-down': !expanded, 'fa-chevron-up': expanded}"></span>
|
<span class="fa" :class="{'fa-chevron-down': !expanded, 'fa-chevron-up': expanded}"></span>
|
||||||
<span :class="'fa fa-rotate-90 ' + icon" style="vertical-align: middle" v-if="!right"></span>
|
<span :class="'fa fa-fw fa-rotate-90 ' + icon" v-if="!right"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
@Prop()
|
@Prop()
|
||||||
readonly label?: string;
|
readonly label?: string;
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly icon: string;
|
readonly icon!: string;
|
||||||
@Prop({default: false})
|
@Prop({default: false})
|
||||||
readonly open: boolean;
|
readonly open!: boolean;
|
||||||
expanded = this.open;
|
expanded = this.open;
|
||||||
|
|
||||||
@Watch('open')
|
@Watch('open')
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :action="l('chat.setStatus')" @submit="setStatus" @close="reset">
|
<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>
|
||||||
<div class="dropdown form-control" style="padding: 0;">
|
<dropdown class="dropdown form-control" style="padding:0">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
<span slot="title"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
||||||
aria-expanded="false" style="width:100%; text-align:left; display:flex; align-items:center">
|
<a href="#" class="dropdown-item" v-for="item in statuses" @click.prevent="status = item">
|
||||||
<span style="flex: 1;"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
||||||
<span class="caret"></span>
|
</a>
|
||||||
</button>
|
</dropdown>
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
|
||||||
<li><a href="#" v-for="item in statuses" @click.prevent="status = item">
|
|
||||||
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label">{{l('chat.setStatus.message')}}</label>
|
<label class="control-label">{{l('chat.setStatus.message')}}</label>
|
||||||
|
@ -29,6 +23,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import CustomDialog from '../components/custom_dialog';
|
import CustomDialog from '../components/custom_dialog';
|
||||||
|
import Dropdown from '../components/Dropdown.vue';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import {Editor} from './bbcode';
|
import {Editor} from './bbcode';
|
||||||
import {getByteLength} from './common';
|
import {getByteLength} from './common';
|
||||||
|
@ -38,7 +33,7 @@
|
||||||
import {getStatusIcon} from './user_view';
|
import {getStatusIcon} from './user_view';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {modal: Modal, editor: Editor}
|
components: {modal: Modal, editor: Editor, dropdown: Dropdown}
|
||||||
})
|
})
|
||||||
export default class StatusSwitcher extends CustomDialog {
|
export default class StatusSwitcher extends CustomDialog {
|
||||||
//tslint:disable:no-null-keyword
|
//tslint:disable:no-null-keyword
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<sidebar id="user-list" :label="l('users.title')" icon="fa-users" :right="true" :open="expanded">
|
<sidebar id="user-list" :label="l('users.title')" icon="fa-users" :right="true" :open="expanded">
|
||||||
<ul class="nav nav-tabs" style="flex-shrink:0">
|
<tabs style="flex-shrink:0" :tabs="channel ? [l('users.friends'), l('users.members')] : [l('users.friends')]" v-model="tab"></tabs>
|
||||||
<li role="presentation" :class="{active: !channel || !memberTabShown}">
|
<div class="users" style="padding-left:10px" v-show="tab == 0">
|
||||||
<a href="#" @click.prevent="memberTabShown = false">{{l('users.friends')}}</a>
|
|
||||||
</li>
|
|
||||||
<li role="presentation" :class="{active: memberTabShown}" v-show="channel">
|
|
||||||
<a href="#" @click.prevent="memberTabShown = true">{{l('users.members')}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div v-show="!channel || !memberTabShown" class="users" style="padding-left:10px">
|
|
||||||
<h4>{{l('users.friends')}}</h4>
|
<h4>{{l('users.friends')}}</h4>
|
||||||
<div v-for="character in friends" :key="character.name">
|
<div v-for="character in friends" :key="character.name">
|
||||||
<user :character="character" :showStatus="true"></user>
|
<user :character="character" :showStatus="true"></user>
|
||||||
|
@ -18,7 +11,7 @@
|
||||||
<user :character="character" :showStatus="true"></user>
|
<user :character="character" :showStatus="true"></user>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="channel" v-show="memberTabShown" class="users" style="padding:5px">
|
<div v-if="channel" class="users" style="padding:5px" v-show="tab == 1">
|
||||||
<h4>{{l('users.memberCount', channel.sortedMembers.length)}}</h4>
|
<h4>{{l('users.memberCount', channel.sortedMembers.length)}}</h4>
|
||||||
<div v-for="member in channel.sortedMembers" :key="member.character.name">
|
<div v-for="member in channel.sortedMembers" :key="member.character.name">
|
||||||
<user :character="member.character" :channel="channel" :showStatus="true"></user>
|
<user :character="member.character" :channel="channel" :showStatus="true"></user>
|
||||||
|
@ -30,6 +23,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import Tabs from '../components/tabs';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import {Channel, Character, Conversation} from './interfaces';
|
import {Channel, Character, Conversation} from './interfaces';
|
||||||
import l from './localize';
|
import l from './localize';
|
||||||
|
@ -37,11 +31,11 @@
|
||||||
import UserView from './user_view';
|
import UserView from './user_view';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {user: UserView, sidebar: Sidebar}
|
components: {user: UserView, sidebar: Sidebar, tabs: Tabs}
|
||||||
})
|
})
|
||||||
export default class UserList extends Vue {
|
export default class UserList extends Vue {
|
||||||
memberTabShown = false;
|
tab = '0';
|
||||||
expanded = window.innerWidth >= 900;
|
expanded = window.innerWidth >= 992;
|
||||||
l = l;
|
l = l;
|
||||||
sorter = (x: Character, y: Character) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0));
|
sorter = (x: Character, y: Character) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0));
|
||||||
|
|
||||||
|
@ -59,8 +53,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
@import "../less/flist_variables.less";
|
@import "~bootstrap/scss/functions";
|
||||||
|
@import "~bootstrap/scss/variables";
|
||||||
|
@import "~bootstrap/scss/mixins/breakpoints";
|
||||||
|
|
||||||
#user-list {
|
#user-list {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
h4 {
|
h4 {
|
||||||
|
@ -77,7 +74,7 @@
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: @screen-md-min) {
|
@media (min-width: breakpoint-min(md)) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -1,46 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="userMenu" class="dropdown-menu" v-show="showContextMenu" :style="position"
|
<div id="userMenu" class="list-group" v-show="showContextMenu" :style="position" v-if="character"
|
||||||
style="position:fixed;padding:10px 10px 5px;display:block;width:200px;z-index:1100" ref="menu">
|
style="position:fixed;padding:10px 10px 5px;display:block;width:220px;z-index:1100" ref="menu">
|
||||||
<div v-if="character">
|
<div style="min-height: 65px;padding:5px" class="list-group-item" @click.stop>
|
||||||
<div style="min-height: 65px;" @click.stop>
|
<img :src="characterImage" style="width:60px;height:60px;margin-right:5px;float:left" v-if="showAvatars"/>
|
||||||
<img :src="characterImage" style="width: 60px; height:60px; margin-right: 5px; float: left;" v-if="showAvatars"/>
|
<h5 style="margin:0;line-height:1">{{character.name}}</h5>
|
||||||
<h4 style="margin:0;">{{character.name}}</h4>
|
{{l('status.' + character.status)}}
|
||||||
{{l('status.' + character.status)}}
|
|
||||||
</div>
|
|
||||||
<bbcode :text="character.statusText" @click.stop></bbcode>
|
|
||||||
<ul class="dropdown-menu border-top" role="menu"
|
|
||||||
style="display:block; position:static; border-width:1px 0 0 0; box-shadow:none; padding:0; width:100%; border-radius:0;">
|
|
||||||
<li><a tabindex="-1" :href="profileLink" target="_blank" v-if="showProfileFirst">
|
|
||||||
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="openConversation(true)">
|
|
||||||
<span class="fa fa-fw fa-comment"></span>{{l('user.messageJump')}}</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="openConversation(false)">
|
|
||||||
<span class="fa fa-fw fa-plus"></span>{{l('user.message')}}</a></li>
|
|
||||||
<li><a tabindex="-1" :href="profileLink" target="_blank" v-if="!showProfileFirst">
|
|
||||||
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="showMemo">
|
|
||||||
<span class="fa fa-fw fa-sticky-note-o"></span>{{l('user.memo')}}</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="setBookmarked">
|
|
||||||
<span class="fa fa-fw fa-bookmark-o"></span>{{l('user.' + (character.isBookmarked ? 'unbookmark' : 'bookmark'))}}
|
|
||||||
</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="setIgnored">
|
|
||||||
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}
|
|
||||||
</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="setHidden">
|
|
||||||
<span class="fa fa-fw fa-eye-slash"></span>{{l('user.' + (isHidden ? 'unhide' : 'hide'))}}
|
|
||||||
</a></li>
|
|
||||||
<li><a tabindex="-1" href="#" @click.prevent="report">
|
|
||||||
<span class="fa fa-fw fa-exclamation-triangle"></span>{{l('user.report')}}</a></li>
|
|
||||||
<li v-show="isChannelMod"><a tabindex="-1" href="#" @click.prevent="channelKick">
|
|
||||||
<span class="fa fa-fw fa-ban"></span>{{l('user.channelKick')}}</a></li>
|
|
||||||
<li v-show="isChatOp"><a tabindex="-1" href="#" @click.prevent="chatKick" style="color:#f00">
|
|
||||||
<span class="fa fa-fw fa-trash-o"></span>{{l('user.chatKick')}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<bbcode :text="character.statusText" v-show="character.statusText" class="list-group-item" @click.stop></bbcode>
|
||||||
|
<a tabindex="-1" :href="profileLink" target="_blank" v-if="showProfileFirst" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="openConversation(true)" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-comment"></span>{{l('user.messageJump')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="openConversation(false)" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-plus"></span>{{l('user.message')}}</a>
|
||||||
|
<a tabindex="-1" :href="profileLink" target="_blank" v-if="!showProfileFirst" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="showMemo" class="list-group-item list-group-item-action">
|
||||||
|
<span class="far fa-fw fa-sticky-note"></span>{{l('user.memo')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="setBookmarked" class="list-group-item list-group-item-action">
|
||||||
|
<span class="far fa-fw fa-bookmark"></span>{{l('user.' + (character.isBookmarked ? 'unbookmark' : 'bookmark'))}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="setIgnored" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="setHidden" class="list-group-item list-group-item-action" v-show="!isChatOp">
|
||||||
|
<span class="fa fa-fw fa-eye-slash"></span>{{l('user.' + (isHidden ? 'unhide' : 'hide'))}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="report" class="list-group-item list-group-item-action">
|
||||||
|
<span class="fa fa-fw fa-exclamation-triangle"></span>{{l('user.report')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="channelKick" class="list-group-item list-group-item-action" v-show="isChannelMod">
|
||||||
|
<span class="fa fa-fw fa-ban"></span>{{l('user.channelKick')}}</a>
|
||||||
|
<a tabindex="-1" href="#" @click.prevent="chatKick" style="color:#f00" class="list-group-item list-group-item-action"
|
||||||
|
v-show="isChatOp"><span class="far fa-fw fa-trash"></span>{{l('user.chatKick')}}</a>
|
||||||
</div>
|
</div>
|
||||||
<modal :action="l('user.memo.action')" ref="memo" :disabled="memoLoading" @submit="updateMemo">
|
<modal :action="l('user.memo.action')" ref="memo" :disabled="memoLoading" @submit="updateMemo" dialogClass="w-100">
|
||||||
<div style="float:right;text-align:right;">{{getByteLength(memo)}} / 1000</div>
|
<div style="float:right;text-align:right;">{{getByteLength(memo)}} / 1000</div>
|
||||||
<textarea class="form-control" v-model="memo" :disabled="memoLoading" maxlength="1000"></textarea>
|
<textarea class="form-control" v-model="memo" :disabled="memoLoading" maxlength="1000"></textarea>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -65,17 +56,17 @@
|
||||||
export default class UserMenu extends Vue {
|
export default class UserMenu extends Vue {
|
||||||
//tslint:disable:no-null-keyword
|
//tslint:disable:no-null-keyword
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly reportDialog: ReportDialog;
|
readonly reportDialog!: ReportDialog;
|
||||||
l = l;
|
l = l;
|
||||||
showContextMenu = false;
|
showContextMenu = false;
|
||||||
getByteLength = getByteLength;
|
getByteLength = getByteLength;
|
||||||
character: Character | null = null;
|
character: Character | null = null;
|
||||||
position = {left: '', top: ''};
|
position = {left: '', top: ''};
|
||||||
characterImage: string | null = null;
|
characterImage: string | null = null;
|
||||||
touchTimer: number | undefined;
|
touchedElement: HTMLElement | undefined;
|
||||||
channel: Channel | null = null;
|
channel: Channel | null = null;
|
||||||
memo = '';
|
memo = '';
|
||||||
memoId: number;
|
memoId = 0;
|
||||||
memoLoading = false;
|
memoLoading = false;
|
||||||
|
|
||||||
openConversation(jump: boolean): void {
|
openConversation(jump: boolean): void {
|
||||||
|
@ -159,7 +150,7 @@
|
||||||
|
|
||||||
handleEvent(e: MouseEvent | TouchEvent): void {
|
handleEvent(e: MouseEvent | TouchEvent): void {
|
||||||
const touch = e instanceof TouchEvent ? e.changedTouches[0] : e;
|
const touch = e instanceof TouchEvent ? e.changedTouches[0] : e;
|
||||||
let node = <HTMLElement & {character?: Character, channel?: Channel}>touch.target;
|
let node = <HTMLElement & {character?: Character, channel?: Channel, touched?: boolean}>touch.target;
|
||||||
while(node !== document.body) {
|
while(node !== document.body) {
|
||||||
if(e.type !== 'click' && node === this.$refs['menu']) return;
|
if(e.type !== 'click' && node === this.$refs['menu']) return;
|
||||||
if(node.character !== undefined || node.dataset['character'] !== undefined || node.parentNode === null) break;
|
if(node.character !== undefined || node.dataset['character'] !== undefined || node.parentNode === null) break;
|
||||||
|
@ -170,25 +161,18 @@
|
||||||
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;
|
||||||
|
this.touchedElement = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch(e.type) {
|
switch(e.type) {
|
||||||
case 'click':
|
case 'click':
|
||||||
if(node.dataset['character'] === undefined) this.onClick(node.character);
|
if(node.dataset['character'] === undefined)
|
||||||
|
if(node === this.touchedElement) this.openMenu(touch, node.character, node.channel);
|
||||||
|
else this.onClick(node.character);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
case 'touchstart':
|
case 'touchstart':
|
||||||
this.touchTimer = window.setTimeout(() => {
|
this.touchedElement = node;
|
||||||
this.openMenu(touch, node.character!, node.channel);
|
|
||||||
this.touchTimer = undefined;
|
|
||||||
}, 500);
|
|
||||||
break;
|
|
||||||
case 'touchend':
|
|
||||||
if(this.touchTimer !== undefined) {
|
|
||||||
clearTimeout(this.touchTimer);
|
|
||||||
this.touchTimer = undefined;
|
|
||||||
if(node.dataset['character'] === undefined) this.onClick(node.character);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'contextmenu':
|
case 'contextmenu':
|
||||||
this.openMenu(touch, node.character, node.channel);
|
this.openMenu(touch, node.character, node.channel);
|
||||||
|
@ -222,8 +206,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#userMenu li a {
|
#userMenu .list-group-item {
|
||||||
padding: 3px 0;
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#userMenu .list-group-item-action {
|
||||||
|
border-top: 0;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-view {
|
.user-view {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import l from './localize';
|
||||||
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:9799';
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private errorHandler: (error: Error) => void;
|
private errorHandler: ((error: Error) => void) | undefined;
|
||||||
private lastHandler: Promise<void> = Promise.resolve();
|
private lastHandler: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {format, isToday} from 'date-fns';
|
import {format, isToday} from 'date-fns';
|
||||||
|
import {Keys} from '../keys';
|
||||||
import {Character, Conversation, Settings as ISettings} from './interfaces';
|
import {Character, Conversation, Settings as ISettings} from './interfaces';
|
||||||
|
|
||||||
export function profileLink(this: void | never, character: string): string {
|
export function profileLink(this: void | never, character: string): string {
|
||||||
|
@ -40,6 +41,7 @@ export class Settings implements ISettings {
|
||||||
logMessages = true;
|
logMessages = true;
|
||||||
logAds = false;
|
logAds = false;
|
||||||
fontSize = 14;
|
fontSize = 14;
|
||||||
|
showNeedsReply = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConversationSettings implements Conversation.Settings {
|
export class ConversationSettings implements Conversation.Settings {
|
||||||
|
@ -63,9 +65,8 @@ export function messageToString(this: void | never, msg: Conversation.Message, t
|
||||||
return `${text} ${msg.text}\r\n`;
|
return `${text} ${msg.text}\r\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKey(e: KeyboardEvent): string {
|
export function getKey(e: KeyboardEvent): Keys {
|
||||||
/*tslint:disable-next-line:strict-boolean-expressions no-any*///because of old browsers.
|
return e.keyCode;
|
||||||
return (e.key || (<KeyboardEvent & {keyIdentifier: string}>e).keyIdentifier).toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*tslint:disable:no-any no-unsafe-any*///because errors can be any
|
/*tslint:disable:no-any no-unsafe-any*///because errors can be any
|
||||||
|
|
|
@ -29,10 +29,10 @@ abstract class Conversation implements Interfaces.Conversation {
|
||||||
lastRead: Interfaces.Message | undefined = undefined;
|
lastRead: Interfaces.Message | undefined = undefined;
|
||||||
infoText = '';
|
infoText = '';
|
||||||
abstract readonly maxMessageLength: number | undefined;
|
abstract readonly maxMessageLength: number | undefined;
|
||||||
_settings: Interfaces.Settings;
|
_settings: Interfaces.Settings | undefined;
|
||||||
protected abstract context: CommandContext;
|
protected abstract context: CommandContext;
|
||||||
protected maxMessages = 100;
|
protected maxMessages = 100;
|
||||||
protected allMessages: Interfaces.Message[];
|
protected allMessages: Interfaces.Message[] = [];
|
||||||
private lastSent = '';
|
private lastSent = '';
|
||||||
|
|
||||||
constructor(readonly key: string, public _isPinned: boolean) {
|
constructor(readonly key: string, public _isPinned: boolean) {
|
||||||
|
@ -199,7 +199,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
||||||
private chat: Interfaces.Message[] = [];
|
private chat: Interfaces.Message[] = [];
|
||||||
private ads: Interfaces.Message[] = [];
|
private ads: Interfaces.Message[] = [];
|
||||||
private both: Interfaces.Message[] = [];
|
private both: Interfaces.Message[] = [];
|
||||||
private _mode: Channel.Mode;
|
private _mode!: Channel.Mode;
|
||||||
private adEnteredText = '';
|
private adEnteredText = '';
|
||||||
private chatEnteredText = '';
|
private chatEnteredText = '';
|
||||||
private logPromise = core.logs.getBacklog(this).then((messages) => {
|
private logPromise = core.logs.getBacklog(this).then((messages) => {
|
||||||
|
@ -220,7 +220,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
||||||
this.mode = value;
|
this.mode = value;
|
||||||
if(value !== 'both') this.isSendingAds = value === 'ads';
|
if(value !== 'both') this.isSendingAds = value === 'ads';
|
||||||
});
|
});
|
||||||
this.mode = this.channel.mode;
|
this.mode = channel.mode === 'both' && channel.id in state.modes ? state.modes[channel.id]! : channel.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxMessageLength(): number {
|
get maxMessageLength(): number {
|
||||||
|
@ -236,6 +236,10 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
||||||
this.maxMessages = 100;
|
this.maxMessages = 100;
|
||||||
this.allMessages = this[mode];
|
this.allMessages = this[mode];
|
||||||
this.messages = this.allMessages.slice(-this.maxMessages);
|
this.messages = this.allMessages.slice(-this.maxMessages);
|
||||||
|
if(mode === this.channel.mode && this.channel.id in state.modes) delete state.modes[this.channel.id];
|
||||||
|
else if(mode !== this.channel.mode && mode !== state.modes[this.channel.id]) state.modes[this.channel.id] = mode;
|
||||||
|
else return;
|
||||||
|
state.saveModes(); //tslint:disable-line:no-floating-promises
|
||||||
}
|
}
|
||||||
|
|
||||||
get enteredText(): string {
|
get enteredText(): string {
|
||||||
|
@ -272,7 +276,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
||||||
if(message.type !== Interfaces.Message.Type.Event) {
|
if(message.type !== Interfaces.Message.Type.Event) {
|
||||||
if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message);
|
if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message);
|
||||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||||
if(this !== state.selectedConversation && this.unread === Interfaces.UnreadState.None || !state.windowFocused)
|
if(this.unread === Interfaces.UnreadState.None && (this !== state.selectedConversation || !state.windowFocused)
|
||||||
|
&& this.mode !== 'ads')
|
||||||
this.unread = Interfaces.UnreadState.Unread;
|
this.unread = Interfaces.UnreadState.Unread;
|
||||||
} else this.addModeMessage('ads', message);
|
} else this.addModeMessage('ads', message);
|
||||||
}
|
}
|
||||||
|
@ -291,6 +296,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
||||||
|
|
||||||
protected async doSend(): Promise<void> {
|
protected async doSend(): Promise<void> {
|
||||||
const isAd = this.isSendingAds;
|
const isAd = this.isSendingAds;
|
||||||
|
if(isAd && this.adCountdown > 0) return;
|
||||||
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
|
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
|
||||||
await this.addMessage(
|
await this.addMessage(
|
||||||
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
|
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
|
||||||
|
@ -335,12 +341,13 @@ class State implements Interfaces.State {
|
||||||
channelConversations: ChannelConversation[] = [];
|
channelConversations: ChannelConversation[] = [];
|
||||||
privateMap: {[key: string]: PrivateConversation | undefined} = {};
|
privateMap: {[key: string]: PrivateConversation | undefined} = {};
|
||||||
channelMap: {[key: string]: ChannelConversation | undefined} = {};
|
channelMap: {[key: string]: ChannelConversation | undefined} = {};
|
||||||
consoleTab: ConsoleConversation;
|
consoleTab!: ConsoleConversation;
|
||||||
selectedConversation: Conversation = this.consoleTab;
|
selectedConversation: Conversation = this.consoleTab;
|
||||||
recent: Interfaces.RecentConversation[] = [];
|
recent: Interfaces.RecentConversation[] = [];
|
||||||
pinned: {channels: string[], private: string[]};
|
pinned!: {channels: string[], private: string[]};
|
||||||
settings: {[key: string]: Interfaces.Settings};
|
settings!: {[key: string]: Interfaces.Settings};
|
||||||
windowFocused: boolean;
|
modes!: {[key: string]: Channel.Mode | undefined};
|
||||||
|
windowFocused = document.hasFocus();
|
||||||
|
|
||||||
get hasNew(): boolean {
|
get hasNew(): boolean {
|
||||||
return this.privateConversations.some((x) => x.unread === Interfaces.UnreadState.Mention) ||
|
return this.privateConversations.some((x) => x.unread === Interfaces.UnreadState.Mention) ||
|
||||||
|
@ -369,6 +376,10 @@ class State implements Interfaces.State {
|
||||||
await core.settingsStore.set('pinned', this.pinned);
|
await core.settingsStore.set('pinned', this.pinned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveModes(): Promise<void> {
|
||||||
|
await core.settingsStore.set('modes', this.modes);
|
||||||
|
}
|
||||||
|
|
||||||
async setSettings(key: string, value: Interfaces.Settings): Promise<void> {
|
async setSettings(key: string, value: Interfaces.Settings): Promise<void> {
|
||||||
this.settings[key] = value;
|
this.settings[key] = value;
|
||||||
await core.settingsStore.set('conversationSettings', this.settings);
|
await core.settingsStore.set('conversationSettings', this.settings);
|
||||||
|
@ -402,6 +413,7 @@ class State implements Interfaces.State {
|
||||||
async reloadSettings(): Promise<void> {
|
async reloadSettings(): Promise<void> {
|
||||||
//tslint:disable:strict-boolean-expressions
|
//tslint:disable:strict-boolean-expressions
|
||||||
this.pinned = await core.settingsStore.get('pinned') || {private: [], channels: []};
|
this.pinned = await core.settingsStore.get('pinned') || {private: [], channels: []};
|
||||||
|
this.modes = await core.settingsStore.get('modes') || {};
|
||||||
for(const conversation of this.channelConversations)
|
for(const conversation of this.channelConversations)
|
||||||
conversation._isPinned = this.pinned.channels.indexOf(conversation.channel.id) !== -1;
|
conversation._isPinned = this.pinned.channels.indexOf(conversation.channel.id) !== -1;
|
||||||
for(const conversation of this.privateConversations)
|
for(const conversation of this.privateConversations)
|
||||||
|
@ -433,13 +445,22 @@ 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', () => {
|
||||||
state.windowFocused = true;
|
state.windowFocused = true;
|
||||||
if(state.selectedConversation !== undefined!) state.selectedConversation.unread = Interfaces.UnreadState.None;
|
if(state.selectedConversation !== undefined!) state.selectedConversation.unread = Interfaces.UnreadState.None;
|
||||||
});
|
});
|
||||||
window.addEventListener('blur', () => state.windowFocused = false);
|
window.addEventListener('blur', () => {
|
||||||
|
state.windowFocused = false;
|
||||||
|
if(state.selectedConversation !== undefined!)
|
||||||
|
state.selectedConversation.lastRead = state.selectedConversation.messages[state.selectedConversation.messages.length - 1];
|
||||||
|
});
|
||||||
const connection = core.connection;
|
const connection = core.connection;
|
||||||
connection.onEvent('connecting', async(isReconnect) => {
|
connection.onEvent('connecting', async(isReconnect) => {
|
||||||
state.channelConversations = [];
|
state.channelConversations = [];
|
||||||
|
@ -465,20 +486,23 @@ export default function(this: void): Interfaces.State {
|
||||||
state.channelConversations.push(conv);
|
state.channelConversations.push(conv);
|
||||||
await state.addRecent(conv);
|
await state.addRecent(conv);
|
||||||
} else {
|
} else {
|
||||||
const conv = state.channelMap[channel.id]!;
|
const conv = state.channelMap[channel.id];
|
||||||
|
if(conv === undefined) return;
|
||||||
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
||||||
!core.state.settings.joinMessages) return;
|
!core.state.settings.joinMessages) return;
|
||||||
const text = l('events.channelJoin', `[user]${member.character.name}[/user]`);
|
const text = l('events.channelJoin', `[user]${member.character.name}[/user]`);
|
||||||
await conv.addMessage(new EventMessage(text));
|
await conv.addMessage(new EventMessage(text));
|
||||||
}
|
}
|
||||||
else if(member === undefined) {
|
else if(member === undefined) {
|
||||||
const conv = state.channelMap[channel.id]!;
|
const conv = state.channelMap[channel.id];
|
||||||
|
if(conv === undefined) return;
|
||||||
state.channelConversations.splice(state.channelConversations.indexOf(conv), 1);
|
state.channelConversations.splice(state.channelConversations.indexOf(conv), 1);
|
||||||
delete state.channelMap[channel.id];
|
delete state.channelMap[channel.id];
|
||||||
await state.savePinned();
|
await state.savePinned();
|
||||||
if(state.selectedConversation === conv) state.show(state.consoleTab);
|
if(state.selectedConversation === conv) state.show(state.consoleTab);
|
||||||
} else {
|
} else {
|
||||||
const conv = state.channelMap[channel.id]!;
|
const conv = state.channelMap[channel.id];
|
||||||
|
if(conv === undefined) return;
|
||||||
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
||||||
!core.state.settings.joinMessages) return;
|
!core.state.settings.joinMessages) return;
|
||||||
const text = l('events.channelLeave', `[user]${member.character.name}[/user]`);
|
const text = l('events.channelLeave', `[user]${member.character.name}[/user]`);
|
||||||
|
@ -495,9 +519,9 @@ export default function(this: void): Interfaces.State {
|
||||||
});
|
});
|
||||||
connection.onMessage('MSG', async(data, time) => {
|
connection.onMessage('MSG', async(data, time) => {
|
||||||
const char = core.characters.get(data.character);
|
const char = core.characters.get(data.character);
|
||||||
if(char.isIgnored) return;
|
|
||||||
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;
|
||||||
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);
|
||||||
|
|
||||||
|
@ -512,20 +536,21 @@ export default function(this: void): Interfaces.State {
|
||||||
characterImage(data.character), 'attention');
|
characterImage(data.character), 'attention');
|
||||||
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||||
message.isHighlight = true;
|
message.isHighlight = true;
|
||||||
} else if(conversation.settings.notify === Interfaces.Setting.True)
|
} else if(conversation.settings.notify === Interfaces.Setting.True) {
|
||||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||||
characterImage(data.character), 'attention');
|
characterImage(data.character), 'attention');
|
||||||
|
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
connection.onMessage('LRP', async(data, time) => {
|
connection.onMessage('LRP', async(data, time) => {
|
||||||
const char = core.characters.get(data.character);
|
const char = core.characters.get(data.character);
|
||||||
if(char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) return;
|
|
||||||
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;
|
||||||
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) => {
|
||||||
const sender = core.characters.get(data.character);
|
const sender = core.characters.get(data.character);
|
||||||
if(sender.isIgnored) return;
|
|
||||||
let text: string;
|
let text: string;
|
||||||
if(data.type === 'bottle')
|
if(data.type === 'bottle')
|
||||||
text = l('chat.bottle', `[user]${data.target}[/user]`);
|
text = l('chat.bottle', `[user]${data.target}[/user]`);
|
||||||
|
@ -538,11 +563,17 @@ 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);
|
||||||
await conversation.addMessage(message);
|
if(sender.isIgnored && !isOp(conversation)) return;
|
||||||
if(data.type === 'bottle' && data.target === core.connection.character)
|
if(data.type === 'bottle' && data.target === core.connection.character) {
|
||||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||||
characterImage(data.character), 'attention');
|
characterImage(data.character), 'attention');
|
||||||
|
if(conversation !== state.selectedConversation || !state.windowFocused)
|
||||||
|
conversation.unread = Interfaces.UnreadState.Mention;
|
||||||
|
message.isHighlight = true;
|
||||||
|
}
|
||||||
|
await conversation.addMessage(message);
|
||||||
} else {
|
} else {
|
||||||
|
if(sender.isIgnored) return;
|
||||||
const char = core.characters.get(
|
const char = core.characters.get(
|
||||||
data.character === connection.character ? (<{recipient: string}>data).recipient : data.character);
|
data.character === connection.character ? (<{recipient: string}>data).recipient : data.character);
|
||||||
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
|
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
|
||||||
|
@ -590,7 +621,6 @@ export default function(this: void): Interfaces.State {
|
||||||
conv.infoText = text;
|
conv.infoText = text;
|
||||||
return addEventMessage(new EventMessage(text, time));
|
return addEventMessage(new EventMessage(text, time));
|
||||||
});
|
});
|
||||||
connection.onMessage('HLO', async(data, time) => addEventMessage(new EventMessage(data.message, time)));
|
|
||||||
connection.onMessage('BRO', async(data, time) => {
|
connection.onMessage('BRO', async(data, time) => {
|
||||||
const text = data.character === undefined ? decodeHTML(data.message) :
|
const text = data.character === undefined ? decodeHTML(data.message) :
|
||||||
l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
|
l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
|
||||||
|
@ -703,10 +733,11 @@ export default function(this: void): Interfaces.State {
|
||||||
state.selectedConversation.infoText = data.message;
|
state.selectedConversation.infoText = data.message;
|
||||||
return addEventMessage(new EventMessage(data.message, time));
|
return addEventMessage(new EventMessage(data.message, time));
|
||||||
});
|
});
|
||||||
|
connection.onMessage('UPT', async(data, time) => addEventMessage(new EventMessage(l('events.uptime',
|
||||||
|
data.startstring, data.channels.toString(), data.users.toString(), data.accepted.toString(), data.maxusers.toString()), time)));
|
||||||
connection.onMessage('ZZZ', async(data, time) => {
|
connection.onMessage('ZZZ', async(data, time) => {
|
||||||
state.selectedConversation.infoText = data.message;
|
state.selectedConversation.infoText = data.message;
|
||||||
return addEventMessage(new EventMessage(data.message, time));
|
return addEventMessage(new EventMessage(data.message, time));
|
||||||
});
|
});
|
||||||
//TODO connection.onMessage('UPT', data =>
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
|
@ -18,6 +18,7 @@ export namespace Conversation {
|
||||||
readonly type: Message.Type.Event,
|
readonly type: Message.Type.Event,
|
||||||
readonly text: string,
|
readonly text: string,
|
||||||
readonly time: Date
|
readonly time: Date
|
||||||
|
readonly sender?: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
|
@ -141,7 +142,8 @@ export namespace Settings {
|
||||||
export type Keys = {
|
export type Keys = {
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
pinned: {channels: string[], private: string[]},
|
pinned: {channels: string[], private: string[]},
|
||||||
conversationSettings: {[key: string]: Conversation.Settings}
|
conversationSettings: {[key: string]: Conversation.Settings | undefined}
|
||||||
|
modes: {[key: string]: Channel.Mode | undefined}
|
||||||
recent: Conversation.RecentConversation[]
|
recent: Conversation.RecentConversation[]
|
||||||
hiddenUsers: string[]
|
hiddenUsers: string[]
|
||||||
};
|
};
|
||||||
|
@ -169,6 +171,7 @@ export namespace Settings {
|
||||||
readonly logMessages: boolean;
|
readonly logMessages: boolean;
|
||||||
readonly logAds: boolean;
|
readonly logAds: boolean;
|
||||||
readonly fontSize: number;
|
readonly fontSize: number;
|
||||||
|
readonly showNeedsReply: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ const strings: {[key: string]: string | undefined} = {
|
||||||
'help.report': 'How to report a user',
|
'help.report': 'How to report a user',
|
||||||
'help.changelog': 'Changelog',
|
'help.changelog': 'Changelog',
|
||||||
'fs.error': 'Error writing to disk',
|
'fs.error': 'Error writing to disk',
|
||||||
|
'spellchecker.add': 'Add to Dictionary',
|
||||||
|
'spellchecker.remove': 'Remove from Dictionary',
|
||||||
|
'spellchecker.noCorrections': 'No corrections available',
|
||||||
'window.newTab': 'New tab',
|
'window.newTab': 'New tab',
|
||||||
'title': 'F-Chat',
|
'title': 'F-Chat',
|
||||||
'version': 'Version {0}',
|
'version': 'Version {0}',
|
||||||
|
@ -63,7 +66,6 @@ const strings: {[key: string]: string | undefined} = {
|
||||||
'chat.highlight': 'mentioned {0} in {1}:\n{2}',
|
'chat.highlight': 'mentioned {0} in {1}:\n{2}',
|
||||||
'chat.roll': 'rolls {0}: {1}',
|
'chat.roll': 'rolls {0}: {1}',
|
||||||
'chat.bottle': 'spins the bottle: {0}',
|
'chat.bottle': 'spins the bottle: {0}',
|
||||||
'chat.adCountdown': 'You must wait {0}m{1}s to post another ad in this channel.',
|
|
||||||
'chat.consoleChat': 'You cannot chat here.',
|
'chat.consoleChat': 'You cannot chat here.',
|
||||||
'chat.typing.typing': '{0} is typing...',
|
'chat.typing.typing': '{0} is typing...',
|
||||||
'chat.typing.paused': '{0} has entered text.',
|
'chat.typing.paused': '{0} has entered text.',
|
||||||
|
@ -72,9 +74,11 @@ const strings: {[key: string]: string | undefined} = {
|
||||||
'chat.disconnected': 'You were disconnected from chat.\nAttempting to reconnect.',
|
'chat.disconnected': 'You were disconnected from chat.\nAttempting to reconnect.',
|
||||||
'chat.disconnected.title': 'Disconnected',
|
'chat.disconnected.title': 'Disconnected',
|
||||||
'chat.ignoreList': 'You are currently ignoring: {0}',
|
'chat.ignoreList': 'You are currently ignoring: {0}',
|
||||||
|
'chat.search': 'Search in messages...',
|
||||||
'logs.title': 'Logs',
|
'logs.title': 'Logs',
|
||||||
'logs.conversation': 'Conversation',
|
'logs.conversation': 'Conversation',
|
||||||
'logs.date': 'Date',
|
'logs.date': 'Date',
|
||||||
|
'logs.selectDate': 'Select a date...',
|
||||||
'user.profile': 'Profile',
|
'user.profile': 'Profile',
|
||||||
'user.message': 'Open conversation',
|
'user.message': 'Open conversation',
|
||||||
'user.messageJump': 'View conversation',
|
'user.messageJump': 'View conversation',
|
||||||
|
@ -150,7 +154,9 @@ Are you sure?`,
|
||||||
'settings.logMessages': 'Log messages',
|
'settings.logMessages': 'Log messages',
|
||||||
'settings.logAds': 'Log ads',
|
'settings.logAds': 'Log ads',
|
||||||
'settings.fontSize': 'Font size (experimental)',
|
'settings.fontSize': 'Font size (experimental)',
|
||||||
|
'settings.showNeedsReply': 'Show indicator on PM tabs with unanswered messages',
|
||||||
'settings.defaultHighlights': 'Use global highlight words',
|
'settings.defaultHighlights': 'Use global highlight words',
|
||||||
|
'settings.beta': 'Opt-in to test unstable prerelease updates',
|
||||||
'conversationSettings.title': 'Tab Settings',
|
'conversationSettings.title': 'Tab Settings',
|
||||||
'conversationSettings.action': 'Edit settings for {0}',
|
'conversationSettings.action': 'Edit settings for {0}',
|
||||||
'conversationSettings.default': 'Default',
|
'conversationSettings.default': 'Default',
|
||||||
|
@ -160,6 +166,7 @@ Are you sure?`,
|
||||||
'channel.mode.ads': 'Ads',
|
'channel.mode.ads': 'Ads',
|
||||||
'channel.mode.chat': 'Chat',
|
'channel.mode.chat': 'Chat',
|
||||||
'channel.mode.both': 'Both',
|
'channel.mode.both': 'Both',
|
||||||
|
'channel.mode.ads.countdown': 'Ads ({0}m{1}s)',
|
||||||
'channel.official': 'Official channel',
|
'channel.official': 'Official channel',
|
||||||
'channel.description': 'Description',
|
'channel.description': 'Description',
|
||||||
'manageChannel.open': 'Manage',
|
'manageChannel.open': 'Manage',
|
||||||
|
@ -219,6 +226,7 @@ Are you sure?`,
|
||||||
'events.channelLeave': '{0} has left the channel.',
|
'events.channelLeave': '{0} has left the channel.',
|
||||||
'events.ignore_add': 'You are now ignoring {0}\'s messages. Should they go around this by any means, please report it using the Alert Staff button.',
|
'events.ignore_add': 'You are now ignoring {0}\'s messages. Should they go around this by any means, please report it using the Alert Staff button.',
|
||||||
'events.ignore_delete': '{0} is now allowed to send you messages again.',
|
'events.ignore_delete': '{0} is now allowed to send you messages again.',
|
||||||
|
'events.uptime': 'Server has been running since {0}, with currently {1} channels and {2} users, a total of {3} accepted connections, and {4} maximum users.',
|
||||||
'commands.unknown': 'Unknown command. For a list of valid commands, please click the ? button.',
|
'commands.unknown': 'Unknown command. For a list of valid commands, please click the ? button.',
|
||||||
'commands.badContext': 'This command cannot be used here. Please use the Help (click the ? button) if you need further information.',
|
'commands.badContext': 'This command cannot be used here. Please use the Help (click the ? button) if you need further information.',
|
||||||
'commands.tooFewParams': 'This command requires more parameters. Please use the Help (click the ? button) if you need further information.',
|
'commands.tooFewParams': 'This command requires more parameters. Please use the Help (click the ? button) if you need further information.',
|
||||||
|
@ -267,7 +275,7 @@ Are you sure?`,
|
||||||
'commands.makeroom.param0': 'Room name',
|
'commands.makeroom.param0': 'Room name',
|
||||||
'commands.makeroom.param0.help': 'A name for your new private room. Must be 1-64 in length.',
|
'commands.makeroom.param0.help': 'A name for your new private room. Must be 1-64 in length.',
|
||||||
'commands.ignore': 'Ignore a character',
|
'commands.ignore': 'Ignore a character',
|
||||||
'commands.ignore.help': 'Ignores the given character, and discards all of their messages.',
|
'commands.ignore.help': 'Ignores the given character, and discards all of their messages, except in channels where you are a moderator.',
|
||||||
'commands.unignore': 'Unignore a character',
|
'commands.unignore': 'Unignore a character',
|
||||||
'commands.unignore.help': 'Removes the given character from your ignore list, and allows them to send you messages again.',
|
'commands.unignore.help': 'Removes the given character from your ignore list, and allows them to send you messages again.',
|
||||||
'commands.ignorelist': 'Ignore list',
|
'commands.ignorelist': 'Ignore list',
|
||||||
|
|
|
@ -8,6 +8,7 @@ export default class Notifications implements Interface {
|
||||||
|
|
||||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
||||||
|
if(core.characters.ownCharacter.status === 'dnd') return;
|
||||||
this.playSound(sound);
|
this.playSound(sound);
|
||||||
if(core.state.settings.notifications) {
|
if(core.state.settings.notifications) {
|
||||||
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import Axios from 'axios';
|
import Axios from 'axios';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import Editor from '../bbcode/Editor.vue';
|
||||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
import {InlineDisplayMode} from '../bbcode/interfaces';
|
||||||
import {initParser, standardParser} from '../bbcode/standard';
|
import {initParser, standardParser} from '../bbcode/standard';
|
||||||
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 {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 {registerMethod, Store} from '../site/character_page/data_store';
|
import {registerMethod, Store} from '../site/character_page/data_store';
|
||||||
import {
|
import {
|
||||||
Character, CharacterCustom, CharacterFriend, CharacterImage, CharacterImageOld, CharacterInfo, CharacterInfotag, CharacterKink,
|
Character, CharacterCustom, CharacterFriend, CharacterImage, CharacterImageOld, CharacterInfo, CharacterInfotag, CharacterKink,
|
||||||
|
@ -115,7 +117,7 @@ async function fieldsGet(): Promise<void> {
|
||||||
validator: oldInfotag.list,
|
validator: oldInfotag.list,
|
||||||
search_field: '',
|
search_field: '',
|
||||||
allow_legacy: true,
|
allow_legacy: true,
|
||||||
infotag_group: parseInt(oldInfotag.group_id, 10)
|
infotag_group: oldInfotag.group_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for(const id in fields.listitems) {
|
for(const id in fields.listitems) {
|
||||||
|
@ -175,6 +177,8 @@ export function init(characters: {[key: string]: number}): void {
|
||||||
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('bbcode-editor', Editor);
|
||||||
setCharacters(Object.keys(characters).map((name) => ({name, id: characters[name]})));
|
setCharacters(Object.keys(characters).map((name) => ({name, id: characters[name]})));
|
||||||
core.connection.onEvent('connecting', () => {
|
core.connection.onEvent('connecting', () => {
|
||||||
Utils.Settings.defaultCharacter = characters[core.connection.character];
|
Utils.Settings.defaultCharacter = characters[core.connection.character];
|
||||||
|
|
|
@ -9,21 +9,21 @@ import {Channel, Character} from './interfaces';
|
||||||
export function getStatusIcon(status: Character.Status): string {
|
export function getStatusIcon(status: Character.Status): string {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 'online':
|
case 'online':
|
||||||
return 'fa-user-o';
|
return 'far fa-user';
|
||||||
case 'looking':
|
case 'looking':
|
||||||
return 'fa-eye';
|
return 'fa fa-eye';
|
||||||
case 'dnd':
|
case 'dnd':
|
||||||
return 'fa-minus-circle';
|
return 'fa fa-minus-circle';
|
||||||
case 'offline':
|
case 'offline':
|
||||||
return 'fa-ban';
|
return 'fa fa-ban';
|
||||||
case 'away':
|
case 'away':
|
||||||
return 'fa-circle-o';
|
return 'far fa-circle';
|
||||||
case 'busy':
|
case 'busy':
|
||||||
return 'fa-cog';
|
return 'fa fa-cog';
|
||||||
case 'idle':
|
case 'idle':
|
||||||
return 'fa-clock-o';
|
return 'far fa-clock';
|
||||||
case 'crown':
|
case 'crown':
|
||||||
return 'fa-birthday-cake';
|
return 'fa fa-birthday-cake';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,21 +35,18 @@ const UserView = Vue.extend({
|
||||||
context !== undefined ? context.props : (<Vue>this).$options.propsData);
|
context !== undefined ? context.props : (<Vue>this).$options.propsData);
|
||||||
const character = props.character;
|
const character = props.character;
|
||||||
let rankIcon;
|
let rankIcon;
|
||||||
if(character.isChatOp) rankIcon = 'fa-diamond';
|
if(character.isChatOp) rankIcon = 'far fa-gem';
|
||||||
else if(props.channel !== undefined) {
|
else if(props.channel !== undefined)
|
||||||
const member = props.channel.members[character.name];
|
rankIcon = props.channel.owner === character.name ? 'fa fa-key' : props.channel.opList.indexOf(character.name) !== -1 ?
|
||||||
if(member !== undefined)
|
(props.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star') : '';
|
||||||
rankIcon = member.rank === Channel.Rank.Owner ? 'fa-asterisk' :
|
else rankIcon = '';
|
||||||
member.rank === Channel.Rank.Op ? (props.channel.id.substr(0, 4) === 'adh-' ? 'fa-at' : 'fa-star') : '';
|
|
||||||
else rankIcon = '';
|
|
||||||
} else rankIcon = '';
|
|
||||||
|
|
||||||
const html = (props.showStatus !== undefined || character.status === 'crown'
|
const html = (props.showStatus !== undefined || character.status === 'crown'
|
||||||
? `<span class="fa fa-fw ${getStatusIcon(character.status)}"></span>` : '') +
|
? `<span class="fa-fw ${getStatusIcon(character.status)}"></span>` : '') +
|
||||||
(rankIcon !== '' ? `<span class="fa ${rankIcon}"></span>` : '') + character.name;
|
(rankIcon !== '' ? `<span class="${rankIcon}"></span>` : '') + character.name;
|
||||||
return createElement('span', {
|
return createElement('span', {
|
||||||
attrs: {class: `user-view gender-${character.gender !== undefined ? character.gender.toLowerCase() : 'none'}`},
|
attrs: {class: `user-view gender-${character.gender !== undefined ? character.gender.toLowerCase() : 'none'}`},
|
||||||
domProps: {character, channel: props.channel, innerHTML: html}
|
domProps: {character, channel: props.channel, innerHTML: html, bbcodeTag: 'user'}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
||||||
|
@blur="isOpen = false" style="width:100%;text-align:left;display:flex;align-items:center">
|
||||||
|
<div style="flex:1">
|
||||||
|
<slot name="title" style="flex:1"></slot>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" :style="isOpen ? 'display:block' : ''" @mousedown.stop.prevent @click="isOpen = false">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Dropdown extends Vue {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,59 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dropdown filterable-select">
|
<dropdown class="dropdown filterable-select">
|
||||||
<button class="btn btn-default dropdown-toggle" :class="buttonClass" data-toggle="dropdown">
|
<template slot="title" v-if="multiple">{{label}}</template>
|
||||||
<span style="flex:1">
|
<slot v-else slot="title" :option="selected">{{label}}</slot>
|
||||||
<template v-if="multiple">{{label}}</template>
|
|
||||||
<slot v-else :option="selected">{{label}}</slot>
|
<div style="padding:10px;">
|
||||||
</span>
|
<input v-model="filter" class="form-control" :placeholder="placeholder"/>
|
||||||
<span class="caret" style="align-self:center;margin-left:5px"></span>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu filterable-select" @click.stop>
|
|
||||||
<div style="padding:10px;">
|
|
||||||
<input v-model="filter" class="form-control" :placeholder="placeholder"/>
|
|
||||||
</div>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<template v-if="multiple">
|
|
||||||
<li v-for="option in filtered">
|
|
||||||
<a href="#" @click.stop="select(option)">
|
|
||||||
<input type="checkbox" :checked="selected.indexOf(option) !== -1"/>
|
|
||||||
<slot :option="option">{{option}}</slot>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<li v-for="option in filtered">
|
|
||||||
<a href="#" @click="select(option)">
|
|
||||||
<slot :option="option">{{option}}</slot>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="dropdown-items">
|
||||||
|
<template v-if="multiple">
|
||||||
|
<a href="#" @click.stop="select(option)" v-for="option in filtered" class="dropdown-item">
|
||||||
|
<input type="checkbox" :checked="selected.indexOf(option) !== -1"/>
|
||||||
|
<slot :option="option">{{option}}</slot>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a href="#" @click="select(option)" v-for="option in filtered" class="dropdown-item">
|
||||||
|
<slot :option="option">{{option}}</slot>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import {Prop, Watch} from 'vue-property-decorator';
|
import {Prop, Watch} from 'vue-property-decorator';
|
||||||
|
import Dropdown from '../components/Dropdown.vue';
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
components: {dropdown: Dropdown}
|
||||||
|
})
|
||||||
export default class FilterableSelect extends Vue {
|
export default class FilterableSelect extends Vue {
|
||||||
//tslint:disable:no-null-keyword
|
//tslint:disable:no-null-keyword
|
||||||
@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;
|
||||||
@Prop()
|
|
||||||
readonly buttonClass?: string;
|
|
||||||
filter = '';
|
filter = '';
|
||||||
selected: object | object[] | null = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : null);
|
selected: object | object[] | null = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : null);
|
||||||
|
|
||||||
|
@ -68,10 +59,7 @@
|
||||||
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.selected = item;
|
||||||
this.selected = item;
|
|
||||||
$('.dropdown-toggle', this.$el).dropdown('toggle');
|
|
||||||
}
|
|
||||||
this.$emit('input', this.selected);
|
this.$emit('input', this.selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,17 +78,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
.filterable-select {
|
.filterable-select {
|
||||||
ul.dropdown-menu {
|
.dropdown-items {
|
||||||
padding: 0;
|
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: static;
|
|
||||||
display: block;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<span v-show="isShown">
|
<span v-show="isShown">
|
||||||
<div tabindex="-1" class="modal flex-modal" @click.self="hideWithCheck"
|
<div tabindex="-1" class="modal" @click.self="hideWithCheck" style="display:flex">
|
||||||
style="align-items:flex-start;padding:30px;justify-content:center;display:flex">
|
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center">
|
||||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;flex-direction:column;max-height:100%;margin:0">
|
<div class="modal-content" style="max-height:100%">
|
||||||
<div class="modal-content" style="display:flex;flex-direction:column;flex-grow:1">
|
|
||||||
<div class="modal-header" style="flex-shrink:0">
|
<div class="modal-header" style="flex-shrink:0">
|
||||||
<button type="button" class="close" @click="hide" aria-label="Close" v-show="!keepOpen">×</button>
|
|
||||||
<h4 class="modal-title">
|
<h4 class="modal-title">
|
||||||
<slot name="title">{{action}}</slot>
|
<slot name="title">{{action}}</slot>
|
||||||
</h4>
|
</h4>
|
||||||
|
<button type="button" class="close" @click="hide" aria-label="Close" v-show="!keepOpen">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow: auto; display: flex; flex-direction: column">
|
<div class="modal-body" style="overflow:auto">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" v-if="buttons">
|
<div class="modal-footer" v-if="buttons">
|
||||||
<button type="button" class="btn btn-default" @click="hideWithCheck" v-if="showCancel">Cancel</button>
|
<button type="button" class="btn btn-secondary" @click="hideWithCheck" v-if="showCancel">Cancel</button>
|
||||||
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="disabled">
|
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="disabled">
|
||||||
{{submitText}}
|
{{submitText}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -31,26 +30,34 @@
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import {Prop} from 'vue-property-decorator';
|
import {Prop} from 'vue-property-decorator';
|
||||||
import {getKey} from '../chat/common';
|
import {getKey} from '../chat/common';
|
||||||
|
import {Keys} from '../keys';
|
||||||
|
|
||||||
const dialogStack: Modal[] = [];
|
const dialogStack: Modal[] = [];
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if(getKey(e) === 'escape' && dialogStack.length > 0) dialogStack.pop()!.isShown = false;
|
if(getKey(e) === Keys.Escape && dialogStack.length > 0) dialogStack[dialogStack.length - 1].hideWithCheck();
|
||||||
});
|
});
|
||||||
|
window.addEventListener('backbutton', (e) => {
|
||||||
|
if(dialogStack.length > 0) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
dialogStack.pop()!.isShown = false;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
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;
|
||||||
|
@ -79,37 +86,11 @@
|
||||||
dialogStack.pop();
|
dialogStack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideWithCheck(): void {
|
hideWithCheck(): void {
|
||||||
if(this.keepOpen) return;
|
if(this.keepOpen) return;
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
fixDropdowns(): void {
|
|
||||||
//tslint:disable-next-line:no-this-assignment
|
|
||||||
const vm = this;
|
|
||||||
$('.dropdown', this.$el).on('show.bs.dropdown', function(this: HTMLElement & {menu?: HTMLElement}): void {
|
|
||||||
if(this.menu !== undefined) {
|
|
||||||
this.menu.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const $this = $(this).children('.dropdown-menu');
|
|
||||||
this.menu = $this[0];
|
|
||||||
vm.$nextTick(() => {
|
|
||||||
const offset = $this.offset();
|
|
||||||
if(offset === undefined) return;
|
|
||||||
$('body').append($this.css({
|
|
||||||
display: 'block',
|
|
||||||
left: offset.left,
|
|
||||||
position: 'absolute',
|
|
||||||
top: offset.top,
|
|
||||||
'z-index': 1100
|
|
||||||
}).detach());
|
|
||||||
});
|
|
||||||
}).on('hide.bs.dropdown', function(this: HTMLElement & {menu: HTMLElement}): void {
|
|
||||||
this.menu.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeDestroy(): void {
|
beforeDestroy(): void {
|
||||||
if(this.isShown) this.hide();
|
if(this.isShown) this.hide();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
@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!: {name: string, id: number, deleted: boolean} | 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;
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
@Component
|
@Component
|
||||||
export default class CharacterSelect extends Vue {
|
export default class CharacterSelect extends Vue {
|
||||||
@Prop({required: true, type: Number})
|
@Prop({required: true, type: Number})
|
||||||
readonly value: number;
|
readonly value!: number;
|
||||||
|
|
||||||
get characters(): SelectItem[] {
|
get characters(): SelectItem[] {
|
||||||
const characterList = getCharacters();
|
const characterList = getCharacters();
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
@Component
|
@Component
|
||||||
export default class DateDisplay extends Vue {
|
export default class DateDisplay extends Vue {
|
||||||
@Prop({required: true})
|
@Prop({required: true})
|
||||||
readonly time: string | null | number;
|
readonly time!: string | null | number;
|
||||||
primary: string;
|
primary: string | undefined;
|
||||||
secondary: string;
|
secondary: string | undefined;
|
||||||
|
|
||||||
constructor(options?: ComponentOptions<Vue>) {
|
constructor(options?: ComponentOptions<Vue>) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="form-group" :class="allClasses">
|
|
||||||
<slot></slot>
|
|
||||||
<div :class="classes" v-if="hasErrors">
|
|
||||||
<ul>
|
|
||||||
<li v-for="error in errorList">{{ error }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import {Prop} from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class FormErrors extends Vue {
|
|
||||||
@Prop({required: true})
|
|
||||||
readonly errors: {[key: string]: string[] | undefined};
|
|
||||||
@Prop({required: true})
|
|
||||||
readonly field: string;
|
|
||||||
@Prop({default: 'col-xs-3'})
|
|
||||||
readonly classes: string;
|
|
||||||
@Prop()
|
|
||||||
readonly extraClasses?: {[key: string]: boolean};
|
|
||||||
|
|
||||||
get hasErrors(): boolean {
|
|
||||||
return typeof this.errors[this.field] !== 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
get errorList(): string[] {
|
|
||||||
return this.errors[this.field] !== undefined ? this.errors[this.field]! : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get allClasses(): {[key: string]: boolean} {
|
|
||||||
const classes: {[key: string]: boolean} = {'hash-error': this.hasErrors};
|
|
||||||
if(this.extraClasses === undefined) return classes;
|
|
||||||
for(const key in this.extraClasses)
|
|
||||||
classes[key] = this.extraClasses[key];
|
|
||||||
|
|
||||||
return classes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label v-if="label && id" :for="id">{{ label }}</label>
|
||||||
|
<slot :cls="{'is-invalid': hasErrors, 'is-valid': valid}" :invalid="hasErrors" :valid="valid"></slot>
|
||||||
|
<small v-if="helptext" class="form-text" :id="helpId">{{ helptext }}</small>
|
||||||
|
<div v-if="hasErrors" class="invalid-feedback">
|
||||||
|
<ul v-if="errorList.length > 1">
|
||||||
|
<li v-for="error in errorList">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
<template v-if="errorList.length === 1"> {{ errorList[0] }}</template>
|
||||||
|
</div>
|
||||||
|
<slot v-if="valid" name="valid"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import {Prop} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class FormGroup extends Vue {
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly field!: string;
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||||
|
@Prop()
|
||||||
|
readonly label?: string;
|
||||||
|
@Prop()
|
||||||
|
readonly id?: string;
|
||||||
|
@Prop({default: false})
|
||||||
|
readonly valid!: boolean;
|
||||||
|
@Prop()
|
||||||
|
readonly helptext?: string;
|
||||||
|
|
||||||
|
get hasErrors(): boolean {
|
||||||
|
return typeof this.errors[this.field] !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorList(): ReadonlyArray<string> {
|
||||||
|
return this.errors[this.field] || []; //tslint:disable-line:strict-boolean-expressions
|
||||||
|
}
|
||||||
|
|
||||||
|
get helpId(): string | undefined {
|
||||||
|
return this.id !== undefined ? `${this.id}Help` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label v-if="label && id" :for="id">{{ label }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<slot :cls="{'is-invalid': hasErrors, 'is-valid': valid}"></slot>
|
||||||
|
<slot name="button"></slot>
|
||||||
|
<small v-if="helptext" class="form-text" :id="helpId">{{ helptext }}</small>
|
||||||
|
<div v-if="hasErrors" class="invalid-feedback">
|
||||||
|
<ul v-if="errorList.length > 1">
|
||||||
|
<li v-for="error in errorList">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
<template v-if="errorList.length === 1"> {{ errorList[0] }}</template>
|
||||||
|
</div>
|
||||||
|
<slot v-if="valid" name="valid"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import {Prop} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class FormGroupInputgroup extends Vue {
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly field!: string;
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||||
|
@Prop()
|
||||||
|
readonly label?: string;
|
||||||
|
@Prop()
|
||||||
|
readonly id?: string;
|
||||||
|
@Prop({default: false})
|
||||||
|
readonly valid!: boolean;
|
||||||
|
@Prop()
|
||||||
|
readonly helptext?: string;
|
||||||
|
|
||||||
|
get hasErrors(): boolean {
|
||||||
|
return typeof this.errors[this.field] !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorList(): ReadonlyArray<string> {
|
||||||
|
return this.errors[this.field] || []; //tslint:disable-line:strict-boolean-expressions
|
||||||
|
}
|
||||||
|
|
||||||
|
get helpId(): string | undefined {
|
||||||
|
return this.id !== undefined ? `${this.id}Help` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex w-100 my-2 justify-content-between">
|
||||||
|
<div>
|
||||||
|
<slot name="previous" v-if="!routed">
|
||||||
|
<a class="btn btn-secondary" :class="{'disabled': !prev}" role="button" @click.prevent="previousPage">
|
||||||
|
<span aria-hidden="true">←</span> {{prevLabel}}
|
||||||
|
</a>
|
||||||
|
</slot>
|
||||||
|
<router-link v-if="routed" :to="prevRoute" class="btn btn-secondary" :class="{'disabled': !prev}" role="button">
|
||||||
|
<span aria-hidden="true">←</span> {{prevLabel}}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="next" v-if="!routed">
|
||||||
|
<a class="btn btn-secondary" :class="{'disabled': !next}" role="button" @click.prevent="nextPage">
|
||||||
|
{{nextLabel}} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</slot>
|
||||||
|
<router-link v-if="routed" :to="nextRoute" class="btn btn-secondary" :class="{'disabled': !next}" role="button">
|
||||||
|
{{nextLabel}} <span aria-hidden="true">→</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import cloneDeep = require('lodash/cloneDeep'); //tslint:disable-line:no-require-imports
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import {Prop} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
type ParamDictionary = {[key: string]: number | undefined};
|
||||||
|
interface RouteParams {
|
||||||
|
name?: string
|
||||||
|
params?: ParamDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class SimplePager extends Vue {
|
||||||
|
@Prop({default: 'Next Page'})
|
||||||
|
readonly nextLabel!: string;
|
||||||
|
@Prop({default: 'Previous Page'})
|
||||||
|
readonly prevLabel!: string;
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly next!: boolean;
|
||||||
|
@Prop({required: true})
|
||||||
|
readonly prev!: boolean;
|
||||||
|
@Prop({default: false})
|
||||||
|
readonly routed!: boolean;
|
||||||
|
@Prop({default: () => ({})})
|
||||||
|
readonly route!: RouteParams;
|
||||||
|
@Prop({default: 'page'})
|
||||||
|
readonly paramName!: string;
|
||||||
|
|
||||||
|
nextPage(): void {
|
||||||
|
if(!this.next)
|
||||||
|
return;
|
||||||
|
this.$emit('next');
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPage(): void {
|
||||||
|
if(!this.prev)
|
||||||
|
return;
|
||||||
|
this.$emit('prev');
|
||||||
|
}
|
||||||
|
|
||||||
|
get prevRoute(): RouteParams {
|
||||||
|
if(this.route.params !== undefined && this.route.params[this.paramName] !== undefined) {
|
||||||
|
const newPage = this.route.params[this.paramName]! - 1;
|
||||||
|
const clone = cloneDeep(this.route);
|
||||||
|
clone.params![this.paramName] = newPage;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get nextRoute(): RouteParams {
|
||||||
|
if(this.route.params !== undefined && this.route.params[this.paramName] !== undefined) {
|
||||||
|
const newPage = this.route.params[this.paramName]! + 1;
|
||||||
|
const clone = cloneDeep(this.route);
|
||||||
|
clone.params![this.paramName] = newPage;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,27 @@
|
||||||
|
import Vue, {CreateElement, VNode} from 'vue';
|
||||||
|
|
||||||
|
//tslint:disable-next-line:variable-name
|
||||||
|
const Tabs = Vue.extend({
|
||||||
|
props: ['value', 'tabs'],
|
||||||
|
render(this: Vue & {readonly value?: string, tabs: {readonly [key: string]: string}}, createElement: CreateElement): VNode {
|
||||||
|
let children: {[key: string]: string | VNode | undefined};
|
||||||
|
if(<VNode[] | undefined>this.$slots['default'] !== undefined) {
|
||||||
|
children = {};
|
||||||
|
this.$slots['default'].forEach((child, i) => {
|
||||||
|
if(child.context !== undefined) children[child.key !== undefined ? child.key : i] = child;
|
||||||
|
});
|
||||||
|
} else children = this.tabs;
|
||||||
|
const keys = Object.keys(children);
|
||||||
|
if(this.value === undefined || children[this.value] === undefined) this.$emit('input', keys[0]);
|
||||||
|
return createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'},
|
||||||
|
[createElement('a', {
|
||||||
|
staticClass: 'nav-link', class: {active: this.value === key}, on: {
|
||||||
|
click: () => {
|
||||||
|
this.$emit('input', key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [children[key]!])])));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Tabs;
|
|
@ -1,34 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<div @mouseover="onMouseOver" id="page" style="position:relative;padding:5px 10px 10px">
|
<div @mouseover="onMouseOver" id="page" style="position:relative;padding:5px 10px 10px" @auxclick.prevent>
|
||||||
<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="well well-lg" style="width: 400px;">
|
<div class="card bg-light" style="width: 400px;">
|
||||||
<h3 style="margin-top:0">{{l('title')}}</h3>
|
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
||||||
<div class="alert alert-danger" v-show="error">
|
<div class="card-body">
|
||||||
{{error}}
|
<div class="alert alert-danger" v-show="error">
|
||||||
</div>
|
{{error}}
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
<div class="form-group">
|
||||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login"/>
|
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||||
</div>
|
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
<div class="form-group">
|
||||||
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login"/>
|
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||||
</div>
|
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group" v-show="showAdvanced">
|
</div>
|
||||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
<div class="form-group" v-show="showAdvanced">
|
||||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login"/>
|
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||||
</div>
|
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="advanced"><input type="checkbox" id="advanced" v-model="showAdvanced"/> {{l('login.advanced')}}</label>
|
<div class="form-group">
|
||||||
</div>
|
<label for="advanced"><input type="checkbox" id="advanced" v-model="showAdvanced"/> {{l('login.advanced')}}</label>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
<div class="form-group">
|
||||||
</div>
|
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
||||||
<div class="form-group text-right" style="margin:0">
|
</div>
|
||||||
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
<div class="form-group" style="margin:0;text-align:right">
|
||||||
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
||||||
</button>
|
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +44,8 @@
|
||||||
</modal>
|
</modal>
|
||||||
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||||
<character-page :authenticated="true" :oldApi="true" :name="profileName" :image-preview="true"></character-page>
|
<character-page :authenticated="true" :oldApi="true" :name="profileName" :image-preview="true"></character-page>
|
||||||
<template slot="title">{{profileName}} <a class="btn fa fa-external-link" @click="openProfileInBrowser"></a></template>
|
<template slot="title">{{profileName}} <a class="btn" @click="openProfileInBrowser"><i class="fa fa-external-link-alt"/></a>
|
||||||
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -50,6 +53,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Axios from 'axios';
|
import Axios from 'axios';
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
|
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 * as qs from 'querystring';
|
import * as qs from 'querystring';
|
||||||
|
@ -71,15 +75,10 @@
|
||||||
import * as SlimcatImporter from './importer';
|
import * as SlimcatImporter from './importer';
|
||||||
import Notifications from './notifications';
|
import Notifications from './notifications';
|
||||||
|
|
||||||
declare module '../chat/interfaces' {
|
|
||||||
interface State {
|
|
||||||
generalSettings?: GeneralSettings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const webContents = electron.remote.getCurrentWebContents();
|
const webContents = electron.remote.getCurrentWebContents();
|
||||||
const parent = electron.remote.getCurrentWindow().webContents;
|
const parent = electron.remote.getCurrentWindow().webContents;
|
||||||
|
|
||||||
|
log.info('About to load keytar');
|
||||||
/*tslint:disable:no-any*///because this is hacky
|
/*tslint:disable:no-any*///because this is hacky
|
||||||
const keyStore = nativeRequire<{
|
const keyStore = nativeRequire<{
|
||||||
getPassword(account: string): Promise<string>
|
getPassword(account: string): Promise<string>
|
||||||
|
@ -89,6 +88,7 @@
|
||||||
}>('keytar/build/Release/keytar.node');
|
}>('keytar/build/Release/keytar.node');
|
||||||
for(const key in keyStore) keyStore[key] = promisify(<(...args: any[]) => any>keyStore[key].bind(keyStore, 'fchat'));
|
for(const key in keyStore) keyStore[key] = promisify(<(...args: any[]) => any>keyStore[key].bind(keyStore, 'fchat'));
|
||||||
//tslint:enable
|
//tslint:enable
|
||||||
|
log.info('Loaded keytar.');
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
error = '';
|
error = '';
|
||||||
defaultCharacter: string | null = null;
|
defaultCharacter: string | null = null;
|
||||||
l = l;
|
l = l;
|
||||||
settings: GeneralSettings;
|
settings!: GeneralSettings;
|
||||||
importProgress = 0;
|
importProgress = 0;
|
||||||
profileName = '';
|
profileName = '';
|
||||||
|
|
||||||
|
@ -115,7 +115,8 @@
|
||||||
|
|
||||||
Vue.set(core.state, 'generalSettings', this.settings);
|
Vue.set(core.state, 'generalSettings', this.settings);
|
||||||
|
|
||||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => core.state.generalSettings = this.settings = settings);
|
electron.ipcRenderer.on('settings',
|
||||||
|
(_: Event, settings: GeneralSettings) => core.state.generalSettings = this.settings = settings);
|
||||||
electron.ipcRenderer.on('open-profile', (_: Event, name: string) => {
|
electron.ipcRenderer.on('open-profile', (_: Event, name: string) => {
|
||||||
const profileViewer = <Modal>this.$refs['profileViewer'];
|
const profileViewer = <Modal>this.$refs['profileViewer'];
|
||||||
this.profileName = name;
|
this.profileName = name;
|
||||||
|
|
|
@ -3,27 +3,31 @@
|
||||||
<div v-html="styling"></div>
|
<div v-html="styling"></div>
|
||||||
<div style="display:flex;align-items:stretch;" class="border-bottom" id="window-tabs">
|
<div style="display:flex;align-items:stretch;" class="border-bottom" id="window-tabs">
|
||||||
<h4>F-Chat</h4>
|
<h4>F-Chat</h4>
|
||||||
<div :class="'fa fa-cog btn btn-' + (hasUpdate ? 'warning' : 'default')" @click="openMenu"></div>
|
<div class="btn" :class="'btn-' + (hasUpdate ? 'warning' : 'light')" @click="openMenu">
|
||||||
|
<i class="fa fa-cog"></i>
|
||||||
|
</div>
|
||||||
<ul class="nav nav-tabs" style="border-bottom:0" ref="tabs">
|
<ul class="nav nav-tabs" style="border-bottom:0" ref="tabs">
|
||||||
<li role="presentation" :class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}" v-for="tab in tabs"
|
<li v-for="tab in tabs" :key="tab.view.id" class="nav-item">
|
||||||
:key="tab.view.id">
|
<a href="#" @click.prevent="show(tab)" class="nav-link"
|
||||||
<a href="#" @click.prevent="show(tab)">
|
:class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}">
|
||||||
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/>
|
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/>
|
||||||
{{tab.user || l('window.newTab')}}
|
{{tab.user || l('window.newTab')}}
|
||||||
<a href="#" class="fa fa-close btn" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit"
|
<a href="#" class="btn" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit"
|
||||||
@click.stop="remove(tab)">
|
@click.stop="remove(tab)"><i class="fa fa-times"></i>
|
||||||
</a>
|
</a>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" v-show="canOpenTab" class="addTab" id="addTab">
|
<li v-show="canOpenTab" class="addTab nav-item" id="addTab">
|
||||||
<a href="#" @click.prevent="addTab" class="fa fa-plus"></a>
|
<a href="#" @click.prevent="addTab" class="nav-link"><i class="fa fa-plus"></i></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag;margin-top:3px" class="btn-group"
|
<div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag;margin-top:3px" class="btn-group"
|
||||||
id="windowButtons">
|
id="windowButtons">
|
||||||
<span class="fa fa-window-minimize btn btn-default" @click.stop="minimize"></span>
|
<i class="far fa-window-minimize btn btn-light" @click.stop="minimize"></i>
|
||||||
<span :class="'fa btn btn-default fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></span>
|
<i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></i>
|
||||||
<span class="fa fa-close fa-lg btn btn-default" @click.stop="close"></span>
|
<span class="btn btn-light" @click.stop="close">
|
||||||
|
<i class="fa fa-times fa-lg"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +65,7 @@
|
||||||
@Component
|
@Component
|
||||||
export default class Window extends Vue {
|
export default class Window extends Vue {
|
||||||
//tslint:disable:no-null-keyword
|
//tslint:disable:no-null-keyword
|
||||||
settings: GeneralSettings;
|
settings!: GeneralSettings;
|
||||||
tabs: Tab[] = [];
|
tabs: Tab[] = [];
|
||||||
activeTab: Tab | null = null;
|
activeTab: Tab | null = null;
|
||||||
tabMap: {[key: number]: Tab} = {};
|
tabMap: {[key: number]: Tab} = {};
|
||||||
|
@ -77,6 +81,7 @@
|
||||||
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());
|
||||||
electron.ipcRenderer.on('update-available', () => this.hasUpdate = true);
|
electron.ipcRenderer.on('update-available', () => this.hasUpdate = true);
|
||||||
|
electron.ipcRenderer.on('quit', () => this.tabs.forEach((tab) => this.remove(tab, false)));
|
||||||
electron.ipcRenderer.on('connect', (_: Event, id: number, name: string) => {
|
electron.ipcRenderer.on('connect', (_: Event, id: number, name: string) => {
|
||||||
const tab = this.tabMap[id];
|
const tab = this.tabMap[id];
|
||||||
tab.user = name;
|
tab.user = name;
|
||||||
|
@ -87,6 +92,10 @@
|
||||||
});
|
});
|
||||||
electron.ipcRenderer.on('disconnect', (_: Event, id: number) => {
|
electron.ipcRenderer.on('disconnect', (_: Event, id: number) => {
|
||||||
const tab = this.tabMap[id];
|
const tab = this.tabMap[id];
|
||||||
|
if(tab.hasNew) {
|
||||||
|
tab.hasNew = false;
|
||||||
|
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||||
|
}
|
||||||
tab.user = undefined;
|
tab.user = undefined;
|
||||||
tab.tray.setToolTip(l('title'));
|
tab.tray.setToolTip(l('title'));
|
||||||
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
|
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
|
||||||
|
@ -186,6 +195,7 @@
|
||||||
remove(tab: Tab, shouldConfirm: boolean = true): void {
|
remove(tab: Tab, shouldConfirm: boolean = true): void {
|
||||||
if(shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return;
|
if(shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return;
|
||||||
this.tabs.splice(this.tabs.indexOf(tab), 1);
|
this.tabs.splice(this.tabs.indexOf(tab), 1);
|
||||||
|
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||||
delete this.tabMap[tab.view.webContents.id];
|
delete this.tabMap[tab.view.webContents.id];
|
||||||
tab.tray.destroy();
|
tab.tray.destroy();
|
||||||
tab.view.webContents.loadURL('about:blank');
|
tab.view.webContents.loadURL('about:blank');
|
||||||
|
@ -210,12 +220,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
openMenu(): void {
|
openMenu(): void {
|
||||||
electron.remote.Menu.getApplicationMenu().popup();
|
electron.remote.Menu.getApplicationMenu()!.popup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="scss">
|
||||||
#window-tabs {
|
#window-tabs {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
.btn {
|
.btn {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "fchat",
|
"name": "fchat",
|
||||||
"version": "0.2.16",
|
"version": "0.2.17",
|
||||||
"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",
|
||||||
|
|
|
@ -29,11 +29,8 @@
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
||||||
*/
|
*/
|
||||||
import 'bootstrap/js/collapse.js';
|
|
||||||
import 'bootstrap/js/dropdown.js';
|
|
||||||
import 'bootstrap/js/tab.js';
|
|
||||||
import 'bootstrap/js/transition.js';
|
|
||||||
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';
|
||||||
import * as qs from 'querystring';
|
import * as qs from 'querystring';
|
||||||
import * as Raven from 'raven-js';
|
import * as Raven from 'raven-js';
|
||||||
|
@ -41,12 +38,13 @@ import Vue from 'vue';
|
||||||
import {getKey} from '../chat/common';
|
import {getKey} from '../chat/common';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import VueRaven from '../chat/vue-raven';
|
import VueRaven from '../chat/vue-raven';
|
||||||
|
import {Keys} from '../keys';
|
||||||
import {GeneralSettings, nativeRequire} from './common';
|
import {GeneralSettings, nativeRequire} from './common';
|
||||||
import * as SlimcatImporter from './importer';
|
import * as SlimcatImporter from './importer';
|
||||||
import Index from './Index.vue';
|
import Index from './Index.vue';
|
||||||
|
|
||||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if(e.ctrlKey && e.shiftKey && getKey(e) === 'i')
|
if(e.ctrlKey && e.shiftKey && getKey(e) === Keys.KeyI)
|
||||||
electron.remote.getCurrentWebContents().toggleDevTools();
|
electron.remote.getCurrentWebContents().toggleDevTools();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,8 +52,10 @@ process.env.SPELLCHECKER_PREFER_HUNSPELL = '1';
|
||||||
const sc = nativeRequire<{
|
const sc = nativeRequire<{
|
||||||
Spellchecker: {
|
Spellchecker: {
|
||||||
new(): {
|
new(): {
|
||||||
isMisspelled(x: string): boolean,
|
add(word: string): void
|
||||||
setDictionary(name: string | undefined, dir: string): void,
|
remove(word: string): void
|
||||||
|
isMisspelled(x: string): boolean
|
||||||
|
setDictionary(name: string | undefined, dir: string): void
|
||||||
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,14 +122,30 @@ webContents.on('context-menu', (_, props) => {
|
||||||
});
|
});
|
||||||
if(props.misspelledWord !== '') {
|
if(props.misspelledWord !== '') {
|
||||||
const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
|
const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
|
||||||
if(corrections.length > 0) {
|
menuTemplate.unshift({
|
||||||
menuTemplate.unshift({type: 'separator'});
|
label: l('spellchecker.add'),
|
||||||
|
click: () => {
|
||||||
|
if(customDictionary.indexOf(props.misspelledWord) !== -1) return;
|
||||||
|
spellchecker.add(props.misspelledWord);
|
||||||
|
customDictionary.push(props.misspelledWord);
|
||||||
|
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
|
||||||
|
}
|
||||||
|
}, {type: 'separator'});
|
||||||
|
if(corrections.length > 0)
|
||||||
menuTemplate.unshift(...corrections.map((correction: string) => ({
|
menuTemplate.unshift(...corrections.map((correction: string) => ({
|
||||||
label: correction,
|
label: correction,
|
||||||
click: () => webContents.replaceMisspelling(correction)
|
click: () => webContents.replaceMisspelling(correction)
|
||||||
})));
|
})));
|
||||||
}
|
else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')});
|
||||||
}
|
} else if(customDictionary.indexOf(props.selectionText) !== -1)
|
||||||
|
menuTemplate.unshift({
|
||||||
|
label: l('spellchecker.remove'),
|
||||||
|
click: () => {
|
||||||
|
spellchecker.remove(props.selectionText);
|
||||||
|
customDictionary.splice(customDictionary.indexOf(props.selectionText), 1);
|
||||||
|
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
|
||||||
|
}
|
||||||
|
}, {type: 'separator'});
|
||||||
|
|
||||||
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup();
|
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup();
|
||||||
});
|
});
|
||||||
|
@ -151,6 +167,10 @@ if(params['import'] !== undefined)
|
||||||
}
|
}
|
||||||
spellchecker.setDictionary(settings.spellcheckLang, dictDir);
|
spellchecker.setDictionary(settings.spellcheckLang, dictDir);
|
||||||
|
|
||||||
|
const customDictionaryPath = path.join(settings.logDirectory, 'words');
|
||||||
|
const customDictionary = fs.existsSync(customDictionaryPath) ? <string[]>JSON.parse(fs.readFileSync(customDictionaryPath, 'utf8')) : [];
|
||||||
|
for(const word of customDictionary) spellchecker.add(word);
|
||||||
|
|
||||||
//tslint:disable-next-line:no-unused-expression
|
//tslint:disable-next-line:no-unused-expression
|
||||||
new Index({
|
new Index({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class GeneralSettings {
|
||||||
spellcheckLang: string | undefined = 'en-GB';
|
spellcheckLang: string | undefined = 'en-GB';
|
||||||
theme = 'default';
|
theme = 'default';
|
||||||
version = electron.app.getVersion();
|
version = electron.app.getVersion();
|
||||||
|
beta = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkdir(dir: string): void {
|
export function mkdir(dir: string): void {
|
||||||
|
|
|
@ -6,7 +6,13 @@ import {Message as MessageImpl} from '../chat/common';
|
||||||
import core from '../chat/core';
|
import core from '../chat/core';
|
||||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||||
import l from '../chat/localize';
|
import l from '../chat/localize';
|
||||||
import {mkdir} from './common';
|
import {GeneralSettings, mkdir} from './common';
|
||||||
|
|
||||||
|
declare module '../chat/interfaces' {
|
||||||
|
interface State {
|
||||||
|
generalSettings?: GeneralSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dayMs = 86400000;
|
const dayMs = 86400000;
|
||||||
|
|
||||||
|
@ -204,9 +210,12 @@ function getSettingsDir(character: string = core.connection.character): string {
|
||||||
|
|
||||||
export class SettingsStore implements Settings.Store {
|
export class SettingsStore implements Settings.Store {
|
||||||
async get<K extends keyof Settings.Keys>(key: K, character?: string): Promise<Settings.Keys[K] | undefined> {
|
async get<K extends keyof Settings.Keys>(key: K, character?: string): Promise<Settings.Keys[K] | undefined> {
|
||||||
const file = path.join(getSettingsDir(character), key);
|
try {
|
||||||
if(!fs.existsSync(file)) return undefined;
|
const file = path.join(getSettingsDir(character), key);
|
||||||
return <Settings.Keys[K]>JSON.parse(fs.readFileSync(file, 'utf8'));
|
return <Settings.Keys[K]>JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
} catch(e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>F-Chat</title>
|
<title>F-Chat</title>
|
||||||
|
<link href="fa.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|
|
@ -100,6 +100,7 @@ async function setDictionary(lang: string | undefined): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsDir = path.join(electron.app.getPath('userData'), 'data');
|
const settingsDir = path.join(electron.app.getPath('userData'), 'data');
|
||||||
|
mkdir(settingsDir);
|
||||||
const file = path.join(settingsDir, 'settings');
|
const file = path.join(settingsDir, 'settings');
|
||||||
const settings = new GeneralSettings();
|
const settings = new GeneralSettings();
|
||||||
let shouldImportSettings = false;
|
let shouldImportSettings = false;
|
||||||
|
@ -137,7 +138,7 @@ async function addSpellcheckerItems(menu: Electron.Menu): Promise<void> {
|
||||||
function setUpWebContents(webContents: Electron.WebContents): void {
|
function setUpWebContents(webContents: Electron.WebContents): void {
|
||||||
const openLinkExternally = (e: Event, linkUrl: string) => {
|
const openLinkExternally = (e: Event, linkUrl: string) => {
|
||||||
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 electron.shell.openExternal(linkUrl);
|
||||||
};
|
};
|
||||||
|
@ -179,6 +180,7 @@ function showPatchNotes(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReady(): void {
|
function onReady(): void {
|
||||||
|
app.setAppUserModelId('net.f-list.f-chat');
|
||||||
app.on('open-file', createWindow);
|
app.on('open-file', createWindow);
|
||||||
|
|
||||||
if(settings.version !== app.getVersion()) {
|
if(settings.version !== app.getVersion()) {
|
||||||
|
@ -188,6 +190,7 @@ function onReady(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(process.env.NODE_ENV === 'production') {
|
if(process.env.NODE_ENV === 'production') {
|
||||||
|
if(settings.beta) autoUpdater.channel = 'beta';
|
||||||
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
|
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
|
||||||
const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
|
const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
|
||||||
let hasUpdate = false;
|
let hasUpdate = false;
|
||||||
|
@ -195,12 +198,15 @@ function onReady(): void {
|
||||||
clearInterval(updateTimer);
|
clearInterval(updateTimer);
|
||||||
if(hasUpdate) return;
|
if(hasUpdate) return;
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
const menu = electron.Menu.getApplicationMenu();
|
const menu = electron.Menu.getApplicationMenu()!;
|
||||||
menu.append(new electron.MenuItem({
|
menu.append(new electron.MenuItem({
|
||||||
label: l('action.updateAvailable'),
|
label: l('action.updateAvailable'),
|
||||||
submenu: electron.Menu.buildFromTemplate([{
|
submenu: electron.Menu.buildFromTemplate([{
|
||||||
label: l('action.update'),
|
label: l('action.update'),
|
||||||
click: () => autoUpdater.quitAndInstall(false, true)
|
click: () => {
|
||||||
|
for(const w of windows) w.webContents.send('quit');
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
label: l('help.changelog'),
|
label: l('help.changelog'),
|
||||||
click: showPatchNotes
|
click: showPatchNotes
|
||||||
|
@ -288,6 +294,13 @@ function onReady(): void {
|
||||||
label: x,
|
label: x,
|
||||||
type: <'radio'>'radio'
|
type: <'radio'>'radio'
|
||||||
}))
|
}))
|
||||||
|
}, {
|
||||||
|
label: l('settings.beta'), type: 'checkbox', checked: settings.beta,
|
||||||
|
click: (item: Electron.MenuItem) => {
|
||||||
|
settings.beta = item.checked;
|
||||||
|
setGeneralSettings(settings);
|
||||||
|
autoUpdater.channel = item.checked ? 'beta' : 'latest';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{type: 'separator'},
|
{type: 'separator'},
|
||||||
{role: 'minimize'},
|
{role: 'minimize'},
|
||||||
|
|
|
@ -5,21 +5,10 @@
|
||||||
"description": "F-List.net Chat Client",
|
"description": "F-List.net Chat Client",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"keytar": "^4.0.4",
|
|
||||||
"spellchecker": "^3.4.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"electron": "^1.8.1",
|
|
||||||
"electron-builder": "^19.33.0",
|
|
||||||
"electron-log": "^2.2.9",
|
|
||||||
"electron-updater": "^2.8.9",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../node_modules/.bin/webpack",
|
"build": "node ../webpack development",
|
||||||
"build:dist": "../node_modules/.bin/webpack --env production",
|
"build:dist": "node ../webpack production",
|
||||||
"watch": "../node_modules/.bin/webpack --watch",
|
"watch": "node ../webpack watch",
|
||||||
"start": "electron app"
|
"start": "electron app"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
@ -43,8 +32,7 @@
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "generic",
|
"provider": "generic",
|
||||||
"url": "https://client.f-list.net/",
|
"url": "https://client.f-list.net/"
|
||||||
"channel": "latest"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"noEmitHelpers": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true
|
||||||
|
},
|
||||||
|
"include": ["main.ts"]
|
||||||
|
}
|
|
@ -13,10 +13,5 @@
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true
|
"noUnusedParameters": true
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "../**/*.d.ts"],
|
"include": ["chat.ts", "window.ts", "../**/*.d.ts"]
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
"app"
|
|
||||||
]
|
|
||||||
}
|
}
|
|
@ -1,11 +1,8 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const UglifyPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
const exportLoader = require('../export-loader');
|
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
|
|
||||||
const mainConfig = {
|
const mainConfig = {
|
||||||
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'application.json')],
|
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'application.json')],
|
||||||
|
@ -16,16 +13,16 @@ const mainConfig = {
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
target: 'electron-main',
|
target: 'electron-main',
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
loader: 'ts-loader',
|
loader: 'ts-loader',
|
||||||
options: {
|
options: {
|
||||||
configFile: __dirname + '/tsconfig.json',
|
configFile: __dirname + '/tsconfig-main.json',
|
||||||
transpileOnly: true
|
transpileOnly: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{test: /application.json$/, loader: 'file-loader?name=package.json'},
|
{test: path.join(__dirname, 'application.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
|
||||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
|
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -34,16 +31,15 @@ const mainConfig = {
|
||||||
__filename: false
|
__filename: false
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}),
|
new ForkTsCheckerWebpackPlugin({
|
||||||
exportLoader.delayTypecheck
|
workers: 2,
|
||||||
|
async: false,
|
||||||
|
tslint: path.join(__dirname, '../tslint.json'),
|
||||||
|
tsconfig: './tsconfig-main.json'
|
||||||
|
})
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js']
|
extensions: ['.ts', '.js']
|
||||||
},
|
|
||||||
resolveLoader: {
|
|
||||||
modules: [
|
|
||||||
'node_modules', path.join(__dirname, '../')
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}, rendererConfig = {
|
}, rendererConfig = {
|
||||||
entry: {
|
entry: {
|
||||||
|
@ -57,12 +53,11 @@ const mainConfig = {
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
target: 'electron-renderer',
|
target: 'electron-renderer',
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: 'vue-loader',
|
loader: 'vue-loader',
|
||||||
options: {
|
options: {
|
||||||
preLoaders: {ts: 'export-loader'},
|
|
||||||
preserveWhitespace: false
|
preserveWhitespace: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -71,7 +66,7 @@ const mainConfig = {
|
||||||
loader: 'ts-loader',
|
loader: 'ts-loader',
|
||||||
options: {
|
options: {
|
||||||
appendTsSuffixTo: [/\.vue$/],
|
appendTsSuffixTo: [/\.vue$/],
|
||||||
configFile: __dirname + '/tsconfig.json',
|
configFile: __dirname + '/tsconfig-renderer.json',
|
||||||
transpileOnly: true
|
transpileOnly: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -88,48 +83,45 @@ const mainConfig = {
|
||||||
__filename: false
|
__filename: false
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ProvidePlugin({
|
new ForkTsCheckerWebpackPlugin({
|
||||||
'$': 'jquery/dist/jquery.slim.js',
|
workers: 2,
|
||||||
'jQuery': 'jquery/dist/jquery.slim.js',
|
async: false,
|
||||||
'window.jQuery': 'jquery/dist/jquery.slim.js'
|
tslint: path.join(__dirname, '../tslint.json'),
|
||||||
}),
|
tsconfig: './tsconfig-renderer.json',
|
||||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}),
|
vue: true
|
||||||
new CommonsChunkPlugin({name: 'common', minChunks: 2}),
|
})
|
||||||
exportLoader.delayTypecheck
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js', '.vue', '.css'],
|
extensions: ['.ts', '.js', '.vue', '.css'],
|
||||||
alias: {qs: path.join(__dirname, 'qs.ts')}
|
alias: {qs: path.join(__dirname, 'qs.ts')}
|
||||||
},
|
},
|
||||||
resolveLoader: {
|
optimization: {
|
||||||
modules: [
|
splitChunks: {chunks: 'all', minChunks: 2, name: 'common'}
|
||||||
'node_modules', path.join(__dirname, '../')
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(env) {
|
module.exports = function(mode) {
|
||||||
const dist = env === 'production';
|
const themesDir = path.join(__dirname, '../scss/themes/chat');
|
||||||
const themesDir = path.join(__dirname, '../less/themes/chat');
|
|
||||||
const themes = fs.readdirSync(themesDir);
|
const themes = fs.readdirSync(themesDir);
|
||||||
const cssOptions = {use: [{loader: 'css-loader', options: {minimize: dist}}, 'less-loader']};
|
const cssOptions = {use: ['css-loader', 'sass-loader']};
|
||||||
for(const theme of themes) {
|
for(const theme of themes) {
|
||||||
if(!theme.endsWith('.less')) continue;
|
if(!theme.endsWith('.scss')) continue;
|
||||||
const absPath = path.join(themesDir, theme);
|
const absPath = path.join(themesDir, theme);
|
||||||
rendererConfig.entry.chat.push(absPath);
|
rendererConfig.entry.chat.push(absPath);
|
||||||
const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css');
|
const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css');
|
||||||
rendererConfig.plugins.push(plugin);
|
rendererConfig.plugins.push(plugin);
|
||||||
rendererConfig.module.loaders.push({test: absPath, use: plugin.extract(cssOptions)});
|
rendererConfig.module.rules.push({test: absPath, use: plugin.extract(cssOptions)});
|
||||||
}
|
}
|
||||||
if(dist) {
|
const faPath = path.join(themesDir, '../../fa.scss');
|
||||||
|
rendererConfig.entry.chat.push(faPath);
|
||||||
|
const faPlugin = new ExtractTextPlugin('./fa.css');
|
||||||
|
rendererConfig.plugins.push(faPlugin);
|
||||||
|
rendererConfig.module.rules.push({test: faPath, use: faPlugin.extract(cssOptions)});
|
||||||
|
if(mode === 'production') {
|
||||||
mainConfig.devtool = rendererConfig.devtool = 'source-map';
|
mainConfig.devtool = rendererConfig.devtool = 'source-map';
|
||||||
const plugins = [new UglifyPlugin({sourceMap: true}),
|
rendererConfig.plugins.push(new OptimizeCssAssetsPlugin());
|
||||||
new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}),
|
|
||||||
new webpack.LoaderOptionsPlugin({minimize: true})];
|
|
||||||
mainConfig.plugins.push(...plugins);
|
|
||||||
rendererConfig.plugins.push(...plugins);
|
|
||||||
} else {
|
} else {
|
||||||
//config.devtool = 'cheap-module-eval-source-map';
|
mainConfig.devtool = rendererConfig.devtool = 'none';
|
||||||
}
|
}
|
||||||
return [mainConfig, rendererConfig];
|
return [mainConfig, rendererConfig];
|
||||||
};
|
};
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>F-Chat</title>
|
<title>F-Chat</title>
|
||||||
|
<link href="fa.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
1862
electron/yarn.lock
1862
electron/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
module.exports = function(source) {
|
|
||||||
fs.writeFileSync(this.resourcePath + '.ts', source);
|
|
||||||
return source;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.delayTypecheck = function() {
|
|
||||||
let callback;
|
|
||||||
this.plugin('fork-ts-checker-service-before-start', (c) => callback = c);
|
|
||||||
this.plugin('after-compile', (compilation, c) => {
|
|
||||||
if(compilation.compiler.parentCompilation) return c();
|
|
||||||
callback();
|
|
||||||
c();
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -33,7 +33,7 @@ function sortMember(this: void | never, array: SortableMember[], member: Sortabl
|
||||||
|
|
||||||
class Channel implements Interfaces.Channel {
|
class Channel implements Interfaces.Channel {
|
||||||
description = '';
|
description = '';
|
||||||
opList: string[];
|
opList: string[] = [];
|
||||||
owner = '';
|
owner = '';
|
||||||
mode: Interfaces.Mode = 'both';
|
mode: Interfaces.Mode = 'both';
|
||||||
members: {[key: string]: SortableMember | undefined} = {};
|
members: {[key: string]: SortableMember | undefined} = {};
|
||||||
|
@ -163,16 +163,18 @@ export default function(this: void, connection: Connection, characters: Characte
|
||||||
const item = state.getChannelItem(data.channel);
|
const item = state.getChannelItem(data.channel);
|
||||||
if(data.character.identity === connection.character) {
|
if(data.character.identity === connection.character) {
|
||||||
const id = data.channel.toLowerCase();
|
const id = data.channel.toLowerCase();
|
||||||
|
if(state.joinedMap[id] !== undefined) return;
|
||||||
const channel = state.joinedMap[id] = new Channel(id, decodeHTML(data.title));
|
const channel = state.joinedMap[id] = new Channel(id, decodeHTML(data.title));
|
||||||
state.joinedChannels.push(channel);
|
state.joinedChannels.push(channel);
|
||||||
if(item !== undefined) item.isJoined = true;
|
if(item !== undefined) item.isJoined = true;
|
||||||
} else {
|
} else {
|
||||||
const channel = state.getChannel(data.channel);
|
const channel = state.getChannel(data.channel);
|
||||||
if(channel === undefined) return state.leave(data.channel);
|
if(channel === undefined) return state.leave(data.channel);
|
||||||
|
if(channel.members[data.character.identity] !== undefined) return;
|
||||||
const member = channel.createMember(characters.get(data.character.identity));
|
const member = channel.createMember(characters.get(data.character.identity));
|
||||||
await channel.addMember(member);
|
await channel.addMember(member);
|
||||||
if(item !== undefined) item.memberCount++;
|
|
||||||
}
|
}
|
||||||
|
if(item !== undefined) item.memberCount++;
|
||||||
});
|
});
|
||||||
connection.onMessage('ICH', async(data) => {
|
connection.onMessage('ICH', async(data) => {
|
||||||
const channel = state.getChannel(data.channel);
|
const channel = state.getChannel(data.channel);
|
||||||
|
@ -214,7 +216,6 @@ export default function(this: void, connection: Connection, characters: Characte
|
||||||
connection.onMessage('COA', (data) => {
|
connection.onMessage('COA', (data) => {
|
||||||
const channel = state.getChannel(data.channel);
|
const channel = state.getChannel(data.channel);
|
||||||
if(channel === undefined) return state.leave(data.channel);
|
if(channel === undefined) return state.leave(data.channel);
|
||||||
channel.opList.push(data.character);
|
|
||||||
const member = channel.members[data.character];
|
const member = channel.members[data.character];
|
||||||
if(member === undefined || member.rank === Interfaces.Rank.Owner) return;
|
if(member === undefined || member.rank === Interfaces.Rank.Owner) return;
|
||||||
member.rank = Interfaces.Rank.Op;
|
member.rank = Interfaces.Rank.Op;
|
||||||
|
@ -229,7 +230,6 @@ export default function(this: void, connection: Connection, characters: Characte
|
||||||
connection.onMessage('COR', (data) => {
|
connection.onMessage('COR', (data) => {
|
||||||
const channel = state.getChannel(data.channel);
|
const channel = state.getChannel(data.channel);
|
||||||
if(channel === undefined) return state.leave(data.channel);
|
if(channel === undefined) return state.leave(data.channel);
|
||||||
channel.opList.splice(channel.opList.indexOf(data.character), 1);
|
|
||||||
const member = channel.members[data.character];
|
const member = channel.members[data.character];
|
||||||
if(member === undefined || member.rank === Interfaces.Rank.Owner) return;
|
if(member === undefined || member.rank === Interfaces.Rank.Owner) return;
|
||||||
member.rank = Interfaces.Rank.Member;
|
member.rank = Interfaces.Rank.Member;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {decodeHTML} from './common';
|
||||||
import {Character as Interfaces, Connection} from './interfaces';
|
import {Character as Interfaces, Connection} from './interfaces';
|
||||||
|
|
||||||
class Character implements Interfaces.Character {
|
class Character implements Interfaces.Character {
|
||||||
gender: Interfaces.Gender;
|
gender: Interfaces.Gender = 'None';
|
||||||
status: Interfaces.Status = 'offline';
|
status: Interfaces.Status = 'offline';
|
||||||
statusText = '';
|
statusText = '';
|
||||||
isFriend = false;
|
isFriend = false;
|
||||||
|
|
|
@ -10,15 +10,15 @@ 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: string;
|
character = '';
|
||||||
vars: Interfaces.Vars & {[key: string]: string} = <any>{}; //tslint:disable-line:no-any
|
vars: Interfaces.Vars & {[key: string]: string} = <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>[]} = {};
|
private messageHandlers: {[key in keyof Interfaces.ServerCommands]?: Interfaces.CommandHandler<key>[]} = {};
|
||||||
private connectionHandlers: {[key in Interfaces.EventType]?: Interfaces.EventHandler[]} = {};
|
private connectionHandlers: {[key in Interfaces.EventType]?: Interfaces.EventHandler[]} = {};
|
||||||
private errorHandlers: ((error: Error) => void)[] = [];
|
private errorHandlers: ((error: Error) => void)[] = [];
|
||||||
private ticket: string;
|
private ticket = '';
|
||||||
private cleanClose = false;
|
private cleanClose = false;
|
||||||
private reconnectTimer: NodeJS.Timer;
|
private reconnectTimer: NodeJS.Timer | undefined;
|
||||||
private ticketProvider: Interfaces.TicketProvider;
|
private ticketProvider: Interfaces.TicketProvider;
|
||||||
private reconnectDelay = 0;
|
private reconnectDelay = 0;
|
||||||
private isReconnect = false;
|
private isReconnect = false;
|
||||||
|
@ -86,7 +86,7 @@ export default class Connection implements Interfaces.Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
clearTimeout(this.reconnectTimer);
|
if(this.reconnectTimer !== undefined) clearTimeout(this.reconnectTimer);
|
||||||
this.cleanClose = true;
|
this.cleanClose = true;
|
||||||
if(this.socket !== undefined) this.socket.close();
|
if(this.socket !== undefined) this.socket.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
export const enum Keys {
|
||||||
|
Backspace = 8,
|
||||||
|
Tab = 9,
|
||||||
|
Enter = 13,
|
||||||
|
Shift = 16,
|
||||||
|
Ctrl = 17,
|
||||||
|
Alt = 18,
|
||||||
|
Pause = 19,
|
||||||
|
CapsLock = 20,
|
||||||
|
Escape = 27,
|
||||||
|
Space = 32,
|
||||||
|
PageUp = 33,
|
||||||
|
PageDown = 34,
|
||||||
|
End = 35,
|
||||||
|
Home = 36,
|
||||||
|
|
||||||
|
ArrowLeft = 37,
|
||||||
|
ArrowUp = 38,
|
||||||
|
ArrowRight = 39,
|
||||||
|
ArrowDown = 40,
|
||||||
|
|
||||||
|
PrintScreen = 44,
|
||||||
|
Insert = 45,
|
||||||
|
Delete = 46,
|
||||||
|
|
||||||
|
Digit0 = 48,
|
||||||
|
Digit1 = 49,
|
||||||
|
Digit2 = 50,
|
||||||
|
Digit3 = 51,
|
||||||
|
Digit4 = 52,
|
||||||
|
Digit5 = 53,
|
||||||
|
Digit6 = 54,
|
||||||
|
Digit7 = 55,
|
||||||
|
Digit8 = 56,
|
||||||
|
Digit9 = 57,
|
||||||
|
|
||||||
|
KeyA = 65,
|
||||||
|
KeyB = 66,
|
||||||
|
KeyC = 67,
|
||||||
|
KeyD = 68,
|
||||||
|
KeyE = 69,
|
||||||
|
KeyF = 70,
|
||||||
|
KeyG = 71,
|
||||||
|
KeyH = 72,
|
||||||
|
KeyI = 73,
|
||||||
|
KeyJ = 74,
|
||||||
|
KeyK = 75,
|
||||||
|
KeyL = 76,
|
||||||
|
KeyM = 77,
|
||||||
|
KeyN = 78,
|
||||||
|
KeyO = 79,
|
||||||
|
KeyP = 80,
|
||||||
|
KeyQ = 81,
|
||||||
|
KeyR = 82,
|
||||||
|
KeyS = 83,
|
||||||
|
KeyT = 84,
|
||||||
|
KeyU = 85,
|
||||||
|
KeyV = 86,
|
||||||
|
KeyW = 87,
|
||||||
|
KeyX = 88,
|
||||||
|
KeyY = 89,
|
||||||
|
KeyZ = 90,
|
||||||
|
|
||||||
|
LeftWindowKey = 91,
|
||||||
|
RightWindowKey = 92,
|
||||||
|
SelectKey = 93,
|
||||||
|
|
||||||
|
Numpad0 = 96,
|
||||||
|
Numpad1 = 97,
|
||||||
|
Numpad2 = 98,
|
||||||
|
Numpad3 = 99,
|
||||||
|
Numpad4 = 100,
|
||||||
|
Numpad5 = 101,
|
||||||
|
Numpad6 = 102,
|
||||||
|
Numpad7 = 103,
|
||||||
|
Numpad8 = 104,
|
||||||
|
Numpad9 = 105,
|
||||||
|
|
||||||
|
NumpadMultiply = 106,
|
||||||
|
NumpadAdd = 107,
|
||||||
|
NumpadSubtract = 109,
|
||||||
|
NumpadDecimal = 110,
|
||||||
|
NumpadDivide = 111,
|
||||||
|
|
||||||
|
F1 = 112,
|
||||||
|
F2 = 113,
|
||||||
|
F3 = 114,
|
||||||
|
F4 = 115,
|
||||||
|
F5 = 116,
|
||||||
|
F6 = 117,
|
||||||
|
F7 = 118,
|
||||||
|
F8 = 119,
|
||||||
|
F9 = 120,
|
||||||
|
F10 = 121,
|
||||||
|
F11 = 122,
|
||||||
|
F12 = 123,
|
||||||
|
|
||||||
|
NumLock = 144,
|
||||||
|
ScrollLock = 145,
|
||||||
|
|
||||||
|
Semicolon = 186,
|
||||||
|
Equal = 187,
|
||||||
|
Comma = 188,
|
||||||
|
Minus = 189,
|
||||||
|
Period = 190,
|
||||||
|
ForwardSlash = 191,
|
||||||
|
Backquote = 192,
|
||||||
|
|
||||||
|
BracketLeft = 219,
|
||||||
|
BracketRight = 221,
|
||||||
|
Quote = 222
|
||||||
|
}
|
126
less/bbcode.less
126
less/bbcode.less
|
@ -1,126 +0,0 @@
|
||||||
.redText {
|
|
||||||
color: @red-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blueText {
|
|
||||||
color: @blue-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greenText {
|
|
||||||
color: @green-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellowText {
|
|
||||||
color: @yellow-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cyanText {
|
|
||||||
color: @cyan-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purpleText {
|
|
||||||
color: @purple-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brownText {
|
|
||||||
color: @brown-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinkText {
|
|
||||||
color: @pink-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grayText {
|
|
||||||
color: @gray-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orangeText {
|
|
||||||
color: @orange-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whiteText {
|
|
||||||
color: @white-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blackText {
|
|
||||||
color: @black-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tweak these to be consistent with how bootstrap does sizing. */
|
|
||||||
span.bigText {
|
|
||||||
font-size: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.smallText {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.leftText {
|
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.centerText {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.rightText {
|
|
||||||
display: block;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.justifyText {
|
|
||||||
display: block;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.indentText {
|
|
||||||
padding-left: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-avatar {
|
|
||||||
display: inline;
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
&.icon {
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapseHeaderText {
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
min-height: @line-height-computed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapseHeader {
|
|
||||||
.well;
|
|
||||||
padding: 5px;
|
|
||||||
border-color: @collapse-border;
|
|
||||||
background-color: @collapse-header-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapseBlock {
|
|
||||||
max-height: 0;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
transition: max-height 0.2s;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.styledText, .bbcode {
|
|
||||||
.force-word-wrapping();
|
|
||||||
max-width: 100%;
|
|
||||||
a {
|
|
||||||
text-decoration: underline;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-domain {
|
|
||||||
color: @gray-light;
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
.bbcodeEditorButton {
|
|
||||||
.btn-default();
|
|
||||||
padding: (@padding-base-vertical/2.0) (@padding-base-horizontal/2.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbcodeTextAreaTextArea {
|
|
||||||
textarea& {
|
|
||||||
min-height: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbcodePreviewWarnings {
|
|
||||||
.alert();
|
|
||||||
.alert-danger();
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbcode-toolbar {
|
|
||||||
@media (max-width: @screen-xs-max) {
|
|
||||||
background: @text-background-color;
|
|
||||||
padding: 10px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
z-index: 20;
|
|
||||||
display: none;
|
|
||||||
.btn {
|
|
||||||
margin: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: @screen-sm-min) {
|
|
||||||
.btn-group();
|
|
||||||
.close {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbcode-btn {
|
|
||||||
@media (min-width: @screen-sm-min) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
261
less/chat.less
261
less/chat.less
|
@ -1,261 +0,0 @@
|
||||||
.bg-solid-text {
|
|
||||||
background: @text-background-color
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-preview {
|
|
||||||
background: @text-background-color;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
max-width: 40%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
font-size: 12px;
|
|
||||||
position: fixed;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
z-index: 100000;
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-new {
|
|
||||||
background-color: @state-danger-bg !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-disable {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0.8;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1;
|
|
||||||
background: #ddd;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-wrapper {
|
|
||||||
.modal-backdrop {
|
|
||||||
display: none;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
.modal-backdrop {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.body {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: @body-bg;
|
|
||||||
z-index: 10;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin: -10px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: none;
|
|
||||||
width: 200px;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expander {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
padding: 5px 6px;
|
|
||||||
border-color: @btn-default-border;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
@media (min-width: @screen-sm-min) {
|
|
||||||
.name {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .name {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sidebar-left {
|
|
||||||
border-right: solid 1px @panel-default-border;
|
|
||||||
left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
.expander {
|
|
||||||
transform: rotate(270deg) translate3d(0, 0, 0);
|
|
||||||
transform-origin: 100% 0;
|
|
||||||
-webkit-transform: rotate(270deg) translate3d(0, 0, 0);
|
|
||||||
-webkit-transform-origin: 100% 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sidebar-right {
|
|
||||||
border-left: solid 1px @panel-default-border;
|
|
||||||
right: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
|
|
||||||
.expander {
|
|
||||||
transform: rotate(90deg) translate3d(0, 0, 0);
|
|
||||||
transform-origin: 0 0;
|
|
||||||
-webkit-transform: rotate(90deg) translate3d(0, 0, 0);
|
|
||||||
-webkit-transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-fixed() {
|
|
||||||
position: static;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
.body {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.expander {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-text-box {
|
|
||||||
min-height: initial !important;
|
|
||||||
max-height: 250px;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ads-text-box {
|
|
||||||
background-color: @state-info-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-top {
|
|
||||||
border-top: solid 1px @panel-default-border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-bottom {
|
|
||||||
border-bottom: solid 1px @panel-default-border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-block {
|
|
||||||
padding: 1px 0;
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: solid 1px @panel-default-border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-warn {
|
|
||||||
background-color: @state-danger-bg;
|
|
||||||
color: @state-danger-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages-both {
|
|
||||||
.message-ad {
|
|
||||||
background-color: @brand-info;
|
|
||||||
padding: 0 2px 2px 2px;
|
|
||||||
box-shadow: @gray -2px -2px 2px inset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-event {
|
|
||||||
color: @gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-highlight {
|
|
||||||
background-color: @state-success-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-action .bbcode {
|
|
||||||
font-style: italic;
|
|
||||||
i, em {
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-read {
|
|
||||||
border-bottom: solid 2px @state-success-bg !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa.active {
|
|
||||||
color: @brand-success;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-shemale {
|
|
||||||
color: #CC66FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-herm {
|
|
||||||
color: #9B30FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-none {
|
|
||||||
color: @gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-female {
|
|
||||||
color: #FF6699;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-male {
|
|
||||||
color: #6699FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-male-herm {
|
|
||||||
color: #007FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-transgender {
|
|
||||||
color: #EE8822;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gender-cunt-boy {
|
|
||||||
color: #00CC66;
|
|
||||||
}
|
|
||||||
|
|
||||||
#character-page-sidebar {
|
|
||||||
margin-top: 0; // Fix up hack for merging the header on the character page, which doesn't work on chat.
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-viewer {
|
|
||||||
width: 98%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#window-tabs .hasNew > a {
|
|
||||||
background-color: @state-warning-bg;
|
|
||||||
border-color: @state-warning-border;
|
|
||||||
color: @state-warning-text;
|
|
||||||
&:hover {
|
|
||||||
background-color: @state-warning-border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-text {
|
|
||||||
margin-left: 3px;
|
|
||||||
@media (max-width: @screen-xs-max) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
@comment-grid-columns: 50;
|
|
||||||
.comment-offset-1 {
|
|
||||||
margin-left: percentage((1 / @comment-grid-columns));
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-well {
|
|
||||||
.well();
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-well.warning {
|
|
||||||
background-color: @state-warning-bg;
|
|
||||||
border-color: @state-warning-border;
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
.flash-messages-fixed {
|
|
||||||
top: 0px;
|
|
||||||
left: auto;
|
|
||||||
right: auto;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message {
|
|
||||||
.alert();
|
|
||||||
position: relative;
|
|
||||||
border-bottom-color: rgba(0, 0, 0, 0.3);
|
|
||||||
margin-bottom: 0;
|
|
||||||
z-index: 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-enter-active, .flash-message-leave-active {
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-enter, .flash-message-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-menu-item {
|
|
||||||
width: 250px;
|
|
||||||
.character-link {
|
|
||||||
display: inline-block;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
.character-edit-link {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 3px 20px 2px 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-top-padded {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.force-word-wrapping {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
//-ms-word-break: break-all;
|
|
||||||
word-break: break-word; // Non standard form used in some browsers.
|
|
||||||
//word-break: break-all;
|
|
||||||
|
|
||||||
-ms-hyphens: auto;
|
|
||||||
-moz-hyphens: auto;
|
|
||||||
-webkit-hyphens: auto;
|
|
||||||
hyphens: auto;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
hr {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog.modal-wide {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix weird style where this is overwritten and cannot be styled inside a well.
|
|
||||||
.well {
|
|
||||||
// The default of 19 doesn't match any existing elements, which use either 15 or @padding-vertical/horizontal-base
|
|
||||||
padding: 15px;
|
|
||||||
blockquote {
|
|
||||||
border-color: @blockquote-border-color;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.well-lg {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@select-indicator: replace("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 2'%3E%3Cpath fill='@{input-color}' d='M2 2L0 0h4z'/%3E%3C/svg%3E", "#", "%23");
|
|
||||||
|
|
||||||
select.form-control {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background: @input-bg url(@select-indicator) no-repeat right 1rem center;
|
|
||||||
background-size: 8px 10px;
|
|
||||||
padding-right: 25px;
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
@import "~bootstrap/less/variables.less";
|
|
||||||
// BBcode colors
|
|
||||||
@red-color: #f00;
|
|
||||||
@green-color: #0f0;
|
|
||||||
@blue-color: #00f;
|
|
||||||
@yellow-color: #ff0;
|
|
||||||
@cyan-color: #0ff;
|
|
||||||
@purple-color: #c0f;
|
|
||||||
@white-color: #fff;
|
|
||||||
@black-color: #000;
|
|
||||||
@brown-color: #8a6d3b;
|
|
||||||
@pink-color: #faa;
|
|
||||||
@gray-color: #ccc;
|
|
||||||
@orange-color: #f60;
|
|
||||||
@collapse-header-bg: @well-bg;
|
|
||||||
@collapse-border: darken(@well-border, 25%);
|
|
||||||
|
|
||||||
|
|
||||||
// Character page quick kink comparison
|
|
||||||
@quick-compare-active-border: @black-color;
|
|
||||||
@quick-compare-favorite-bg: @state-info-bg;
|
|
||||||
@quick-compare-yes-bg: @state-success-bg;
|
|
||||||
@quick-compare-maybe-bg: @state-warning-bg;
|
|
||||||
@quick-compare-no-bg: @state-danger-bg;
|
|
||||||
|
|
||||||
// character page badges
|
|
||||||
@character-badge-bg: darken(@well-bg, 10%);
|
|
||||||
@character-badge-border: darken(@well-border, 10%);
|
|
||||||
@character-badge-subscriber-bg: @alert-info-bg;
|
|
||||||
@character-badge-subscriber-border: @alert-info-border;
|
|
||||||
|
|
||||||
// Character editor
|
|
||||||
@character-list-selected-border: @brand-success;
|
|
||||||
@character-image-selected-border: @brand-success;
|
|
||||||
|
|
||||||
// Notes conversation view
|
|
||||||
@note-conversation-you-bg: @alert-info-bg;
|
|
||||||
@note-conversation-you-text: @alert-info-text;
|
|
||||||
@note-conversation-you-border: @alert-info-border;
|
|
||||||
@note-conversation-them-bg: @well-bg;
|
|
||||||
@note-conversation-them-text: @text-color;
|
|
||||||
@note-conversation-them-border: @well-border;
|
|
||||||
|
|
||||||
@nav-link-hover-color: @link-color;
|
|
||||||
|
|
||||||
// General color extensions missing from bootstrap
|
|
||||||
@text-background-color: @body-bg;
|
|
||||||
@text-background-color-disabled: @gray-lighter;
|
|
||||||
|
|
||||||
@screen-sm-min: 700px;
|
|
||||||
@screen-md-min: 900px;
|
|
||||||
@container-sm: 680px;
|
|
||||||
@container-md: 880px;
|
|
|
@ -1,50 +0,0 @@
|
||||||
.tag-input-control {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)
|
|
||||||
width: 100%;
|
|
||||||
padding: @padding-base-vertical @padding-base-horizontal;
|
|
||||||
font-size: @font-size-base;
|
|
||||||
line-height: @line-height-base;
|
|
||||||
color: @input-color;
|
|
||||||
background-color: @input-bg;
|
|
||||||
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
|
|
||||||
border: 1px solid @input-border;
|
|
||||||
border-radius: @input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS.
|
|
||||||
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075));
|
|
||||||
.transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s");
|
|
||||||
|
|
||||||
.tag-input {
|
|
||||||
background-color: @input-bg;
|
|
||||||
border: none;
|
|
||||||
width: auto;
|
|
||||||
&:focus {
|
|
||||||
box-shadow: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-inline .tag-input-control {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-error {
|
|
||||||
border: 1px solid @state-danger-border;
|
|
||||||
background-color: @state-danger-bg;
|
|
||||||
.tag-input {
|
|
||||||
text-color: @state-danger-text;
|
|
||||||
background-color: @state-danger-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-important {
|
|
||||||
font-weight: bold !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-description {
|
|
||||||
display: block;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: @font-size-small;
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
@import "../variables/dark.less";
|
|
||||||
|
|
||||||
.nav-tabs > li > a:hover {
|
|
||||||
background-color: @gray-darker;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .nav-tabs > li.active > a {
|
|
||||||
background-color: @gray-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-own {
|
|
||||||
background-color: @gray-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base_chat.less";
|
|
||||||
|
|
||||||
* {
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
box-shadow: inset 0 0 8px @panel-default-border;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8);
|
|
||||||
background-color: @gray-dark;
|
|
||||||
&:hover {
|
|
||||||
background-color: @gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: @gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
@import "../variables/default.less";
|
|
||||||
|
|
||||||
.nav-tabs > li > a:hover {
|
|
||||||
background-color: @gray-darker;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .nav-tabs > li.active > a {
|
|
||||||
background-color: @gray-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-own {
|
|
||||||
background-color: @gray-darker;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base_chat.less";
|
|
||||||
|
|
||||||
* {
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
box-shadow: inset 0 0 8px @panel-default-border;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8);
|
|
||||||
background-color: @gray-dark;
|
|
||||||
&:hover {
|
|
||||||
background-color: @gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: @gray-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
@import "../variables/light.less";
|
|
||||||
|
|
||||||
.message-own {
|
|
||||||
background-color: @gray-lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base_chat.less";
|
|
||||||
|
|
||||||
* {
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
box-shadow: inset 0 0 8px @gray;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8);
|
|
||||||
background-color: @gray-lighter;
|
|
||||||
&:hover {
|
|
||||||
background-color: @gray-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: @gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
@import "../variables/dark.less";
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base.less";
|
|
|
@ -1,4 +0,0 @@
|
||||||
@import "../variables/default.less";
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base.less";
|
|
|
@ -1,4 +0,0 @@
|
||||||
@import "../variables/light.less";
|
|
||||||
|
|
||||||
// Apply variables to theme.
|
|
||||||
@import "../theme_base.less";
|
|
|
@ -1,64 +0,0 @@
|
||||||
/*!
|
|
||||||
* Bootstrap v3.3.5 (http://getbootstrap.com)
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
*/
|
|
||||||
// Core variables and mixins
|
|
||||||
//@import "variables.less"; // This file should be drawn in through a theme file and then overwritten.
|
|
||||||
@import "~bootstrap/less/mixins.less";
|
|
||||||
// Reset and dependencies
|
|
||||||
@import "~bootstrap/less/normalize.less";
|
|
||||||
//@import "print.less";
|
|
||||||
//@import "glyphicons.less";
|
|
||||||
// Core CSS
|
|
||||||
@import "~bootstrap/less/scaffolding.less";
|
|
||||||
@import "~bootstrap/less/type.less";
|
|
||||||
//@import "code.less";
|
|
||||||
@import "~bootstrap/less/grid.less";
|
|
||||||
@import "~bootstrap/less/tables.less";
|
|
||||||
@import "~bootstrap/less/forms.less";
|
|
||||||
@import "~bootstrap/less/buttons.less";
|
|
||||||
// Components
|
|
||||||
@import "~bootstrap/less/component-animations.less";
|
|
||||||
@import "~bootstrap/less/dropdowns.less";
|
|
||||||
@import "~bootstrap/less/button-groups.less";
|
|
||||||
//@import "input-groups.less";
|
|
||||||
@import "~bootstrap/less/navs.less";
|
|
||||||
@import "~bootstrap/less/navbar.less";
|
|
||||||
//@import "breadcrumbs.less";
|
|
||||||
@import "~bootstrap/less/pagination.less";
|
|
||||||
@import "~bootstrap/less/pager.less";
|
|
||||||
@import "~bootstrap/less/labels.less";
|
|
||||||
@import "~bootstrap/less/badges.less";
|
|
||||||
//@import "jumbotron.less";
|
|
||||||
//@import "thumbnails.less";
|
|
||||||
@import "~bootstrap/less/alerts.less";
|
|
||||||
//@import "progress-bars.less";
|
|
||||||
//@import "media.less";
|
|
||||||
//@import "list-group.less";
|
|
||||||
@import "~bootstrap/less/panels.less";
|
|
||||||
//@import "responsive-embed.less";
|
|
||||||
@import "~bootstrap/less/wells.less";
|
|
||||||
@import "~bootstrap/less/close.less";
|
|
||||||
// Components w/ JavaScript
|
|
||||||
@import "~bootstrap/less/modals.less";
|
|
||||||
//@import "tooltip.less";
|
|
||||||
@import "~bootstrap/less/popovers.less";
|
|
||||||
//@import "carousel.less";
|
|
||||||
// Utility classes
|
|
||||||
@import "~bootstrap/less/utilities.less";
|
|
||||||
//@import "responsive-utilities.less";
|
|
||||||
@import "~font-awesome/less/font-awesome.less";
|
|
||||||
@import "../core.less";
|
|
||||||
@import "../character_editor.less";
|
|
||||||
@import "../character_page.less";
|
|
||||||
@import "../eicons_editor.less";
|
|
||||||
@import "../bbcode_editor.less";
|
|
||||||
@import "../bbcode.less";
|
|
||||||
@import "../comments.less";
|
|
||||||
@import "../tickets.less";
|
|
||||||
@import "../notes.less";
|
|
||||||
@import "../threads.less";
|
|
||||||
@import "../kink_editor.less";
|
|
||||||
@import "../flist_overrides.less";
|
|
||||||
@import "../tag_input.less";
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*!
|
|
||||||
* Bootstrap v3.3.5 (http://getbootstrap.com)
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
*/
|
|
||||||
// Core variables and mixins
|
|
||||||
//@import "variables.less"; // This file should be drawn in through a theme file and then overwritten.
|
|
||||||
@import "~bootstrap/less/mixins.less";
|
|
||||||
// Reset and dependencies
|
|
||||||
@import "~bootstrap/less/normalize.less";
|
|
||||||
//@import "print.less";
|
|
||||||
//@import "glyphicons.less";
|
|
||||||
// Core CSS
|
|
||||||
@import "~bootstrap/less/scaffolding.less";
|
|
||||||
@import "~bootstrap/less/type.less";
|
|
||||||
//@import "code.less";
|
|
||||||
@import "~bootstrap/less/grid.less";
|
|
||||||
@import "~bootstrap/less/tables.less";
|
|
||||||
@import "~bootstrap/less/forms.less";
|
|
||||||
@import "~bootstrap/less/buttons.less";
|
|
||||||
// Components
|
|
||||||
@import "~bootstrap/less/component-animations.less";
|
|
||||||
@import "~bootstrap/less/dropdowns.less";
|
|
||||||
@import "~bootstrap/less/button-groups.less";
|
|
||||||
//@import "input-groups.less";
|
|
||||||
@import "~bootstrap/less/navs.less";
|
|
||||||
//@import "~bootstrap/less/navbar.less";
|
|
||||||
//@import "breadcrumbs.less";
|
|
||||||
//@import "~bootstrap/less/pagination.less";
|
|
||||||
//@import "~bootstrap/less/pager.less";
|
|
||||||
@import "~bootstrap/less/labels.less";
|
|
||||||
//@import "~bootstrap/less/badges.less";
|
|
||||||
//@import "jumbotron.less";
|
|
||||||
//@import "thumbnails.less";
|
|
||||||
@import "~bootstrap/less/alerts.less";
|
|
||||||
@import "~bootstrap/less/progress-bars.less";
|
|
||||||
//@import "media.less";
|
|
||||||
@import "~bootstrap/less/list-group.less";
|
|
||||||
//@import "~bootstrap/less/panels.less";
|
|
||||||
//@import "responsive-embed.less";
|
|
||||||
@import "~bootstrap/less/wells.less";
|
|
||||||
@import "~bootstrap/less/close.less";
|
|
||||||
// Components w/ JavaScript
|
|
||||||
@import "~bootstrap/less/modals.less";
|
|
||||||
//@import "tooltip.less";
|
|
||||||
@import "~bootstrap/less/popovers.less";
|
|
||||||
//@import "carousel.less";
|
|
||||||
// Utility classes
|
|
||||||
@import "~bootstrap/less/utilities.less";
|
|
||||||
//@import "responsive-utilities.less";
|
|
||||||
@import "~font-awesome/less/font-awesome.less";
|
|
||||||
@import "../core.less";
|
|
||||||
@import "../character_page.less";
|
|
||||||
@import "../bbcode_editor.less";
|
|
||||||
@import "../bbcode.less";
|
|
||||||
@import "../flist_overrides.less";
|
|
||||||
@import "../chat.less";
|
|
||||||
|
|
||||||
html {
|
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
//Import variable defaults first.
|
|
||||||
@import "../../flist_variables.less";
|
|
||||||
|
|
||||||
@gray-base: #000000;
|
|
||||||
@gray-darker: lighten(@gray-base, 5%);
|
|
||||||
@gray-dark: lighten(@gray-base, 25%);
|
|
||||||
@gray: lighten(@gray-base, 50%);
|
|
||||||
@gray-light: lighten(@gray-base, 65%);
|
|
||||||
@gray-lighter: lighten(@gray-base, 85%);
|
|
||||||
|
|
||||||
@body-bg: @gray-darker;
|
|
||||||
@text-color: @gray-lighter;
|
|
||||||
@text-color-disabled: @gray;
|
|
||||||
@link-color: darken(@gray-lighter, 15%);
|
|
||||||
|
|
||||||
@brand-warning: #a50;
|
|
||||||
@brand-danger: #800;
|
|
||||||
@brand-success: #080;
|
|
||||||
@brand-info: #228;
|
|
||||||
@brand-primary: @brand-info;
|
|
||||||
@blue-color: #36f;
|
|
||||||
|
|
||||||
@state-info-bg: darken(@brand-info, 15%);
|
|
||||||
@state-info-text: lighten(@brand-info, 30%);
|
|
||||||
@state-success-bg: darken(@brand-success, 15%);
|
|
||||||
@state-success-text: lighten(@brand-success, 30%);
|
|
||||||
@state-warning-bg: darken(@brand-warning, 15%);
|
|
||||||
@state-warning-text: lighten(@brand-warning, 30%);
|
|
||||||
@state-danger-bg: darken(@brand-danger, 15%);
|
|
||||||
@state-danger-text: lighten(@brand-danger, 30%);
|
|
||||||
|
|
||||||
@text-background-color: @gray-dark;
|
|
||||||
@text-background-color-disabled: @gray-darker;
|
|
||||||
@border-color: lighten(spin(@text-background-color, -10), 15%);
|
|
||||||
@border-color-active: lighten(spin(@text-background-color, -10), 25%);
|
|
||||||
@border-color-disabled: darken(spin(@text-background-color-disabled, -10), 8%);
|
|
||||||
|
|
||||||
@hover-bg: lighten(@gray-dark, 15%);
|
|
||||||
|
|
||||||
|
|
||||||
@hr-border: @text-color;
|
|
||||||
|
|
||||||
@panel-bg: @text-background-color;
|
|
||||||
@panel-default-heading-bg: @gray;
|
|
||||||
@panel-default-border: @border-color;
|
|
||||||
|
|
||||||
@input-color: @gray-lighter;
|
|
||||||
@input-bg: @text-background-color;
|
|
||||||
@input-bg-disabled: @text-background-color-disabled;
|
|
||||||
@input-border: @border-color;
|
|
||||||
@input-border-focus: @gray;
|
|
||||||
|
|
||||||
@dropdown-bg: @text-background-color;
|
|
||||||
@dropdown-color: @text-color;
|
|
||||||
@dropdown-link-color: @link-color;
|
|
||||||
@dropdown-link-hover-color: @gray-dark;
|
|
||||||
@dropdown-link-hover-bg: @gray-light;
|
|
||||||
|
|
||||||
@navbar-default-bg: @text-background-color;
|
|
||||||
@navbar-default-color: @text-color;
|
|
||||||
@navbar-default-link-color: @link-color;
|
|
||||||
@navbar-default-link-hover-color: @link-hover-color;
|
|
||||||
|
|
||||||
@nav-link-hover-bg: @gray-dark;
|
|
||||||
@nav-link-hover-color: @gray-darker;
|
|
||||||
|
|
||||||
@nav-tabs-border-color: @border-color;
|
|
||||||
@nav-tabs-link-hover-border-color: @border-color;
|
|
||||||
@nav-tabs-active-link-hover-bg: @body-bg;
|
|
||||||
@nav-tabs-active-link-hover-color: @text-color;
|
|
||||||
@nav-tabs-active-link-hover-border-color: @border-color;
|
|
||||||
|
|
||||||
@component-active-color: @gray-dark;
|
|
||||||
@component-active-bg: @gray-light;
|
|
||||||
|
|
||||||
@list-group-bg: @gray-darker;
|
|
||||||
@list-group-border: @gray-dark;
|
|
||||||
@list-group-link-color: @text-color;
|
|
||||||
@list-group-hover-bg: @gray-dark;
|
|
||||||
|
|
||||||
@btn-default-bg: @text-background-color;
|
|
||||||
@btn-default-color: @text-color;
|
|
||||||
@btn-default-border: @border-color;
|
|
||||||
|
|
||||||
@pagination-bg: @text-background-color;
|
|
||||||
@pagination-color: @text-color;
|
|
||||||
@pagination-border: @border-color;
|
|
||||||
@pagination-disabled-bg: @text-background-color-disabled;
|
|
||||||
@pagination-disabled-color: @text-color-disabled;
|
|
||||||
@pagination-disabled-border: @border-color-disabled;
|
|
||||||
@pagination-active-bg: @gray;
|
|
||||||
@pagination-active-color: @gray-lighter;
|
|
||||||
@pagination-active-border: @border-color-active;
|
|
||||||
|
|
||||||
@modal-content-bg: @text-background-color;
|
|
||||||
@modal-footer-border-color: lighten(spin(@modal-content-bg, -10), 15%);
|
|
||||||
@modal-header-border-color: @modal-footer-border-color;
|
|
||||||
|
|
||||||
@popover-bg: @body-bg;
|
|
||||||
@popover-border-color: @border-color;
|
|
||||||
@popover-title-bg: @text-background-color;
|
|
||||||
|
|
||||||
@badge-color: @gray-darker;
|
|
||||||
|
|
||||||
@close-color: saturate(@text-color, 10%);
|
|
||||||
@close-text-shadow: 0 1px 0 @text-color;
|
|
||||||
|
|
||||||
@well-bg: @text-background-color;
|
|
||||||
@well-border: @border-color;
|
|
||||||
|
|
||||||
@blockquote-border-color: @border-color-active;
|
|
||||||
|
|
||||||
@collapse-border: desaturate(@well-border, 20%);
|
|
||||||
@collapse-header-bg: desaturate(@well-bg, 20%);
|
|
||||||
|
|
||||||
@white-color: @text-color;
|
|
||||||
|
|
||||||
.blackText {
|
|
||||||
text-shadow: @gray-lighter 1px 1px 1px;
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
//Import variable defaults first.
|
|
||||||
@import "../../flist_variables.less";
|
|
||||||
|
|
||||||
@gray-base: #080810;
|
|
||||||
@gray-darker: lighten(@gray-base, 15%);
|
|
||||||
@gray-dark: lighten(@gray-base, 25%);
|
|
||||||
@gray: lighten(@gray-base, 60%);
|
|
||||||
@gray-light: lighten(@gray-base, 75%);
|
|
||||||
@gray-lighter: lighten(@gray-base, 95%);
|
|
||||||
|
|
||||||
// @body-bg: #262626;
|
|
||||||
@body-bg: darken(@text-background-color-disabled, 3%);
|
|
||||||
@text-color: @gray-lighter;
|
|
||||||
@text-color-disabled: @gray;
|
|
||||||
@link-color: darken(@gray-lighter, 15%);
|
|
||||||
|
|
||||||
@brand-warning: #c26c00;
|
|
||||||
@brand-danger: #930300;
|
|
||||||
@brand-success: #009900;
|
|
||||||
@brand-info: #0447af;
|
|
||||||
@brand-primary: @brand-info;
|
|
||||||
@blue-color: #36f;
|
|
||||||
|
|
||||||
@state-info-bg: darken(@brand-info, 15%);
|
|
||||||
@state-info-text: lighten(@brand-info, 30%);
|
|
||||||
@state-success-bg: darken(@brand-success, 15%);
|
|
||||||
@state-success-text: lighten(@brand-success, 30%);
|
|
||||||
@state-warning-bg: darken(@brand-warning, 15%);
|
|
||||||
@state-warning-text: lighten(@brand-warning, 30%);
|
|
||||||
@state-danger-bg: darken(@brand-danger, 15%);
|
|
||||||
@state-danger-text: lighten(@brand-danger, 30%);
|
|
||||||
|
|
||||||
@text-background-color: @gray-dark;
|
|
||||||
@text-background-color-disabled: @gray-darker;
|
|
||||||
@border-color: lighten(spin(@text-background-color, -10), 15%);
|
|
||||||
@border-color-active: lighten(spin(@text-background-color, -10), 25%);
|
|
||||||
@border-color-disabled: darken(spin(@text-background-color-disabled, -10), 8%);
|
|
||||||
|
|
||||||
@hover-bg: lighten(@gray-dark, 15%);
|
|
||||||
|
|
||||||
|
|
||||||
@hr-border: @text-color;
|
|
||||||
|
|
||||||
@panel-bg: @text-background-color;
|
|
||||||
@panel-default-heading-bg: @gray;
|
|
||||||
@panel-default-border: @border-color;
|
|
||||||
|
|
||||||
@input-color: @gray-lighter;
|
|
||||||
@input-bg: @text-background-color;
|
|
||||||
@input-bg-disabled: @text-background-color-disabled;
|
|
||||||
@input-border: @border-color;
|
|
||||||
@input-border-focus: @gray;
|
|
||||||
|
|
||||||
@dropdown-bg: @text-background-color;
|
|
||||||
@dropdown-color: @text-color;
|
|
||||||
@dropdown-link-color: @link-color;
|
|
||||||
@dropdown-link-hover-color: @gray-dark;
|
|
||||||
@dropdown-link-hover-bg: @gray-light;
|
|
||||||
|
|
||||||
@navbar-default-bg: @text-background-color;
|
|
||||||
@navbar-default-color: @text-color;
|
|
||||||
@navbar-default-link-color: @link-color;
|
|
||||||
@navbar-default-link-hover-color: @link-hover-color;
|
|
||||||
|
|
||||||
@nav-link-hover-bg: @gray-dark;
|
|
||||||
@nav-link-hover-color: @gray-darker;
|
|
||||||
|
|
||||||
@nav-tabs-border-color: @border-color;
|
|
||||||
@nav-tabs-link-hover-border-color: @border-color;
|
|
||||||
@nav-tabs-active-link-hover-bg: @body-bg;
|
|
||||||
@nav-tabs-active-link-hover-color: @text-color;
|
|
||||||
@nav-tabs-active-link-hover-border-color: @border-color;
|
|
||||||
|
|
||||||
@component-active-color: @gray-dark;
|
|
||||||
@component-active-bg: @gray-light;
|
|
||||||
|
|
||||||
@list-group-bg: @gray-darker;
|
|
||||||
@list-group-border: @gray-dark;
|
|
||||||
@list-group-link-color: @text-color;
|
|
||||||
@list-group-hover-bg: @gray-dark;
|
|
||||||
|
|
||||||
@btn-default-bg: @text-background-color;
|
|
||||||
@btn-default-color: @text-color;
|
|
||||||
@btn-default-border: @border-color;
|
|
||||||
|
|
||||||
@pagination-bg: @text-background-color;
|
|
||||||
@pagination-color: @text-color;
|
|
||||||
@pagination-border: @border-color;
|
|
||||||
@pagination-disabled-bg: @text-background-color-disabled;
|
|
||||||
@pagination-disabled-color: @text-color-disabled;
|
|
||||||
@pagination-disabled-border: @border-color-disabled;
|
|
||||||
@pagination-active-bg: @gray;
|
|
||||||
@pagination-active-color: @gray-lighter;
|
|
||||||
@pagination-active-border: @border-color-active;
|
|
||||||
|
|
||||||
@modal-content-bg: @text-background-color;
|
|
||||||
@modal-footer-border-color: lighten(spin(@modal-content-bg, -10), 15%);
|
|
||||||
@modal-header-border-color: @modal-footer-border-color;
|
|
||||||
|
|
||||||
@popover-bg: @body-bg;
|
|
||||||
@popover-border-color: @border-color;
|
|
||||||
@popover-title-bg: @text-background-color;
|
|
||||||
|
|
||||||
@badge-color: @gray-darker;
|
|
||||||
|
|
||||||
@close-color: saturate(@text-color, 10%);
|
|
||||||
@close-text-shadow: 0 1px 0 @text-color;
|
|
||||||
|
|
||||||
@well-bg: @text-background-color;
|
|
||||||
@well-border: @border-color;
|
|
||||||
|
|
||||||
@blockquote-border-color: @border-color-active;
|
|
||||||
|
|
||||||
@collapse-border: desaturate(@well-border, 20%);
|
|
||||||
@collapse-header-bg: desaturate(@well-bg, 20%);
|
|
||||||
|
|
||||||
@white-color: @text-color;
|
|
||||||
|
|
||||||
.blackText {
|
|
||||||
text-shadow: @gray-lighter 1px 1px 1px;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//Import variable defaults first.
|
|
||||||
@import "../../flist_variables.less";
|
|
||||||
|
|
||||||
// Update variables here.
|
|
||||||
// @body-bg: #00ff00;
|
|
||||||
@hr-border: @text-color;
|
|
||||||
@body-bg: #fafafa;
|
|
||||||
@brand-warning: #e09d3e;
|
|
||||||
|
|
||||||
.whiteText {
|
|
||||||
text-shadow: @gray-darker 1px 1px 1px;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
.ticket-reply-well {
|
|
||||||
.well();
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-reply-well.staff {
|
|
||||||
background-color: @state-info-bg;
|
|
||||||
border-color: @state-info-border;
|
|
||||||
}
|
|
380
less/yarn.lock
380
less/yarn.lock
|
@ -1,380 +0,0 @@
|
||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
ajv@^4.9.1:
|
|
||||||
version "4.11.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
|
||||||
dependencies:
|
|
||||||
co "^4.6.0"
|
|
||||||
json-stable-stringify "^1.0.1"
|
|
||||||
|
|
||||||
asap@~2.0.3:
|
|
||||||
version "2.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
|
||||||
|
|
||||||
asn1@~0.2.3:
|
|
||||||
version "0.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
|
|
||||||
|
|
||||||
assert-plus@1.0.0, assert-plus@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
|
||||||
|
|
||||||
assert-plus@^0.2.0:
|
|
||||||
version "0.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
|
|
||||||
|
|
||||||
asynckit@^0.4.0:
|
|
||||||
version "0.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
|
||||||
|
|
||||||
aws-sign2@~0.6.0:
|
|
||||||
version "0.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
|
||||||
|
|
||||||
aws4@^1.2.1:
|
|
||||||
version "1.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
|
||||||
|
|
||||||
bcrypt-pbkdf@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
|
|
||||||
dependencies:
|
|
||||||
tweetnacl "^0.14.3"
|
|
||||||
|
|
||||||
boom@2.x.x:
|
|
||||||
version "2.10.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
|
||||||
dependencies:
|
|
||||||
hoek "2.x.x"
|
|
||||||
|
|
||||||
bootstrap@^3.3.7:
|
|
||||||
version "3.3.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
|
|
||||||
|
|
||||||
caseless@~0.12.0:
|
|
||||||
version "0.12.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
|
||||||
|
|
||||||
co@^4.6.0:
|
|
||||||
version "4.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
|
||||||
|
|
||||||
combined-stream@^1.0.5, combined-stream@~1.0.5:
|
|
||||||
version "1.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
|
|
||||||
dependencies:
|
|
||||||
delayed-stream "~1.0.0"
|
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
|
||||||
|
|
||||||
cryptiles@2.x.x:
|
|
||||||
version "2.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
|
||||||
dependencies:
|
|
||||||
boom "2.x.x"
|
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
|
||||||
version "1.14.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
|
||||||
dependencies:
|
|
||||||
assert-plus "^1.0.0"
|
|
||||||
|
|
||||||
delayed-stream@~1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
|
||||||
|
|
||||||
ecc-jsbn@~0.1.1:
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
|
|
||||||
dependencies:
|
|
||||||
jsbn "~0.1.0"
|
|
||||||
|
|
||||||
errno@^0.1.1:
|
|
||||||
version "0.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
|
|
||||||
dependencies:
|
|
||||||
prr "~1.0.1"
|
|
||||||
|
|
||||||
extend@~3.0.0:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
|
||||||
|
|
||||||
extsprintf@1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
|
||||||
|
|
||||||
extsprintf@^1.2.0:
|
|
||||||
version "1.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
|
||||||
|
|
||||||
font-awesome@^4.7.0:
|
|
||||||
version "4.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
|
|
||||||
|
|
||||||
forever-agent@~0.6.1:
|
|
||||||
version "0.6.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
|
||||||
|
|
||||||
form-data@~2.1.1:
|
|
||||||
version "2.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
|
|
||||||
dependencies:
|
|
||||||
asynckit "^0.4.0"
|
|
||||||
combined-stream "^1.0.5"
|
|
||||||
mime-types "^2.1.12"
|
|
||||||
|
|
||||||
getpass@^0.1.1:
|
|
||||||
version "0.1.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
|
||||||
dependencies:
|
|
||||||
assert-plus "^1.0.0"
|
|
||||||
|
|
||||||
graceful-fs@^4.1.2:
|
|
||||||
version "4.1.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
|
||||||
|
|
||||||
har-schema@^1.0.5:
|
|
||||||
version "1.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
|
|
||||||
|
|
||||||
har-validator@~4.2.1:
|
|
||||||
version "4.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
|
|
||||||
dependencies:
|
|
||||||
ajv "^4.9.1"
|
|
||||||
har-schema "^1.0.5"
|
|
||||||
|
|
||||||
hawk@~3.1.3:
|
|
||||||
version "3.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
|
|
||||||
dependencies:
|
|
||||||
boom "2.x.x"
|
|
||||||
cryptiles "2.x.x"
|
|
||||||
hoek "2.x.x"
|
|
||||||
sntp "1.x.x"
|
|
||||||
|
|
||||||
hoek@2.x.x:
|
|
||||||
version "2.16.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
|
||||||
|
|
||||||
http-signature@~1.1.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
|
|
||||||
dependencies:
|
|
||||||
assert-plus "^0.2.0"
|
|
||||||
jsprim "^1.2.2"
|
|
||||||
sshpk "^1.7.0"
|
|
||||||
|
|
||||||
image-size@~0.5.0:
|
|
||||||
version "0.5.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
|
||||||
|
|
||||||
is-typedarray@~1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
|
||||||
|
|
||||||
isstream@~0.1.2:
|
|
||||||
version "0.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
|
||||||
|
|
||||||
jsbn@~0.1.0:
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
|
||||||
|
|
||||||
json-schema@0.2.3:
|
|
||||||
version "0.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
|
||||||
|
|
||||||
json-stable-stringify@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
|
||||||
dependencies:
|
|
||||||
jsonify "~0.0.0"
|
|
||||||
|
|
||||||
json-stringify-safe@~5.0.1:
|
|
||||||
version "5.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
|
||||||
|
|
||||||
jsonify@~0.0.0:
|
|
||||||
version "0.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
|
||||||
|
|
||||||
jsprim@^1.2.2:
|
|
||||||
version "1.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
|
|
||||||
dependencies:
|
|
||||||
assert-plus "1.0.0"
|
|
||||||
extsprintf "1.3.0"
|
|
||||||
json-schema "0.2.3"
|
|
||||||
verror "1.10.0"
|
|
||||||
|
|
||||||
less-plugin-npm-import@^2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/less-plugin-npm-import/-/less-plugin-npm-import-2.1.0.tgz#823e6986c93318a98171ca858848b6bead55bf3e"
|
|
||||||
dependencies:
|
|
||||||
promise "~7.0.1"
|
|
||||||
resolve "~1.1.6"
|
|
||||||
|
|
||||||
less@^2.7.2:
|
|
||||||
version "2.7.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/less/-/less-2.7.3.tgz#cc1260f51c900a9ec0d91fb6998139e02507b63b"
|
|
||||||
optionalDependencies:
|
|
||||||
errno "^0.1.1"
|
|
||||||
graceful-fs "^4.1.2"
|
|
||||||
image-size "~0.5.0"
|
|
||||||
mime "^1.2.11"
|
|
||||||
mkdirp "^0.5.0"
|
|
||||||
promise "^7.1.1"
|
|
||||||
request "2.81.0"
|
|
||||||
source-map "^0.5.3"
|
|
||||||
|
|
||||||
mime-db@~1.30.0:
|
|
||||||
version "1.30.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
|
|
||||||
|
|
||||||
mime-types@^2.1.12, mime-types@~2.1.7:
|
|
||||||
version "2.1.17"
|
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
|
|
||||||
dependencies:
|
|
||||||
mime-db "~1.30.0"
|
|
||||||
|
|
||||||
mime@^1.2.11:
|
|
||||||
version "1.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
|
||||||
|
|
||||||
minimist@0.0.8:
|
|
||||||
version "0.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
|
||||||
|
|
||||||
mkdirp@^0.5.0:
|
|
||||||
version "0.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
|
||||||
dependencies:
|
|
||||||
minimist "0.0.8"
|
|
||||||
|
|
||||||
oauth-sign@~0.8.1:
|
|
||||||
version "0.8.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
|
||||||
|
|
||||||
performance-now@^0.2.0:
|
|
||||||
version "0.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
|
||||||
|
|
||||||
promise@^7.1.1:
|
|
||||||
version "7.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
|
||||||
dependencies:
|
|
||||||
asap "~2.0.3"
|
|
||||||
|
|
||||||
promise@~7.0.1:
|
|
||||||
version "7.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.0.4.tgz#363e84a4c36c8356b890fed62c91ce85d02ed539"
|
|
||||||
dependencies:
|
|
||||||
asap "~2.0.3"
|
|
||||||
|
|
||||||
prr@~1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
|
||||||
|
|
||||||
punycode@^1.4.1:
|
|
||||||
version "1.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
|
||||||
|
|
||||||
qs@~6.4.0:
|
|
||||||
version "6.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
|
||||||
|
|
||||||
request@2.81.0:
|
|
||||||
version "2.81.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
|
|
||||||
dependencies:
|
|
||||||
aws-sign2 "~0.6.0"
|
|
||||||
aws4 "^1.2.1"
|
|
||||||
caseless "~0.12.0"
|
|
||||||
combined-stream "~1.0.5"
|
|
||||||
extend "~3.0.0"
|
|
||||||
forever-agent "~0.6.1"
|
|
||||||
form-data "~2.1.1"
|
|
||||||
har-validator "~4.2.1"
|
|
||||||
hawk "~3.1.3"
|
|
||||||
http-signature "~1.1.0"
|
|
||||||
is-typedarray "~1.0.0"
|
|
||||||
isstream "~0.1.2"
|
|
||||||
json-stringify-safe "~5.0.1"
|
|
||||||
mime-types "~2.1.7"
|
|
||||||
oauth-sign "~0.8.1"
|
|
||||||
performance-now "^0.2.0"
|
|
||||||
qs "~6.4.0"
|
|
||||||
safe-buffer "^5.0.1"
|
|
||||||
stringstream "~0.0.4"
|
|
||||||
tough-cookie "~2.3.0"
|
|
||||||
tunnel-agent "^0.6.0"
|
|
||||||
uuid "^3.0.0"
|
|
||||||
|
|
||||||
resolve@~1.1.6:
|
|
||||||
version "1.1.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
|
|
||||||
|
|
||||||
safe-buffer@^5.0.1:
|
|
||||||
version "5.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
|
||||||
|
|
||||||
sntp@1.x.x:
|
|
||||||
version "1.0.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
|
|
||||||
dependencies:
|
|
||||||
hoek "2.x.x"
|
|
||||||
|
|
||||||
source-map@^0.5.3:
|
|
||||||
version "0.5.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
|
||||||
|
|
||||||
sshpk@^1.7.0:
|
|
||||||
version "1.13.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
|
|
||||||
dependencies:
|
|
||||||
asn1 "~0.2.3"
|
|
||||||
assert-plus "^1.0.0"
|
|
||||||
dashdash "^1.12.0"
|
|
||||||
getpass "^0.1.1"
|
|
||||||
optionalDependencies:
|
|
||||||
bcrypt-pbkdf "^1.0.0"
|
|
||||||
ecc-jsbn "~0.1.1"
|
|
||||||
jsbn "~0.1.0"
|
|
||||||
tweetnacl "~0.14.0"
|
|
||||||
|
|
||||||
stringstream@~0.0.4:
|
|
||||||
version "0.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
|
||||||
|
|
||||||
tough-cookie@~2.3.0:
|
|
||||||
version "2.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
|
|
||||||
dependencies:
|
|
||||||
punycode "^1.4.1"
|
|
||||||
|
|
||||||
tunnel-agent@^0.6.0:
|
|
||||||
version "0.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
|
||||||
dependencies:
|
|
||||||
safe-buffer "^5.0.1"
|
|
||||||
|
|
||||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|
||||||
version "0.14.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
|
||||||
|
|
||||||
uuid@^3.0.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
|
||||||
|
|
||||||
verror@1.10.0:
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
|
||||||
dependencies:
|
|
||||||
assert-plus "^1.0.0"
|
|
||||||
core-util-is "1.0.2"
|
|
||||||
extsprintf "^1.2.0"
|
|
123
mobile/Index.vue
123
mobile/Index.vue
|
@ -2,47 +2,51 @@
|
||||||
<div id="page" style="position: relative; padding: 10px;" v-if="settings">
|
<div id="page" style="position: relative; padding: 10px;" v-if="settings">
|
||||||
<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="well well-lg" style="width: 400px;">
|
<div class="card bg-light" style="width: 400px;">
|
||||||
<h3 style="margin-top:0">{{l('title')}}</h3>
|
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
||||||
<div class="alert alert-danger" v-show="error">
|
<div class="card-body">
|
||||||
{{error}}
|
<div class="alert alert-danger" v-show="error">
|
||||||
</div>
|
{{error}}
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
<div class="form-group">
|
||||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login"/>
|
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||||
</div>
|
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
<div class="form-group">
|
||||||
<input class="form-control" type="password" id="password" v-model="settings.password" @keypress.enter="login"/>
|
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||||
</div>
|
<input class="form-control" type="password" id="password" v-model="settings.password" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group" v-show="showAdvanced">
|
</div>
|
||||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
<div class="form-group" v-show="showAdvanced">
|
||||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login"/>
|
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||||
</div>
|
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login" :disabled="loggingIn"/>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label class="control-label" for="theme">{{l('settings.theme')}}</label>
|
<div class="form-group">
|
||||||
<select class="form-control" id="theme" v-model="settings.theme">
|
<label class="control-label" for="theme">{{l('settings.theme')}}</label>
|
||||||
<option>default</option>
|
<select class="form-control custom-select" id="theme" v-model="settings.theme">
|
||||||
<option>dark</option>
|
<option>default</option>
|
||||||
<option>light</option>
|
<option>dark</option>
|
||||||
</select>
|
<option>light</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="advanced"><input type="checkbox" id="advanced" v-model="showAdvanced"/> {{l('login.advanced')}}</label>
|
<div class="form-group">
|
||||||
</div>
|
<label for="advanced"><input type="checkbox" id="advanced" v-model="showAdvanced"/> {{l('login.advanced')}}</label>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
<div class="form-group">
|
||||||
</div>
|
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
||||||
<div class="form-group text-right">
|
</div>
|
||||||
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
<div class="form-group" style="text-align:right">
|
||||||
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
||||||
</button>
|
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<chat v-else :ownCharacters="characters" :defaultCharacter="defaultCharacter" ref="chat"></chat>
|
<chat v-else :ownCharacters="characters" :defaultCharacter="defaultCharacter" ref="chat"></chat>
|
||||||
<modal action="Profile" :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||||
<character-page :authenticated="false" :oldApi="true" :name="profileName"></character-page>
|
<character-page :authenticated="false" :oldApi="true" :name="profileName"></character-page>
|
||||||
|
<template slot="title">{{profileName}} <a class="btn" @click="openProfileInBrowser"><i class="fa fa-external-link-alt"/></a>
|
||||||
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -61,7 +65,7 @@
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
import Connection from '../fchat/connection';
|
import Connection from '../fchat/connection';
|
||||||
import CharacterPage from '../site/character_page/character_page.vue';
|
import CharacterPage from '../site/character_page/character_page.vue';
|
||||||
import {GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
import {appVersion, GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
||||||
import Notifications from './notifications';
|
import Notifications from './notifications';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -70,10 +74,15 @@
|
||||||
setTheme(theme: string): void
|
setTheme(theme: string): void
|
||||||
} | undefined;
|
} | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NativeBackground: {
|
||||||
|
start(): void
|
||||||
|
stop(): void
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmBack(): void {
|
function confirmBack(e: Event): void {
|
||||||
if(confirm(l('chat.confirmLeave'))) (<Navigator & {app: {exitApp(): void}}>navigator).app.exitApp();
|
if(!confirm(l('chat.confirmLeave'))) e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -94,25 +103,26 @@
|
||||||
profileName = '';
|
profileName = '';
|
||||||
|
|
||||||
async created(): Promise<void> {
|
async created(): Promise<void> {
|
||||||
const oldOpen = window.open.bind(window);
|
document.addEventListener('open-profile', (e: Event) => {
|
||||||
window.open = (url?: string, target?: string, features?: string, replace?: boolean) => {
|
const profileViewer = <Modal>this.$refs['profileViewer'];
|
||||||
const profileMatch = url !== undefined ? url.match(/^https?:\/\/(www\.)?f-list.net\/c\/(.+)/) : null;
|
this.profileName = (<Event & {detail: string}>e).detail;
|
||||||
if(profileMatch !== null) {
|
profileViewer.show();
|
||||||
const profileViewer = <Modal>this.$refs['profileViewer'];
|
});
|
||||||
this.profileName = profileMatch[2];
|
|
||||||
profileViewer.show();
|
|
||||||
return null;
|
|
||||||
} else return oldOpen(url, target, features, replace);
|
|
||||||
};
|
|
||||||
let settings = await getGeneralSettings();
|
let settings = await getGeneralSettings();
|
||||||
if(settings === undefined) settings = new GeneralSettings();
|
if(settings === undefined) settings = new GeneralSettings();
|
||||||
|
if(settings.version !== appVersion) {
|
||||||
|
alert('Your beta version of F-Chat 3.0 has been updated. If you are experiencing any issues after this update, please perform a full reinstall of the application. If the issue persists, please report it.');
|
||||||
|
settings.version = appVersion;
|
||||||
|
await setGeneralSettings(settings);
|
||||||
|
}
|
||||||
if(settings.account.length > 0) this.saveLogin = true;
|
if(settings.account.length > 0) this.saveLogin = true;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
get styling(): string {
|
get styling(): string {
|
||||||
if(window.NativeView !== undefined) window.NativeView.setTheme(this.settings!.theme);
|
if(window.NativeView !== undefined) window.NativeView.setTheme(this.settings!.theme);
|
||||||
return `<style>${require(`../less/themes/chat/${this.settings!.theme}.less`)}</style>`;
|
//tslint:disable-next-line:no-require-imports
|
||||||
|
return `<style>${require('../scss/fa.scss')}${require(`../scss/themes/chat/${this.settings!.theme}.scss`)}</style>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(): Promise<void> {
|
async login(): Promise<void> {
|
||||||
|
@ -130,16 +140,17 @@
|
||||||
}
|
}
|
||||||
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 version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket,
|
||||||
const connection = new Connection(`F-Chat 3.0 (Mobile)`, version, Socket,
|
|
||||||
this.settings!.account, this.settings!.password);
|
this.settings!.account, this.settings!.password);
|
||||||
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();
|
||||||
});
|
});
|
||||||
connection.onEvent('closed', () => {
|
connection.onEvent('closed', () => {
|
||||||
Raven.setUserContext();
|
Raven.setUserContext();
|
||||||
document.removeEventListener('backbutton', confirmBack);
|
document.removeEventListener('backbutton', confirmBack);
|
||||||
|
NativeBackground.stop();
|
||||||
});
|
});
|
||||||
initCore(connection, Logs, SettingsStore, Notifications);
|
initCore(connection, Logs, SettingsStore, Notifications);
|
||||||
const charNames = Object.keys(data.characters);
|
const charNames = Object.keys(data.characters);
|
||||||
|
@ -157,6 +168,10 @@
|
||||||
this.loggingIn = false;
|
this.loggingIn = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openProfileInBrowser(): void {
|
||||||
|
window.open(`profile://${this.profileName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -164,4 +179,8 @@
|
||||||
html, body, #page {
|
html, body, #page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, .modal {
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,8 +8,8 @@ android {
|
||||||
applicationId "net.f_list.fchat"
|
applicationId "net.f_list.fchat"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 4
|
versionCode 11
|
||||||
versionName "0.1.0"
|
versionName "0.1.4"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -13,12 +14,13 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Holo.NoActionBar">
|
android:theme="@android:style/Theme.Holo.NoActionBar">
|
||||||
<activity android:name=".MainActivity" android:launchMode="singleInstance"
|
<activity android:name=".MainActivity" android:launchMode="singleInstance"
|
||||||
android:configChanges="orientation|screenSize">
|
android:configChanges="density|fontScale|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name=".BackgroundService" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.f_list.fchat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
|
||||||
|
class Background(private val ctx: Context) {
|
||||||
|
private val serviceIntent: Intent by lazy { Intent(ctx, BackgroundService::class.java) }
|
||||||
|
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun start() {
|
||||||
|
ctx.startService(serviceIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun stop() {
|
||||||
|
ctx.stopService(serviceIntent)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package net.f_list.fchat
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
class BackgroundService : Service() {
|
||||||
|
override fun onBind(p0: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
val notification = Notification.Builder(this).setContentTitle(getString(R.string.app_name))
|
||||||
|
.setContentIntent(PendingIntent.getActivity(this, 1, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
.setSmallIcon(R.drawable.ic_notification).setAutoCancel(true).setPriority(Notification.PRIORITY_LOW)
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
manager.createNotificationChannel(NotificationChannel("background", getString(R.string.channel_background), NotificationManager.IMPORTANCE_LOW));
|
||||||
|
notification.setChannelId("background");
|
||||||
|
}
|
||||||
|
startForeground(1, notification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,48 +4,30 @@ import android.content.Context
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class File(private val ctx: Context) {
|
class File(private val ctx: Context) {
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun readFile(name: String, s: Long, l: Int): String? {
|
fun read(name: String): String? {
|
||||||
val file = File(ctx.filesDir, name)
|
val file = File(ctx.filesDir, name)
|
||||||
if(!file.exists()) return null
|
if(!file.exists()) return null
|
||||||
FileInputStream(file).use { fs ->
|
Scanner(file).useDelimiter("\\Z").use { return it.next() }
|
||||||
val start = if(s != -1L) s else 0
|
|
||||||
fs.channel.position(start)
|
|
||||||
val maxLength = fs.channel.size() - start
|
|
||||||
val length = if(l != -1 && l < maxLength) l else maxLength.toInt()
|
|
||||||
val bytes = ByteArray(length)
|
|
||||||
fs.read(bytes, 0, length)
|
|
||||||
return String(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JavascriptInterface
|
|
||||||
fun readFile(name: String): String? {
|
|
||||||
return readFile(name, -1, -1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getSize(name: String) = File(ctx.filesDir, name).length()
|
fun getSize(name: String) = File(ctx.filesDir, name).length()
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun writeFile(name: String, data: String) {
|
fun write(name: String, data: String) {
|
||||||
FileOutputStream(File(ctx.filesDir, name)).use { it.write(data.toByteArray()) }
|
FileOutputStream(File(ctx.filesDir, name)).use { it.write(data.toByteArray()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun append(name: String, data: String) {
|
fun listFilesN(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isFile }.map { it.name }).toString()
|
||||||
FileOutputStream(File(ctx.filesDir, name), true).use { it.write(data.toByteArray()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun listFiles(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isFile }.map { it.name }).toString()
|
fun listDirectoriesN(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isDirectory }.map { it.name }).toString()
|
||||||
|
|
||||||
@JavascriptInterface
|
|
||||||
fun listDirectories(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isDirectory }.map { it.name }).toString()
|
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun ensureDirectory(name: String) {
|
fun ensureDirectory(name: String) {
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
package net.f_list.fchat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONStringer
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.CharBuffer
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Logs(private val ctx: Context) {
|
||||||
|
data class IndexItem(val name: String, val index: MutableMap<Int, Long> = HashMap(), val dates: MutableList<Int> = LinkedList())
|
||||||
|
|
||||||
|
private lateinit var index: MutableMap<String, IndexItem>
|
||||||
|
private lateinit var baseDir: File
|
||||||
|
private val encoder = Charsets.UTF_8.newEncoder()
|
||||||
|
private val decoder = Charsets.UTF_8.newDecoder()
|
||||||
|
private val buffer = ByteBuffer.allocateDirect(51000).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun initN(character: String): String {
|
||||||
|
baseDir = File(ctx.filesDir, "$character/logs")
|
||||||
|
baseDir.mkdirs()
|
||||||
|
val files = baseDir.listFiles({ _, name -> name.endsWith(".idx") })
|
||||||
|
index = HashMap(files.size)
|
||||||
|
for(file in files) {
|
||||||
|
FileInputStream(file).use { stream ->
|
||||||
|
buffer.clear()
|
||||||
|
val read = stream.channel.read(buffer)
|
||||||
|
buffer.rewind()
|
||||||
|
val nameLength = buffer.get().toInt()
|
||||||
|
buffer.limit(nameLength + 1)
|
||||||
|
val cb = CharBuffer.allocate(nameLength)
|
||||||
|
decoder.reset()
|
||||||
|
decoder.decode(buffer, cb, true)
|
||||||
|
decoder.flush(cb)
|
||||||
|
cb.flip()
|
||||||
|
val indexItem = IndexItem(cb.toString())
|
||||||
|
buffer.limit(read)
|
||||||
|
while(buffer.position() < buffer.limit()) {
|
||||||
|
val key = buffer.short.toInt()
|
||||||
|
indexItem.index[key] = buffer.int.toLong() or (buffer.get().toLong() shl 32)
|
||||||
|
indexItem.dates.add(key)
|
||||||
|
}
|
||||||
|
index[file.nameWithoutExtension] = indexItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val json = JSONStringer().`object`()
|
||||||
|
for(item in index) json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.dates)).endObject()
|
||||||
|
return json.endObject().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun logMessage(key: String, conversation: String, time: Int, type: Int, sender: String, text: String) {
|
||||||
|
val day = time / 86400
|
||||||
|
val file = File(baseDir, key)
|
||||||
|
buffer.clear()
|
||||||
|
if(!index.containsKey(key)) {
|
||||||
|
index[key] = IndexItem(conversation, HashMap())
|
||||||
|
buffer.position(1)
|
||||||
|
encoder.encode(CharBuffer.wrap(conversation), buffer, true)
|
||||||
|
buffer.put(0, (buffer.position() - 1).toByte())
|
||||||
|
}
|
||||||
|
val item = index[key]!!
|
||||||
|
if(!item.index.containsKey(day)) {
|
||||||
|
buffer.putShort(day.toShort())
|
||||||
|
val size = file.length()
|
||||||
|
item.index[day] = size
|
||||||
|
item.dates.add(day)
|
||||||
|
buffer.putInt((size and 0xffffffffL).toInt())
|
||||||
|
buffer.put((size shr 32).toByte())
|
||||||
|
FileOutputStream(File(baseDir, "$key.idx"), true).use { file ->
|
||||||
|
buffer.flip()
|
||||||
|
file.channel.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileOutputStream(file, true).use { file ->
|
||||||
|
buffer.clear()
|
||||||
|
buffer.putInt(time)
|
||||||
|
buffer.put(type.toByte())
|
||||||
|
buffer.position(6)
|
||||||
|
encoder.encode(CharBuffer.wrap(sender), buffer, true)
|
||||||
|
val senderLength = buffer.position() - 6
|
||||||
|
buffer.put(5, senderLength.toByte())
|
||||||
|
buffer.position(8 + senderLength)
|
||||||
|
encoder.encode(CharBuffer.wrap(text), buffer, true)
|
||||||
|
buffer.putShort(senderLength + 6, (buffer.position() - senderLength - 8).toShort())
|
||||||
|
buffer.putShort(buffer.position().toShort())
|
||||||
|
buffer.flip()
|
||||||
|
file.channel.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getBacklogN(key: String): String {
|
||||||
|
buffer.clear()
|
||||||
|
val file = File(baseDir, key)
|
||||||
|
if(!file.exists()) return "[]"
|
||||||
|
val list = LinkedList<JSONStringer>()
|
||||||
|
FileInputStream(file).use { stream ->
|
||||||
|
val channel = stream.channel
|
||||||
|
val lengthBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
channel.position(channel.size())
|
||||||
|
while(channel.position() > 0 && list.size < 20) {
|
||||||
|
lengthBuffer.rewind()
|
||||||
|
lengthBuffer.limit(2)
|
||||||
|
channel.position(channel.position() - 2)
|
||||||
|
channel.read(lengthBuffer)
|
||||||
|
lengthBuffer.clear()
|
||||||
|
val length = lengthBuffer.int
|
||||||
|
channel.position(channel.position() - length - 2)
|
||||||
|
buffer.rewind()
|
||||||
|
buffer.limit(length)
|
||||||
|
channel.read(buffer)
|
||||||
|
buffer.rewind()
|
||||||
|
val stringer = JSONStringer()
|
||||||
|
deserializeMessage(buffer, stringer)
|
||||||
|
list.addFirst(stringer)
|
||||||
|
channel.position(channel.position() - length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val json = StringBuilder("[")
|
||||||
|
for(item in list) json.append(item).append(",")
|
||||||
|
json.setLength(json.length - 1)
|
||||||
|
return json.append("]").toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getLogsN(key: String, date: Int): String {
|
||||||
|
val offset = index[key]?.index?.get(date) ?: return "[]"
|
||||||
|
val json = JSONStringer()
|
||||||
|
json.array()
|
||||||
|
FileInputStream(File(baseDir, key)).use { stream ->
|
||||||
|
val channel = stream.channel
|
||||||
|
channel.position(offset)
|
||||||
|
while(channel.position() < channel.size()) {
|
||||||
|
buffer.clear()
|
||||||
|
val oldPosition = channel.position()
|
||||||
|
channel.read(buffer)
|
||||||
|
buffer.rewind()
|
||||||
|
deserializeMessage(buffer, json, date)
|
||||||
|
if(buffer.position() == 0) break
|
||||||
|
channel.position(oldPosition + buffer.position() + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.endArray().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deserializeMessage(buffer: ByteBuffer, json: JSONStringer, checkDate: Int = -1) {
|
||||||
|
val date = buffer.int
|
||||||
|
if(checkDate != -1 && date / 86400 != checkDate) return
|
||||||
|
json.`object`()
|
||||||
|
json.key("time")
|
||||||
|
json.value(date)
|
||||||
|
json.key("type")
|
||||||
|
json.value(buffer.get())
|
||||||
|
json.key("sender")
|
||||||
|
val senderLength = buffer.get()
|
||||||
|
buffer.limit(6 + senderLength)
|
||||||
|
json.value(decoder.decode(buffer))
|
||||||
|
buffer.limit(buffer.capacity())
|
||||||
|
val textLength = buffer.short.toInt() and 0xffff
|
||||||
|
json.key("text")
|
||||||
|
buffer.limit(8 + senderLength + textLength)
|
||||||
|
json.value(decoder.decode(buffer))
|
||||||
|
json.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +1,100 @@
|
||||||
package net.f_list.fchat
|
package net.f_list.fchat
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.JsResult
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class MainActivity : Activity() {
|
class MainActivity : Activity() {
|
||||||
private lateinit var webView: WebView
|
private lateinit var webView: WebView
|
||||||
|
private val profileRegex = Regex("^https?://(www\\.)?f-list.net/c/(.+)/?#?")
|
||||||
|
private val backgroundPlugin = Background(this)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
if(BuildConfig.DEBUG) WebView.setWebContentsDebuggingEnabled(true)
|
||||||
webView = findViewById(R.id.webview)
|
webView = findViewById(R.id.webview)
|
||||||
webView.settings.javaScriptEnabled = true
|
webView.settings.javaScriptEnabled = true
|
||||||
webView.settings.mediaPlaybackRequiresUserGesture = false
|
webView.settings.mediaPlaybackRequiresUserGesture = false
|
||||||
webView.loadUrl("file:///android_asset/www/index.html")
|
webView.loadUrl("file:///android_asset/www/index.html")
|
||||||
webView.addJavascriptInterface(File(this), "NativeFile")
|
webView.addJavascriptInterface(File(this), "NativeFile")
|
||||||
webView.addJavascriptInterface(Notifications(this), "NativeNotification")
|
webView.addJavascriptInterface(Notifications(this), "NativeNotification")
|
||||||
webView.webChromeClient = WebChromeClient()
|
webView.addJavascriptInterface(backgroundPlugin, "NativeBackground")
|
||||||
|
webView.addJavascriptInterface(Logs(this), "NativeLogs")
|
||||||
|
webView.webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||||
|
AlertDialog.Builder(this@MainActivity).setTitle(R.string.app_name).setMessage(message).setPositiveButton(R.string.ok, { _, _ -> result.confirm() }).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||||
|
var ok = false
|
||||||
|
AlertDialog.Builder(this@MainActivity).setTitle(R.string.app_name).setMessage(message).setOnDismissListener({ if(ok) result.confirm() else result.cancel() })
|
||||||
|
.setPositiveButton(R.string.ok, { _, _ -> ok = true}).setNegativeButton(R.string.cancel, null).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||||
|
val match = profileRegex.find(url)
|
||||||
|
if(match != null) {
|
||||||
|
val char = URLDecoder.decode(match.groupValues[2], "UTF-8")
|
||||||
|
webView.evaluateJavascript("document.dispatchEvent(new CustomEvent('open-profile',{detail:'$char'}))", null)
|
||||||
|
} else {
|
||||||
|
var uri = Uri.parse(url)
|
||||||
|
if(uri.scheme == "profile") uri = Uri.parse("https://www.f-list.net/c/${uri.authority}")
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
webView.evaluateJavascript("(function(n){n.listFiles=function(p){return JSON.parse(n.listFilesN(p))};n.listDirectories=function(p){return JSON.parse(n.listDirectoriesN(p))}})(NativeFile)", null)
|
||||||
|
webView.evaluateJavascript("(function(n){n.init=function(c){return JSON.parse(n.initN(c))};n.getBacklog=function(k){return JSON.parse(n.getBacklogN(k))};n.getLogs=function(k,d){return JSON.parse(n.getLogsN(k,d))}})(NativeLogs)", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
webView.evaluateJavascript("var e=new Event('backbutton',{cancelable:true});document.dispatchEvent(e);e.defaultPrevented", {
|
||||||
|
if(it != "true") super.onBackPressed()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if(intent.action == "notification") {
|
if(intent.action == "notification") {
|
||||||
val data = intent.extras.getString("data")
|
val data = intent.extras.getString("data")
|
||||||
webView.evaluateJavascript("document.dispatchEvent(new CustomEvent('notification-clicked',{detail:{data:'$data'}}))", {}) //TODO
|
webView.evaluateJavascript("document.dispatchEvent(new CustomEvent('notification-clicked',{detail:{data:'$data'}}))", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
webView.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
webView.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
findViewById<ViewGroup>(R.id.content).removeAllViews()
|
||||||
|
webView.removeAllViews()
|
||||||
|
webView.destroy()
|
||||||
|
backgroundPlugin.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package net.f_list.fchat
|
package net.f_list.fchat
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -9,21 +10,30 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.net.Uri
|
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
|
import android.os.Build
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class Notifications(private val ctx: Context) {
|
class Notifications(private val ctx: Context) {
|
||||||
|
init {
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_LOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun notify(notify: Boolean, title: String, text: String, icon: String, sound: String?, data: String?): Int {
|
fun notify(notify: Boolean, title: String, text: String, icon: String, sound: String?, data: String?): Int {
|
||||||
val soundUri = if(sound != null) Uri.parse("file://android_asset/www/sounds/$sound.mp3") else null
|
|
||||||
if(!notify) {
|
if(!notify) {
|
||||||
(ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(400)
|
val vibrator = (ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator)
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
vibrator.vibrate(400, Notification.AUDIO_ATTRIBUTES_DEFAULT)
|
||||||
|
else vibrator.vibrate(400)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if(soundUri != null) {
|
if(sound != null) {
|
||||||
val player = MediaPlayer()
|
val player = MediaPlayer()
|
||||||
val asset = ctx.assets.openFd("www/sounds/$sound.mp3")
|
val asset = ctx.assets.openFd("www/sounds/$sound.mp3")
|
||||||
player.setDataSource(asset.fileDescriptor, asset.startOffset, asset.length)
|
player.setDataSource(asset.fileDescriptor, asset.startOffset, asset.length)
|
||||||
|
@ -34,8 +44,9 @@ class Notifications(private val ctx: Context) {
|
||||||
val intent = Intent(ctx, MainActivity::class.java)
|
val intent = Intent(ctx, MainActivity::class.java)
|
||||||
intent.action = "notification"
|
intent.action = "notification"
|
||||||
intent.putExtra("data", data)
|
intent.putExtra("data", data)
|
||||||
val notification = Notification.Builder(ctx).setContentTitle(title).setContentText(text).setSmallIcon(R.drawable.ic_notification).setDefaults(Notification.DEFAULT_VIBRATE)
|
val notification = Notification.Builder(ctx).setContentTitle(title).setContentText(text).setSmallIcon(R.drawable.ic_notification).setAutoCancel(true)
|
||||||
.setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setAutoCancel(true)
|
.setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_LIGHTS)
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notification.setChannelId("messages")
|
||||||
object : AsyncTask<String, Void, Bitmap>() {
|
object : AsyncTask<String, Void, Bitmap>() {
|
||||||
override fun doInBackground(vararg args: String): Bitmap {
|
override fun doInBackground(vararg args: String): Bitmap {
|
||||||
val connection = URL(args[0]).openConnection()
|
val connection = URL(args[0]).openConnection()
|
||||||
|
@ -44,10 +55,10 @@ class Notifications(private val ctx: Context) {
|
||||||
|
|
||||||
override fun onPostExecute(result: Bitmap?) {
|
override fun onPostExecute(result: Bitmap?) {
|
||||||
notification.setLargeIcon(result)
|
notification.setLargeIcon(result)
|
||||||
(ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(1, notification.build())
|
(ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(2, notification.build())
|
||||||
}
|
}
|
||||||
}.execute(icon)
|
}.execute(icon)
|
||||||
return 1
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
tools:context="net.f_list.fchat.MainActivity">
|
tools:context="net.f_list.fchat.MainActivity">
|
||||||
|
|
||||||
<WebView
|
<WebView
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">F-Chat</string>
|
<string name="app_name">F-Chat</string>
|
||||||
|
<string name="channel_background">Running in Background</string>
|
||||||
|
<string name="channel_messages">Messages</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.2.10'
|
ext.kotlin_version = '1.2.21'
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,6 @@
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
||||||
*/
|
*/
|
||||||
import 'bootstrap/js/dropdown.js';
|
|
||||||
import 'bootstrap/js/modal.js';
|
|
||||||
import 'bootstrap/js/tab.js';
|
|
||||||
import * as Raven from 'raven-js';
|
import * as Raven from 'raven-js';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueRaven from '../chat/vue-raven';
|
import VueRaven from '../chat/vue-raven';
|
||||||
|
|
|
@ -1,150 +1,72 @@
|
||||||
import {getByteLength, Message as MessageImpl} from '../chat/common';
|
import {Message as MessageImpl} from '../chat/common';
|
||||||
import core from '../chat/core';
|
import core from '../chat/core';
|
||||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const NativeFile: {
|
const NativeFile: {
|
||||||
readFile(name: string): Promise<string | undefined>
|
read(name: string): Promise<string | undefined>
|
||||||
readFile(name: string, start: number, length: number): Promise<string | undefined>
|
write(name: string, data: string): Promise<void>
|
||||||
writeFile(name: string, data: string): Promise<void>
|
listDirectories(name: string): Promise<string[]>
|
||||||
listDirectories(name: string): Promise<string>
|
listFiles(name: string): Promise<string[]>
|
||||||
listFiles(name: string): Promise<string>
|
|
||||||
getSize(name: string): Promise<number>
|
getSize(name: string): Promise<number>
|
||||||
append(name: string, data: string): Promise<void>
|
|
||||||
ensureDirectory(name: string): Promise<void>
|
ensureDirectory(name: string): Promise<void>
|
||||||
};
|
};
|
||||||
|
type NativeMessage = {time: number, type: number, sender: string, text: string};
|
||||||
|
const NativeLogs: {
|
||||||
|
init(character: string): Promise<Index>
|
||||||
|
logMessage(key: string, conversation: string, time: number, type: Conversation.Message.Type, sender: string,
|
||||||
|
message: string): Promise<void>;
|
||||||
|
getBacklog(key: string): Promise<ReadonlyArray<NativeMessage>>;
|
||||||
|
getLogs(key: string, date: number): Promise<ReadonlyArray<NativeMessage>>
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayMs = 86400000;
|
const dayMs = 86400000;
|
||||||
|
export const appVersion = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
||||||
|
|
||||||
export class GeneralSettings {
|
export class GeneralSettings {
|
||||||
account = '';
|
account = '';
|
||||||
password = '';
|
password = '';
|
||||||
host = 'wss://chat.f-list.net:9799';
|
host = 'wss://chat.f-list.net:9799';
|
||||||
theme = 'default';
|
theme = 'default';
|
||||||
|
version = appVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Index = {[key: string]: {name: string, index: {[key: number]: number | undefined}} | undefined};
|
type Index = {[key: string]: {name: string, dates: number[]} | undefined};
|
||||||
|
|
||||||
function serializeMessage(message: Conversation.Message): string {
|
|
||||||
const time = message.time.getTime() / 1000;
|
|
||||||
let str = String.fromCharCode((time >> 24) % 256) + String.fromCharCode((time >> 16) % 256)
|
|
||||||
+ String.fromCharCode((time >> 8) % 256) + String.fromCharCode(time % 256);
|
|
||||||
str += String.fromCharCode(message.type);
|
|
||||||
if(message.type !== Conversation.Message.Type.Event) {
|
|
||||||
str += String.fromCharCode(message.sender.name.length);
|
|
||||||
str += message.sender.name;
|
|
||||||
} else str += '\0';
|
|
||||||
const textLength = message.text.length;
|
|
||||||
str += String.fromCharCode((textLength >> 8) % 256) + String.fromCharCode(textLength % 256);
|
|
||||||
str += message.text;
|
|
||||||
const length = getByteLength(str);
|
|
||||||
str += String.fromCharCode((length >> 8) % 256) + String.fromCharCode(length % 256);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeMessage(str: string): {message: Conversation.Message, end: number} {
|
|
||||||
let index = 0;
|
|
||||||
const time = str.charCodeAt(index++) << 24 | str.charCodeAt(index++) << 16 | str.charCodeAt(index++) << 8 | str.charCodeAt(index++);
|
|
||||||
const type = str.charCodeAt(index++);
|
|
||||||
const senderLength = str.charCodeAt(index++);
|
|
||||||
const sender = str.substring(index, index += senderLength);
|
|
||||||
const messageLength = str.charCodeAt(index++) << 8 | str.charCodeAt(index++);
|
|
||||||
const text = str.substring(index, index += messageLength);
|
|
||||||
const end = str.charCodeAt(index++) << 8 | str.charCodeAt(index);
|
|
||||||
return {message: new MessageImpl(type, core.characters.get(sender), text, new Date(time * 1000)), end: end + 2};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Logs implements Logging.Persistent {
|
export class Logs implements Logging.Persistent {
|
||||||
private index: Index = {};
|
private index: Index = {};
|
||||||
private logDir: string;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
core.connection.onEvent('connecting', async() => {
|
core.connection.onEvent('connecting', async() => {
|
||||||
this.index = {};
|
this.index = await NativeLogs.init(core.connection.character);
|
||||||
this.logDir = `${core.connection.character}/logs`;
|
|
||||||
await NativeFile.ensureDirectory(this.logDir);
|
|
||||||
const entries = <string[]>JSON.parse(await NativeFile.listFiles(this.logDir));
|
|
||||||
for(const entry of entries)
|
|
||||||
if(entry.substr(-4) === '.idx') {
|
|
||||||
const str = (await NativeFile.readFile(`${this.logDir}/${entry}`))!;
|
|
||||||
let i = str.charCodeAt(0);
|
|
||||||
const name = str.substr(1, i++);
|
|
||||||
const index: {[key: number]: number} = {};
|
|
||||||
while(i < str.length) {
|
|
||||||
const key = str.charCodeAt(i++) << 8 | str.charCodeAt(i++);
|
|
||||||
index[key] = str.charCodeAt(i++) << 32 | str.charCodeAt(i++) << 24 | str.charCodeAt(i++) << 16 |
|
|
||||||
str.charCodeAt(i++) << 8 | str.charCodeAt(i++);
|
|
||||||
}
|
|
||||||
this.index[entry.slice(0, -4).toLowerCase()] = {name, index};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> {
|
async logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> {
|
||||||
const file = `${this.logDir}/${conversation.key}`;
|
const time = message.time.getTime();
|
||||||
const serialized = serializeMessage(message);
|
const date = Math.floor(time / dayMs);
|
||||||
const date = Math.floor(message.time.getTime() / dayMs);
|
|
||||||
let indexBuffer: string | undefined;
|
|
||||||
let index = this.index[conversation.key];
|
let index = this.index[conversation.key];
|
||||||
if(index !== undefined) {
|
if(index === undefined) index = this.index[conversation.key] = {name: conversation.name, dates: []};
|
||||||
if(index.index[date] === undefined) indexBuffer = '';
|
if(index.dates[index.dates.length - 1] !== date) index.dates.push(date);
|
||||||
} else {
|
return NativeLogs.logMessage(conversation.key, conversation.name, time / 1000, message.type,
|
||||||
index = this.index[conversation.key] = {name: conversation.name, index: {}};
|
message.type === Conversation.Message.Type.Event ? '' : message.sender.name, message.text);
|
||||||
const nameLength = getByteLength(conversation.name);
|
|
||||||
indexBuffer = String.fromCharCode(nameLength) + conversation.name;
|
|
||||||
}
|
|
||||||
if(indexBuffer !== undefined) {
|
|
||||||
const size = await NativeFile.getSize(file);
|
|
||||||
index.index[date] = size;
|
|
||||||
indexBuffer += String.fromCharCode((date >> 8) % 256) + String.fromCharCode(date % 256) +
|
|
||||||
String.fromCharCode((size >> 32) % 256) + String.fromCharCode((size >> 24) % 256) +
|
|
||||||
String.fromCharCode((size >> 16) % 256) + String.fromCharCode((size >> 8) % 256) + String.fromCharCode(size % 256);
|
|
||||||
await NativeFile.append(`${file}.idx`, indexBuffer);
|
|
||||||
}
|
|
||||||
await NativeFile.append(file, serialized);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBacklog(conversation: Conversation): Promise<Conversation.Message[]> {
|
async getBacklog(conversation: Conversation): Promise<ReadonlyArray<Conversation.Message>> {
|
||||||
const file = `${this.logDir}/${conversation.key}`;
|
return (await NativeLogs.getBacklog(conversation.key))
|
||||||
let count = 20;
|
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
|
||||||
let messages = new Array<Conversation.Message>(count);
|
|
||||||
let pos = await NativeFile.getSize(file);
|
|
||||||
while(pos > 0 && count > 0) {
|
|
||||||
const l = (await NativeFile.readFile(file, pos - 2, pos))!;
|
|
||||||
const length = (l.charCodeAt(0) << 8 | l.charCodeAt(1));
|
|
||||||
pos = pos - length - 2;
|
|
||||||
messages[--count] = deserializeMessage((await NativeFile.readFile(file, pos, length))!).message;
|
|
||||||
}
|
|
||||||
if(count !== 0) messages = messages.slice(count);
|
|
||||||
return messages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs(key: string, date: Date): Promise<Conversation.Message[]> {
|
async getLogs(key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>> {
|
||||||
const file = `${this.logDir}/${key}`;
|
return (await NativeLogs.getLogs(key, date.getTime() / dayMs))
|
||||||
const messages: Conversation.Message[] = [];
|
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
|
||||||
const day = date.getTime() / dayMs;
|
|
||||||
const index = this.index[key];
|
|
||||||
if(index === undefined) return [];
|
|
||||||
let pos = index.index[date.getTime() / dayMs];
|
|
||||||
if(pos === undefined) return [];
|
|
||||||
const size = await NativeFile.getSize(file);
|
|
||||||
while(pos < size) {
|
|
||||||
const deserialized = deserializeMessage((await NativeFile.readFile(file, pos, 51000))!);
|
|
||||||
if(Math.floor(deserialized.message.time.getTime() / dayMs) !== day) break;
|
|
||||||
messages.push(deserialized.message);
|
|
||||||
pos += deserialized.end;
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogDates(key: string): ReadonlyArray<Date> {
|
getLogDates(key: string): ReadonlyArray<Date> {
|
||||||
const entry = this.index[key];
|
const entry = this.index[key];
|
||||||
if(entry === undefined) return [];
|
if(entry === undefined) return [];
|
||||||
const dates = [];
|
return entry.dates.map((x) => new Date(x * dayMs));
|
||||||
for(const date in entry.index)
|
|
||||||
dates.push(new Date(parseInt(date, 10) * dayMs));
|
|
||||||
return dates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get conversations(): ReadonlyArray<{id: string, name: string}> {
|
get conversations(): ReadonlyArray<{id: string, name: string}> {
|
||||||
|
@ -156,27 +78,27 @@ export class Logs implements Logging.Persistent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
||||||
const file = await NativeFile.readFile('!settings');
|
const file = await NativeFile.read('!settings');
|
||||||
if(file === undefined) return undefined;
|
if(file === undefined) return undefined;
|
||||||
return <GeneralSettings>JSON.parse(file);
|
return <GeneralSettings>JSON.parse(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
||||||
return NativeFile.writeFile('!settings', JSON.stringify(value));
|
return NativeFile.write('!settings', JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingsStore implements Settings.Store {
|
export class SettingsStore implements Settings.Store {
|
||||||
async get<K extends keyof Settings.Keys>(key: K, character: string = core.connection.character): Promise<Settings.Keys[K] | undefined> {
|
async get<K extends keyof Settings.Keys>(key: K, character: string = core.connection.character): Promise<Settings.Keys[K] | undefined> {
|
||||||
const file = await NativeFile.readFile(`${character}/${key}`);
|
const file = await NativeFile.read(`${character}/${key}`);
|
||||||
if(file === undefined) return undefined;
|
if(file === undefined) return undefined;
|
||||||
return <Settings.Keys[K]>JSON.parse(file);
|
return <Settings.Keys[K]>JSON.parse(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
return NativeFile.writeFile(`${core.connection.character}/${key}`, JSON.stringify(value));
|
return NativeFile.write(`${core.connection.character}/${key}`, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableCharacters(): Promise<string[]> {
|
async getAvailableCharacters(): Promise<string[]> {
|
||||||
return <string[]>JSON.parse(await NativeFile.listDirectories('/'));
|
return NativeFile.listDirectories('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
6C28207F1FF5839A00AB9E78 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6C2820811FF5839A00AB9E78 /* Localizable.strings */; };
|
6C28207F1FF5839A00AB9E78 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6C2820811FF5839A00AB9E78 /* Localizable.strings */; };
|
||||||
6C5C1C591FF14432006A3BA1 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5C1C581FF14432006A3BA1 /* View.swift */; };
|
6C4C230D201E7DF1009B3460 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4C230C201E7DF1009B3460 /* Background.swift */; };
|
||||||
|
6C8ED6192024A820007685DA /* Logs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8ED6182024A820007685DA /* Logs.swift */; };
|
||||||
6CA94BAC1FEFEE7800183A1A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */; };
|
6CA94BAC1FEFEE7800183A1A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */; };
|
||||||
6CA94BAE1FEFEE7800183A1A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA94BAD1FEFEE7800183A1A /* ViewController.swift */; };
|
6CA94BAE1FEFEE7800183A1A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA94BAD1FEFEE7800183A1A /* ViewController.swift */; };
|
||||||
6CA94BB11FEFEE7800183A1A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6CA94BAF1FEFEE7800183A1A /* Main.storyboard */; };
|
6CA94BB11FEFEE7800183A1A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6CA94BAF1FEFEE7800183A1A /* Main.storyboard */; };
|
||||||
|
@ -22,7 +23,8 @@
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
6C2820801FF5839A00AB9E78 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
6C2820801FF5839A00AB9E78 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
6C5C1C581FF14432006A3BA1 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
|
6C4C230C201E7DF1009B3460 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = "<group>"; };
|
||||||
|
6C8ED6182024A820007685DA /* Logs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logs.swift; sourceTree = "<group>"; };
|
||||||
6CA94BA81FEFEE7800183A1A /* F-Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "F-Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
6CA94BA81FEFEE7800183A1A /* F-Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "F-Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
6CA94BAD1FEFEE7800183A1A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
6CA94BAD1FEFEE7800183A1A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -66,18 +68,19 @@
|
||||||
6CA94BAA1FEFEE7800183A1A /* F-Chat */ = {
|
6CA94BAA1FEFEE7800183A1A /* F-Chat */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6C2820811FF5839A00AB9E78 /* Localizable.strings */,
|
|
||||||
6CA94BBD1FEFF2C200183A1A /* www */,
|
6CA94BBD1FEFF2C200183A1A /* www */,
|
||||||
|
6CA94BBF1FEFFC2F00183A1A /* native.js */,
|
||||||
|
6CA94BB71FEFEE7800183A1A /* Info.plist */,
|
||||||
6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */,
|
6CA94BAB1FEFEE7800183A1A /* AppDelegate.swift */,
|
||||||
|
6C4C230C201E7DF1009B3460 /* Background.swift */,
|
||||||
|
6CA94BC11FF009B000183A1A /* File.swift */,
|
||||||
|
6C8ED6182024A820007685DA /* Logs.swift */,
|
||||||
|
6CA94BC31FF070C800183A1A /* Notification.swift */,
|
||||||
6CA94BAD1FEFEE7800183A1A /* ViewController.swift */,
|
6CA94BAD1FEFEE7800183A1A /* ViewController.swift */,
|
||||||
6CA94BAF1FEFEE7800183A1A /* Main.storyboard */,
|
|
||||||
6CA94BB21FEFEE7800183A1A /* Assets.xcassets */,
|
6CA94BB21FEFEE7800183A1A /* Assets.xcassets */,
|
||||||
6CA94BB41FEFEE7800183A1A /* LaunchScreen.storyboard */,
|
6CA94BB41FEFEE7800183A1A /* LaunchScreen.storyboard */,
|
||||||
6CA94BB71FEFEE7800183A1A /* Info.plist */,
|
6C2820811FF5839A00AB9E78 /* Localizable.strings */,
|
||||||
6CA94BBF1FEFFC2F00183A1A /* native.js */,
|
6CA94BAF1FEFEE7800183A1A /* Main.storyboard */,
|
||||||
6CA94BC11FF009B000183A1A /* File.swift */,
|
|
||||||
6CA94BC31FF070C800183A1A /* Notification.swift */,
|
|
||||||
6C5C1C581FF14432006A3BA1 /* View.swift */,
|
|
||||||
);
|
);
|
||||||
path = "F-Chat";
|
path = "F-Chat";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -164,9 +167,10 @@
|
||||||
files = (
|
files = (
|
||||||
6CA94BC41FF070C800183A1A /* Notification.swift in Sources */,
|
6CA94BC41FF070C800183A1A /* Notification.swift in Sources */,
|
||||||
6CA94BAE1FEFEE7800183A1A /* ViewController.swift in Sources */,
|
6CA94BAE1FEFEE7800183A1A /* ViewController.swift in Sources */,
|
||||||
6C5C1C591FF14432006A3BA1 /* View.swift in Sources */,
|
|
||||||
6CA94BC21FF009B000183A1A /* File.swift in Sources */,
|
6CA94BC21FF009B000183A1A /* File.swift in Sources */,
|
||||||
6CA94BAC1FEFEE7800183A1A /* AppDelegate.swift in Sources */,
|
6CA94BAC1FEFEE7800183A1A /* AppDelegate.swift in Sources */,
|
||||||
|
6C8ED6192024A820007685DA /* Logs.swift in Sources */,
|
||||||
|
6C4C230D201E7DF1009B3460 /* Background.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
{
|
{
|
||||||
"idiom" : "ios-marketing",
|
"idiom" : "ios-marketing",
|
||||||
"size" : "1024x1024",
|
"size" : "1024x1024",
|
||||||
"filename" : "icon-1024.png",
|
"filename" : "icon-1024.jpg",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
Binary file not shown.
Before Width: | Height: | Size: 382 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 996 B |
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue