0.2.16
|
@ -1,7 +1,5 @@
|
|||
node_modules/
|
||||
/electron/app
|
||||
/electron/dist
|
||||
/cordova/platforms
|
||||
/cordova/plugins
|
||||
/cordova/www
|
||||
/mobile/www
|
||||
*.vue.ts
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div class="bbcodeEditorContainer">
|
||||
<slot></slot>
|
||||
<div class="btn-group" role="toolbar">
|
||||
<a tabindex="0" class="btn bbcodeEditorButton bbcode-btn" role="button" @click="showToolbar = true" @blur="showToolbar = false">
|
||||
<span class="fa fa-code"></span></a>
|
||||
<div class="bbcode-toolbar" role="toolbar" :style="showToolbar ? 'display:block' : ''" @mousedown.stop.prevent>
|
||||
<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>
|
||||
|
@ -11,7 +14,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="bbcodeEditorTextarea">
|
||||
<textarea ref="input" :value="text" @input="$emit('input', $event.target.value)" 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"
|
||||
:placeholder="placeholder" @keypress="$emit('keypress', $event)" @keydown="onKeyDown"></textarea>
|
||||
<div class="bbcodePreviewArea" v-show="preview">
|
||||
|
@ -57,9 +60,13 @@
|
|||
element: HTMLTextAreaElement;
|
||||
maxHeight: number;
|
||||
minHeight: number;
|
||||
showToolbar = false;
|
||||
protected parser: BBCodeParser;
|
||||
protected defaultButtons = defaultButtons;
|
||||
private isShiftPressed = false;
|
||||
private undoStack: string[] = [];
|
||||
private undoIndex = 0;
|
||||
private lastInput = 0;
|
||||
|
||||
created(): void {
|
||||
this.parser = new CoreBBCodeParser();
|
||||
|
@ -71,6 +78,12 @@
|
|||
this.maxHeight = parseInt($element.css('max-height'), 10);
|
||||
//tslint:disable-next-line:strict-boolean-expressions
|
||||
this.minHeight = parseInt($element.css('min-height'), 10) || $element.outerHeight() || 50;
|
||||
setInterval(() => {
|
||||
if(Date.now() - this.lastInput >= 500 && this.text !== this.undoStack[0] && this.undoIndex === 0) {
|
||||
if(this.undoStack.length >= 30) this.undoStack.pop();
|
||||
this.undoStack.unshift(this.text);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
get buttons(): EditorButton[] {
|
||||
|
@ -83,8 +96,12 @@
|
|||
|
||||
@Watch('value')
|
||||
watchValue(newValue: string): void {
|
||||
this.text = newValue;
|
||||
this.$nextTick(() => this.resize());
|
||||
if(this.text === newValue) return;
|
||||
this.text = newValue;
|
||||
this.lastInput = 0;
|
||||
this.undoIndex = 0;
|
||||
this.undoStack = [];
|
||||
}
|
||||
|
||||
getSelection(): EditorSelection {
|
||||
|
@ -138,11 +155,35 @@
|
|||
if(button.endText === undefined)
|
||||
button.endText = `[/${button.tag}]`;
|
||||
this.applyText(button.startText, button.endText);
|
||||
this.lastInput = Date.now();
|
||||
}
|
||||
|
||||
onInput(): void {
|
||||
if(this.undoIndex > 0) {
|
||||
this.undoStack = this.undoStack.slice(this.undoIndex);
|
||||
this.undoIndex = 0;
|
||||
}
|
||||
this.$emit('input', this.text);
|
||||
this.lastInput = Date.now();
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent): void {
|
||||
const key = getKey(e);
|
||||
if((e.metaKey || e.ctrlKey) && !e.shiftKey && key !== 'Control' && key !== 'Meta') { //tslint:disable-line:curly
|
||||
if((e.metaKey || e.ctrlKey) && !e.shiftKey && key !== 'control' && key !== 'meta') {
|
||||
if(key === 'z') {
|
||||
e.preventDefault();
|
||||
if(this.undoIndex === 0 && this.undoStack[0] !== this.text) this.undoStack.unshift(this.text);
|
||||
if(this.undoStack.length > this.undoIndex + 1) {
|
||||
this.text = this.undoStack[++this.undoIndex];
|
||||
this.lastInput = Date.now();
|
||||
}
|
||||
} else if(key === 'y') {
|
||||
e.preventDefault();
|
||||
if(this.undoIndex > 0) {
|
||||
this.text = this.undoStack[--this.undoIndex];
|
||||
this.lastInput = Date.now();
|
||||
}
|
||||
}
|
||||
for(const button of this.buttons)
|
||||
if(button.key === key) {
|
||||
e.stopPropagation();
|
||||
|
@ -150,12 +191,12 @@
|
|||
this.apply(button);
|
||||
break;
|
||||
}
|
||||
} else if(key === 'Shift') this.isShiftPressed = true;
|
||||
} else if(key === 'shift') this.isShiftPressed = true;
|
||||
this.$emit('keydown', e);
|
||||
}
|
||||
|
||||
onKeyUp(e: KeyboardEvent): void {
|
||||
if(getKey(e) === 'Shift') this.isShiftPressed = false;
|
||||
if(getKey(e) === 'shift') this.isShiftPressed = false;
|
||||
this.$emit('keyup', e);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {BBCodeCustomTag, BBCodeParser, BBCodeSimpleTag} from './parser';
|
||||
|
||||
const urlFormat = '((?:(?:https?|ftps?|irc):)?\\/\\/[^\\s\\/$.?#"\']+\\.[^\\s"]*)';
|
||||
const urlFormat = '((?:https?|ftps?|irc)://[^\\s/$.?#"\']+\\.[^\\s"]+)';
|
||||
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
|
||||
export const urlRegex = new RegExp(`^${urlFormat}$`);
|
||||
|
||||
|
|
|
@ -54,13 +54,13 @@ export let defaultButtons: ReadonlyArray<EditorButton> = [
|
|||
title: 'Superscript (Ctrl+↑)\n\nLifts text above the text baseline. Makes text slightly smaller. Cannot be nested.',
|
||||
tag: 'sup',
|
||||
icon: 'fa-superscript',
|
||||
key: 'ArrowUp'
|
||||
key: 'arrowup'
|
||||
},
|
||||
{
|
||||
title: 'Subscript (Ctrl+↓)\n\nPushes text below the text baseline. Makes text slightly smaller. Cannot be nested.',
|
||||
tag: 'sub',
|
||||
icon: 'fa-subscript',
|
||||
key: 'ArrowDown'
|
||||
key: 'arrowdown'
|
||||
},
|
||||
{
|
||||
title: 'URL (Ctrl+L)\n\nCreates a clickable link to another page of your choosing.',
|
||||
|
|
|
@ -160,7 +160,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
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="imageBlock" src="${this.settings.staticDomain}images/charinline/${showP1}/${showP2}/${showInline.hash}.${showInline.extension}"/></div>`);
|
||||
$(element).replaceWith(`<div><img class="inline-image" src="${this.settings.staticDomain}images/charinline/${showP1}/${showP2}/${showInline.hash}.${showInline.extension}"/></div>`);
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
@ -171,7 +171,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
} else {
|
||||
const outerEl = parser.createElement('div');
|
||||
const el = parser.createElement('img');
|
||||
el.className = 'imageBlock';
|
||||
el.className = 'inline-image';
|
||||
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||
outerEl.appendChild(el);
|
||||
parent.appendChild(outerEl);
|
||||
|
@ -179,7 +179,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
}
|
||||
}, (_, element, __, ___) => {
|
||||
// Need to remove any appended contents, because this is a total hack job.
|
||||
if(element.className !== 'imageBlock')
|
||||
if(element.className !== 'inline-image')
|
||||
return;
|
||||
while(element.firstChild !== null)
|
||||
element.removeChild(element.firstChild);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<modal :buttons="false" :action="l('chat.channels')" @close="closed">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<ul class="nav nav-tabs">
|
||||
<ul class="nav nav-tabs" style="flex-shrink:0">
|
||||
<li role="presentation" :class="{active: !privateTabShown}">
|
||||
<a href="#" @click.prevent="privateTabShown = false">{{l('channelList.public')}}</a>
|
||||
</li>
|
||||
|
@ -73,7 +73,6 @@
|
|||
const channels: Channel.ListItem[] = [];
|
||||
if(this.filter.length > 0) {
|
||||
const search = new RegExp(this.filter.replace(/[^\w]/gi, '\\$&'), 'i');
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in list) {
|
||||
const item = list[key]!;
|
||||
if(search.test(item.name)) channels.push(item);
|
||||
|
|
|
@ -112,8 +112,10 @@
|
|||
this.error = l('characterSearch.error.tooManyResults');
|
||||
}
|
||||
});
|
||||
core.connection.onMessage('FKS', (data) => this.results = data.characters.filter((x) =>
|
||||
core.state.hiddenUsers.indexOf(x) === -1).map((x) => core.characters.get(x)).sort(sort));
|
||||
core.connection.onMessage('FKS', (data) => {
|
||||
this.results = data.characters.filter((x) => core.state.hiddenUsers.indexOf(x) === -1)
|
||||
.map((x) => core.characters.get(x)).sort(sort);
|
||||
});
|
||||
(<Modal>this.$children[0]).fixDropdowns();
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
import Channels from '../fchat/channels';
|
||||
import Characters from '../fchat/characters';
|
||||
import ChatView from './ChatView.vue';
|
||||
import {errorToString, requestNotificationsPermission} from './common';
|
||||
import {errorToString} from './common';
|
||||
import Conversations from './conversations';
|
||||
import core from './core';
|
||||
import l from './localize';
|
||||
|
@ -44,8 +44,8 @@
|
|||
@Prop({required: true})
|
||||
readonly ownCharacters: string[];
|
||||
@Prop({required: true})
|
||||
readonly defaultCharacter: string;
|
||||
selectedCharacter = this.defaultCharacter;
|
||||
readonly defaultCharacter: string | undefined;
|
||||
selectedCharacter = this.defaultCharacter || this.ownCharacters[0]; //tslint:disable-line:strict-boolean-expressions
|
||||
error = '';
|
||||
connecting = false;
|
||||
connected = false;
|
||||
|
@ -59,10 +59,11 @@
|
|||
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
|
||||
if(this.connected) core.notifications.playSound('logout');
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
});
|
||||
core.connection.onEvent('connecting', async() => {
|
||||
this.connecting = true;
|
||||
if(core.state.settings.notifications) await requestNotificationsPermission();
|
||||
if(core.state.settings.notifications) await core.notifications.requestPermission();
|
||||
});
|
||||
core.connection.onEvent('connected', () => {
|
||||
(<Modal>this.$refs['reconnecting']).hide();
|
||||
|
|
|
@ -1,77 +1,68 @@
|
|||
<template>
|
||||
<div style="height:100%; display: flex; position: relative;" id="chatView" @click="$refs['userMenu'].handleEvent($event)"
|
||||
@contextmenu="$refs['userMenu'].handleEvent($event)" @touchstart="$refs['userMenu'].handleEvent($event)"
|
||||
@contextmenu="$refs['userMenu'].handleEvent($event)" @touchstart.passive="$refs['userMenu'].handleEvent($event)"
|
||||
@touchend="$refs['userMenu'].handleEvent($event)">
|
||||
<div class="sidebar sidebar-left" id="sidebar">
|
||||
<button @click="sidebarExpanded = !sidebarExpanded" class="btn btn-default btn-xs expander" :aria-label="l('chat.menu')">
|
||||
<span class="fa" :class="{'fa-chevron-up': sidebarExpanded, 'fa-chevron-down': !sidebarExpanded}"></span>
|
||||
<span class="fa fa-bars fa-rotate-90" style="vertical-align: middle"></span>
|
||||
</button>
|
||||
<div class="body" :style="sidebarExpanded ? 'display:block' : ''"
|
||||
style="width: 200px; padding-right: 5px; height: 100%; overflow: auto;">
|
||||
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left; margin-right:5px; width:60px;"/>
|
||||
{{ownCharacter.name}}
|
||||
<a href="#" @click.prevent="logOut" class="btn"><span class="fa fa-sign-out"></span>{{l('chat.logout')}}</a><br/>
|
||||
<div>
|
||||
{{l('chat.status')}}
|
||||
<a href="#" @click.prevent="$refs['statusDialog'].show()" class="btn">
|
||||
<span class="fa fa-fw" :class="getStatusIcon(ownCharacter.status)"></span>{{l('status.' + ownCharacter.status)}}
|
||||
</a>
|
||||
</div>
|
||||
<div style="clear:both;">
|
||||
<a href="#" @click.prevent="$refs['searchDialog'].show()" class="btn"><span class="fa fa-search"></span>
|
||||
{{l('characterSearch.open')}}</a>
|
||||
</div>
|
||||
<div><a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn"><span class="fa fa-cog"></span>
|
||||
{{l('settings.open')}}</a></div>
|
||||
<div><a href="#" @click.prevent="$refs['recentDialog'].show()" class="btn"><span class="fa fa-history"></span>
|
||||
{{l('chat.recentConversations')}}</a></div>
|
||||
<div>
|
||||
<div class="list-group conversation-nav">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
{{conversations.consoleTab.name}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{l('chat.pms')}}
|
||||
<div class="list-group conversation-nav" ref="privateConversations">
|
||||
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" :data-character="conversation.character.name"
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key">
|
||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||
<div class="name">
|
||||
<span>{{conversation.character.name}}</span>
|
||||
<div style="text-align:right;line-height:0">
|
||||
<span class="fa"
|
||||
:class="{'fa-commenting': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||
></span><span class="pin fa fa-thumb-tack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" @click.prevent="$refs['channelsDialog'].show()" class="btn"><span class="fa fa-list"></span>
|
||||
{{l('chat.channels')}}</a>
|
||||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel"
|
||||
:key="conversation.key"><span class="name">{{conversation.name}}</span><span><span class="pin fa fa-thumb-tack"
|
||||
:class="{'active': conversation.isPinned}" @click.stop="conversation.isPinned = !conversation.isPinned"
|
||||
: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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<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"/>
|
||||
{{ownCharacter.name}}
|
||||
<a href="#" @click.prevent="logOut" class="btn"><span class="fa fa-sign-out"></span>{{l('chat.logout')}}</a><br/>
|
||||
<div>
|
||||
{{l('chat.status')}}
|
||||
<a href="#" @click.prevent="$refs['statusDialog'].show()" class="btn">
|
||||
<span class="fa fa-fw" :class="getStatusIcon(ownCharacter.status)"></span>{{l('status.' + ownCharacter.status)}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="clear:both">
|
||||
<a href="#" @click.prevent="$refs['searchDialog'].show()" class="btn"><span class="fa fa-search"></span>
|
||||
{{l('characterSearch.open')}}</a>
|
||||
</div>
|
||||
<div><a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn"><span class="fa fa-cog"></span>
|
||||
{{l('settings.open')}}</a></div>
|
||||
<div><a href="#" @click.prevent="$refs['recentDialog'].show()" class="btn"><span class="fa fa-history"></span>
|
||||
{{l('chat.recentConversations')}}</a></div>
|
||||
<div class="list-group conversation-nav">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
{{conversations.consoleTab.name}}
|
||||
</a>
|
||||
</div>
|
||||
{{l('chat.pms')}}
|
||||
<div class="list-group conversation-nav" ref="privateConversations">
|
||||
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" :data-character="conversation.character.name" data-touch="false"
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key">
|
||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||
<div class="name">
|
||||
<span>{{conversation.character.name}}</span>
|
||||
<div style="text-align:right;line-height:0">
|
||||
<span class="fa"
|
||||
:class="{'fa-commenting': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||
></span><span class="pin fa fa-thumb-tack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href="#" @click.prevent="$refs['channelsDialog'].show()" class="btn"><span class="fa fa-list"></span>
|
||||
{{l('chat.channels')}}</a>
|
||||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel"
|
||||
:key="conversation.key"><span class="name">{{conversation.name}}</span><span><span class="pin fa fa-thumb-tack"
|
||||
:class="{'active': conversation.isPinned}" @click.stop="conversation.isPinned = !conversation.isPinned"
|
||||
: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>
|
||||
</a>
|
||||
</div>
|
||||
</sidebar>
|
||||
<div style="width: 100%; display:flex; flex-direction:column;">
|
||||
<div id="quick-switcher" class="list-group">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
<span class="fa fa-home conversation-icon"></span>
|
||||
{{conversations.consoleTab.name}}
|
||||
</a>
|
||||
<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">
|
||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||
|
@ -112,6 +103,7 @@
|
|||
import RecentConversations from './RecentConversations.vue';
|
||||
import ReportDialog from './ReportDialog.vue';
|
||||
import SettingsView from './SettingsView.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import StatusSwitcher from './StatusSwitcher.vue';
|
||||
import {getStatusIcon} from './user_view';
|
||||
import UserList from './UserList.vue';
|
||||
|
@ -120,13 +112,13 @@
|
|||
const unreadClasses = {
|
||||
[Conversation.UnreadState.None]: '',
|
||||
[Conversation.UnreadState.Mention]: 'list-group-item-warning',
|
||||
[Conversation.UnreadState.Unread]: 'has-new'
|
||||
[Conversation.UnreadState.Unread]: 'list-group-item-danger'
|
||||
};
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'user-list': UserList, channels: ChannelList, 'status-switcher': StatusSwitcher, 'character-search': CharacterSearch,
|
||||
settings: SettingsView, conversation: ConversationView, 'report-dialog': ReportDialog,
|
||||
settings: SettingsView, conversation: ConversationView, 'report-dialog': ReportDialog, sidebar: Sidebar,
|
||||
'user-menu': UserMenu, 'recent-conversations': RecentConversations
|
||||
}
|
||||
})
|
||||
|
@ -140,19 +132,25 @@
|
|||
|
||||
mounted(): void {
|
||||
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);
|
||||
document.addEventListener('keydown', this.keydownListener);
|
||||
window.addEventListener('keydown', this.keydownListener);
|
||||
this.setFontSize(core.state.settings.fontSize);
|
||||
Sortable.create(this.$refs['privateConversations'], {
|
||||
animation: 50,
|
||||
onEnd: (e: {oldIndex: number, newIndex: number}) => core.conversations.privateConversations[e.oldIndex].sort(e.newIndex)
|
||||
onEnd: async(e: {oldIndex: number, newIndex: number}) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.privateConversations[e.oldIndex].sort(e.newIndex);
|
||||
}
|
||||
});
|
||||
Sortable.create(this.$refs['channelConversations'], {
|
||||
animation: 50,
|
||||
onEnd: (e: {oldIndex: number, newIndex: number}) => core.conversations.channelConversations[e.oldIndex].sort(e.newIndex)
|
||||
onEnd: async(e: {oldIndex: number, newIndex: number}) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.channelConversations[e.oldIndex].sort(e.newIndex);
|
||||
}
|
||||
});
|
||||
const ownCharacter = core.characters.ownCharacter;
|
||||
let idleTimer: number | undefined, idleStatus: Connection.ClientCommands['STA'] | undefined, lastUpdate = 0;
|
||||
window.focus = () => {
|
||||
window.addEventListener('focus', () => {
|
||||
core.notifications.isInBackground = false;
|
||||
if(idleTimer !== undefined) {
|
||||
clearTimeout(idleTimer);
|
||||
|
@ -164,8 +162,8 @@
|
|||
idleStatus = undefined;
|
||||
}
|
||||
}, Math.max(lastUpdate + 5 /*core.connection.vars.sta_flood*/ * 1000 + 1000 - Date.now(), 0));
|
||||
};
|
||||
window.blur = () => {
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
core.notifications.isInBackground = true;
|
||||
if(idleTimer !== undefined) clearTimeout(idleTimer);
|
||||
if(core.state.settings.idleTimer !== 0)
|
||||
|
@ -174,7 +172,7 @@
|
|||
idleStatus = {status: ownCharacter.status, statusmsg: ownCharacter.statusText};
|
||||
core.connection.send('STA', {status: 'idle', statusmsg: ownCharacter.statusText});
|
||||
}, core.state.settings.idleTimer * 60000);
|
||||
};
|
||||
});
|
||||
core.connection.onEvent('closed', () => {
|
||||
if(idleTimer !== undefined) {
|
||||
window.clearTimeout(idleTimer);
|
||||
|
@ -189,7 +187,7 @@
|
|||
}
|
||||
|
||||
destroyed(): void {
|
||||
document.removeEventListener('keydown', this.keydownListener);
|
||||
window.removeEventListener('keydown', this.keydownListener);
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent): void {
|
||||
|
@ -197,9 +195,11 @@
|
|||
const pms = this.conversations.privateConversations;
|
||||
const channels = this.conversations.channelConversations;
|
||||
const console = this.conversations.consoleTab;
|
||||
if(getKey(e) === 'ArrowUp' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
|
||||
if(selected === console) return;
|
||||
if(Conversation.isPrivate(selected)) {
|
||||
if(getKey(e) === 'arrowup' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
||||
if(selected === console) { //tslint:disable-line:curly
|
||||
if(channels.length > 0) channels[channels.length - 1].show();
|
||||
else if(pms.length > 0) pms[pms.length - 1].show();
|
||||
} else if(Conversation.isPrivate(selected)) {
|
||||
const index = pms.indexOf(selected);
|
||||
if(index === 0) console.show();
|
||||
else pms[index - 1].show();
|
||||
|
@ -210,7 +210,7 @@
|
|||
else console.show();
|
||||
else channels[index - 1].show();
|
||||
}
|
||||
} else if(getKey(e) === 'ArrowDown' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
||||
else if(getKey(e) === 'arrowdown' && e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
|
||||
if(selected === console) { //tslint:disable-line:curly - false positive
|
||||
if(pms.length > 0) pms[0].show();
|
||||
else if(channels.length > 0) channels[0].show();
|
||||
|
@ -221,7 +221,8 @@
|
|||
} else pms[index + 1].show();
|
||||
} else {
|
||||
const index = channels.indexOf(<Conversation.ChannelConversation>selected);
|
||||
if(index !== channels.length - 1) channels[index + 1].show();
|
||||
if(index < channels.length - 1) channels[index + 1].show();
|
||||
else console.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,13 +258,13 @@
|
|||
}
|
||||
|
||||
getClasses(conversation: Conversation): string {
|
||||
return unreadClasses[conversation.unread] + (conversation === core.conversations.selectedConversation ? ' active' : '');
|
||||
return conversation === core.conversations.selectedConversation ? ' active' : unreadClasses[conversation.unread];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import '~bootstrap/less/variables.less';
|
||||
@import "../less/flist_variables.less";
|
||||
|
||||
.list-group.conversation-nav {
|
||||
margin-bottom: 10px;
|
||||
|
@ -271,6 +272,9 @@
|
|||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-right: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
.name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
@ -303,6 +307,10 @@
|
|||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item-danger:not(.active) {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
#quick-switcher {
|
||||
|
@ -319,6 +327,8 @@
|
|||
text-align: center;
|
||||
line-height: 1;
|
||||
padding: 5px 5px 0;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
&:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
&:last-child {
|
||||
|
@ -343,6 +353,10 @@
|
|||
font-size: 2em;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.list-group-item-danger:not(.active) {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
|
@ -350,7 +364,13 @@
|
|||
padding: 2px 0;
|
||||
}
|
||||
@media (min-width: @screen-sm-min) {
|
||||
position: static;
|
||||
.sidebar {
|
||||
position: static;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
|
||||
mounted(): void {
|
||||
const permissions = core.connection.vars.permissions;
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in commands) {
|
||||
const command = commands[key]!;
|
||||
if(command.documented !== undefined ||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
<user :character="conversation.character"></user>
|
||||
<logs :conversation="conversation"></logs>
|
||||
<a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn">
|
||||
<span class="fa fa-cog"></span> {{l('conversationSettings.title')}}
|
||||
<span class="fa fa-cog"></span> <span class="btn-text">{{l('conversationSettings.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="reportDialog.report();" class="btn"><span class="fa fa-exclamation"></span>
|
||||
{{l('chat.report')}}</a>
|
||||
<a href="#" @click.prevent="reportDialog.report();" class="btn"><span class="fa fa-exclamation-triangle"></span>
|
||||
<span class="btn-text">{{l('chat.report')}}</span></a>
|
||||
</div>
|
||||
<div style="overflow: auto">
|
||||
{{l('status.' + conversation.character.status)}}
|
||||
|
@ -26,15 +26,15 @@
|
|||
<h4 style="margin: 0; display:inline; vertical-align: middle;">{{conversation.name}}</h4>
|
||||
<a @click="descriptionExpanded = !descriptionExpanded" class="btn">
|
||||
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
|
||||
{{l('channel.description')}}
|
||||
<span class="btn-text">{{l('channel.description')}}</span>
|
||||
</a>
|
||||
<manage-channel :channel="conversation.channel" v-if="isChannelMod"></manage-channel>
|
||||
<logs :conversation="conversation"></logs>
|
||||
<a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn">
|
||||
<span class="fa fa-cog"></span> {{l('conversationSettings.title')}}
|
||||
<span class="fa fa-cog"></span> <span class="btn-text">{{l('conversationSettings.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="reportDialog.report();" class="btn"><span class="fa fa-exclamation-triangle"></span>
|
||||
{{l('chat.report')}}</a>
|
||||
<span class="btn-text">{{l('chat.report')}}</span></a>
|
||||
</div>
|
||||
<ul class="nav nav-pills mode-switcher">
|
||||
<li v-for="mode in modes" :class="{active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'}">
|
||||
|
@ -72,18 +72,18 @@
|
|||
{{l('chat.typing.' + conversation.typingStatus, conversation.name)}}
|
||||
</span>
|
||||
<div v-show="conversation.infoText" style="display:flex;align-items:center">
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.infoText = '';"></span>
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.infoText = ''"></span>
|
||||
<span style="flex:1;margin-left:5px">{{conversation.infoText}}</span>
|
||||
</div>
|
||||
<div v-show="conversation.errorText" style="display:flex;align-items:center">
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.errorText = '';"></span>
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.errorText = ''"></span>
|
||||
<span class="redText" style="flex:1;margin-left:5px">{{conversation.errorText}}</span>
|
||||
</div>
|
||||
<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="onInput"
|
||||
classes="form-control chat-text-box" :disabled="adCountdown" ref="textBox" style="position:relative;"
|
||||
:maxlength="conversation.maxMessageLength">
|
||||
:classes="'form-control chat-text-box' + (conversation.isSendingAds ? ' ads-text-box' : '')" :disabled="adCountdown"
|
||||
ref="textBox" style="position:relative" :maxlength="conversation.maxMessageLength">
|
||||
<div style="float:right;text-align:right;display:flex;align-items:center">
|
||||
<div v-show="conversation.maxMessageLength" style="margin-right: 5px;">
|
||||
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
||||
|
@ -206,9 +206,9 @@
|
|||
});
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent): void {
|
||||
async onKeyDown(e: KeyboardEvent): Promise<void> {
|
||||
const editor = <Editor>this.$refs['textBox'];
|
||||
if(getKey(e) === 'Tab') {
|
||||
if(getKey(e) === 'tab') {
|
||||
e.preventDefault();
|
||||
if(this.conversation.enteredText.length === 0 || this.isConsoleTab) return;
|
||||
if(this.tabOptions === undefined) {
|
||||
|
@ -242,13 +242,13 @@
|
|||
}
|
||||
} else {
|
||||
if(this.tabOptions !== undefined) this.tabOptions = undefined;
|
||||
if(getKey(e) === 'ArrowUp' && this.conversation.enteredText.length === 0
|
||||
if(getKey(e) === 'arrowup' && this.conversation.enteredText.length === 0
|
||||
&& !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey)
|
||||
this.conversation.loadLastSent();
|
||||
else if(getKey(e) === 'Enter') {
|
||||
else if(getKey(e) === 'enter') {
|
||||
if(e.shiftKey) return;
|
||||
e.preventDefault();
|
||||
this.conversation.send();
|
||||
await this.conversation.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -302,8 +302,7 @@
|
|||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import '~bootstrap/less/variables.less';
|
||||
|
||||
@import "../less/flist_variables.less";
|
||||
#conversation {
|
||||
.header {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<span>
|
||||
<a href="#" @click.prevent="showLogs" class="btn">
|
||||
<span class="fa" :class="isPersistent ? 'fa-file-text-o' : 'fa-download'"></span> {{l('logs.title')}}
|
||||
<span class="fa" :class="isPersistent ? 'fa-file-text-o' : 'fa-download'"></span>
|
||||
<span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
<modal v-if="isPersistent" :buttons="false" ref="dialog" id="logs-dialog" :action="l('logs.title')" dialogClass="modal-lg"
|
||||
@open="onOpen" class="form-horizontal">
|
||||
|
@ -9,7 +10,7 @@
|
|||
<label class="col-sm-2">{{l('logs.conversation')}}</label>
|
||||
<div class="col-sm-10">
|
||||
<filterable-select v-model="selectedConversation" :options="conversations" :filterFunc="filterConversation"
|
||||
buttonClass="form-control" :placeholder="l('filter')">
|
||||
buttonClass="form-control" :placeholder="l('filter')" @input="loadMessages">
|
||||
<template slot-scope="s">{{s.option && ((s.option.id[0] == '#' ? '#' : '') + s.option.name)}}</template>
|
||||
</filterable-select>
|
||||
</div>
|
||||
|
@ -60,7 +61,7 @@
|
|||
@Prop({required: true})
|
||||
readonly conversation: Conversation;
|
||||
selectedConversation: {id: string, name: string} | null = null;
|
||||
selectedDate: Date | null = null;
|
||||
selectedDate: string | null = null;
|
||||
isPersistent = LogInterfaces.isPersistent(core.logs);
|
||||
conversations = LogInterfaces.isPersistent(core.logs) ? core.logs.conversations : undefined;
|
||||
l = l;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span>
|
||||
<a href="#" @click.prevent="openDialog" class="btn">
|
||||
<span class="fa fa-edit"></span> {{l('manageChannel.open')}}
|
||||
<span class="fa fa-edit"></span> <span class="btn-text">{{l('manageChannel.open')}}</span>
|
||||
</a>
|
||||
<modal ref="dialog" :action="l('manageChannel.action', channel.name)" :buttonText="l('manageChannel.submit')" @submit="submit">
|
||||
<div class="form-group" v-show="channel.id.substr(0, 4) === 'adh-'">
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="fontSize">{{l('settings.fontSize')}}</label>
|
||||
<input id="fontSize" type="number" min="10" max="24" number class="form-control" v-model="fontSize"/>
|
||||
<input id="fontSize" type="number" min="10" max="24" class="form-control" v-model="fontSize"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="selectedTab == 'notifications'">
|
||||
|
@ -111,7 +111,6 @@
|
|||
import Component from 'vue-class-component';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {requestNotificationsPermission} from './common';
|
||||
import core from './core';
|
||||
import {Settings as SettingsInterface} from './interfaces';
|
||||
import l from './localize';
|
||||
|
@ -206,9 +205,9 @@
|
|||
alwaysNotify: this.alwaysNotify,
|
||||
logMessages: this.logMessages,
|
||||
logAds: this.logAds,
|
||||
fontSize: this.fontSize
|
||||
fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize
|
||||
};
|
||||
if(this.notifications) await requestNotificationsPermission();
|
||||
if(this.notifications) await core.notifications.requestPermission();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="sidebar-wrapper" :class="{open: expanded}">
|
||||
<div :class="'sidebar sidebar-' + (right ? 'right' : 'left')">
|
||||
<button @click="expanded = !expanded" class="btn btn-default btn-xs expander" :aria-label="label">
|
||||
<span :class="'fa fa-rotate-270 ' + icon" style="vertical-align: middle" v-if="right"></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>
|
||||
</button>
|
||||
<div class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop in" @click="expanded = false"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Sidebar extends Vue {
|
||||
@Prop()
|
||||
readonly right?: true;
|
||||
@Prop()
|
||||
readonly label?: string;
|
||||
@Prop({required: true})
|
||||
readonly icon: string;
|
||||
@Prop({default: false})
|
||||
readonly open: boolean;
|
||||
expanded = this.open;
|
||||
|
||||
@Watch('open')
|
||||
watchOpen(): void {
|
||||
this.expanded = this.open;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,35 +1,30 @@
|
|||
<template>
|
||||
<div id="user-list" class="sidebar sidebar-right">
|
||||
<button @click="expanded = !expanded" class="btn btn-default btn-xs expander" :aria-label="l('users.title')">
|
||||
<span class="fa fa-users fa-rotate-270" style="vertical-align: middle"></span>
|
||||
<span class="fa" :class="{'fa-chevron-down': !expanded, 'fa-chevron-up': expanded}"></span>
|
||||
</button>
|
||||
<div class="body" :style="expanded ? 'display:flex' : ''" style="min-width: 200px; flex-direction:column; max-height: 100%;">
|
||||
<ul class="nav nav-tabs" style="flex-shrink:0">
|
||||
<li role="presentation" :class="{active: !channel || !memberTabShown}">
|
||||
<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>
|
||||
<div v-for="character in friends" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
</div>
|
||||
<h4>{{l('users.bookmarks')}}</h4>
|
||||
<div v-for="character in bookmarks" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
</div>
|
||||
<sidebar id="user-list" :label="l('users.title')" icon="fa-users" :right="true" :open="expanded">
|
||||
<ul class="nav nav-tabs" style="flex-shrink:0">
|
||||
<li role="presentation" :class="{active: !channel || !memberTabShown}">
|
||||
<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>
|
||||
<div v-for="character in friends" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
</div>
|
||||
<div v-if="channel" v-show="memberTabShown" class="users" style="padding: 5px;">
|
||||
<div v-for="member in channel.sortedMembers" :key="member.character.name">
|
||||
<user :character="member.character" :channel="channel" :showStatus="true"></user>
|
||||
</div>
|
||||
<h4>{{l('users.bookmarks')}}</h4>
|
||||
<div v-for="character in bookmarks" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="channel" v-show="memberTabShown" class="users" style="padding:5px">
|
||||
<h4>{{l('users.memberCount', channel.sortedMembers.length)}}</h4>
|
||||
<div v-for="member in channel.sortedMembers" :key="member.character.name">
|
||||
<user :character="member.character" :channel="channel" :showStatus="true"></user>
|
||||
</div>
|
||||
</div>
|
||||
</sidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -38,14 +33,15 @@
|
|||
import core from './core';
|
||||
import {Channel, Character, Conversation} from './interfaces';
|
||||
import l from './localize';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import UserView from './user_view';
|
||||
|
||||
@Component({
|
||||
components: {user: UserView}
|
||||
components: {user: UserView, sidebar: Sidebar}
|
||||
})
|
||||
export default class UserList extends Vue {
|
||||
memberTabShown = false;
|
||||
expanded = window.innerWidth >= 992;
|
||||
expanded = window.innerWidth >= 900;
|
||||
l = l;
|
||||
sorter = (x: Character, y: Character) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0));
|
||||
|
||||
|
@ -64,8 +60,7 @@
|
|||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import '~bootstrap/less/variables.less';
|
||||
|
||||
@import "../less/flist_variables.less";
|
||||
#user-list {
|
||||
flex-direction: column;
|
||||
h4 {
|
||||
|
@ -82,8 +77,21 @@
|
|||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
position: static;
|
||||
@media (min-width: @screen-md-min) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.open .body {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -115,10 +115,10 @@
|
|||
this.memo = '';
|
||||
(<Modal>this.$refs['memo']).show();
|
||||
try {
|
||||
const memo = <{note: string, id: number}>await core.connection.queryApi('character-memo-get.php',
|
||||
const memo = <{note: string | null, id: number}>await core.connection.queryApi('character-memo-get2.php',
|
||||
{target: this.character!.name});
|
||||
this.memoId = memo.id;
|
||||
this.memo = memo.note;
|
||||
this.memo = memo.note !== null ? memo.note : '';
|
||||
this.memoLoading = false;
|
||||
} catch(e) {
|
||||
alert(errorToString(e));
|
||||
|
@ -165,6 +165,7 @@
|
|||
if(node.character !== undefined || node.dataset['character'] !== undefined || node.parentNode === null) break;
|
||||
node = node.parentElement!;
|
||||
}
|
||||
if(node.dataset['touch'] === 'false' && e.type !== 'contextmenu') return;
|
||||
if(node.character === undefined)
|
||||
if(node.dataset['character'] !== undefined) node.character = core.characters.get(node.dataset['character']!);
|
||||
else {
|
||||
|
@ -174,6 +175,7 @@
|
|||
switch(e.type) {
|
||||
case 'click':
|
||||
if(node.dataset['character'] === undefined) this.onClick(node.character);
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'touchstart':
|
||||
this.touchTimer = window.setTimeout(() => {
|
||||
|
@ -190,8 +192,8 @@
|
|||
break;
|
||||
case 'contextmenu':
|
||||
this.openMenu(touch, node.character, node.channel);
|
||||
e.preventDefault();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
private onClick(character: Character): void {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import {WebSocketConnection} from '../fchat/interfaces';
|
||||
import {WebSocketConnection} from '../fchat';
|
||||
import l from './localize';
|
||||
|
||||
export default class Socket implements WebSocketConnection {
|
||||
static host = 'wss://chat.f-list.net:9799';
|
||||
socket: WebSocket;
|
||||
private socket: WebSocket;
|
||||
private errorHandler: (error: Error) => void;
|
||||
private lastHandler: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor() {
|
||||
this.socket = new WebSocket(Socket.host);
|
||||
|
@ -14,7 +16,9 @@ export default class Socket implements WebSocketConnection {
|
|||
}
|
||||
|
||||
onMessage(handler: (message: string) => void): void {
|
||||
this.socket.addEventListener('message', (e) => handler(<string>e.data));
|
||||
this.socket.addEventListener('message', (e) => {
|
||||
this.lastHandler = this.lastHandler.then(() => handler(<string>e.data), this.errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
onOpen(handler: () => void): void {
|
||||
|
@ -26,6 +30,7 @@ export default class Socket implements WebSocketConnection {
|
|||
}
|
||||
|
||||
onError(handler: (error: Error) => void): void {
|
||||
this.errorHandler = handler;
|
||||
this.socket.addEventListener('error', () => handler(new Error(l('login.connectError'))));
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export function messageToString(this: void | never, msg: Conversation.Message, t
|
|||
|
||||
export function getKey(e: KeyboardEvent): string {
|
||||
/*tslint:disable-next-line:strict-boolean-expressions no-any*///because of old browsers.
|
||||
return e.key || (<KeyboardEvent & {keyIdentifier: string}>e).keyIdentifier;
|
||||
return (e.key || (<KeyboardEvent & {keyIdentifier: string}>e).keyIdentifier).toLowerCase();
|
||||
}
|
||||
|
||||
/*tslint:disable:no-any no-unsafe-any*///because errors can be any
|
||||
|
@ -74,10 +74,6 @@ export function errorToString(e: any): string {
|
|||
}
|
||||
//tslint:enable
|
||||
|
||||
export async function requestNotificationsPermission(): Promise<void> {
|
||||
if((<Window & {Notification: Notification | undefined}>window).Notification !== undefined) await Notification.requestPermission();
|
||||
}
|
||||
|
||||
let messageId = 0;
|
||||
|
||||
export class Message implements Conversation.ChatMessage {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
//tslint:disable:no-floating-promises
|
||||
import {queuedJoin} from '../fchat/channels';
|
||||
import {decodeHTML} from '../fchat/common';
|
||||
import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common';
|
||||
|
@ -46,7 +45,7 @@ abstract class Conversation implements Interfaces.Conversation {
|
|||
|
||||
set settings(value: Interfaces.Settings) {
|
||||
this._settings = value;
|
||||
state.setSettings(this.key, value);
|
||||
state.setSettings(this.key, value); //tslint:disable-line:no-floating-promises
|
||||
}
|
||||
|
||||
get isPinned(): boolean {
|
||||
|
@ -56,14 +55,14 @@ abstract class Conversation implements Interfaces.Conversation {
|
|||
set isPinned(value: boolean) {
|
||||
if(value === this._isPinned) return;
|
||||
this._isPinned = value;
|
||||
state.savePinned();
|
||||
state.savePinned(); //tslint:disable-line:no-floating-promises
|
||||
}
|
||||
|
||||
get reportMessages(): ReadonlyArray<Interfaces.Message> {
|
||||
return this.allMessages;
|
||||
}
|
||||
|
||||
send(): void {
|
||||
async send(): Promise<void> {
|
||||
if(this.enteredText.length === 0) return;
|
||||
if(isCommand(this.enteredText)) {
|
||||
const parsed = parseCommand(this.enteredText, this.context);
|
||||
|
@ -75,11 +74,11 @@ abstract class Conversation implements Interfaces.Conversation {
|
|||
}
|
||||
} else {
|
||||
this.lastSent = this.enteredText;
|
||||
this.doSend();
|
||||
await this.doSend();
|
||||
}
|
||||
}
|
||||
|
||||
abstract addMessage(message: Interfaces.Message): void;
|
||||
abstract async addMessage(message: Interfaces.Message): Promise<void>;
|
||||
|
||||
loadLastSent(): void {
|
||||
this.enteredText = this.lastSent;
|
||||
|
@ -109,7 +108,7 @@ abstract class Conversation implements Interfaces.Conversation {
|
|||
safeAddMessage(this.messages, message, this.maxMessages);
|
||||
}
|
||||
|
||||
protected abstract doSend(): void;
|
||||
protected abstract doSend(): Promise<void> | void;
|
||||
}
|
||||
|
||||
class PrivateConversation extends Conversation implements Interfaces.PrivateConversation {
|
||||
|
@ -144,32 +143,34 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
|
|||
} else if(this.ownTypingStatus !== 'clear') this.setOwnTyping('clear');
|
||||
}
|
||||
|
||||
addMessage(message: Interfaces.Message): void {
|
||||
async addMessage(message: Interfaces.Message): Promise<void> {
|
||||
await this.logPromise;
|
||||
this.safeAddMessage(message);
|
||||
if(message.type !== Interfaces.Message.Type.Event) {
|
||||
if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message));
|
||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||
if(this.settings.notify !== Interfaces.Setting.False && message.sender !== core.characters.ownCharacter)
|
||||
core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention');
|
||||
if(this !== state.selectedConversation)
|
||||
if(this !== state.selectedConversation || !state.windowFocused)
|
||||
this.unread = Interfaces.UnreadState.Mention;
|
||||
this.typingStatus = 'clear';
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
async close(): Promise<void> {
|
||||
state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
|
||||
delete state.privateMap[this.character.name.toLowerCase()];
|
||||
state.savePinned();
|
||||
await state.savePinned();
|
||||
if(state.selectedConversation === this) state.show(state.consoleTab);
|
||||
}
|
||||
|
||||
sort(newIndex: number): void {
|
||||
async sort(newIndex: number): Promise<void> {
|
||||
state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
|
||||
state.privateConversations.splice(newIndex, 0, this);
|
||||
state.savePinned();
|
||||
return state.savePinned();
|
||||
}
|
||||
|
||||
protected doSend(): void {
|
||||
protected async doSend(): Promise<void> {
|
||||
await this.logPromise;
|
||||
if(this.character.status === 'offline') {
|
||||
this.errorText = l('chat.errorOffline', this.character.name);
|
||||
return;
|
||||
|
@ -180,7 +181,7 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
|
|||
core.connection.send('PRI', {recipient: this.name, message: this.enteredText});
|
||||
const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText);
|
||||
this.safeAddMessage(message);
|
||||
if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message));
|
||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||
this.enteredText = '';
|
||||
}
|
||||
|
||||
|
@ -255,7 +256,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
|||
else safeAddMessage(this[mode], message, 500);
|
||||
}
|
||||
|
||||
addMessage(message: Interfaces.Message): void {
|
||||
async addMessage(message: Interfaces.Message): Promise<void> {
|
||||
await this.logPromise;
|
||||
if((message.type === MessageType.Message || message.type === MessageType.Ad) && message.text.match(/^\/warn\b/) !== null) {
|
||||
const member = this.channel.members[message.sender.name];
|
||||
if(member !== undefined && member.rank > Channel.Rank.Member || message.sender.isChatOp)
|
||||
|
@ -264,13 +266,13 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
|||
|
||||
if(message.type === MessageType.Ad) {
|
||||
this.addModeMessage('ads', message);
|
||||
if(core.state.settings.logAds) this.logPromise.then(() => core.logs.logMessage(this, message));
|
||||
if(core.state.settings.logAds) await core.logs.logMessage(this, message);
|
||||
} else {
|
||||
this.addModeMessage('chat', message);
|
||||
if(message.type !== Interfaces.Message.Type.Event) {
|
||||
if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message);
|
||||
if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message));
|
||||
if(this !== state.selectedConversation && this.unread === Interfaces.UnreadState.None)
|
||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||
if(this !== state.selectedConversation && this.unread === Interfaces.UnreadState.None || !state.windowFocused)
|
||||
this.unread = Interfaces.UnreadState.Unread;
|
||||
} else this.addModeMessage('ads', message);
|
||||
}
|
||||
|
@ -281,16 +283,16 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
|
|||
core.connection.send('LCH', {channel: this.channel.id});
|
||||
}
|
||||
|
||||
sort(newIndex: number): void {
|
||||
async sort(newIndex: number): Promise<void> {
|
||||
state.channelConversations.splice(state.channelConversations.indexOf(this), 1);
|
||||
state.channelConversations.splice(newIndex, 0, this);
|
||||
state.savePinned();
|
||||
return state.savePinned();
|
||||
}
|
||||
|
||||
protected doSend(): void {
|
||||
protected async doSend(): Promise<void> {
|
||||
const isAd = this.isSendingAds;
|
||||
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
|
||||
this.addMessage(
|
||||
await this.addMessage(
|
||||
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
|
||||
if(isAd) {
|
||||
this.adCountdown = core.connection.vars.lfrp_flood;
|
||||
|
@ -317,10 +319,10 @@ class ConsoleConversation extends Conversation {
|
|||
close(): void {
|
||||
}
|
||||
|
||||
addMessage(message: Interfaces.Message): void {
|
||||
async addMessage(message: Interfaces.Message): Promise<void> {
|
||||
this.safeAddMessage(message);
|
||||
if(core.state.settings.logMessages) core.logs.logMessage(this, message);
|
||||
if(this !== state.selectedConversation) this.unread = Interfaces.UnreadState.Unread;
|
||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||
if(this !== state.selectedConversation || !state.windowFocused) this.unread = Interfaces.UnreadState.Unread;
|
||||
}
|
||||
|
||||
protected doSend(): void {
|
||||
|
@ -338,6 +340,12 @@ class State implements Interfaces.State {
|
|||
recent: Interfaces.RecentConversation[] = [];
|
||||
pinned: {channels: string[], private: string[]};
|
||||
settings: {[key: string]: Interfaces.Settings};
|
||||
windowFocused: boolean;
|
||||
|
||||
get hasNew(): boolean {
|
||||
return this.privateConversations.some((x) => x.unread === Interfaces.UnreadState.Mention) ||
|
||||
this.channelConversations.some((x) => x.unread === Interfaces.UnreadState.Mention);
|
||||
}
|
||||
|
||||
getPrivate(character: Character): PrivateConversation {
|
||||
const key = character.name.toLowerCase();
|
||||
|
@ -346,7 +354,7 @@ class State implements Interfaces.State {
|
|||
conv = new PrivateConversation(character);
|
||||
this.privateConversations.push(conv);
|
||||
this.privateMap[key] = conv;
|
||||
state.addRecent(conv);
|
||||
state.addRecent(conv); //tslint:disable-line:no-floating-promises
|
||||
return conv;
|
||||
}
|
||||
|
||||
|
@ -355,18 +363,18 @@ class State implements Interfaces.State {
|
|||
return (key[0] === '#' ? this.channelMap : this.privateMap)[key];
|
||||
}
|
||||
|
||||
savePinned(): void {
|
||||
async savePinned(): Promise<void> {
|
||||
this.pinned.channels = this.channelConversations.filter((x) => x.isPinned).map((x) => x.channel.id);
|
||||
this.pinned.private = this.privateConversations.filter((x) => x.isPinned).map((x) => x.name);
|
||||
core.settingsStore.set('pinned', this.pinned);
|
||||
await core.settingsStore.set('pinned', this.pinned);
|
||||
}
|
||||
|
||||
setSettings(key: string, value: Interfaces.Settings): void {
|
||||
async setSettings(key: string, value: Interfaces.Settings): Promise<void> {
|
||||
this.settings[key] = value;
|
||||
core.settingsStore.set('conversationSettings', this.settings);
|
||||
await core.settingsStore.set('conversationSettings', this.settings);
|
||||
}
|
||||
|
||||
addRecent(conversation: Conversation): void {
|
||||
async addRecent(conversation: Conversation): Promise<void> {
|
||||
const remove = <T extends Interfaces.RecentConversation>(predicate: (item: T) => boolean) => {
|
||||
for(let i = 0; i < this.recent.length; ++i)
|
||||
if(predicate(<T>this.recent[i])) {
|
||||
|
@ -382,7 +390,7 @@ class State implements Interfaces.State {
|
|||
state.recent.unshift({character: conversation.name});
|
||||
}
|
||||
if(this.recent.length >= 50) this.recent.pop();
|
||||
core.settingsStore.set('recent', this.recent);
|
||||
await core.settingsStore.set('recent', this.recent);
|
||||
}
|
||||
|
||||
show(conversation: Conversation): void {
|
||||
|
@ -400,7 +408,6 @@ class State implements Interfaces.State {
|
|||
conversation._isPinned = this.pinned.private.indexOf(conversation.name) !== -1;
|
||||
this.recent = await core.settingsStore.get('recent') || [];
|
||||
const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in settings) {
|
||||
const settingsItem = new ConversationSettings();
|
||||
for(const itemKey in settings[key])
|
||||
|
@ -416,9 +423,10 @@ class State implements Interfaces.State {
|
|||
|
||||
let state: State;
|
||||
|
||||
function addEventMessage(this: void, message: Interfaces.Message): void {
|
||||
state.consoleTab.addMessage(message);
|
||||
if(core.state.settings.eventMessages && state.selectedConversation !== state.consoleTab) state.selectedConversation.addMessage(message);
|
||||
async function addEventMessage(this: void, message: Interfaces.Message): Promise<void> {
|
||||
await state.consoleTab.addMessage(message);
|
||||
if(core.state.settings.eventMessages && state.selectedConversation !== state.consoleTab)
|
||||
await state.selectedConversation.addMessage(message);
|
||||
}
|
||||
|
||||
function isOfInterest(this: void, character: Character): boolean {
|
||||
|
@ -427,6 +435,11 @@ function isOfInterest(this: void, character: Character): boolean {
|
|||
|
||||
export default function(this: void): Interfaces.State {
|
||||
state = new State();
|
||||
window.addEventListener('focus', () => {
|
||||
state.windowFocused = true;
|
||||
if(state.selectedConversation !== undefined!) state.selectedConversation.unread = Interfaces.UnreadState.None;
|
||||
});
|
||||
window.addEventListener('blur', () => state.windowFocused = false);
|
||||
const connection = core.connection;
|
||||
connection.onEvent('connecting', async(isReconnect) => {
|
||||
state.channelConversations = [];
|
||||
|
@ -444,49 +457,49 @@ export default function(this: void): Interfaces.State {
|
|||
for(const item of state.pinned.private) state.getPrivate(core.characters.get(item));
|
||||
queuedJoin(state.pinned.channels.slice());
|
||||
});
|
||||
core.channels.onEvent((type, channel, member) => {
|
||||
core.channels.onEvent(async(type, channel, member) => {
|
||||
if(type === 'join')
|
||||
if(member === undefined) {
|
||||
const conv = new ChannelConversation(channel);
|
||||
state.channelMap[channel.id] = conv;
|
||||
state.channelConversations.push(conv);
|
||||
state.addRecent(conv);
|
||||
await state.addRecent(conv);
|
||||
} else {
|
||||
const conv = state.channelMap[channel.id]!;
|
||||
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
||||
!core.state.settings.joinMessages) return;
|
||||
const text = l('events.channelJoin', `[user]${member.character.name}[/user]`);
|
||||
conv.addMessage(new EventMessage(text));
|
||||
await conv.addMessage(new EventMessage(text));
|
||||
}
|
||||
else if(member === undefined) {
|
||||
const conv = state.channelMap[channel.id]!;
|
||||
state.channelConversations.splice(state.channelConversations.indexOf(conv), 1);
|
||||
delete state.channelMap[channel.id];
|
||||
state.savePinned();
|
||||
await state.savePinned();
|
||||
if(state.selectedConversation === conv) state.show(state.consoleTab);
|
||||
} else {
|
||||
const conv = state.channelMap[channel.id]!;
|
||||
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
|
||||
!core.state.settings.joinMessages) return;
|
||||
const text = l('events.channelLeave', `[user]${member.character.name}[/user]`);
|
||||
conv.addMessage(new EventMessage(text));
|
||||
await conv.addMessage(new EventMessage(text));
|
||||
}
|
||||
});
|
||||
|
||||
connection.onMessage('PRI', (data, time) => {
|
||||
connection.onMessage('PRI', async(data, time) => {
|
||||
const char = core.characters.get(data.character);
|
||||
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
|
||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
const conv = state.getPrivate(char);
|
||||
conv.addMessage(message);
|
||||
await conv.addMessage(message);
|
||||
});
|
||||
connection.onMessage('MSG', (data, time) => {
|
||||
connection.onMessage('MSG', async(data, time) => {
|
||||
const char = core.characters.get(data.character);
|
||||
if(char.isIgnored) return;
|
||||
const conversation = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conversation === undefined) return core.channels.leave(data.channel);
|
||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
conversation.addMessage(message);
|
||||
await conversation.addMessage(message);
|
||||
|
||||
const words = conversation.settings.highlightWords.slice();
|
||||
if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
|
||||
|
@ -497,20 +510,20 @@ export default function(this: void): Interfaces.State {
|
|||
if(results !== null) {
|
||||
core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text),
|
||||
characterImage(data.character), 'attention');
|
||||
if(conversation !== state.selectedConversation) conversation.unread = Interfaces.UnreadState.Mention;
|
||||
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||
message.isHighlight = true;
|
||||
} else if(conversation.settings.notify === Interfaces.Setting.True)
|
||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
});
|
||||
connection.onMessage('LRP', (data, time) => {
|
||||
connection.onMessage('LRP', async(data, time) => {
|
||||
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()];
|
||||
if(conv === undefined) return core.channels.leave(data.channel);
|
||||
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', (data, time) => {
|
||||
connection.onMessage('RLL', async(data, time) => {
|
||||
const sender = core.characters.get(data.character);
|
||||
if(sender.isIgnored) return;
|
||||
let text: string;
|
||||
|
@ -525,7 +538,7 @@ export default function(this: void): Interfaces.State {
|
|||
const channel = (<{channel: string}>data).channel.toLowerCase();
|
||||
const conversation = state.channelMap[channel];
|
||||
if(conversation === undefined) return core.channels.leave(channel);
|
||||
conversation.addMessage(message);
|
||||
await conversation.addMessage(message);
|
||||
if(data.type === 'bottle' && data.target === core.connection.character)
|
||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
|
@ -534,63 +547,69 @@ export default function(this: void): Interfaces.State {
|
|||
data.character === connection.character ? (<{recipient: string}>data).recipient : data.character);
|
||||
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
|
||||
const conversation = state.getPrivate(char);
|
||||
conversation.addMessage(message);
|
||||
await conversation.addMessage(message);
|
||||
}
|
||||
});
|
||||
connection.onMessage('NLN', (data, time) => {
|
||||
connection.onMessage('NLN', async(data, time) => {
|
||||
const message = new EventMessage(l('events.login', `[user]${data.identity}[/user]`), time);
|
||||
if(isOfInterest(core.characters.get(data.identity))) addEventMessage(message);
|
||||
if(isOfInterest(core.characters.get(data.identity))) await addEventMessage(message);
|
||||
const conv = state.privateMap[data.identity.toLowerCase()];
|
||||
if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation)) conv.addMessage(message);
|
||||
if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation))
|
||||
await conv.addMessage(message);
|
||||
});
|
||||
connection.onMessage('FLN', (data, time) => {
|
||||
connection.onMessage('FLN', async(data, time) => {
|
||||
const message = new EventMessage(l('events.logout', `[user]${data.character}[/user]`), time);
|
||||
if(isOfInterest(core.characters.get(data.character))) addEventMessage(message);
|
||||
if(isOfInterest(core.characters.get(data.character))) await addEventMessage(message);
|
||||
const conv = state.privateMap[data.character.toLowerCase()];
|
||||
if(conv === undefined) return;
|
||||
conv.typingStatus = 'clear';
|
||||
if(!core.state.settings.eventMessages || conv !== state.selectedConversation) conv.addMessage(message);
|
||||
if(!core.state.settings.eventMessages || conv !== state.selectedConversation) await conv.addMessage(message);
|
||||
});
|
||||
connection.onMessage('TPN', (data) => {
|
||||
const conv = state.privateMap[data.character.toLowerCase()];
|
||||
if(conv !== undefined) conv.typingStatus = data.status;
|
||||
});
|
||||
connection.onMessage('CBU', (data, time) => {
|
||||
connection.onMessage('CBU', async(data, time) => {
|
||||
const text = l('events.ban', data.channel, data.character, data.operator);
|
||||
const conv = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conv === undefined) return core.channels.leave(data.channel);
|
||||
conv.infoText = text;
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('CKU', (data, time) => {
|
||||
connection.onMessage('CKU', async(data, time) => {
|
||||
const text = l('events.kick', data.channel, data.character, data.operator);
|
||||
const conv = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conv === undefined) return core.channels.leave(data.channel);
|
||||
conv.infoText = text;
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('CTU', (data, time) => {
|
||||
connection.onMessage('CTU', async(data, time) => {
|
||||
const text = l('events.timeout', data.channel, data.character, data.operator, data.length.toString());
|
||||
const conv = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conv === undefined) return core.channels.leave(data.channel);
|
||||
conv.infoText = text;
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('HLO', (data, time) => addEventMessage(new EventMessage(data.message, time)));
|
||||
connection.onMessage('BRO', (data, time) => {
|
||||
connection.onMessage('HLO', async(data, time) => addEventMessage(new EventMessage(data.message, time)));
|
||||
connection.onMessage('BRO', async(data, time) => {
|
||||
const text = data.character === undefined ? decodeHTML(data.message) :
|
||||
l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('CIU', (data, time) => {
|
||||
connection.onMessage('CIU', async(data, time) => {
|
||||
const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`);
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('ERR', (data, time) => {
|
||||
connection.onMessage('ERR', async(data, time) => {
|
||||
state.selectedConversation.errorText = data.message;
|
||||
addEventMessage(new EventMessage(`[color=red]${l('events.error', data.message)}[/color]`, time));
|
||||
return addEventMessage(new EventMessage(`[color=red]${l('events.error', data.message)}[/color]`, time));
|
||||
});
|
||||
connection.onMessage('RTB', (data, time) => {
|
||||
|
||||
connection.onMessage('IGN', async(data, time) => {
|
||||
if(data.action !== 'add' && data.action !== 'delete') return;
|
||||
return addEventMessage(new EventMessage(l(`events.ignore_${data.action}`, data.character), time));
|
||||
});
|
||||
connection.onMessage('RTB', async(data, time) => {
|
||||
let url = 'https://www.f-list.net/';
|
||||
let text: string, character: string;
|
||||
if(data.type === 'comment') { //tslint:disable-line:prefer-switch
|
||||
|
@ -640,13 +659,13 @@ export default function(this: void): Interfaces.State {
|
|||
data.title !== undefined ? `[url=${url}]${data.title}[/url]` : url);
|
||||
character = data.name;
|
||||
}
|
||||
addEventMessage(new EventMessage(text, time));
|
||||
await addEventMessage(new EventMessage(text, time));
|
||||
if(data.type === 'note')
|
||||
core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
|
||||
});
|
||||
type SFCMessage = (Interfaces.Message & {sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}});
|
||||
const sfcList: SFCMessage[] = [];
|
||||
connection.onMessage('SFC', (data, time) => {
|
||||
connection.onMessage('SFC', async(data, time) => {
|
||||
let text: string, message: Interfaces.Message;
|
||||
if(data.action === 'report') {
|
||||
text = l('events.report', `[user]${data.character}[/user]`, decodeHTML(data.tab), decodeHTML(data.report));
|
||||
|
@ -663,11 +682,11 @@ export default function(this: void): Interfaces.State {
|
|||
}
|
||||
message = new EventMessage(text, time);
|
||||
}
|
||||
addEventMessage(message);
|
||||
return addEventMessage(message);
|
||||
});
|
||||
connection.onMessage('STA', (data, time) => {
|
||||
connection.onMessage('STA', async(data, time) => {
|
||||
if(data.character === core.connection.character) {
|
||||
addEventMessage(new EventMessage(l(data.statusmsg.length > 0 ? 'events.status.ownMessage' : 'events.status.own',
|
||||
await addEventMessage(new EventMessage(l(data.statusmsg.length > 0 ? 'events.status.ownMessage' : 'events.status.own',
|
||||
l(`status.${data.status}`), decodeHTML(data.statusmsg)), time));
|
||||
return;
|
||||
}
|
||||
|
@ -676,17 +695,17 @@ export default function(this: void): Interfaces.State {
|
|||
const status = l(`status.${data.status}`);
|
||||
const key = data.statusmsg.length > 0 ? 'events.status.message' : 'events.status';
|
||||
const message = new EventMessage(l(key, `[user]${data.character}[/user]`, status, decodeHTML(data.statusmsg)), time);
|
||||
addEventMessage(message);
|
||||
await addEventMessage(message);
|
||||
const conv = state.privateMap[data.character.toLowerCase()];
|
||||
if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) conv.addMessage(message);
|
||||
if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) await conv.addMessage(message);
|
||||
});
|
||||
connection.onMessage('SYS', (data, time) => {
|
||||
connection.onMessage('SYS', async(data, time) => {
|
||||
state.selectedConversation.infoText = data.message;
|
||||
addEventMessage(new EventMessage(data.message, time));
|
||||
return addEventMessage(new EventMessage(data.message, time));
|
||||
});
|
||||
connection.onMessage('ZZZ', (data, time) => {
|
||||
connection.onMessage('ZZZ', async(data, time) => {
|
||||
state.selectedConversation.infoText = data.message;
|
||||
addEventMessage(new EventMessage(data.message, time));
|
||||
return addEventMessage(new EventMessage(data.message, time));
|
||||
});
|
||||
//TODO connection.onMessage('UPT', data =>
|
||||
return state;
|
||||
|
|
11
chat/core.ts
|
@ -44,9 +44,8 @@ const vue = <Vue & VueState>new Vue({
|
|||
state
|
||||
},
|
||||
watch: {
|
||||
'state.hiddenUsers': (newValue: string[]) => {
|
||||
//tslint:disable-next-line:no-floating-promises
|
||||
if(data.settingsStore !== undefined) data.settingsStore.set('hiddenUsers', newValue);
|
||||
'state.hiddenUsers': async(newValue: string[]) => {
|
||||
if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -92,7 +91,7 @@ export function init(this: void, connection: Connection, logsClass: new() => Log
|
|||
});
|
||||
}
|
||||
|
||||
const core = <{
|
||||
export interface Core {
|
||||
readonly connection: Connection
|
||||
readonly logs: Logs.Basic
|
||||
readonly state: StateInterface
|
||||
|
@ -107,6 +106,8 @@ const core = <{
|
|||
register(module: 'characters', state: Character.State): void
|
||||
reloadSettings(): void
|
||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
||||
}><any>data; /*tslint:disable-line:no-any*///hack
|
||||
}
|
||||
|
||||
const core = <Core><any>data; /*tslint:disable-line:no-any*///hack
|
||||
|
||||
export default core;
|
|
@ -50,8 +50,8 @@ export namespace Conversation {
|
|||
interface TabConversation extends Conversation {
|
||||
isPinned: boolean
|
||||
readonly maxMessageLength: number
|
||||
close(): void
|
||||
sort(newIndex: number): void
|
||||
close(): Promise<void> | void
|
||||
sort(newIndex: number): Promise<void>
|
||||
}
|
||||
|
||||
export interface PrivateConversation extends TabConversation {
|
||||
|
@ -80,6 +80,7 @@ export namespace Conversation {
|
|||
readonly consoleTab: Conversation
|
||||
readonly recent: ReadonlyArray<RecentConversation>
|
||||
readonly selectedConversation: Conversation
|
||||
readonly hasNew: boolean;
|
||||
byKey(key: string): Conversation | undefined
|
||||
getPrivate(character: Character): PrivateConversation
|
||||
reloadSettings(): void
|
||||
|
@ -110,7 +111,7 @@ export namespace Conversation {
|
|||
readonly key: string
|
||||
readonly unread: UnreadState
|
||||
settings: Settings
|
||||
send(): void
|
||||
send(): Promise<void>
|
||||
loadLastSent(): void
|
||||
show(): void
|
||||
loadMore(): void
|
||||
|
@ -121,7 +122,7 @@ export type Conversation = Conversation.Conversation;
|
|||
|
||||
export namespace Logs {
|
||||
export interface Basic {
|
||||
logMessage(conversation: Conversation, message: Conversation.Message): void
|
||||
logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> | void
|
||||
getBacklog(conversation: Conversation): Promise<ReadonlyArray<Conversation.Message>>
|
||||
}
|
||||
|
||||
|
@ -177,6 +178,7 @@ export interface Notifications {
|
|||
isInBackground: boolean
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void
|
||||
playSound(sound: string): void
|
||||
requestPermission(): Promise<void>
|
||||
}
|
||||
|
||||
export interface State {
|
||||
|
|
|
@ -8,7 +8,10 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'action.copyLink': 'Copy Link',
|
||||
'action.suggestions': 'Suggestions',
|
||||
'action.open': 'Show',
|
||||
'action.close': 'Close',
|
||||
'action.quit': 'Quit',
|
||||
'action.newWindow': 'Open new window',
|
||||
'action.newTab': 'Open new tab',
|
||||
'action.updateAvailable': 'UPDATE AVAILABLE',
|
||||
'action.update': 'Restart now!',
|
||||
'action.cancel': 'Cancel',
|
||||
|
@ -21,9 +24,13 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'help.faq': 'F-List FAQ',
|
||||
'help.report': 'How to report a user',
|
||||
'help.changelog': 'Changelog',
|
||||
'title': 'FChat 3.0',
|
||||
'fs.error': 'Error writing to disk',
|
||||
'window.newTab': 'New tab',
|
||||
'title': 'F-Chat',
|
||||
'version': 'Version {0}',
|
||||
'filter': 'Type to filter...',
|
||||
'confirmYes': 'Yes',
|
||||
'confirmNo': 'No',
|
||||
'login.account': 'Username',
|
||||
'login.password': 'Password',
|
||||
'login.host': 'Host',
|
||||
|
@ -36,6 +43,7 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'login.connect': 'Connect',
|
||||
'login.connecting': 'Connecting...',
|
||||
'login.connectError': 'Connection error: Could not connect to server',
|
||||
'login.alreadyLoggedIn': 'You are already logged in on this character in another tab or window.',
|
||||
'channelList.public': 'Official channels',
|
||||
'channelList.private': 'Open rooms',
|
||||
'channelList.create': 'Create room',
|
||||
|
@ -85,6 +93,7 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'users.friends': 'Friends',
|
||||
'users.bookmarks': 'Bookmarks',
|
||||
'users.members': 'Members',
|
||||
'users.memberCount': '{0} Members',
|
||||
'chat.report': 'Alert Staff',
|
||||
'chat.report.description': `
|
||||
[color=red]Before you alert the moderators, PLEASE READ:[/color]
|
||||
|
@ -136,6 +145,8 @@ Are you sure?`,
|
|||
'settings.spellcheck.disabled': 'Disabled',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.profileViewer': 'Use profile viewer',
|
||||
'settings.logDir': 'Change log location',
|
||||
'settings.logDir.confirm': 'Do you want to set your log location to {0}?\n\nNo files will be moved. If you click Yes here, F-Chat will shut down. If you would like to keep your log files, please move them manually.\n\nCurrent log location: {1}',
|
||||
'settings.logMessages': 'Log messages',
|
||||
'settings.logAds': 'Log ads',
|
||||
'settings.fontSize': 'Font size (experimental)',
|
||||
|
@ -206,6 +217,8 @@ Are you sure?`,
|
|||
'events.logout': '{0} has logged out.',
|
||||
'events.channelJoin': '{0} has joined 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_delete': '{0} is now allowed to send you messages again.',
|
||||
'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.tooFewParams': 'This command requires more parameters. Please use the Help (click the ? button) if you need further information.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {Component, CreateElement, RenderContext, VNode, VNodeChildren} from 'vue';
|
||||
import {Channel} from '../fchat';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {formatTime} from './common';
|
||||
import core from './core';
|
||||
|
@ -20,9 +21,9 @@ const userPostfix: {[key: number]: string | undefined} = {
|
|||
//tslint:disable-next-line:variable-name
|
||||
const MessageView: Component = {
|
||||
functional: true,
|
||||
render(createElement: CreateElement, context: RenderContext): VNode {
|
||||
/*tslint:disable:no-unsafe-any*///context.props is any
|
||||
const message: Conversation.Message = context.props.message;
|
||||
render(createElement: CreateElement,
|
||||
context: RenderContext<{message: Conversation.Message, classes?: string, channel?: Channel}>): VNode {
|
||||
const message = context.props.message;
|
||||
const children: (VNode | string | VNodeChildren)[] = [`[${formatTime(message.time)}] `];
|
||||
/*tslint:disable-next-line:prefer-template*///unreasonable here
|
||||
let classes = `message message-${Conversation.Message.Type[message.type].toLowerCase()}` +
|
||||
|
@ -39,7 +40,6 @@ const MessageView: Component = {
|
|||
const node = createElement('div', {attrs: {class: classes}}, children);
|
||||
node.key = context.data.key;
|
||||
return node;
|
||||
//tslint:enable
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ export default class Notifications implements Interface {
|
|||
if(audio === null) {
|
||||
audio = document.createElement('audio');
|
||||
audio.id = id;
|
||||
//tslint:disable-next-line:forin
|
||||
for(const name in codecs) {
|
||||
const src = document.createElement('source');
|
||||
src.type = `audio/${name}`;
|
||||
|
@ -39,4 +38,8 @@ export default class Notifications implements Interface {
|
|||
//tslint:disable-next-line:no-floating-promises
|
||||
audio.play();
|
||||
}
|
||||
|
||||
async requestPermission(): Promise<void> {
|
||||
await Notification.requestPermission();
|
||||
}
|
||||
}
|
|
@ -2,10 +2,14 @@ import Axios from 'axios';
|
|||
import Vue from 'vue';
|
||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
||||
import {initParser, standardParser} from '../bbcode/standard';
|
||||
import CharacterLink from '../components/character_link.vue';
|
||||
import CharacterSelect from '../components/character_select.vue';
|
||||
import {setCharacters} from '../components/character_select/character_list';
|
||||
import DateDisplay from '../components/date_display.vue';
|
||||
import {registerMethod, Store} from '../site/character_page/data_store';
|
||||
import {
|
||||
Character, CharacterCustom, CharacterFriend, CharacterImage, CharacterImageOld, CharacterInfo, CharacterInfotag, CharacterSettings,
|
||||
GuestbookState, KinkChoiceFull, SharedKinks
|
||||
Character, CharacterCustom, CharacterFriend, CharacterImage, CharacterImageOld, CharacterInfo, CharacterInfotag, CharacterKink,
|
||||
CharacterSettings, Friend, FriendRequest, FriendsByCharacter, GuestbookState, KinkChoice, KinkChoiceFull, SharedKinks
|
||||
} from '../site/character_page/interfaces';
|
||||
import '../site/directives/vue-select'; //tslint:disable-line:no-import-side-effect
|
||||
import * as Utils from '../site/utils';
|
||||
|
@ -15,9 +19,12 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
const data = await core.connection.queryApi('character-data.php', {name}) as CharacterInfo & {
|
||||
badges: string[]
|
||||
customs_first: boolean
|
||||
character_list: {id: number, name: string}[]
|
||||
custom_kinks: {[key: number]: {choice: 'fave' | 'yes' | 'maybe' | 'no', name: string, description: string, children: number[]}}
|
||||
custom_title: string
|
||||
kinks: {[key: string]: string}
|
||||
infotags: {[key: string]: string}
|
||||
memo: {id: number, memo: string}
|
||||
settings: CharacterSettings
|
||||
};
|
||||
const newKinks: {[key: string]: KinkChoiceFull} = {};
|
||||
|
@ -33,8 +40,7 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
description: custom.description
|
||||
});
|
||||
for(const childId of custom.children)
|
||||
if(data.kinks[childId] !== undefined)
|
||||
newKinks[childId] = parseInt(key, 10);
|
||||
newKinks[childId] = parseInt(key, 10);
|
||||
}
|
||||
const newInfotags: {[key: string]: CharacterInfotag} = {};
|
||||
for(const key in data.infotags) {
|
||||
|
@ -61,9 +67,11 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
infotags: newInfotags,
|
||||
online_chat: false
|
||||
},
|
||||
memo: data.memo,
|
||||
character_list: data.character_list,
|
||||
badges: data.badges,
|
||||
settings: data.settings,
|
||||
bookmarked: false,
|
||||
bookmarked: core.characters.get(data.name).isBookmarked,
|
||||
self_staff: false
|
||||
};
|
||||
}
|
||||
|
@ -147,7 +155,15 @@ async function guestbookGet(id: number, page: number): Promise<GuestbookState> {
|
|||
return core.connection.queryApi<GuestbookState>('character-guestbook.php', {id, page: page - 1});
|
||||
}
|
||||
|
||||
export function init(): void {
|
||||
async function kinksGet(id: number): Promise<CharacterKink[]> {
|
||||
const data = await core.connection.queryApi<{kinks: {[key: string]: string}}>('character-data.php', {id});
|
||||
return Object.keys(data.kinks).map((key) => {
|
||||
const choice = data.kinks[key];
|
||||
return {id: parseInt(key, 10), choice: <KinkChoice>(choice === 'fave' ? 'favorite' : choice)};
|
||||
});
|
||||
}
|
||||
|
||||
export function init(characters: {[key: string]: number}): void {
|
||||
Utils.setDomains('https://www.f-list.net/', 'https://static.f-list.net/');
|
||||
initParser({
|
||||
siteDomain: Utils.siteDomain,
|
||||
|
@ -156,6 +172,13 @@ export function init(): void {
|
|||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
|
||||
});
|
||||
|
||||
Vue.component('character-select', CharacterSelect);
|
||||
Vue.component('character-link', CharacterLink);
|
||||
Vue.component('date-display', DateDisplay);
|
||||
setCharacters(Object.keys(characters).map((name) => ({name, id: characters[name]})));
|
||||
core.connection.onEvent('connecting', () => {
|
||||
Utils.Settings.defaultCharacter = characters[core.connection.character];
|
||||
});
|
||||
Vue.directive('bbcode', (el, binding) => {
|
||||
while(el.firstChild !== null)
|
||||
el.removeChild(el.firstChild);
|
||||
|
@ -163,10 +186,34 @@ export function init(): void {
|
|||
});
|
||||
registerMethod('characterData', characterData);
|
||||
registerMethod('contactMethodIconUrl', contactMethodIconUrl);
|
||||
registerMethod('sendNoteUrl', (character: CharacterInfo) => `${Utils.siteDomain}read_notes.php?send=${character.name}`);
|
||||
registerMethod('fieldsGet', fieldsGet);
|
||||
registerMethod('friendsGet', friendsGet);
|
||||
registerMethod('kinksGet', kinksGet);
|
||||
registerMethod('imagesGet', imagesGet);
|
||||
registerMethod('guestbookPageGet', guestbookGet);
|
||||
registerMethod('imageUrl', (image: CharacterImageOld) => image.url);
|
||||
registerMethod('memoUpdate', async(id: number, memo: string) => {
|
||||
await core.connection.queryApi('character-memo-save.php', {target: id, note: memo});
|
||||
return {id, memo, updated_at: Date.now() / 1000};
|
||||
});
|
||||
registerMethod('imageThumbUrl', (image: CharacterImage) => `${Utils.staticDomain}images/charthumb/${image.id}.${image.extension}`);
|
||||
registerMethod('bookmarkUpdate', async(id: number, state: boolean) => {
|
||||
await core.connection.queryApi(`bookmark-${state ? 'add' : 'remove'}.php`, {id});
|
||||
return state;
|
||||
});
|
||||
registerMethod('characterFriends', async(id: number) =>
|
||||
core.connection.queryApi<FriendsByCharacter>('character-friend-list.php', {id}));
|
||||
registerMethod('friendRequest', async(target_id: number, source_id: number) =>
|
||||
(await core.connection.queryApi<{request: FriendRequest}>('request-send2.php', {source_id, target_id})).request);
|
||||
registerMethod('friendDissolve', async(friend: Friend) =>
|
||||
core.connection.queryApi<void>('friend-remove.php', {source_id: friend.source.id, dest_id: friend.target.id}));
|
||||
registerMethod('friendRequestAccept', async(req: FriendRequest) => {
|
||||
await core.connection.queryApi('request-accept.php', {request_id: req.id});
|
||||
return { id: undefined!, source: req.target, target: req.source, createdAt: Date.now() / 1000 };
|
||||
});
|
||||
registerMethod('friendRequestCancel', async(req: FriendRequest) =>
|
||||
core.connection.queryApi<void>('request-cancel.php', {request_id: req.id}));
|
||||
registerMethod('friendRequestIgnore', async(req: FriendRequest) =>
|
||||
core.connection.queryApi<void>('request-deny.php', {request_id: req.id}));
|
||||
}
|
|
@ -27,6 +27,7 @@ export function parse(this: void | never, input: string, context: CommandContext
|
|||
|
||||
if(command.params !== undefined)
|
||||
for(let i = 0; i < command.params.length; ++i) {
|
||||
while(args[index] === ' ') ++index;
|
||||
const param = command.params[i];
|
||||
if(index === -1)
|
||||
if(param.optional !== undefined) continue;
|
||||
|
@ -48,7 +49,6 @@ export function parse(this: void | never, input: string, context: CommandContext
|
|||
return l('commands.invalidParam', l(`commands.${name}.param${i}`));
|
||||
break;
|
||||
case ParamType.Number:
|
||||
console.log(value);
|
||||
const num = parseInt(value, 10);
|
||||
if(isNaN(num))
|
||||
return l('commands.invalidParam', l(`commands.${name}.param${i}`));
|
||||
|
|
|
@ -1,32 +1,41 @@
|
|||
<template>
|
||||
<div tabindex="-1" class="modal flex-modal" :style="isShown ? 'display:flex' : ''"
|
||||
style="align-items: flex-start; padding: 30px; justify-content: center;">
|
||||
<div class="modal-dialog" :class="dialogClass" style="display: flex; flex-direction: column; max-height: 100%; margin: 0;">
|
||||
<div class="modal-content" style="display:flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
<h4 class="modal-title">
|
||||
<slot name="title">{{action}}</slot>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto; display: flex; flex-direction: column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="buttons">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-if="showCancel">Cancel</button>
|
||||
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="disabled">
|
||||
{{submitText}}
|
||||
</button>
|
||||
<span v-show="isShown">
|
||||
<div tabindex="-1" class="modal flex-modal" @click.self="hideWithCheck"
|
||||
style="align-items:flex-start;padding:30px;justify-content:center;display:flex">
|
||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;flex-direction:column;max-height:100%;margin:0">
|
||||
<div class="modal-content" style="display:flex;flex-direction:column;flex-grow:1">
|
||||
<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">
|
||||
<slot name="title">{{action}}</slot>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto; display: flex; flex-direction: column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<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" :class="buttonClass" @click="submit" :disabled="disabled">
|
||||
{{submitText}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop in"></div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {getKey} from '../chat/common';
|
||||
|
||||
const dialogStack: Modal[] = [];
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if(getKey(e) === 'escape' && dialogStack.length > 0) dialogStack.pop()!.isShown = false;
|
||||
});
|
||||
|
||||
@Component
|
||||
export default class Modal extends Vue {
|
||||
|
@ -45,7 +54,7 @@
|
|||
@Prop()
|
||||
readonly buttonText?: string;
|
||||
isShown = false;
|
||||
element: JQuery;
|
||||
keepOpen = false;
|
||||
|
||||
get submitText(): string {
|
||||
return this.buttonText !== undefined ? this.buttonText : this.action;
|
||||
|
@ -53,27 +62,32 @@
|
|||
|
||||
submit(e: Event): void {
|
||||
this.$emit('submit', e);
|
||||
if(!e.defaultPrevented) this.hide();
|
||||
if(!e.defaultPrevented) this.hideWithCheck();
|
||||
}
|
||||
|
||||
/*tslint:disable-next-line:typedef*///https://github.com/palantir/tslint/issues/711
|
||||
show(keepOpen = false): void {
|
||||
if(keepOpen) this.element.on('hide.bs.modal', (e) => e.preventDefault());
|
||||
this.element.modal('show');
|
||||
this.isShown = true;
|
||||
this.keepOpen = keepOpen;
|
||||
dialogStack.push(this);
|
||||
this.$emit('open');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.element.off('hide.bs.modal');
|
||||
this.element.modal('hide');
|
||||
this.isShown = false;
|
||||
this.$emit('close');
|
||||
dialogStack.pop();
|
||||
}
|
||||
|
||||
private hideWithCheck(): void {
|
||||
if(this.keepOpen) return;
|
||||
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 {
|
||||
$(document).off('focusin.bs.modal');
|
||||
if(this.menu !== undefined) {
|
||||
this.menu.style.display = 'block';
|
||||
return;
|
||||
|
@ -96,12 +110,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
mounted(): void {
|
||||
this.element = $(this.$el);
|
||||
this.element.on('shown.bs.modal', () => this.$emit('open'));
|
||||
this.element.on('hidden.bs.modal', () => this.$emit('close'));
|
||||
}
|
||||
|
||||
beforeDestroy(): void {
|
||||
if(this.isShown) this.hide();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<select class="form-control" :value="value" @change="emit">
|
||||
<option v-for="o in characters" :value="o.value" v-once>{{o.text}}</option>
|
||||
<slot></slot>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {getCharacters} from './character_select/character_list';
|
||||
|
||||
interface SelectItem {
|
||||
value: number
|
||||
text: string
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class CharacterSelect extends Vue {
|
||||
@Prop({required: true, type: Number})
|
||||
readonly value: number;
|
||||
|
||||
get characters(): SelectItem[] {
|
||||
const characterList = getCharacters();
|
||||
const characters: SelectItem[] = [];
|
||||
for(const character of characterList)
|
||||
characters.push({value: character.id, text: character.name});
|
||||
return characters;
|
||||
}
|
||||
|
||||
emit(evt: Event): void {
|
||||
this.$emit('input', parseInt((<HTMLSelectElement>evt.target).value, 10));
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
export interface CharacterItem {
|
||||
readonly name: string
|
||||
readonly id: number
|
||||
}
|
||||
|
||||
let characterList: ReadonlyArray<CharacterItem> = [];
|
||||
|
||||
export function setCharacters(characters: ReadonlyArray<CharacterItem>): void {
|
||||
characterList = characters;
|
||||
}
|
||||
|
||||
export function getCharacters(): ReadonlyArray<CharacterItem> {
|
||||
return characterList;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget id="net.f_list.fchat" version="3.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<name>F-Chat 3.0</name>
|
||||
<description>
|
||||
A cross-platform F-Chat client.
|
||||
</description>
|
||||
<author email="maya@f-list.net" href="https://www.f-list.net">The F-list Team</author>
|
||||
<content src="index.html" />
|
||||
<icon src="../electron/build/icon.png" />
|
||||
<access origin="*" />
|
||||
<allow-intent href="http://*/*" />
|
||||
<allow-intent href="https://*/*" />
|
||||
<allow-intent href="tel:*" />
|
||||
<allow-intent href="sms:*" />
|
||||
<allow-intent href="mailto:*" />
|
||||
<allow-intent href="geo:*" />
|
||||
<platform name="android">
|
||||
<allow-intent href="market:*" />
|
||||
</platform>
|
||||
<platform name="ios">
|
||||
<allow-intent href="itms:*" />
|
||||
<allow-intent href="itms-apps:*" />
|
||||
</platform>
|
||||
<engine name="android" spec="^6.2.3" />
|
||||
<plugin name="cordova-plugin-file" spec="^4.3.3" />
|
||||
<plugin name="cordova-plugin-whitelist" spec="^1.3.2" />
|
||||
<plugin name="de.appplant.cordova.plugin.local-notification" spec="^0.8.5" />
|
||||
</widget>
|
|
@ -1,263 +0,0 @@
|
|||
import {getByteLength, Message as MessageImpl} from '../chat/common';
|
||||
import core from '../chat/core';
|
||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||
|
||||
declare global {
|
||||
class TextEncoder {
|
||||
readonly encoding: string;
|
||||
|
||||
encode(input?: string, options?: {stream: boolean}): Uint8Array;
|
||||
}
|
||||
|
||||
class TextDecoder {
|
||||
readonly encoding: string;
|
||||
readonly fatal: boolean;
|
||||
readonly ignoreBOM: boolean;
|
||||
|
||||
constructor(utfLabel?: string, options?: {fatal?: boolean, ignoreBOM?: boolean})
|
||||
|
||||
decode(input?: ArrayBufferView, options?: {stream: boolean}): string;
|
||||
}
|
||||
}
|
||||
|
||||
const dayMs = 86400000;
|
||||
let fs: FileSystem;
|
||||
|
||||
export class GeneralSettings {
|
||||
account = '';
|
||||
password = '';
|
||||
host = 'wss://chat.f-list.net:9799';
|
||||
theme = 'default';
|
||||
}
|
||||
|
||||
type Index = {[key: string]: {name: string, index: {[key: number]: number | undefined}} | undefined};
|
||||
|
||||
/*tslint:disable:promise-function-async*///all of these are simple wrappers
|
||||
export function init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
document.addEventListener('deviceready', () => {
|
||||
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (f) => {
|
||||
fs = f;
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readAsString(file: Blob): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(<string>reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
function readAsArrayBuffer(file: Blob): Promise<ArrayBuffer> {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(<ArrayBuffer>reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
function getFile(root: DirectoryEntry, path: string): Promise<File | undefined> {
|
||||
return new Promise<File | undefined>((resolve, reject) => {
|
||||
root.getFile(path, {create: false}, (entry) => entry.file((file) => {
|
||||
resolve(file);
|
||||
}, reject), (e) => {
|
||||
if(e.code === FileError.NOT_FOUND_ERR) resolve(undefined);
|
||||
else reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWriter(root: DirectoryEntry, path: string): Promise<FileWriter> {
|
||||
return new Promise<FileWriter>((resolve, reject) => root.getFile(path, {create: true},
|
||||
(file) => file.createWriter(resolve, reject), reject));
|
||||
}
|
||||
|
||||
function getDir(root: DirectoryEntry, name: string): Promise<DirectoryEntry> {
|
||||
return new Promise<DirectoryEntry>((resolve, reject) => root.getDirectory(name, {create: true}, resolve, reject));
|
||||
}
|
||||
|
||||
function getEntries(root: DirectoryEntry): Promise<ReadonlyArray<Entry>> {
|
||||
const reader = root.createReader();
|
||||
return new Promise<ReadonlyArray<Entry>>((resolve, reject) => reader.readEntries(resolve, reject));
|
||||
}
|
||||
|
||||
//tslib:enable
|
||||
|
||||
function serializeMessage(message: Conversation.Message): Blob {
|
||||
const name = message.type !== Conversation.Message.Type.Event ? message.sender.name : '';
|
||||
const buffer = new ArrayBuffer(8);
|
||||
const dv = new DataView(buffer);
|
||||
dv.setUint32(0, message.time.getTime() / 1000);
|
||||
dv.setUint8(4, message.type);
|
||||
const senderLength = getByteLength(name);
|
||||
dv.setUint8(5, senderLength);
|
||||
const textLength = getByteLength(message.text);
|
||||
dv.setUint16(6, textLength);
|
||||
const length = senderLength + textLength + 8;
|
||||
return new Blob([buffer, name, message.text, String.fromCharCode(length >> 255), String.fromCharCode(length % 255)]);
|
||||
}
|
||||
|
||||
function deserializeMessage(buffer: ArrayBuffer): {message: Conversation.Message, end: number} {
|
||||
const dv = new DataView(buffer, 0, 8);
|
||||
const time = dv.getUint32(0) * 1000;
|
||||
const type = dv.getUint8(4);
|
||||
const senderLength = dv.getUint8(5);
|
||||
const messageLength = dv.getUint16(6);
|
||||
let index = 8;
|
||||
const sender = decoder.decode(new DataView(buffer, index, senderLength));
|
||||
index += senderLength;
|
||||
const text = decoder.decode(new DataView(buffer, index, messageLength));
|
||||
return {message: new MessageImpl(type, core.characters.get(sender), text, new Date(time)), end: index + messageLength + 2};
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf8');
|
||||
|
||||
export class Logs implements Logging.Persistent {
|
||||
private index: Index = {};
|
||||
private logDir: DirectoryEntry;
|
||||
|
||||
constructor() {
|
||||
core.connection.onEvent('connecting', async() => {
|
||||
this.index = {};
|
||||
const charDir = await getDir(fs.root, core.connection.character);
|
||||
this.logDir = await getDir(charDir, 'logs');
|
||||
const entries = await getEntries(this.logDir);
|
||||
for(const entry of entries)
|
||||
if(entry.name.substr(-4) === '.idx') {
|
||||
const file = await new Promise<File>((s, j) => (<FileEntry>entry).file(s, j));
|
||||
const buffer = await readAsArrayBuffer(file);
|
||||
const dv = new DataView(buffer);
|
||||
let offset = dv.getUint8(0);
|
||||
const name = decoder.decode(new DataView(buffer, 1, offset++));
|
||||
const index: {[key: number]: number} = {};
|
||||
for(; offset < dv.byteLength; offset += 7) {
|
||||
const key = dv.getUint16(offset);
|
||||
index[key] = dv.getUint32(offset + 2) << 8 | dv.getUint8(offset + 6);
|
||||
}
|
||||
this.index[entry.name.slice(0, -4).toLowerCase()] = {name, index};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.logDir.getFile(conversation.key, {create: true}, (file) => {
|
||||
const serialized = serializeMessage(message);
|
||||
const date = Math.floor(message.time.getTime() / dayMs);
|
||||
let indexBuffer: {}[] | undefined;
|
||||
let index = this.index[conversation.key];
|
||||
if(index !== undefined) {
|
||||
if(index.index[date] === undefined) indexBuffer = [];
|
||||
} else {
|
||||
index = this.index[conversation.key] = {name: conversation.name, index: {}};
|
||||
const nameLength = getByteLength(conversation.name);
|
||||
indexBuffer = [String.fromCharCode(nameLength), conversation.name];
|
||||
}
|
||||
if(indexBuffer !== undefined)
|
||||
file.getMetadata((data) => {
|
||||
index!.index[date] = data.size;
|
||||
const dv = new DataView(new ArrayBuffer(7));
|
||||
dv.setUint16(0, date);
|
||||
dv.setUint32(2, data.size >> 8);
|
||||
dv.setUint8(6, data.size % 256);
|
||||
indexBuffer!.push(dv);
|
||||
this.logDir.getFile(`${conversation.key}.idx`, {create: true}, (indexFile) => {
|
||||
indexFile.createWriter((writer) => writer.write(new Blob(indexBuffer)), reject);
|
||||
}, reject);
|
||||
}, reject);
|
||||
file.createWriter((writer) => writer.write(serialized), reject);
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async getBacklog(conversation: Conversation): Promise<Conversation.Message[]> {
|
||||
const file = await getFile(this.logDir, conversation.key);
|
||||
if(file === undefined) return [];
|
||||
let count = 20;
|
||||
let messages = new Array<Conversation.Message>(count);
|
||||
let pos = file.size;
|
||||
while(pos > 0 && count > 0) {
|
||||
const length = new DataView(await readAsArrayBuffer(file.slice(pos - 2, pos))).getUint16(0);
|
||||
pos = pos - length - 2;
|
||||
messages[--count] = deserializeMessage(await readAsArrayBuffer(file.slice(pos, pos + length))).message;
|
||||
}
|
||||
if(count !== 0) messages = messages.slice(count);
|
||||
return messages;
|
||||
}
|
||||
|
||||
async getLogs(key: string, date: Date): Promise<Conversation.Message[]> {
|
||||
const file = await getFile(this.logDir, key);
|
||||
if(file === undefined) return [];
|
||||
const messages: Conversation.Message[] = [];
|
||||
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 [];
|
||||
while(pos < file.size) {
|
||||
const deserialized = deserializeMessage(await readAsArrayBuffer(file.slice(pos, 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> {
|
||||
const entry = this.index[key];
|
||||
if(entry === undefined) return [];
|
||||
const dates = [];
|
||||
for(const date in entry.index) //tslint:disable-line:forin
|
||||
dates.push(new Date(parseInt(date, 10) * dayMs));
|
||||
return dates;
|
||||
}
|
||||
|
||||
get conversations(): ReadonlyArray<{id: string, name: string}> {
|
||||
const conversations: {id: string, name: string}[] = [];
|
||||
for(const key in this.index) conversations.push({id: key, name: this.index[key]!.name});
|
||||
conversations.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0)));
|
||||
return conversations;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
||||
const file = await getFile(fs.root, 'settings');
|
||||
if(file === undefined) return undefined;
|
||||
return <GeneralSettings>JSON.parse(await readAsString(file));
|
||||
}
|
||||
|
||||
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
||||
const writer = await getWriter(fs.root, 'settings');
|
||||
writer.write(new Blob([JSON.stringify(value)]));
|
||||
}
|
||||
|
||||
async function getSettingsDir(character: string = core.connection.character): Promise<DirectoryEntry> {
|
||||
return new Promise<DirectoryEntry>((resolve, reject) => {
|
||||
fs.root.getDirectory(character, {create: true}, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
export class SettingsStore implements Settings.Store {
|
||||
async get<K extends keyof Settings.Keys>(key: K, character?: string): Promise<Settings.Keys[K] | undefined> {
|
||||
const dir = await getSettingsDir(character);
|
||||
const file = await getFile(dir, key);
|
||||
if(file === undefined) return undefined;
|
||||
return <Settings.Keys[K]>JSON.parse(await readAsString(file));
|
||||
}
|
||||
|
||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||
const writer = await getWriter(await getSettingsDir(), key);
|
||||
writer.write(new Blob([JSON.stringify(value)]));
|
||||
}
|
||||
|
||||
async getAvailableCharacters(): Promise<string[]> {
|
||||
return (await getEntries(fs.root)).filter((x) => x.isDirectory).map((x) => x.name);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import core from '../chat/core';
|
||||
import {Conversation} from '../chat/interfaces';
|
||||
import BaseNotifications from '../chat/notifications'; //tslint:disable-line:match-default-export-name
|
||||
|
||||
//tslint:disable
|
||||
declare global {
|
||||
interface Options {
|
||||
id?: number
|
||||
title?: string
|
||||
text?: string
|
||||
every?: string
|
||||
at?: Date | null
|
||||
badge?: number
|
||||
sound?: string
|
||||
data?: any
|
||||
icon?: string
|
||||
smallIcon?: string
|
||||
ongoing?: boolean
|
||||
led?: string
|
||||
}
|
||||
|
||||
interface CordovaPlugins {
|
||||
notification: {
|
||||
local: {
|
||||
getDefaults(): Options
|
||||
setDefaults(options: Options): void
|
||||
schedule(notification: Options, callback?: Function, scope?: Object, args?: {skipPermissions: boolean}): void
|
||||
update(notification: Options, callback?: Function, scope?: Object, args?: {skipPermissions: boolean}): void
|
||||
clear(ids: string, callback?: Function, scope?: Object): void
|
||||
clearAll(callback?: Function, scope?: Object): void
|
||||
cancel(ids: string, callback?: Function, scope?: Object): void
|
||||
cancelAll(callback?: Function, scope?: Object): void
|
||||
isPresent(id: string, callback?: Function, scope?: Object): void
|
||||
isTriggered(id: string, callback?: Function, scope?: Object): void
|
||||
getAllIds(callback?: Function, scope?: Object): void
|
||||
getScheduledIds(callback?: Function, scope?: Object): void
|
||||
getTriggeredIds(callback?: Function, scope?: Object): void
|
||||
get(ids?: number[], callback?: Function, scope?: Object): void
|
||||
getScheduled(ids?: number[], callback?: Function, scope?: Object): void
|
||||
getTriggered(ids?: number[], callback?: Function, scope?: Object): void
|
||||
hasPermission(callback?: Function, scope?: Object): void
|
||||
registerPermission(callback?: Function, scope?: Object): void
|
||||
on(event: 'schedule' | 'update' | 'click' | 'trigger', handler: (notification: Options) => void): void
|
||||
un(event: 'schedule' | 'update' | 'click' | 'trigger', handler: (notification: Options) => void): void
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//tslint:enable
|
||||
document.addEventListener('deviceready', () => {
|
||||
cordova.plugins.notification.local.on('click', (notification) => {
|
||||
const conv = core.conversations.byKey((<{conversation: string}>notification.data).conversation);
|
||||
if(conv !== undefined) conv.show();
|
||||
});
|
||||
});
|
||||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
||||
this.playSound(sound);
|
||||
if(core.state.settings.notifications)
|
||||
cordova.plugins.notification.local.schedule({
|
||||
title, text: body, sound, icon, smallIcon: icon, data: {conversation: conversation.key}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "0.2.0",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
"license": "MIT",
|
||||
"cordova": {
|
||||
"plugins": {
|
||||
"cordova-plugin-whitelist": {},
|
||||
"cordova-plugin-file": {},
|
||||
"de.appplant.cordova.plugin.local-notification": {}
|
||||
},
|
||||
"platforms": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "../node_modules/.bin/webpack",
|
||||
"build:dist": "../node_modules/.bin/webpack --env production",
|
||||
"watch": "../node_modules/.bin/webpack --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"cordova-android": "^6.2.3",
|
||||
"cordova-plugin-app-event": "^1.2.1",
|
||||
"cordova-plugin-compat": "^1.0.0",
|
||||
"cordova-plugin-device": "^1.1.6",
|
||||
"cordova-plugin-file": "^4.3.3",
|
||||
"cordova-plugin-whitelist": "^1.3.2",
|
||||
"de.appplant.cordova.plugin.local-notification": "^0.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cordova": "^0.0.34",
|
||||
"qs": "^6.5.0"
|
||||
}
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/cordova@^0.0.34":
|
||||
version "0.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/cordova/-/cordova-0.0.34.tgz#ea7addf74ecec3d7629827a0c39e2c9addc73d04"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
android-versions@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/android-versions/-/android-versions-1.2.1.tgz#3f50baf693e73a512c3c5403542291cead900063"
|
||||
|
||||
ansi@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
|
||||
base64-js@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
|
||||
|
||||
big-integer@^1.6.7:
|
||||
version "1.6.25"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.25.tgz#1de45a9f57542ac20121c682f8d642220a34e823"
|
||||
|
||||
bplist-parser@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6"
|
||||
dependencies:
|
||||
big-integer "^1.6.7"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
cordova-android@^6.2.3:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cordova-android/-/cordova-android-6.3.0.tgz#da5418433d25c75a5977b428244bbe437d0128d2"
|
||||
dependencies:
|
||||
android-versions "^1.2.0"
|
||||
cordova-common "^2.1.0"
|
||||
elementtree "0.1.6"
|
||||
nopt "^3.0.1"
|
||||
properties-parser "^0.2.3"
|
||||
q "^1.4.1"
|
||||
shelljs "^0.5.3"
|
||||
|
||||
cordova-common@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cordova-common/-/cordova-common-2.1.0.tgz#bb357ee1b9825031ed9db3c56b592efe973d1640"
|
||||
dependencies:
|
||||
ansi "^0.3.1"
|
||||
bplist-parser "^0.1.0"
|
||||
cordova-registry-mapper "^1.1.8"
|
||||
elementtree "0.1.6"
|
||||
glob "^5.0.13"
|
||||
minimatch "^3.0.0"
|
||||
osenv "^0.1.3"
|
||||
plist "^1.2.0"
|
||||
q "^1.4.1"
|
||||
semver "^5.0.1"
|
||||
shelljs "^0.5.3"
|
||||
underscore "^1.8.3"
|
||||
unorm "^1.3.3"
|
||||
|
||||
cordova-plugin-app-event@>=1.1.0, cordova-plugin-app-event@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cordova-plugin-app-event/-/cordova-plugin-app-event-1.2.1.tgz#0eebb14132aa43bb2e5c081a9abdbd97ca2d8132"
|
||||
|
||||
cordova-plugin-compat@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cordova-plugin-compat/-/cordova-plugin-compat-1.2.0.tgz#0bc65757276ebd920c012ce920e274177576373e"
|
||||
|
||||
cordova-plugin-device@*, cordova-plugin-device@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/cordova-plugin-device/-/cordova-plugin-device-1.1.6.tgz#2d21764cad7c9b801523e4e09a30e024b249334b"
|
||||
|
||||
cordova-plugin-file@^4.3.3:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/cordova-plugin-file/-/cordova-plugin-file-4.3.3.tgz#012e97aa1afb91f84916e6341b548366d23de9b9"
|
||||
|
||||
cordova-plugin-whitelist@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.2.tgz#5b6335feb9f5301f3c013b9096cb8885bdbd5076"
|
||||
|
||||
cordova-registry-mapper@^1.1.8:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/cordova-registry-mapper/-/cordova-registry-mapper-1.1.15.tgz#e244b9185b8175473bff6079324905115f83dc7c"
|
||||
|
||||
de.appplant.cordova.plugin.local-notification@^0.8.5:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/de.appplant.cordova.plugin.local-notification/-/de.appplant.cordova.plugin.local-notification-0.8.5.tgz#e0c6a86ea52ac4f41dba67521d91a58a9a42a3bd"
|
||||
dependencies:
|
||||
cordova-plugin-app-event ">=1.1.0"
|
||||
cordova-plugin-device "*"
|
||||
|
||||
elementtree@0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.6.tgz#2ac4c46ea30516c8c4cbdb5e3ac7418e592de20c"
|
||||
dependencies:
|
||||
sax "0.3.5"
|
||||
|
||||
glob@^5.0.13:
|
||||
version "5.0.15"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||
dependencies:
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "2 || 3"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
lodash@^3.5.0:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
|
||||
"minimatch@2 || 3", minimatch@^3.0.0:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
nopt@^3.0.1:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
os-homedir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
|
||||
osenv@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
|
||||
dependencies:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
plist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593"
|
||||
dependencies:
|
||||
base64-js "0.0.8"
|
||||
util-deprecate "1.0.2"
|
||||
xmlbuilder "4.0.0"
|
||||
xmldom "0.1.x"
|
||||
|
||||
properties-parser@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/properties-parser/-/properties-parser-0.2.3.tgz#f7591255f707abbff227c7b56b637dbb0373a10f"
|
||||
|
||||
q@^1.4.1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
|
||||
|
||||
qs@^6.5.0:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
sax@0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-0.3.5.tgz#88fcfc1f73c0c8bbd5b7c776b6d3f3501eed073d"
|
||||
|
||||
semver@^5.0.1:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
shelljs@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.5.3.tgz#c54982b996c76ef0c1e6b59fbdc5825f5b713113"
|
||||
|
||||
underscore@^1.8.3:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
||||
|
||||
unorm@^1.3.3:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.4.1.tgz#364200d5f13646ca8bcd44490271335614792300"
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
||||
xmlbuilder@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.0.0.tgz#98b8f651ca30aa624036f127d11cc66dc7b907a3"
|
||||
dependencies:
|
||||
lodash "^3.5.0"
|
||||
|
||||
xmldom@0.1.x:
|
||||
version "0.1.27"
|
||||
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div @mouseover="onMouseOver" id="page" style="position: relative; padding: 10px;">
|
||||
<div @mouseover="onMouseOver" id="page" style="position:relative;padding:5px 10px 10px">
|
||||
<div v-html="styling"></div>
|
||||
<div v-if="!characters" style="display:flex; align-items:center; justify-content:center; height: 100%;">
|
||||
<div class="well well-lg" style="width: 400px;">
|
||||
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||
<input class="form-control" id="account" v-model="account" @keypress.enter="login"/>
|
||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
<div class="form-group" v-show="showAdvanced">
|
||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||
<input class="form-control" id="host" v-model="host" @keypress.enter="login"/>
|
||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="advanced"><input type="checkbox" id="advanced" v-model="showAdvanced"/> {{l('login.advanced')}}</label>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<div class="form-group">
|
||||
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<div class="form-group text-right" style="margin:0">
|
||||
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
||||
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
||||
</button>
|
||||
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</modal>
|
||||
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||
<character-page :authenticated="false" :hideGroups="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>
|
||||
</modal>
|
||||
</div>
|
||||
|
@ -55,7 +55,7 @@
|
|||
import * as qs from 'querystring';
|
||||
import * as Raven from 'raven-js';
|
||||
import {promisify} from 'util';
|
||||
import Vue, {ComponentOptions} from 'vue';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Chat from '../chat/Chat.vue';
|
||||
import {Settings} from '../chat/common';
|
||||
|
@ -66,51 +66,19 @@
|
|||
import Modal from '../components/Modal.vue';
|
||||
import Connection from '../fchat/connection';
|
||||
import CharacterPage from '../site/character_page/character_page.vue';
|
||||
import {nativeRequire} from './common';
|
||||
import {GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
||||
import {GeneralSettings, nativeRequire} from './common';
|
||||
import {Logs, SettingsStore} from './filesystem';
|
||||
import * as SlimcatImporter from './importer';
|
||||
import {createAppMenu, createContextMenu} from './menu';
|
||||
import Notifications from './notifications';
|
||||
import * as spellchecker from './spellchecker';
|
||||
|
||||
declare module '../chat/interfaces' {
|
||||
interface State {
|
||||
generalSettings?: GeneralSettings
|
||||
}
|
||||
}
|
||||
|
||||
const webContents = electron.remote.getCurrentWebContents();
|
||||
webContents.on('context-menu', (_, props) => {
|
||||
const menuTemplate = createContextMenu(<Electron.ContextMenuParams & {editFlags: {[key: string]: boolean}}>props);
|
||||
if(props.misspelledWord !== '') {
|
||||
const corrections = spellchecker.getCorrections(props.misspelledWord);
|
||||
if(corrections.length > 0) {
|
||||
menuTemplate.unshift({type: 'separator'});
|
||||
menuTemplate.unshift(...corrections.map((correction: string) => ({
|
||||
label: correction,
|
||||
click: () => webContents.replaceMisspelling(correction)
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup();
|
||||
});
|
||||
|
||||
const defaultTrayMenu = [
|
||||
{label: l('action.open'), click: () => mainWindow!.show()},
|
||||
{
|
||||
label: l('action.quit'),
|
||||
click: () => {
|
||||
isClosing = true;
|
||||
mainWindow!.close();
|
||||
mainWindow = undefined;
|
||||
electron.remote.app.quit();
|
||||
}
|
||||
}
|
||||
];
|
||||
let trayMenu = electron.remote.Menu.buildFromTemplate(defaultTrayMenu);
|
||||
|
||||
let isClosing = false;
|
||||
let mainWindow: Electron.BrowserWindow | undefined = electron.remote.getCurrentWindow();
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
const tray = new electron.remote.Tray(path.join(__dirname, <string>require('./build/tray.png')));
|
||||
tray.setToolTip(l('title'));
|
||||
tray.on('click', (_) => mainWindow!.show());
|
||||
tray.setContextMenu(trayMenu);
|
||||
const parent = electron.remote.getCurrentWindow().webContents;
|
||||
|
||||
/*tslint:disable:no-any*///because this is hacky
|
||||
const keyStore = nativeRequire<{
|
||||
|
@ -122,8 +90,6 @@
|
|||
for(const key in keyStore) keyStore[key] = promisify(<(...args: any[]) => any>keyStore[key].bind(keyStore, 'fchat'));
|
||||
//tslint:enable
|
||||
|
||||
profileApiInit();
|
||||
|
||||
@Component({
|
||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||
})
|
||||
|
@ -132,205 +98,83 @@
|
|||
showAdvanced = false;
|
||||
saveLogin = false;
|
||||
loggingIn = false;
|
||||
account: string;
|
||||
password = '';
|
||||
host: string;
|
||||
character: string | undefined;
|
||||
characters: string[] | null = null;
|
||||
error = '';
|
||||
defaultCharacter: string | null = null;
|
||||
settings = new SettingsStore();
|
||||
l = l;
|
||||
currentSettings: GeneralSettings;
|
||||
isConnected = false;
|
||||
settings: GeneralSettings;
|
||||
importProgress = 0;
|
||||
profileName = '';
|
||||
|
||||
constructor(options?: ComponentOptions<Index>) {
|
||||
super(options);
|
||||
let settings = getGeneralSettings();
|
||||
if(settings === undefined) {
|
||||
try {
|
||||
if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral')))
|
||||
settings = SlimcatImporter.importGeneral();
|
||||
} catch {
|
||||
alert(l('importer.error'));
|
||||
}
|
||||
settings = settings !== undefined ? settings : new GeneralSettings();
|
||||
}
|
||||
this.account = settings.account;
|
||||
this.host = settings.host;
|
||||
this.currentSettings = settings;
|
||||
}
|
||||
async created(): Promise<void> {
|
||||
if(this.settings.account.length > 0) this.saveLogin = true;
|
||||
keyStore.getPassword(this.settings.account)
|
||||
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
|
||||
|
||||
created(): void {
|
||||
if(this.currentSettings.account.length > 0) {
|
||||
keyStore.getPassword(this.currentSettings.account)
|
||||
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
|
||||
this.saveLogin = true;
|
||||
}
|
||||
window.onbeforeunload = () => {
|
||||
if(process.env.NODE_ENV !== 'production' || isClosing || !this.isConnected) {
|
||||
tray.destroy();
|
||||
return;
|
||||
}
|
||||
if(!this.currentSettings.closeToTray)
|
||||
return setImmediate(() => {
|
||||
if(confirm(l('chat.confirmLeave'))) {
|
||||
isClosing = true;
|
||||
mainWindow!.close();
|
||||
}
|
||||
});
|
||||
mainWindow!.hide();
|
||||
return false;
|
||||
};
|
||||
Vue.set(core.state, 'generalSettings', this.settings);
|
||||
|
||||
const appMenu = createAppMenu();
|
||||
const themes = fs.readdirSync(path.join(__dirname, 'themes')).filter((x) => x.substr(-4) === '.css').map((x) => x.slice(0, -4));
|
||||
const setTheme = (theme: string) => {
|
||||
this.currentSettings.theme = theme;
|
||||
setGeneralSettings(this.currentSettings);
|
||||
};
|
||||
const spellcheckerMenu = new electron.remote.Menu();
|
||||
//tslint:disable-next-line:no-floating-promises
|
||||
this.addSpellcheckerItems(spellcheckerMenu);
|
||||
appMenu[0].submenu = [
|
||||
{
|
||||
label: l('settings.closeToTray'), type: 'checkbox', checked: this.currentSettings.closeToTray,
|
||||
click: (item: Electron.MenuItem) => {
|
||||
this.currentSettings.closeToTray = item.checked;
|
||||
setGeneralSettings(this.currentSettings);
|
||||
}
|
||||
}, {
|
||||
label: l('settings.profileViewer'), type: 'checkbox', checked: this.currentSettings.profileViewer,
|
||||
click: (item: Electron.MenuItem) => {
|
||||
this.currentSettings.profileViewer = item.checked;
|
||||
setGeneralSettings(this.currentSettings);
|
||||
}
|
||||
},
|
||||
{label: l('settings.spellcheck'), submenu: spellcheckerMenu},
|
||||
{
|
||||
label: l('settings.theme'),
|
||||
submenu: themes.map((x) => ({
|
||||
checked: this.currentSettings.theme === x,
|
||||
click: () => setTheme(x),
|
||||
label: x,
|
||||
type: <'radio'>'radio'
|
||||
}))
|
||||
},
|
||||
{type: 'separator'},
|
||||
{role: 'minimize'},
|
||||
{
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : undefined,
|
||||
label: l('action.quit'),
|
||||
click(): void {
|
||||
isClosing = true;
|
||||
mainWindow!.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
electron.remote.Menu.setApplicationMenu(electron.remote.Menu.buildFromTemplate(appMenu));
|
||||
|
||||
let hasUpdate = false;
|
||||
electron.ipcRenderer.on('updater-status', (_: Event, status: string) => {
|
||||
if(status !== 'update-downloaded' || hasUpdate) return;
|
||||
hasUpdate = true;
|
||||
const menu = electron.remote.Menu.getApplicationMenu();
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: l('action.updateAvailable'),
|
||||
submenu: electron.remote.Menu.buildFromTemplate([{
|
||||
label: l('action.update'),
|
||||
click: () => {
|
||||
if(!this.isConnected || confirm(l('chat.confirmLeave'))) {
|
||||
isClosing = true;
|
||||
electron.ipcRenderer.send('install-update');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: l('help.changelog'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog')
|
||||
}])
|
||||
}));
|
||||
electron.remote.Menu.setApplicationMenu(menu);
|
||||
});
|
||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => core.state.generalSettings = this.settings = settings);
|
||||
electron.ipcRenderer.on('open-profile', (_: Event, name: string) => {
|
||||
if(this.currentSettings.profileViewer) {
|
||||
const profileViewer = <Modal>this.$refs['profileViewer'];
|
||||
this.profileName = name;
|
||||
profileViewer.show();
|
||||
} else electron.remote.shell.openExternal(`https://www.f-list.net/c/${name}`);
|
||||
const profileViewer = <Modal>this.$refs['profileViewer'];
|
||||
this.profileName = name;
|
||||
profileViewer.show();
|
||||
});
|
||||
}
|
||||
|
||||
async addSpellcheckerItems(menu: Electron.Menu): Promise<void> {
|
||||
const dictionaries = await spellchecker.getAvailableDictionaries();
|
||||
const selected = this.currentSettings.spellcheckLang;
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
type: 'radio',
|
||||
label: l('settings.spellcheck.disabled'),
|
||||
click: this.setSpellcheck.bind(this, undefined)
|
||||
}));
|
||||
for(const lang of dictionaries)
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
type: 'radio',
|
||||
label: lang,
|
||||
checked: lang === selected,
|
||||
click: this.setSpellcheck.bind(this, lang)
|
||||
}));
|
||||
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: spellchecker.check});
|
||||
await spellchecker.setDictionary(selected);
|
||||
}
|
||||
|
||||
async setSpellcheck(lang: string | undefined): Promise<void> {
|
||||
this.currentSettings.spellcheckLang = lang;
|
||||
setGeneralSettings(this.currentSettings);
|
||||
await spellchecker.setDictionary(lang);
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if(this.character !== undefined) electron.ipcRenderer.send('disconnect', this.character);
|
||||
});
|
||||
}
|
||||
|
||||
async login(): Promise<void> {
|
||||
if(this.loggingIn) return;
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
if(!this.saveLogin) await keyStore.deletePassword(this.account);
|
||||
const data = <{ticket?: string, error: string, characters: string[], default_character: string}>
|
||||
(await Axios.post('https://www.f-list.net/json/getApiTicket.php',
|
||||
qs.stringify({account: this.account, password: this.password, no_friends: true, no_bookmarks: true}))).data;
|
||||
if(!this.saveLogin) await keyStore.deletePassword(this.settings.account);
|
||||
const data = <{ticket?: string, error: string, characters: {[key: string]: number}, default_character: number}>
|
||||
(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify({
|
||||
account: this.settings.account, password: this.password, no_friends: true, no_bookmarks: true,
|
||||
new_character_list: true
|
||||
}))).data;
|
||||
if(data.error !== '') {
|
||||
this.error = data.error;
|
||||
return;
|
||||
}
|
||||
if(this.saveLogin) {
|
||||
this.currentSettings.account = this.account;
|
||||
await keyStore.setPassword(this.account, this.password);
|
||||
this.currentSettings.host = this.host;
|
||||
setGeneralSettings(this.currentSettings);
|
||||
}
|
||||
Socket.host = this.host;
|
||||
const connection = new Connection(Socket, this.account, this.password);
|
||||
if(this.saveLogin) electron.ipcRenderer.send('save-login', this.settings.account, this.settings.host);
|
||||
Socket.host = this.settings.host;
|
||||
const connection = new Connection(`F-Chat 3.0 (${process.platform})`, electron.remote.app.getVersion(), Socket,
|
||||
this.settings.account, this.password);
|
||||
connection.onEvent('connecting', async() => {
|
||||
if((await this.settings.get('settings')) === undefined && SlimcatImporter.canImportCharacter(core.connection.character)) {
|
||||
if(!confirm(l('importer.importGeneral'))) return this.settings.set('settings', new Settings());
|
||||
if(!electron.ipcRenderer.sendSync('connect', core.connection.character) && process.env.NODE_ENV === 'production') {
|
||||
alert(l('login.alreadyLoggedIn'));
|
||||
return core.connection.close();
|
||||
}
|
||||
this.character = core.connection.character;
|
||||
if((await core.settingsStore.get('settings')) === undefined &&
|
||||
SlimcatImporter.canImportCharacter(core.connection.character)) {
|
||||
if(!confirm(l('importer.importGeneral'))) return core.settingsStore.set('settings', new Settings());
|
||||
(<Modal>this.$refs['importModal']).show(true);
|
||||
await SlimcatImporter.importCharacter(core.connection.character, (progress) => this.importProgress = progress);
|
||||
(<Modal>this.$refs['importModal']).hide();
|
||||
}
|
||||
});
|
||||
connection.onEvent('connected', () => {
|
||||
this.isConnected = true;
|
||||
tray.setToolTip(document.title = `FChat 3.0 - ${core.connection.character}`);
|
||||
core.watch(() => core.conversations.hasNew, (newValue) => parent.send('has-new', webContents.id, newValue));
|
||||
parent.send('connect', webContents.id, core.connection.character);
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
trayMenu.insert(0, new electron.remote.MenuItem({label: core.connection.character, enabled: false}));
|
||||
trayMenu.insert(1, new electron.remote.MenuItem({type: 'separator'}));
|
||||
tray.setContextMenu(trayMenu);
|
||||
});
|
||||
connection.onEvent('closed', () => {
|
||||
this.isConnected = false;
|
||||
tray.setToolTip(document.title = 'FChat 3.0');
|
||||
this.character = undefined;
|
||||
electron.ipcRenderer.send('disconnect', connection.character);
|
||||
parent.send('disconnect', webContents.id);
|
||||
Raven.setUserContext();
|
||||
tray.setContextMenu(trayMenu = electron.remote.Menu.buildFromTemplate(defaultTrayMenu));
|
||||
});
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
this.characters = data.characters.sort();
|
||||
this.defaultCharacter = data.default_character;
|
||||
const charNames = Object.keys(data.characters);
|
||||
this.characters = charNames.sort();
|
||||
this.defaultCharacter = charNames.find((x) => data.characters[x] === data.default_character)!;
|
||||
profileApiInit(data.characters);
|
||||
} catch(e) {
|
||||
this.error = l('login.error');
|
||||
if(process.env.NODE_ENV !== 'production') throw e;
|
||||
|
@ -362,10 +206,10 @@
|
|||
|
||||
get styling(): string {
|
||||
try {
|
||||
return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.currentSettings.theme}.css`))}</style>`;
|
||||
return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.settings.theme}.css`))}</style>`;
|
||||
} catch(e) {
|
||||
if((<Error & {code: string}>e).code === 'ENOENT' && this.currentSettings.theme !== 'default') {
|
||||
this.currentSettings.theme = 'default';
|
||||
if((<Error & {code: string}>e).code === 'ENOENT' && this.settings.theme !== 'default') {
|
||||
this.settings.theme = 'default';
|
||||
return this.styling;
|
||||
}
|
||||
throw e;
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
<template>
|
||||
<div style="display:flex;flex-direction:column;height:100%;padding:1px" :class="'platform-' + platform">
|
||||
<div v-html="styling"></div>
|
||||
<div style="display:flex;align-items:stretch;" class="border-bottom" id="window-tabs">
|
||||
<h4>F-Chat</h4>
|
||||
<div :class="'fa fa-cog btn btn-' + (hasUpdate ? 'warning' : 'default')" @click="openMenu"></div>
|
||||
<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"
|
||||
:key="tab.view.id">
|
||||
<a href="#" @click.prevent="show(tab)">
|
||||
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/>
|
||||
{{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"
|
||||
@click.stop="remove(tab)">
|
||||
</a>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" v-show="canOpenTab" class="addTab" id="addTab">
|
||||
<a href="#" @click.prevent="addTab" class="fa fa-plus"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag;margin-top:3px" class="btn-group"
|
||||
id="windowButtons">
|
||||
<span class="fa fa-window-minimize btn btn-default" @click.stop="minimize"></span>
|
||||
<span :class="'fa btn btn-default fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></span>
|
||||
<span class="fa fa-close fa-lg btn btn-default" @click.stop="close"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Sortable = require('sortablejs'); //tslint:disable-line:no-require-imports
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import l from '../chat/localize';
|
||||
import {GeneralSettings} from './common';
|
||||
|
||||
const browserWindow = electron.remote.getCurrentWindow();
|
||||
|
||||
function getWindowBounds(): Electron.Rectangle {
|
||||
const bounds = browserWindow.getContentBounds();
|
||||
const height = document.body.offsetHeight;
|
||||
return {x: 0, y: height, width: bounds.width, height: bounds.height - height};
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
user: string | undefined,
|
||||
view: Electron.BrowserView
|
||||
hasNew: boolean
|
||||
tray: Electron.Tray
|
||||
}
|
||||
|
||||
const trayIcon = path.join(__dirname, <string>require('./build/tray.png')); //tslint:disable-line:no-require-imports
|
||||
|
||||
@Component
|
||||
export default class Window extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
settings: GeneralSettings;
|
||||
tabs: Tab[] = [];
|
||||
activeTab: Tab | null = null;
|
||||
tabMap: {[key: number]: Tab} = {};
|
||||
isMaximized = browserWindow.isMaximized();
|
||||
canOpenTab = true;
|
||||
l = l;
|
||||
hasUpdate = false;
|
||||
platform = process.platform;
|
||||
|
||||
mounted(): void {
|
||||
this.addTab();
|
||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
|
||||
electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
|
||||
electron.ipcRenderer.on('open-tab', () => this.addTab());
|
||||
electron.ipcRenderer.on('update-available', () => this.hasUpdate = true);
|
||||
electron.ipcRenderer.on('connect', (_: Event, id: number, name: string) => {
|
||||
const tab = this.tabMap[id];
|
||||
tab.user = name;
|
||||
tab.tray.setToolTip(`${l('title')} - ${tab.user}`);
|
||||
const menu = this.createTrayMenu(tab);
|
||||
menu.unshift({label: tab.user, enabled: false}, {type: 'separator'});
|
||||
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(menu));
|
||||
});
|
||||
electron.ipcRenderer.on('disconnect', (_: Event, id: number) => {
|
||||
const tab = this.tabMap[id];
|
||||
tab.user = undefined;
|
||||
tab.tray.setToolTip(l('title'));
|
||||
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
|
||||
});
|
||||
electron.ipcRenderer.on('has-new', (_: Event, id: number, hasNew: boolean) => {
|
||||
const tab = this.tabMap[id];
|
||||
tab.hasNew = hasNew;
|
||||
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||
});
|
||||
browserWindow.on('maximize', () => {
|
||||
this.isMaximized = true;
|
||||
this.activeTab!.view.setBounds(getWindowBounds());
|
||||
});
|
||||
browserWindow.on('unmaximize', () => {
|
||||
this.isMaximized = false;
|
||||
this.activeTab!.view.setBounds(getWindowBounds());
|
||||
});
|
||||
document.addEventListener('click', () => this.activeTab!.view.webContents.focus());
|
||||
window.addEventListener('focus', () => this.activeTab!.view.webContents.focus());
|
||||
|
||||
Sortable.create(this.$refs['tabs'], {
|
||||
animation: 50,
|
||||
onEnd: (e: {oldIndex: number, newIndex: number}) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
const tab = this.tabs.splice(e.oldIndex, 1)[0];
|
||||
this.tabs.splice(e.newIndex, 0, tab);
|
||||
},
|
||||
onMove: (e: {related: HTMLElement}) => e.related.id !== 'addTab',
|
||||
filter: '.addTab'
|
||||
});
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
const isConnected = this.tabs.reduce((cur, tab) => cur || tab.user !== undefined, false);
|
||||
if(process.env.NODE_ENV !== 'production' || !isConnected) {
|
||||
this.tabs.forEach((tab) => this.remove(tab, false));
|
||||
return;
|
||||
}
|
||||
if(!this.settings.closeToTray)
|
||||
return setImmediate(() => {
|
||||
if(confirm(l('chat.confirmLeave'))) this.tabs.forEach((tab) => this.remove(tab, false));
|
||||
});
|
||||
browserWindow.hide();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
get styling(): string {
|
||||
try {
|
||||
return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.settings.theme}.css`))}</style>`;
|
||||
} catch(e) {
|
||||
if((<Error & {code: string}>e).code === 'ENOENT' && this.settings.theme !== 'default') {
|
||||
this.settings.theme = 'default';
|
||||
return this.styling;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
createTrayMenu(tab: Tab): Electron.MenuItemConstructorOptions[] {
|
||||
return [
|
||||
{
|
||||
label: l('action.open'), click: () => {
|
||||
browserWindow.show();
|
||||
this.show(tab);
|
||||
}
|
||||
},
|
||||
{label: l('action.quit'), click: () => this.remove(tab, false)}
|
||||
];
|
||||
}
|
||||
|
||||
addTab(): void {
|
||||
const tray = new electron.remote.Tray(trayIcon);
|
||||
tray.setToolTip(l('title'));
|
||||
tray.on('click', (_) => browserWindow.show());
|
||||
const view = new electron.remote.BrowserView();
|
||||
view.setAutoResize({width: true, height: true});
|
||||
view.webContents.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
query: {settings: JSON.stringify(this.settings)}
|
||||
}));
|
||||
electron.ipcRenderer.send('tab-added', view.webContents.id);
|
||||
const tab = {active: false, view, user: undefined, hasNew: false, tray};
|
||||
tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
|
||||
this.tabs.push(tab);
|
||||
this.tabMap[view.webContents.id] = tab;
|
||||
this.show(tab);
|
||||
}
|
||||
|
||||
show(tab: Tab): void {
|
||||
this.activeTab = tab;
|
||||
browserWindow.setBrowserView(tab.view);
|
||||
tab.view.setBounds(getWindowBounds());
|
||||
}
|
||||
|
||||
remove(tab: Tab, shouldConfirm: boolean = true): void {
|
||||
if(shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return;
|
||||
this.tabs.splice(this.tabs.indexOf(tab), 1);
|
||||
delete this.tabMap[tab.view.webContents.id];
|
||||
tab.tray.destroy();
|
||||
tab.view.webContents.loadURL('about:blank');
|
||||
electron.ipcRenderer.send('tab-closed');
|
||||
delete tab.view;
|
||||
if(this.tabs.length === 0) {
|
||||
if(process.env.NODE_ENV === 'production') browserWindow.close();
|
||||
} else if(this.activeTab === tab) this.show(this.tabs[0]);
|
||||
}
|
||||
|
||||
minimize(): void {
|
||||
browserWindow.minimize();
|
||||
}
|
||||
|
||||
maximize(): void {
|
||||
if(browserWindow.isMaximized()) browserWindow.unmaximize();
|
||||
else browserWindow.maximize();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
browserWindow.close();
|
||||
}
|
||||
|
||||
openMenu(): void {
|
||||
electron.remote.Menu.getApplicationMenu().popup();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
#window-tabs {
|
||||
user-select: none;
|
||||
.btn {
|
||||
border-radius: 0;
|
||||
padding: 5px 15px;
|
||||
display: flex;
|
||||
margin: 0px -1px -1px 0;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 100%;
|
||||
a {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
&:first-child {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 28px;
|
||||
margin: -5px 3px -5px -5px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 10px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
align-self: center;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
}
|
||||
|
||||
#windowButtons .btn {
|
||||
margin: -4px -1px -1px 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.platform-darwin {
|
||||
#windowButtons .btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#window-tabs h4 {
|
||||
margin: 9px 34px 9px 77px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.16",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
After Width: | Height: | Size: 336 B |
100
electron/chat.ts
|
@ -31,17 +31,37 @@
|
|||
*/
|
||||
import 'bootstrap/js/collapse.js';
|
||||
import 'bootstrap/js/dropdown.js';
|
||||
import 'bootstrap/js/modal.js';
|
||||
import 'bootstrap/js/tab.js';
|
||||
import 'bootstrap/js/transition.js';
|
||||
import * as electron from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as qs from 'querystring';
|
||||
import * as Raven from 'raven-js';
|
||||
import Vue from 'vue';
|
||||
import {getKey} from '../chat/common';
|
||||
import l from '../chat/localize';
|
||||
import VueRaven from '../chat/vue-raven';
|
||||
import {GeneralSettings, nativeRequire} from './common';
|
||||
import * as SlimcatImporter from './importer';
|
||||
import Index from './Index.vue';
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if(e.ctrlKey && e.shiftKey && getKey(e) === 'i')
|
||||
electron.remote.getCurrentWebContents().toggleDevTools();
|
||||
});
|
||||
|
||||
process.env.SPELLCHECKER_PREFER_HUNSPELL = '1';
|
||||
const sc = nativeRequire<{
|
||||
Spellchecker: {
|
||||
new(): {
|
||||
isMisspelled(x: string): boolean,
|
||||
setDictionary(name: string | undefined, dir: string): void,
|
||||
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
}>('spellchecker/build/Release/spellchecker.node');
|
||||
const spellchecker = new sc.Spellchecker();
|
||||
|
||||
if(process.env.NODE_ENV === 'production') {
|
||||
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
|
||||
release: electron.remote.app.getVersion(),
|
||||
|
@ -58,19 +78,81 @@ if(process.env.NODE_ENV === 'production') {
|
|||
Raven.captureException(<Error>e.reason);
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if(e.ctrlKey && e.shiftKey && getKey(e) === 'I')
|
||||
electron.remote.getCurrentWebContents().toggleDevTools();
|
||||
});
|
||||
electron.remote.getCurrentWebContents().on('devtools-opened', () => {
|
||||
console.log(`%c${l('consoleWarning.head')}`, 'background: red; color: yellow; font-size: 30pt');
|
||||
console.log(`%c${l('consoleWarning.body')}`, 'font-size: 16pt; color:red');
|
||||
});
|
||||
}
|
||||
|
||||
//tslint:disable-next-line:no-unused-expression
|
||||
new Index({
|
||||
el: '#app'
|
||||
const webContents = electron.remote.getCurrentWebContents();
|
||||
webContents.on('context-menu', (_, props) => {
|
||||
const hasText = props.selectionText.trim().length > 0;
|
||||
const can = (type: string) => (<Electron.EditFlags & {[key: string]: boolean}>props.editFlags)[`can${type}`] && hasText;
|
||||
|
||||
const menuTemplate: Electron.MenuItemConstructorOptions[] = [];
|
||||
if(hasText || props.isEditable)
|
||||
menuTemplate.push({
|
||||
id: 'copy',
|
||||
label: l('action.copy'),
|
||||
role: can('Copy') ? 'copy' : '',
|
||||
enabled: can('Copy')
|
||||
});
|
||||
if(props.isEditable)
|
||||
menuTemplate.push({
|
||||
id: 'cut',
|
||||
label: l('action.cut'),
|
||||
role: can('Cut') ? 'cut' : '',
|
||||
enabled: can('Cut')
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: l('action.paste'),
|
||||
role: props.editFlags.canPaste ? 'paste' : '',
|
||||
enabled: props.editFlags.canPaste
|
||||
});
|
||||
else if(props.linkURL.length > 0 && props.mediaType === 'none' && props.linkURL.substr(0, props.pageURL.length) !== props.pageURL)
|
||||
menuTemplate.push({
|
||||
id: 'copyLink',
|
||||
label: l('action.copyLink'),
|
||||
click(): void {
|
||||
if(process.platform === 'darwin')
|
||||
electron.clipboard.writeBookmark(props.linkText, props.linkURL);
|
||||
else
|
||||
electron.clipboard.writeText(props.linkURL);
|
||||
}
|
||||
});
|
||||
if(props.misspelledWord !== '') {
|
||||
const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
|
||||
if(corrections.length > 0) {
|
||||
menuTemplate.unshift({type: 'separator'});
|
||||
menuTemplate.unshift(...corrections.map((correction: string) => ({
|
||||
label: correction,
|
||||
click: () => webContents.replaceMisspelling(correction)
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup();
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('focus', (_: Event, message: boolean) => message ? window.focus() : window.blur());
|
||||
const dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)});
|
||||
electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => spellchecker.setDictionary(s.spellcheckLang, dictDir));
|
||||
|
||||
const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1));
|
||||
const settings = <GeneralSettings>JSON.parse(params['settings']!);
|
||||
if(params['import'] !== undefined)
|
||||
try {
|
||||
if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral'))) {
|
||||
SlimcatImporter.importGeneral(settings);
|
||||
electron.ipcRenderer.send('save-login', settings.account, settings.host);
|
||||
}
|
||||
} catch {
|
||||
alert(l('importer.error'));
|
||||
}
|
||||
spellchecker.setDictionary(settings.spellcheckLang, dictDir);
|
||||
|
||||
//tslint:disable-next-line:no-unused-expression
|
||||
new Index({
|
||||
el: '#app',
|
||||
data: {settings}
|
||||
});
|
|
@ -1,6 +1,18 @@
|
|||
import * as electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class GeneralSettings {
|
||||
account = '';
|
||||
closeToTray = true;
|
||||
profileViewer = true;
|
||||
host = 'wss://chat.f-list.net:9799';
|
||||
logDirectory = path.join(electron.app.getPath('userData'), 'data');
|
||||
spellcheckLang: string | undefined = 'en-GB';
|
||||
theme = 'default';
|
||||
version = electron.app.getVersion();
|
||||
}
|
||||
|
||||
export function mkdir(dir: string): void {
|
||||
try {
|
||||
fs.mkdirSync(dir);
|
||||
|
@ -27,7 +39,9 @@ export function mkdir(dir: string): void {
|
|||
|
||||
//tslint:disable
|
||||
const Module = require('module');
|
||||
|
||||
export function nativeRequire<T>(module: string): T {
|
||||
return Module.prototype.require.call({paths: Module._nodeModulePaths(__dirname)}, module);
|
||||
}
|
||||
|
||||
//tslint:enable
|
|
@ -5,21 +5,20 @@ import * as path from 'path';
|
|||
import {Message as MessageImpl} from '../chat/common';
|
||||
import core from '../chat/core';
|
||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||
import l from '../chat/localize';
|
||||
import {mkdir} from './common';
|
||||
|
||||
const dayMs = 86400000;
|
||||
const baseDir = path.join(electron.remote.app.getPath('userData'), 'data');
|
||||
mkdir(baseDir);
|
||||
|
||||
const noAssert = process.env.NODE_ENV === 'production';
|
||||
|
||||
export class GeneralSettings {
|
||||
account = '';
|
||||
closeToTray = true;
|
||||
profileViewer = true;
|
||||
host = 'wss://chat.f-list.net:9799';
|
||||
spellcheckLang: string | undefined = 'en-GB';
|
||||
theme = 'default';
|
||||
function writeFile(p: fs.PathLike | number, data: string | object | number,
|
||||
options?: {encoding?: string | null; mode?: number | string; flag?: string} | string | null): void {
|
||||
try {
|
||||
fs.writeFileSync(p, data, options);
|
||||
} catch(e) {
|
||||
electron.remote.dialog.showErrorBox(l('fs.error'), (<Error>e).message);
|
||||
}
|
||||
}
|
||||
|
||||
export type Message = Conversation.EventMessage | {
|
||||
|
@ -40,7 +39,7 @@ interface Index {
|
|||
}
|
||||
|
||||
export function getLogDir(this: void, character: string = core.connection.character): string {
|
||||
const dir = path.join(baseDir, character, 'logs');
|
||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'logs');
|
||||
mkdir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
@ -152,7 +151,7 @@ export class Logs implements Logging.Persistent {
|
|||
const entry = this.index[key];
|
||||
if(entry === undefined) return [];
|
||||
const dates = [];
|
||||
for(const item in entry.index) { //tslint:disable:forin
|
||||
for(const item in entry.index) {
|
||||
const date = new Date(parseInt(item, 10) * dayMs);
|
||||
dates.push(addMinutes(date, date.getTimezoneOffset()));
|
||||
}
|
||||
|
@ -185,8 +184,8 @@ export class Logs implements Logging.Persistent {
|
|||
const hasIndex = this.index[conversation.key] !== undefined;
|
||||
const indexBuffer = checkIndex(this.index, message, conversation.key, conversation.name,
|
||||
() => fs.existsSync(file) ? fs.statSync(file).size : 0);
|
||||
if(indexBuffer !== undefined) fs.writeFileSync(`${file}.idx`, indexBuffer, {flag: hasIndex ? 'a' : 'wx'});
|
||||
fs.writeFileSync(file, buffer, {flag: 'a'});
|
||||
if(indexBuffer !== undefined) writeFile(`${file}.idx`, indexBuffer, {flag: hasIndex ? 'a' : 'wx'});
|
||||
writeFile(file, buffer, {flag: 'a'});
|
||||
}
|
||||
|
||||
get conversations(): ReadonlyArray<{id: string, name: string}> {
|
||||
|
@ -197,18 +196,8 @@ export class Logs implements Logging.Persistent {
|
|||
}
|
||||
}
|
||||
|
||||
export function getGeneralSettings(): GeneralSettings | undefined {
|
||||
const file = path.join(baseDir, 'settings');
|
||||
if(!fs.existsSync(file)) return undefined;
|
||||
return <GeneralSettings>JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
export function setGeneralSettings(value: GeneralSettings): void {
|
||||
fs.writeFileSync(path.join(baseDir, 'settings'), JSON.stringify(value));
|
||||
}
|
||||
|
||||
function getSettingsDir(character: string = core.connection.character): string {
|
||||
const dir = path.join(baseDir, character, 'settings');
|
||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'settings');
|
||||
mkdir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
@ -221,10 +210,11 @@ export class SettingsStore implements Settings.Store {
|
|||
}
|
||||
|
||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||
const baseDir = core.state.generalSettings!.logDirectory;
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory());
|
||||
}
|
||||
|
||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||
fs.writeFileSync(path.join(getSettingsDir(), key), JSON.stringify(value));
|
||||
writeFile(path.join(getSettingsDir(), key), JSON.stringify(value));
|
||||
}
|
||||
}
|
|
@ -4,7 +4,8 @@ import * as path from 'path';
|
|||
import {promisify} from 'util';
|
||||
import {Settings} from '../chat/common';
|
||||
import {Conversation} from '../chat/interfaces';
|
||||
import {checkIndex, GeneralSettings, getLogDir, Message as LogMessage, serializeMessage, SettingsStore} from './filesystem';
|
||||
import {GeneralSettings} from './common';
|
||||
import {checkIndex, getLogDir, Message as LogMessage, serializeMessage, SettingsStore} from './filesystem';
|
||||
|
||||
function getRoamingDir(): string | undefined {
|
||||
const appdata = process.env.APPDATA;
|
||||
|
@ -37,7 +38,7 @@ export function canImportCharacter(character: string): boolean {
|
|||
return getSettingsDir(character) !== undefined;
|
||||
}
|
||||
|
||||
export function importGeneral(): GeneralSettings | undefined {
|
||||
export function importGeneral(data: GeneralSettings): void {
|
||||
let dir = getLocalDir();
|
||||
let files: string[] = [];
|
||||
if(dir !== undefined)
|
||||
|
@ -57,7 +58,6 @@ export function importGeneral(): GeneralSettings | undefined {
|
|||
}
|
||||
if(file.length === 0) return;
|
||||
let elm = new DOMParser().parseFromString(fs.readFileSync(file, 'utf8'), 'application/xml').firstElementChild;
|
||||
const data = new GeneralSettings();
|
||||
if(file.slice(-3) === 'xml') {
|
||||
if(elm === null) return;
|
||||
let elements;
|
||||
|
@ -76,7 +76,6 @@ export function importGeneral(): GeneralSettings | undefined {
|
|||
else if(element.getAttribute('name') === 'Host') data.host = element.firstElementChild.textContent;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const charRegex = /([A-Za-z0-9][A-Za-z0-9 \-_]{0,18}[A-Za-z0-9\-_])\b/;
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FChat 3.0</title>
|
||||
<title>F-Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
</div>
|
||||
<script type="text/javascript" src="common.js"></script>
|
||||
<script type="text/javascript" src="chat.js"></script>
|
||||
</body>
|
||||
</html>
|
343
electron/main.ts
|
@ -29,22 +29,27 @@
|
|||
* @version 3.0
|
||||
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
||||
*/
|
||||
import Axios from 'axios';
|
||||
import * as electron from 'electron';
|
||||
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
||||
import {autoUpdater} from 'electron-updater';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import {mkdir} from './common';
|
||||
import {promisify} from 'util';
|
||||
import l from '../chat/localize';
|
||||
import {GeneralSettings, mkdir} from './common';
|
||||
import * as windowState from './window_state';
|
||||
import BrowserWindow = Electron.BrowserWindow;
|
||||
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
const datadir = process.argv.filter((x) => x.startsWith('--datadir='));
|
||||
if(datadir.length > 0) app.setPath('userData', datadir[0].substr('--datadir='.length));
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
const windows: Electron.BrowserWindow[] = [];
|
||||
const characters: string[] = [];
|
||||
let tabCount = 0;
|
||||
|
||||
const baseDir = app.getPath('userData');
|
||||
mkdir(baseDir);
|
||||
|
@ -55,70 +60,322 @@ log.transports.file.maxSize = 5 * 1024 * 1024;
|
|||
log.transports.file.file = path.join(baseDir, 'log.txt');
|
||||
log.info('Starting application.');
|
||||
|
||||
function sendUpdaterStatusToWindow(status: string, progress?: object): void {
|
||||
log.info(status);
|
||||
for(const window of windows) window.webContents.send('updater-status', status, progress);
|
||||
const dictDir = path.join(baseDir, 'spellchecker');
|
||||
mkdir(dictDir);
|
||||
const downloadUrl = 'https://client.f-list.net/dictionaries/';
|
||||
type DictionaryIndex = {[key: string]: {file: string, time: number} | undefined};
|
||||
let availableDictionaries: DictionaryIndex | undefined;
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
const requestConfig = {responseType: 'arraybuffer'};
|
||||
|
||||
async function getAvailableDictionaries(): Promise<ReadonlyArray<string>> {
|
||||
if(availableDictionaries === undefined) {
|
||||
const indexPath = path.join(dictDir, 'index.json');
|
||||
try {
|
||||
if(!fs.existsSync(indexPath) || fs.statSync(indexPath).mtimeMs + 86400000 * 7 < Date.now()) {
|
||||
availableDictionaries = (await Axios.get<DictionaryIndex>(`${downloadUrl}index.json`)).data;
|
||||
await writeFile(indexPath, JSON.stringify(availableDictionaries));
|
||||
} else availableDictionaries = <DictionaryIndex>JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||||
} catch(e) {
|
||||
availableDictionaries = {};
|
||||
log.error(`Error loading dictionaries: ${e}`);
|
||||
}
|
||||
}
|
||||
return Object.keys(availableDictionaries).sort();
|
||||
}
|
||||
|
||||
const updaterEvents = ['checking-for-update', 'update-available', 'update-not-available', 'error', 'update-downloaded'];
|
||||
for(const eventName of updaterEvents)
|
||||
autoUpdater.on(eventName, () => {
|
||||
sendUpdaterStatusToWindow(eventName);
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (_, progress: object) => {
|
||||
sendUpdaterStatusToWindow('download-progress', progress);
|
||||
});
|
||||
|
||||
function runUpdater(): void {
|
||||
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
|
||||
setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
|
||||
electron.ipcMain.on('install-update', () => autoUpdater.quitAndInstall(false, true));
|
||||
async function setDictionary(lang: string | undefined): Promise<void> {
|
||||
const dict = availableDictionaries![lang!];
|
||||
if(dict !== undefined) {
|
||||
const dicPath = path.join(dictDir, `${lang}.dic`);
|
||||
if(!fs.existsSync(dicPath) || fs.statSync(dicPath).mtimeMs / 1000 < dict.time) {
|
||||
await writeFile(dicPath, new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.dic`, requestConfig)).data));
|
||||
await writeFile(path.join(dictDir, `${lang}.aff`),
|
||||
new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.aff`, requestConfig)).data));
|
||||
fs.utimesSync(dicPath, dict.time, dict.time);
|
||||
}
|
||||
}
|
||||
settings.spellcheckLang = lang;
|
||||
setGeneralSettings(settings);
|
||||
}
|
||||
|
||||
function bindWindowEvents(window: Electron.BrowserWindow): void {
|
||||
// Prevent page navigation by opening links in an external browser.
|
||||
const settingsDir = path.join(electron.app.getPath('userData'), 'data');
|
||||
const file = path.join(settingsDir, 'settings');
|
||||
const settings = new GeneralSettings();
|
||||
let shouldImportSettings = false;
|
||||
if(!fs.existsSync(file)) shouldImportSettings = true;
|
||||
else
|
||||
try {
|
||||
Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(file, 'utf8')));
|
||||
} catch(e) {
|
||||
log.error(`Error loading settings: ${e}`);
|
||||
}
|
||||
|
||||
function setGeneralSettings(value: GeneralSettings): void {
|
||||
fs.writeFileSync(path.join(settingsDir, 'settings'), JSON.stringify(value));
|
||||
for(const w of electron.webContents.getAllWebContents()) w.send('settings', settings);
|
||||
shouldImportSettings = false;
|
||||
}
|
||||
|
||||
async function addSpellcheckerItems(menu: Electron.Menu): Promise<void> {
|
||||
const dictionaries = await getAvailableDictionaries();
|
||||
const selected = settings.spellcheckLang;
|
||||
menu.append(new electron.MenuItem({
|
||||
type: 'radio',
|
||||
label: l('settings.spellcheck.disabled'),
|
||||
click: async() => setDictionary(undefined)
|
||||
}));
|
||||
for(const lang of dictionaries)
|
||||
menu.append(new electron.MenuItem({
|
||||
type: 'radio',
|
||||
label: lang,
|
||||
checked: lang === selected,
|
||||
click: async() => setDictionary(lang)
|
||||
}));
|
||||
}
|
||||
|
||||
function setUpWebContents(webContents: Electron.WebContents): void {
|
||||
const openLinkExternally = (e: Event, linkUrl: string) => {
|
||||
e.preventDefault();
|
||||
const profileMatch = linkUrl.match(/^https?:\/\/(www\.)?f-list.net\/c\/(.+)/);
|
||||
if(profileMatch !== null) window.webContents.send('open-profile', decodeURIComponent(profileMatch[2]));
|
||||
const profileMatch = linkUrl.match(/^https?:\/\/(www\.)?f-list.net\/c\/(.+)\/?#?/);
|
||||
if(profileMatch !== null && settings.profileViewer) webContents.send('open-profile', decodeURIComponent(profileMatch[2]));
|
||||
else electron.shell.openExternal(linkUrl);
|
||||
};
|
||||
|
||||
window.webContents.on('will-navigate', openLinkExternally);
|
||||
window.webContents.on('new-window', openLinkExternally);
|
||||
// Fix focus events not properly propagating down to the document.
|
||||
window.on('focus', () => window.webContents.send('focus', true));
|
||||
window.on('blur', () => window.webContents.send('focus', false));
|
||||
|
||||
// Save window state when it is being closed.
|
||||
window.on('close', () => windowState.setSavedWindowState(window));
|
||||
webContents.on('will-navigate', openLinkExternally);
|
||||
webContents.on('new-window', openLinkExternally);
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
function createWindow(): Electron.BrowserWindow | undefined {
|
||||
if(tabCount >= 3) return;
|
||||
const lastState = windowState.getSavedWindowState();
|
||||
const windowProperties = {...lastState, center: lastState.x === undefined};
|
||||
const windowProperties: Electron.BrowserWindowConstructorOptions & {maximized: boolean} = {
|
||||
...lastState, center: lastState.x === undefined
|
||||
};
|
||||
if(process.platform === 'darwin') windowProperties.titleBarStyle = 'hiddenInset';
|
||||
else windowProperties.frame = false;
|
||||
const window = new electron.BrowserWindow(windowProperties);
|
||||
windows.push(window);
|
||||
if(lastState.maximized) window.maximize();
|
||||
|
||||
window.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
pathname: path.join(__dirname, 'window.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
slashes: true,
|
||||
query: {settings: JSON.stringify(settings), import: shouldImportSettings ? 'true' : []}
|
||||
}));
|
||||
|
||||
bindWindowEvents(window);
|
||||
setUpWebContents(window.webContents);
|
||||
|
||||
// Save window state when it is being closed.
|
||||
window.on('close', () => windowState.setSavedWindowState(window));
|
||||
window.on('closed', () => windows.splice(windows.indexOf(window), 1));
|
||||
|
||||
if(process.env.NODE_ENV === 'production') runUpdater();
|
||||
return window;
|
||||
}
|
||||
|
||||
const running = app.makeSingleInstance(() => {
|
||||
if(windows.length < 3) createWindow();
|
||||
return true;
|
||||
});
|
||||
function showPatchNotes(): void {
|
||||
electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog');
|
||||
}
|
||||
|
||||
function onReady(): void {
|
||||
app.on('open-file', createWindow);
|
||||
|
||||
if(settings.version !== app.getVersion()) {
|
||||
showPatchNotes();
|
||||
settings.version = app.getVersion();
|
||||
setGeneralSettings(settings);
|
||||
}
|
||||
|
||||
if(process.env.NODE_ENV === 'production') {
|
||||
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
|
||||
const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
|
||||
let hasUpdate = false;
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
clearInterval(updateTimer);
|
||||
if(hasUpdate) return;
|
||||
hasUpdate = true;
|
||||
const menu = electron.Menu.getApplicationMenu();
|
||||
menu.append(new electron.MenuItem({
|
||||
label: l('action.updateAvailable'),
|
||||
submenu: electron.Menu.buildFromTemplate([{
|
||||
label: l('action.update'),
|
||||
click: () => autoUpdater.quitAndInstall(false, true)
|
||||
}, {
|
||||
label: l('help.changelog'),
|
||||
click: showPatchNotes
|
||||
}])
|
||||
}));
|
||||
electron.Menu.setApplicationMenu(menu);
|
||||
for(const w of windows) w.webContents.send('update-available');
|
||||
});
|
||||
}
|
||||
|
||||
const viewItem = {
|
||||
label: `&${l('action.view')}`,
|
||||
submenu: <Electron.MenuItemConstructorOptions[]>[
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'}
|
||||
]
|
||||
};
|
||||
if(process.env.NODE_ENV !== 'production')
|
||||
viewItem.submenu.unshift({role: 'reload'}, {role: 'forcereload'}, {role: 'toggledevtools'}, {type: 'separator'});
|
||||
const spellcheckerMenu = new electron.Menu();
|
||||
//tslint:disable-next-line:no-floating-promises
|
||||
addSpellcheckerItems(spellcheckerMenu);
|
||||
const themes = fs.readdirSync(path.join(__dirname, 'themes')).filter((x) => x.substr(-4) === '.css').map((x) => x.slice(0, -4));
|
||||
const setTheme = (theme: string) => {
|
||||
settings.theme = theme;
|
||||
setGeneralSettings(settings);
|
||||
};
|
||||
electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate([
|
||||
{
|
||||
label: `&${l('title')}`,
|
||||
submenu: [
|
||||
{label: l('action.newWindow'), click: createWindow, accelerator: 'CmdOrCtrl+n'},
|
||||
{
|
||||
label: l('action.newTab'),
|
||||
click: (_: Electron.MenuItem, w: Electron.BrowserWindow) => {
|
||||
if(tabCount < 3) w.webContents.send('open-tab');
|
||||
},
|
||||
accelerator: 'CmdOrCtrl+t'
|
||||
},
|
||||
{
|
||||
label: l('settings.logDir'),
|
||||
click: (_, window: BrowserWindow) => {
|
||||
const dir = <string[] | undefined>electron.dialog.showOpenDialog(
|
||||
{defaultPath: new GeneralSettings().logDirectory, properties: ['openDirectory']});
|
||||
if(dir !== undefined) {
|
||||
const button = electron.dialog.showMessageBox(window, {
|
||||
message: l('settings.logDir.confirm', dir[0], settings.logDirectory),
|
||||
buttons: [l('confirmYes'), l('confirmNo')],
|
||||
cancelId: 1
|
||||
});
|
||||
if(button === 0) {
|
||||
for(const w of windows) {
|
||||
w.webContents.on('will-prevent-unload', (e) => e.preventDefault());
|
||||
w.close();
|
||||
}
|
||||
settings.logDirectory = dir[0];
|
||||
setGeneralSettings(settings);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: l('settings.closeToTray'), type: 'checkbox', checked: settings.closeToTray,
|
||||
click: (item: Electron.MenuItem) => {
|
||||
settings.closeToTray = item.checked;
|
||||
setGeneralSettings(settings);
|
||||
}
|
||||
}, {
|
||||
label: l('settings.profileViewer'), type: 'checkbox', checked: settings.profileViewer,
|
||||
click: (item: Electron.MenuItem) => {
|
||||
settings.profileViewer = item.checked;
|
||||
setGeneralSettings(settings);
|
||||
}
|
||||
},
|
||||
{label: l('settings.spellcheck'), submenu: spellcheckerMenu},
|
||||
{
|
||||
label: l('settings.theme'),
|
||||
submenu: themes.map((x) => ({
|
||||
checked: settings.theme === x,
|
||||
click: () => setTheme(x),
|
||||
label: x,
|
||||
type: <'radio'>'radio'
|
||||
}))
|
||||
},
|
||||
{type: 'separator'},
|
||||
{role: 'minimize'},
|
||||
{
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : undefined,
|
||||
label: l('action.quit'),
|
||||
click(_: Electron.MenuItem, w: Electron.BrowserWindow): void {
|
||||
if(characters.length === 0) return app.quit();
|
||||
const button = electron.dialog.showMessageBox(w, {
|
||||
message: l('chat.confirmLeave'),
|
||||
buttons: [l('confirmYes'), l('confirmNo')],
|
||||
cancelId: 1
|
||||
});
|
||||
if(button === 0) app.quit();
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
label: `&${l('action.edit')}`,
|
||||
submenu: [
|
||||
{role: 'undo'},
|
||||
{role: 'redo'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
{role: 'selectall'}
|
||||
]
|
||||
}, viewItem, {
|
||||
label: `&${l('help')}`,
|
||||
submenu: [
|
||||
{
|
||||
label: l('help.fchat'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0')
|
||||
},
|
||||
{
|
||||
label: l('help.feedback'),
|
||||
click: () => electron.shell.openExternal('https://goo.gl/forms/WnLt3Qm3TPt64jQt2')
|
||||
},
|
||||
{
|
||||
label: l('help.rules'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/Rules')
|
||||
},
|
||||
{
|
||||
label: l('help.faq'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/Frequently_Asked_Questions')
|
||||
},
|
||||
{
|
||||
label: l('help.report'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/How_to_Report_a_User#In_chat')
|
||||
},
|
||||
{label: l('version', app.getVersion()), click: showPatchNotes}
|
||||
]
|
||||
}
|
||||
]));
|
||||
electron.ipcMain.on('tab-added', (_: Event, id: number) => {
|
||||
const webContents = electron.webContents.fromId(id);
|
||||
setUpWebContents(webContents);
|
||||
++tabCount;
|
||||
if(tabCount === 3)
|
||||
for(const w of windows) w.webContents.send('allow-new-tabs', false);
|
||||
});
|
||||
electron.ipcMain.on('tab-closed', () => {
|
||||
--tabCount;
|
||||
for(const w of windows) w.webContents.send('allow-new-tabs', true);
|
||||
});
|
||||
electron.ipcMain.on('save-login', (_: Event, account: string, host: string) => {
|
||||
settings.account = account;
|
||||
settings.host = host;
|
||||
setGeneralSettings(settings);
|
||||
});
|
||||
electron.ipcMain.on('connect', (e: Event & {sender: Electron.WebContents}, character: string) => {
|
||||
if(characters.indexOf(character) !== -1) return e.returnValue = false;
|
||||
else characters.push(character);
|
||||
e.returnValue = true;
|
||||
});
|
||||
electron.ipcMain.on('disconnect', (_: Event, character: string) => characters.splice(characters.indexOf(character), 1));
|
||||
const emptyBadge = electron.nativeImage.createEmpty();
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
const badge = electron.nativeImage.createFromPath(path.join(__dirname, <string>require('./build/badge.png')));
|
||||
electron.ipcMain.on('has-new', (e: Event & {sender: Electron.WebContents}, hasNew: boolean) => {
|
||||
if(process.platform === 'darwin') app.dock.setBadge(hasNew ? '!' : '');
|
||||
electron.BrowserWindow.fromWebContents(e.sender).setOverlayIcon(hasNew ? badge : emptyBadge, hasNew ? 'New messages' : '');
|
||||
});
|
||||
createWindow();
|
||||
}
|
||||
|
||||
const running = app.makeSingleInstance(createWindow);
|
||||
if(running) app.quit();
|
||||
else app.on('ready', createWindow);
|
||||
else app.on('ready', onReady);
|
||||
app.on('window-all-closed', () => app.quit());
|
|
@ -1,98 +0,0 @@
|
|||
import * as electron from 'electron';
|
||||
import l from '../chat/localize';
|
||||
|
||||
export function createContextMenu(props: Electron.ContextMenuParams & {editFlags: {[key: string]: boolean}}):
|
||||
Electron.MenuItemConstructorOptions[] {
|
||||
const hasText = props.selectionText.trim().length > 0;
|
||||
const can = (type: string) => props.editFlags[`can${type}`] && hasText;
|
||||
|
||||
const menuTemplate: Electron.MenuItemConstructorOptions[] = [];
|
||||
if(hasText || props.isEditable)
|
||||
menuTemplate.push({
|
||||
id: 'copy',
|
||||
label: l('action.copy'),
|
||||
role: can('Copy') ? 'copy' : '',
|
||||
enabled: can('Copy')
|
||||
});
|
||||
if(props.isEditable)
|
||||
menuTemplate.push({
|
||||
id: 'cut',
|
||||
label: l('action.cut'),
|
||||
role: can('Cut') ? 'cut' : '',
|
||||
enabled: can('Cut')
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: l('action.paste'),
|
||||
role: props.editFlags.canPaste ? 'paste' : '',
|
||||
enabled: props.editFlags.canPaste
|
||||
});
|
||||
else if(props.linkURL.length > 0 && props.mediaType === 'none' && props.linkURL.substr(0, props.pageURL.length) !== props.pageURL)
|
||||
menuTemplate.push({
|
||||
id: 'copyLink',
|
||||
label: l('action.copyLink'),
|
||||
click(): void {
|
||||
if(process.platform === 'darwin')
|
||||
electron.clipboard.writeBookmark(props.linkText, props.linkURL);
|
||||
else
|
||||
electron.clipboard.writeText(props.linkURL);
|
||||
}
|
||||
});
|
||||
return menuTemplate;
|
||||
}
|
||||
|
||||
export function createAppMenu(): Electron.MenuItemConstructorOptions[] {
|
||||
const viewItem = {
|
||||
label: `&${l('action.view')}`,
|
||||
submenu: [
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'}
|
||||
]
|
||||
};
|
||||
const menu: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: `&${l('title')}`
|
||||
}, {
|
||||
label: `&${l('action.edit')}`,
|
||||
submenu: [
|
||||
{role: 'undo'},
|
||||
{role: 'redo'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
{role: 'selectall'}
|
||||
]
|
||||
}, viewItem, {
|
||||
label: `&${l('help')}`,
|
||||
submenu: [
|
||||
{
|
||||
label: l('help.fchat'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0')
|
||||
},
|
||||
{
|
||||
label: l('help.feedback'),
|
||||
click: () => electron.shell.openExternal('https://goo.gl/forms/WnLt3Qm3TPt64jQt2')
|
||||
},
|
||||
{
|
||||
label: l('help.rules'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/Rules')
|
||||
},
|
||||
{
|
||||
label: l('help.faq'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/Frequently_Asked_Questions')
|
||||
},
|
||||
{
|
||||
label: l('help.report'),
|
||||
click: () => electron.shell.openExternal('https://wiki.f-list.net/How_to_Report_a_User#In_chat')
|
||||
},
|
||||
{label: l('version', electron.remote.app.getVersion()), enabled: false}
|
||||
]
|
||||
}
|
||||
];
|
||||
if(process.env.NODE_ENV !== 'production')
|
||||
viewItem.submenu.unshift({role: 'reload'}, {role: 'forcereload'}, {role: 'toggledevtools'}, {type: 'separator'});
|
||||
return menu;
|
||||
}
|
|
@ -4,11 +4,13 @@ import {Conversation} from '../chat/interfaces';
|
|||
//tslint:disable-next-line:match-default-export-name
|
||||
import BaseNotifications from '../chat/notifications';
|
||||
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
||||
this.playSound(sound);
|
||||
remote.getCurrentWindow().flashFrame(true);
|
||||
browserWindow.flashFrame(true);
|
||||
if(core.state.settings.notifications) {
|
||||
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
||||
const notification = new Notification(title, <NotificationOptions & {silent: boolean}>{
|
||||
|
@ -18,7 +20,7 @@ export default class Notifications extends BaseNotifications {
|
|||
});
|
||||
notification.onclick = () => {
|
||||
conversation.show();
|
||||
remote.getCurrentWindow().focus();
|
||||
browserWindow.focus();
|
||||
notification.close();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"name": "fchat",
|
||||
"version": "3.0.0",
|
||||
"author": "The F-List Team",
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import Axios from 'axios';
|
||||
import * as electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {promisify} from 'util';
|
||||
import {mkdir, nativeRequire} from './common';
|
||||
|
||||
process.env.SPELLCHECKER_PREFER_HUNSPELL = '1';
|
||||
const downloadUrl = 'https://client.f-list.net/dictionaries/';
|
||||
const dir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||
mkdir(dir);
|
||||
//tslint:disable-next-line
|
||||
const sc = nativeRequire<{
|
||||
Spellchecker: {
|
||||
new(): {
|
||||
isMisspelled(x: string): boolean,
|
||||
setDictionary(name: string | undefined, dir: string): void,
|
||||
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
}>('spellchecker/build/Release/spellchecker.node');
|
||||
type DictionaryIndex = {[key: string]: {file: string, time: number} | undefined};
|
||||
let availableDictionaries: DictionaryIndex | undefined;
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
const requestConfig = {responseType: 'arraybuffer'};
|
||||
const spellchecker = new sc.Spellchecker();
|
||||
|
||||
export async function getAvailableDictionaries(): Promise<ReadonlyArray<string>> {
|
||||
if(availableDictionaries === undefined) {
|
||||
const indexPath = path.join(dir, 'index.json');
|
||||
if(!fs.existsSync(indexPath) || fs.statSync(indexPath).mtimeMs + 86400000 * 7 < Date.now()) {
|
||||
availableDictionaries = (await Axios.get<DictionaryIndex>(`${downloadUrl}index.json`)).data;
|
||||
await writeFile(indexPath, JSON.stringify(availableDictionaries));
|
||||
} else availableDictionaries = <DictionaryIndex>JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||||
}
|
||||
return Object.keys(availableDictionaries).sort();
|
||||
}
|
||||
|
||||
export async function setDictionary(lang: string | undefined): Promise<void> {
|
||||
const dict = availableDictionaries![lang!];
|
||||
if(dict !== undefined) {
|
||||
const dicPath = path.join(dir, `${lang}.dic`);
|
||||
if(!fs.existsSync(dicPath) || fs.statSync(dicPath).mtimeMs / 1000 < dict.time) {
|
||||
await writeFile(dicPath, new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.dic`, requestConfig)).data));
|
||||
await writeFile(path.join(dir, `${lang}.aff`),
|
||||
new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.aff`, requestConfig)).data));
|
||||
fs.utimesSync(dicPath, dict.time, dict.time);
|
||||
}
|
||||
}
|
||||
spellchecker.setDictionary(lang, dir);
|
||||
}
|
||||
|
||||
export function getCorrections(word: string): ReadonlyArray<string> {
|
||||
return spellchecker.getCorrectionsForMisspelling(word);
|
||||
}
|
||||
|
||||
export const check = (text: string) => !spellchecker.isMisspelled(text);
|
|
@ -1,4 +1,5 @@
|
|||
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');
|
||||
|
@ -6,17 +7,55 @@ const fs = require('fs');
|
|||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const exportLoader = require('../export-loader');
|
||||
|
||||
const config = {
|
||||
const mainConfig = {
|
||||
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'application.json')],
|
||||
output: {
|
||||
path: __dirname + '/app',
|
||||
filename: 'main.js'
|
||||
},
|
||||
context: __dirname,
|
||||
target: 'electron-main',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: __dirname + '/tsconfig.json',
|
||||
transpileOnly: true
|
||||
}
|
||||
},
|
||||
{test: /application.json$/, loader: 'file-loader?name=package.json'},
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
exportLoader.delayTypecheck
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
'node_modules', path.join(__dirname, '../')
|
||||
]
|
||||
}
|
||||
}, rendererConfig = {
|
||||
entry: {
|
||||
chat: [path.join(__dirname, 'chat.ts')],
|
||||
main: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'index.html'), path.join(__dirname, 'application.json')]
|
||||
chat: [path.join(__dirname, 'chat.ts'), path.join(__dirname, 'index.html')],
|
||||
window: [path.join(__dirname, 'window.ts'), path.join(__dirname, 'window.html')]
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/app',
|
||||
filename: '[name].js'
|
||||
},
|
||||
context: __dirname,
|
||||
target: 'electron',
|
||||
target: 'electron-renderer',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
|
@ -41,8 +80,7 @@ const config = {
|
|||
{test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream'},
|
||||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml'},
|
||||
{test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'},
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
|
||||
{test: /application.json$/, loader: 'file-loader?name=package.json'}
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
|
@ -56,6 +94,7 @@ const config = {
|
|||
'window.jQuery': 'jquery/dist/jquery.slim.js'
|
||||
}),
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
new CommonsChunkPlugin({name: 'common', minChunks: 2}),
|
||||
exportLoader.delayTypecheck
|
||||
],
|
||||
resolve: {
|
||||
|
@ -77,20 +116,20 @@ module.exports = function(env) {
|
|||
for(const theme of themes) {
|
||||
if(!theme.endsWith('.less')) continue;
|
||||
const absPath = path.join(themesDir, theme);
|
||||
config.entry.chat.push(absPath);
|
||||
rendererConfig.entry.chat.push(absPath);
|
||||
const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css');
|
||||
config.plugins.push(plugin);
|
||||
config.module.loaders.push({test: absPath, use: plugin.extract(cssOptions)});
|
||||
rendererConfig.plugins.push(plugin);
|
||||
rendererConfig.module.loaders.push({test: absPath, use: plugin.extract(cssOptions)});
|
||||
}
|
||||
if(dist) {
|
||||
config.devtool = 'source-map';
|
||||
config.plugins.push(
|
||||
new UglifyPlugin({sourceMap: true}),
|
||||
mainConfig.devtool = rendererConfig.devtool = 'source-map';
|
||||
const plugins = [new UglifyPlugin({sourceMap: true}),
|
||||
new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}),
|
||||
new webpack.LoaderOptionsPlugin({minimize: true})
|
||||
);
|
||||
new webpack.LoaderOptionsPlugin({minimize: true})];
|
||||
mainConfig.plugins.push(...plugins);
|
||||
rendererConfig.plugins.push(...plugins);
|
||||
} else {
|
||||
//config.devtool = 'cheap-module-eval-source-map';
|
||||
}
|
||||
return config;
|
||||
return [mainConfig, rendererConfig];
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>F-Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="common.js"></script>
|
||||
<script type="text/javascript" src="window.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
import * as qs from 'querystring';
|
||||
import {GeneralSettings} from './common';
|
||||
import Window from './Window.vue';
|
||||
|
||||
const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1));
|
||||
const settings = <GeneralSettings>JSON.parse(params['settings']!);
|
||||
//tslint:disable-next-line:no-unused-expression
|
||||
new Window({
|
||||
el: '#app',
|
||||
data: {settings}
|
||||
});
|
|
@ -42,18 +42,18 @@ class Channel implements Interfaces.Channel {
|
|||
constructor(readonly id: string, readonly name: string) {
|
||||
}
|
||||
|
||||
addMember(member: SortableMember): void {
|
||||
async addMember(member: SortableMember): Promise<void> {
|
||||
this.members[member.character.name] = member;
|
||||
sortMember(this.sortedMembers, member);
|
||||
for(const handler of state.handlers) handler('join', this, member);
|
||||
for(const handler of state.handlers) await handler('join', this, member);
|
||||
}
|
||||
|
||||
removeMember(name: string): void {
|
||||
async removeMember(name: string): Promise<void> {
|
||||
const member = this.members[name];
|
||||
if(member !== undefined) {
|
||||
delete this.members[name];
|
||||
this.sortedMembers.splice(this.sortedMembers.indexOf(member), 1);
|
||||
for(const handler of state.handlers) handler('leave', this, member);
|
||||
for(const handler of state.handlers) await handler('leave', this, member);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ export default function(this: void, connection: Connection, characters: Characte
|
|||
}
|
||||
state.openRooms = channels;
|
||||
});
|
||||
connection.onMessage('JCH', (data) => {
|
||||
connection.onMessage('JCH', async(data) => {
|
||||
const item = state.getChannelItem(data.channel);
|
||||
if(data.character.identity === connection.character) {
|
||||
const id = data.channel.toLowerCase();
|
||||
|
@ -170,11 +170,11 @@ export default function(this: void, connection: Connection, characters: Characte
|
|||
const channel = state.getChannel(data.channel);
|
||||
if(channel === undefined) return state.leave(data.channel);
|
||||
const member = channel.createMember(characters.get(data.character.identity));
|
||||
channel.addMember(member);
|
||||
await channel.addMember(member);
|
||||
if(item !== undefined) item.memberCount++;
|
||||
}
|
||||
});
|
||||
connection.onMessage('ICH', (data) => {
|
||||
connection.onMessage('ICH', async(data) => {
|
||||
const channel = state.getChannel(data.channel);
|
||||
if(channel === undefined) return state.leave(data.channel);
|
||||
channel.mode = data.mode;
|
||||
|
@ -190,24 +190,24 @@ export default function(this: void, connection: Connection, characters: Characte
|
|||
channel.sortedMembers = sorted;
|
||||
const item = state.getChannelItem(data.channel);
|
||||
if(item !== undefined) item.memberCount = data.users.length;
|
||||
for(const handler of state.handlers) handler('join', channel);
|
||||
for(const handler of state.handlers) await handler('join', channel);
|
||||
});
|
||||
connection.onMessage('CDS', (data) => {
|
||||
const channel = state.getChannel(data.channel);
|
||||
if(channel === undefined) return state.leave(data.channel);
|
||||
channel.description = decodeHTML(data.description);
|
||||
});
|
||||
connection.onMessage('LCH', (data) => {
|
||||
connection.onMessage('LCH', async(data) => {
|
||||
const channel = state.getChannel(data.channel);
|
||||
if(channel === undefined) return;
|
||||
const item = state.getChannelItem(data.channel);
|
||||
if(data.character === connection.character) {
|
||||
state.joinedChannels.splice(state.joinedChannels.indexOf(channel), 1);
|
||||
delete state.joinedMap[channel.id];
|
||||
for(const handler of state.handlers) handler('leave', channel);
|
||||
for(const handler of state.handlers) await handler('leave', channel);
|
||||
if(item !== undefined) item.isJoined = false;
|
||||
} else {
|
||||
channel.removeMember(data.character);
|
||||
await channel.removeMember(data.character);
|
||||
if(item !== undefined) item.memberCount--;
|
||||
}
|
||||
});
|
||||
|
@ -255,12 +255,11 @@ export default function(this: void, connection: Connection, characters: Characte
|
|||
if(channel === undefined) return state.leave(data.channel);
|
||||
channel.mode = data.mode;
|
||||
});
|
||||
connection.onMessage('FLN', (data) => {
|
||||
connection.onMessage('FLN', async(data) => {
|
||||
for(const key in state.joinedMap)
|
||||
state.joinedMap[key]!.removeMember(data.character);
|
||||
await state.joinedMap[key]!.removeMember(data.character);
|
||||
});
|
||||
const globalHandler = (data: Connection.ServerCommands['AOP'] | Connection.ServerCommands['DOP']) => {
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in state.joinedMap) {
|
||||
const channel = state.joinedMap[key]!;
|
||||
const member = channel.members[data.character];
|
||||
|
|
|
@ -62,7 +62,6 @@ export default function(this: void, connection: Connection): Interfaces.State {
|
|||
state.bookmarkList = (<{characters: string[]}>await connection.queryApi('bookmark-list.php')).characters;
|
||||
state.friendList = ((<{friends: {source: string, dest: string, last_online: number}[]}>await connection.queryApi('friend-list.php'))
|
||||
.friends).map((x) => x.dest);
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in state.characters) {
|
||||
const character = state.characters[key]!;
|
||||
character.isFriend = state.friendList.indexOf(character.name) !== -1;
|
||||
|
@ -76,7 +75,6 @@ export default function(this: void, connection: Connection): Interfaces.State {
|
|||
connection.onEvent('connected', async(isReconnect) => {
|
||||
if(!isReconnect) return;
|
||||
connection.send('STA', reconnectStatus);
|
||||
//tslint:disable-next-line:forin
|
||||
for(const key in state.characters) {
|
||||
const char = state.characters[key]!;
|
||||
char.isIgnored = state.ignoreList.indexOf(key) !== -1;
|
||||
|
@ -97,7 +95,9 @@ export default function(this: void, connection: Connection): Interfaces.State {
|
|||
state.get(data.character).isIgnored = false;
|
||||
}
|
||||
});
|
||||
connection.onMessage('ADL', (data) => state.opList = data.ops.slice());
|
||||
connection.onMessage('ADL', (data) => {
|
||||
state.opList = data.ops.slice();
|
||||
});
|
||||
connection.onMessage('LIS', (data) => {
|
||||
for(const char of data.characters) {
|
||||
const character = state.get(char[0]);
|
||||
|
@ -143,6 +143,7 @@ export default function(this: void, connection: Connection): Interfaces.State {
|
|||
if(character.status !== 'offline') state.bookmarks.splice(state.bookmarks.indexOf(character), 1);
|
||||
break;
|
||||
case 'friendadd':
|
||||
if(character.isFriend) return;
|
||||
state.friendList.push(data.name);
|
||||
character.isFriend = true;
|
||||
if(character.status !== 'offline') state.friends.push(character);
|
||||
|
|
|
@ -21,44 +21,50 @@ export default class Connection implements Interfaces.Connection {
|
|||
private reconnectTimer: NodeJS.Timer;
|
||||
private ticketProvider: Interfaces.TicketProvider;
|
||||
private reconnectDelay = 0;
|
||||
private isReconnect = false;
|
||||
|
||||
constructor(private readonly socketProvider: new() => WebSocketConnection, private readonly account: string,
|
||||
constructor(private readonly clientName: string, private readonly version: string,
|
||||
private readonly socketProvider: new() => WebSocketConnection, private readonly account: string,
|
||||
ticketProvider: Interfaces.TicketProvider | string) {
|
||||
this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider;
|
||||
}
|
||||
|
||||
async connect(character: string): Promise<void> {
|
||||
this.cleanClose = false;
|
||||
const isReconnect = this.character === character;
|
||||
this.isReconnect = this.character === character;
|
||||
this.character = character;
|
||||
try {
|
||||
this.ticket = await this.ticketProvider();
|
||||
} catch(e) {
|
||||
for(const handler of this.errorHandlers) handler(<Error>e);
|
||||
await this.invokeHandlers('closed', true);
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
await this.invokeHandlers('connecting', this.isReconnect);
|
||||
if(this.cleanClose) {
|
||||
this.cleanClose = false;
|
||||
await this.invokeHandlers('closed', false);
|
||||
return;
|
||||
}
|
||||
await this.invokeHandlers('connecting', isReconnect);
|
||||
const socket = this.socket = new this.socketProvider();
|
||||
socket.onOpen(() => {
|
||||
this.send('IDN', {
|
||||
account: this.account,
|
||||
character: this.character,
|
||||
cname: 'F-Chat',
|
||||
cversion: '3.0',
|
||||
cname: this.clientName,
|
||||
cversion: this.version,
|
||||
method: 'ticket',
|
||||
ticket: this.ticket
|
||||
});
|
||||
});
|
||||
socket.onMessage((msg: string) => {
|
||||
socket.onMessage(async(msg: string) => {
|
||||
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
|
||||
const data = msg.length > 6 ? <object>JSON.parse(msg.substr(4)) : undefined;
|
||||
this.handleMessage(type, data);
|
||||
return this.handleMessage(type, data);
|
||||
});
|
||||
socket.onClose(async() => {
|
||||
if(!this.cleanClose) {
|
||||
setTimeout(async() => this.connect(this.character), this.reconnectDelay);
|
||||
this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000;
|
||||
}
|
||||
if(!this.cleanClose) this.reconnect();
|
||||
this.socket = undefined;
|
||||
await this.invokeHandlers('closed', !this.cleanClose);
|
||||
});
|
||||
|
@ -74,6 +80,11 @@ export default class Connection implements Interfaces.Connection {
|
|||
});
|
||||
}
|
||||
|
||||
private reconnect(): void {
|
||||
this.reconnectTimer = setTimeout(async() => this.connect(this.character), this.reconnectDelay);
|
||||
this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.cleanClose = true;
|
||||
|
@ -131,7 +142,11 @@ export default class Connection implements Interfaces.Connection {
|
|||
}
|
||||
|
||||
//tslint:disable:no-unsafe-any no-any
|
||||
protected handleMessage<T extends keyof Interfaces.ServerCommands>(type: T, data: any): void {
|
||||
protected async handleMessage<T extends keyof Interfaces.ServerCommands>(type: T, data: any): Promise<void> {
|
||||
const time = new Date();
|
||||
const handlers = <Interfaces.CommandHandler<T>[] | undefined>this.messageHandlers[type];
|
||||
if(handlers !== undefined)
|
||||
for(const handler of handlers) await handler(data, time);
|
||||
switch(type) {
|
||||
case 'VAR':
|
||||
this.vars[data.variable] = data.value;
|
||||
|
@ -149,14 +164,10 @@ export default class Connection implements Interfaces.Connection {
|
|||
break;
|
||||
case 'NLN':
|
||||
if(data.identity === this.character) {
|
||||
this.invokeHandlers('connected', this.reconnectDelay !== 0); //tslint:disable-line:no-floating-promises
|
||||
await this.invokeHandlers('connected', this.isReconnect);
|
||||
this.reconnectDelay = 0;
|
||||
}
|
||||
}
|
||||
const time = new Date();
|
||||
const handlers = <Interfaces.CommandHandler<T>[] | undefined>this.messageHandlers[type];
|
||||
if(handlers !== undefined)
|
||||
for(const handler of handlers) handler(data, time);
|
||||
}
|
||||
|
||||
//tslint:enable
|
||||
|
|
|
@ -114,7 +114,7 @@ export namespace Connection {
|
|||
ZZZ: {message: string}
|
||||
};
|
||||
|
||||
export type CommandHandler<T extends keyof ServerCommands> = (data: ServerCommands[T], date: Date) => void;
|
||||
export type CommandHandler<T extends keyof ServerCommands> = (data: ServerCommands[T], date: Date) => Promise<void> | void;
|
||||
export type TicketProvider = () => Promise<string>;
|
||||
export type EventType = 'connecting' | 'connected' | 'closed';
|
||||
export type EventHandler = (isReconnect: boolean) => Promise<void> | void;
|
||||
|
@ -180,7 +180,7 @@ export namespace Character {
|
|||
export type Character = Character.Character;
|
||||
|
||||
export namespace Channel {
|
||||
export type EventHandler = (type: 'join' | 'leave', channel: Channel, member?: Member) => void;
|
||||
export type EventHandler = (type: 'join' | 'leave', channel: Channel, member?: Member) => Promise<void> | void;
|
||||
|
||||
export interface State {
|
||||
readonly officialChannels: {readonly [key: string]: (ListItem | undefined)};
|
||||
|
@ -230,7 +230,7 @@ export type Channel = Channel.Channel;
|
|||
|
||||
export interface WebSocketConnection {
|
||||
close(): void
|
||||
onMessage(handler: (message: string) => void): void
|
||||
onMessage(handler: (message: string) => Promise<void>): void
|
||||
onOpen(handler: () => void): void
|
||||
onClose(handler: () => void): void
|
||||
onError(handler: (error: Error) => void): void
|
||||
|
|
|
@ -13,3 +13,30 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
}
|
||||
.character-links-block {
|
||||
a {
|
||||
padding: 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -78,10 +79,12 @@
|
|||
}
|
||||
|
||||
.character-kinks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
> .col-xs-3 {
|
||||
> div {
|
||||
// Fix up padding on columns so they look distinct without being miles apart.
|
||||
padding: 0 5px 0 0;
|
||||
padding: 0 5px 5px 0;
|
||||
}
|
||||
.kinks-column {
|
||||
padding: 15px;
|
||||
|
@ -95,6 +98,7 @@
|
|||
}
|
||||
|
||||
.character-kink {
|
||||
position: relative;
|
||||
.subkink-list {
|
||||
.well();
|
||||
margin-bottom: 0;
|
||||
|
@ -143,6 +147,19 @@
|
|||
background-color: @well-bg;
|
||||
height: 100%;
|
||||
margin-top: -20px;
|
||||
.character-image-container {
|
||||
@media (max-width: @screen-xs-max) {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.profile-body {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Character Images
|
||||
|
@ -150,6 +167,7 @@
|
|||
.character-image {
|
||||
.col-xs-2();
|
||||
.img-thumbnail();
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
|
@ -213,4 +231,13 @@
|
|||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.date {
|
||||
margin-left: 10px;
|
||||
flex:1;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
@import "~bootstrap/less/variables.less";
|
||||
|
||||
.bg-solid-text {
|
||||
background: @text-background-color
|
||||
}
|
||||
|
@ -43,15 +41,38 @@
|
|||
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 {
|
||||
|
@ -61,7 +82,7 @@
|
|||
border-color: @btn-default-border;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
@media(min-width: @screen-sm-min) {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.name {
|
||||
display: none;
|
||||
}
|
||||
|
@ -75,10 +96,14 @@
|
|||
&.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;
|
||||
}
|
||||
}
|
||||
|
@ -86,16 +111,23 @@
|
|||
&.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;
|
||||
}
|
||||
|
@ -110,13 +142,22 @@
|
|||
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 {
|
||||
|
@ -133,12 +174,14 @@
|
|||
|
||||
.messages-both {
|
||||
.message-ad {
|
||||
background-color: @state-info-bg;
|
||||
background-color: @brand-info;
|
||||
padding: 0 2px 2px 2px;
|
||||
box-shadow: @gray -2px -2px 2px inset;
|
||||
}
|
||||
}
|
||||
|
||||
.message-event {
|
||||
color: @gray-light;
|
||||
color: @gray;
|
||||
}
|
||||
|
||||
.message-highlight {
|
||||
|
@ -198,4 +241,21 @@
|
|||
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -17,9 +17,19 @@ hr {
|
|||
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,15 +1,16 @@
|
|||
@import "~bootstrap/less/variables.less";
|
||||
// BBcode colors
|
||||
@red-color: #f00;
|
||||
@green-color: #0f0;
|
||||
@blue-color: #00f;
|
||||
@yellow-color: #ff0;
|
||||
@cyan-color: #0ff;
|
||||
@purple-color: #f0f;
|
||||
@purple-color: #c0f;
|
||||
@white-color: #fff;
|
||||
@black-color: #000;
|
||||
@brown-color: #8a6d3b;
|
||||
@pink-color: #faa;
|
||||
@gray-color: #cccc;
|
||||
@gray-color: #ccc;
|
||||
@orange-color: #f60;
|
||||
@collapse-header-bg: @well-bg;
|
||||
@collapse-border: darken(@well-border, 25%);
|
||||
|
@ -17,10 +18,10 @@
|
|||
|
||||
// Character page quick kink comparison
|
||||
@quick-compare-active-border: @black-color;
|
||||
@quick-compare-favorite-bg: @brand-success;
|
||||
@quick-compare-yes-bg: @brand-info;
|
||||
@quick-compare-maybe-bg: @brand-warning;
|
||||
@quick-compare-no-bg: @brand-danger;
|
||||
@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%);
|
||||
|
@ -44,4 +45,9 @@
|
|||
|
||||
// General color extensions missing from bootstrap
|
||||
@text-background-color: @body-bg;
|
||||
@text-background-color-disabled: @gray-lighter;
|
||||
@text-background-color-disabled: @gray-lighter;
|
||||
|
||||
@screen-sm-min: 700px;
|
||||
@screen-md-min: 900px;
|
||||
@container-sm: 680px;
|
||||
@container-md: 880px;
|
|
@ -4,10 +4,6 @@
|
|||
background-color: @gray-lighter;
|
||||
}
|
||||
|
||||
.whiteText {
|
||||
text-shadow: 1px 1px @gray;
|
||||
}
|
||||
|
||||
// Apply variables to theme.
|
||||
@import "../theme_base_chat.less";
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
// Components w/ JavaScript
|
||||
@import "~bootstrap/less/modals.less";
|
||||
//@import "tooltip.less";
|
||||
//@import "popovers.less";
|
||||
@import "~bootstrap/less/popovers.less";
|
||||
//@import "carousel.less";
|
||||
// Utility classes
|
||||
@import "~bootstrap/less/utilities.less";
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
@import "~bootstrap/less/button-groups.less";
|
||||
//@import "input-groups.less";
|
||||
@import "~bootstrap/less/navs.less";
|
||||
@import "~bootstrap/less/navbar.less";
|
||||
//@import "~bootstrap/less/navbar.less";
|
||||
//@import "breadcrumbs.less";
|
||||
//@import "~bootstrap/less/pagination.less";
|
||||
//@import "~bootstrap/less/pager.less";
|
||||
|
@ -36,14 +36,14 @@
|
|||
@import "~bootstrap/less/progress-bars.less";
|
||||
//@import "media.less";
|
||||
@import "~bootstrap/less/list-group.less";
|
||||
@import "~bootstrap/less/panels.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 "popovers.less";
|
||||
@import "~bootstrap/less/popovers.less";
|
||||
//@import "carousel.less";
|
||||
// Utility classes
|
||||
@import "~bootstrap/less/utilities.less";
|
||||
|
@ -55,3 +55,7 @@
|
|||
@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,13 +1,12 @@
|
|||
//Import variable defaults first.
|
||||
@import "~bootstrap/less/variables.less";
|
||||
@import "../../flist_variables.less";
|
||||
|
||||
@gray-base: #000000;
|
||||
@gray-darker: lighten(@gray-base, 4%);
|
||||
@gray-dark: lighten(@gray-base, 20%);
|
||||
@gray: lighten(@gray-base, 55%);
|
||||
@gray-light: lighten(@gray-base, 80%);
|
||||
@gray-lighter: lighten(@gray-base, 95%);
|
||||
@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;
|
||||
|
@ -17,7 +16,7 @@
|
|||
@brand-warning: #a50;
|
||||
@brand-danger: #800;
|
||||
@brand-success: #080;
|
||||
@brand-info: #13b;
|
||||
@brand-info: #228;
|
||||
@brand-primary: @brand-info;
|
||||
@blue-color: #36f;
|
||||
|
||||
|
@ -45,7 +44,7 @@
|
|||
@panel-default-heading-bg: @gray;
|
||||
@panel-default-border: @border-color;
|
||||
|
||||
@input-color: @gray-light;
|
||||
@input-color: @gray-lighter;
|
||||
@input-bg: @text-background-color;
|
||||
@input-bg-disabled: @text-background-color-disabled;
|
||||
@input-border: @border-color;
|
||||
|
@ -62,8 +61,8 @@
|
|||
@navbar-default-link-color: @link-color;
|
||||
@navbar-default-link-hover-color: @link-hover-color;
|
||||
|
||||
@nav-link-hover-bg: @gray-light;
|
||||
@nav-link-hover-color: @gray-dark;
|
||||
@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;
|
||||
|
@ -97,6 +96,10 @@
|
|||
@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%);
|
||||
|
@ -111,4 +114,7 @@
|
|||
@collapse-header-bg: desaturate(@well-bg, 20%);
|
||||
|
||||
@white-color: @text-color;
|
||||
@purple-color: @gray-light;
|
||||
|
||||
.blackText {
|
||||
text-shadow: @gray-lighter 1px 1px 1px;
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
//Import variable defaults first.
|
||||
@import "~bootstrap/less/variables.less";
|
||||
@import "../../flist_variables.less";
|
||||
|
||||
@gray-base: #080810;
|
||||
@gray-darker: lighten(@gray-base, 15%);
|
||||
@gray-dark: lighten(@gray-base, 25%);
|
||||
@gray: lighten(@gray-base, 55%);
|
||||
@gray-light: lighten(@gray-base, 73%);
|
||||
@gray: lighten(@gray-base, 60%);
|
||||
@gray-light: lighten(@gray-base, 75%);
|
||||
@gray-lighter: lighten(@gray-base, 95%);
|
||||
|
||||
// @body-bg: #262626;
|
||||
|
@ -46,7 +45,7 @@
|
|||
@panel-default-heading-bg: @gray;
|
||||
@panel-default-border: @border-color;
|
||||
|
||||
@input-color: @gray-light;
|
||||
@input-color: @gray-lighter;
|
||||
@input-bg: @text-background-color;
|
||||
@input-bg-disabled: @text-background-color-disabled;
|
||||
@input-border: @border-color;
|
||||
|
@ -63,8 +62,8 @@
|
|||
@navbar-default-link-color: @link-color;
|
||||
@navbar-default-link-hover-color: @link-hover-color;
|
||||
|
||||
@nav-link-hover-bg: @gray-light;
|
||||
@nav-link-hover-color: @gray-dark;
|
||||
@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;
|
||||
|
@ -98,6 +97,10 @@
|
|||
@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%);
|
||||
|
@ -112,4 +115,7 @@
|
|||
@collapse-header-bg: desaturate(@well-bg, 20%);
|
||||
|
||||
@white-color: @text-color;
|
||||
@purple-color: @gray-light;
|
||||
|
||||
.blackText {
|
||||
text-shadow: @gray-lighter 1px 1px 1px;
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
//Import variable defaults first.
|
||||
@import "~bootstrap/less/variables.less";
|
||||
@import "../../flist_variables.less";
|
||||
|
||||
// Update variables here.
|
||||
// @body-bg: #00ff00;
|
||||
@hr-border: @text-color;
|
||||
@body-bg: #fafafa;
|
||||
@body-bg: #fafafa;
|
||||
@brand-warning: #e09d3e;
|
||||
|
||||
.whiteText {
|
||||
text-shadow: @gray-darker 1px 1px 1px;
|
||||
}
|
196
less/yarn.lock
|
@ -2,13 +2,11 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
ajv@^5.1.0:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
|
||||
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"
|
||||
fast-deep-equal "^1.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
asap@~2.0.3:
|
||||
|
@ -23,15 +21,19 @@ 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.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
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.6.0:
|
||||
aws4@^1.2.1:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||
|
||||
|
@ -41,17 +43,11 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
boom@4.x.x:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
|
||||
boom@2.x.x:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||
dependencies:
|
||||
hoek "4.x.x"
|
||||
|
||||
boom@5.x.x:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
|
||||
dependencies:
|
||||
hoek "4.x.x"
|
||||
hoek "2.x.x"
|
||||
|
||||
bootstrap@^3.3.7:
|
||||
version "3.3.7"
|
||||
|
@ -75,11 +71,11 @@ 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@3.x.x:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
|
||||
cryptiles@2.x.x:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||
dependencies:
|
||||
boom "5.x.x"
|
||||
boom "2.x.x"
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
|
@ -98,22 +94,22 @@ ecc-jsbn@~0.1.1:
|
|||
jsbn "~0.1.0"
|
||||
|
||||
errno@^0.1.1:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
|
||||
dependencies:
|
||||
prr "~0.0.0"
|
||||
prr "~1.0.1"
|
||||
|
||||
extend@~3.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, extsprintf@^1.2.0:
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
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"
|
||||
|
@ -123,9 +119,9 @@ 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.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
|
||||
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"
|
||||
|
@ -141,35 +137,35 @@ 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@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
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@~5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
|
||||
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 "^5.1.0"
|
||||
har-schema "^2.0.0"
|
||||
ajv "^4.9.1"
|
||||
har-schema "^1.0.5"
|
||||
|
||||
hawk@~6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
|
||||
hawk@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
|
||||
dependencies:
|
||||
boom "4.x.x"
|
||||
cryptiles "3.x.x"
|
||||
hoek "4.x.x"
|
||||
sntp "2.x.x"
|
||||
boom "2.x.x"
|
||||
cryptiles "2.x.x"
|
||||
hoek "2.x.x"
|
||||
sntp "1.x.x"
|
||||
|
||||
hoek@4.x.x:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
|
||||
hoek@2.x.x:
|
||||
version "2.16.3"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
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 "^1.0.0"
|
||||
assert-plus "^0.2.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
|
@ -189,10 +185,6 @@ jsbn@~0.1.0:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
|
||||
json-schema-traverse@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
|
||||
|
||||
json-schema@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
|
@ -228,8 +220,8 @@ less-plugin-npm-import@^2.1.0:
|
|||
resolve "~1.1.6"
|
||||
|
||||
less@^2.7.2:
|
||||
version "2.7.2"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-2.7.2.tgz#368d6cc73e1fb03981183280918743c5dcf9b3df"
|
||||
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"
|
||||
|
@ -237,22 +229,22 @@ less@^2.7.2:
|
|||
mime "^1.2.11"
|
||||
mkdirp "^0.5.0"
|
||||
promise "^7.1.1"
|
||||
request "^2.72.0"
|
||||
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.17:
|
||||
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.4.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
|
@ -264,13 +256,13 @@ mkdirp@^0.5.0:
|
|||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
oauth-sign@~0.8.2:
|
||||
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@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
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"
|
||||
|
@ -284,58 +276,58 @@ promise@~7.0.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prr@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
|
||||
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.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
qs@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
|
||||
request@^2.72.0:
|
||||
version "2.83.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
|
||||
request@2.81.0:
|
||||
version "2.81.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.6.0"
|
||||
aws-sign2 "~0.6.0"
|
||||
aws4 "^1.2.1"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.1"
|
||||
extend "~3.0.0"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.1"
|
||||
har-validator "~5.0.3"
|
||||
hawk "~6.0.2"
|
||||
http-signature "~1.2.0"
|
||||
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.17"
|
||||
oauth-sign "~0.8.2"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.1"
|
||||
safe-buffer "^5.1.1"
|
||||
stringstream "~0.0.5"
|
||||
tough-cookie "~2.3.3"
|
||||
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.1.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, safe-buffer@^5.1.1:
|
||||
safe-buffer@^5.0.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
sntp@2.x.x:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
|
||||
sntp@1.x.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
|
||||
dependencies:
|
||||
hoek "4.x.x"
|
||||
hoek "2.x.x"
|
||||
|
||||
source-map@^0.5.3:
|
||||
version "0.5.7"
|
||||
|
@ -355,11 +347,11 @@ sshpk@^1.7.0:
|
|||
jsbn "~0.1.0"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
stringstream@~0.0.5:
|
||||
stringstream@~0.0.4:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||
|
||||
tough-cookie@~2.3.3:
|
||||
tough-cookie@~2.3.0:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
|
||||
dependencies:
|
||||
|
@ -375,7 +367,7 @@ 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.1.0:
|
||||
uuid@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<div class="form-group">
|
||||
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group text-right">
|
||||
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
||||
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
||||
</button>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
<chat v-else :ownCharacters="characters" :defaultCharacter="defaultCharacter" ref="chat"></chat>
|
||||
<modal action="Profile" :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||
<character-page :authenticated="false" :hideGroups="true" :name="profileName"></character-page>
|
||||
<character-page :authenticated="false" :oldApi="true" :name="profileName"></character-page>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -64,12 +64,18 @@
|
|||
import {GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
||||
import Notifications from './notifications';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
NativeView: {
|
||||
setTheme(theme: string): void
|
||||
} | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBack(): void {
|
||||
if(confirm(l('chat.confirmLeave'))) (<Navigator & {app: {exitApp(): void}}>navigator).app.exitApp();
|
||||
}
|
||||
|
||||
profileApiInit();
|
||||
|
||||
@Component({
|
||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||
})
|
||||
|
@ -105,7 +111,7 @@
|
|||
}
|
||||
|
||||
get styling(): string {
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
if(window.NativeView !== undefined) window.NativeView.setTheme(this.settings!.theme);
|
||||
return `<style>${require(`../less/themes/chat/${this.settings!.theme}.less`)}</style>`;
|
||||
}
|
||||
|
||||
|
@ -113,18 +119,20 @@
|
|||
if(this.loggingIn) return;
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
const data = <{ticket?: string, error: string, characters: string[], default_character: string}>
|
||||
(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify(
|
||||
{account: this.settings!.account, password: this.settings!.password, no_friends: true, no_bookmarks: true})
|
||||
)).data;
|
||||
const data = <{ticket?: string, error: string, characters: {[key: string]: number}, default_character: number}>
|
||||
(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify({
|
||||
account: this.settings!.account, password: this.settings!.password, no_friends: true, no_bookmarks: true,
|
||||
new_character_list: true
|
||||
}))).data;
|
||||
if(data.error !== '') {
|
||||
this.error = data.error;
|
||||
return;
|
||||
}
|
||||
if(this.saveLogin)
|
||||
await setGeneralSettings(this.settings!);
|
||||
if(this.saveLogin) await setGeneralSettings(this.settings!);
|
||||
Socket.host = this.settings!.host;
|
||||
const connection = new Connection(Socket, this.settings!.account, this.settings!.password);
|
||||
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
||||
const connection = new Connection(`F-Chat 3.0 (Mobile)`, version, Socket,
|
||||
this.settings!.account, this.settings!.password);
|
||||
connection.onEvent('connected', () => {
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
document.addEventListener('backbutton', confirmBack);
|
||||
|
@ -134,8 +142,14 @@
|
|||
document.removeEventListener('backbutton', confirmBack);
|
||||
});
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
this.characters = data.characters.sort();
|
||||
this.defaultCharacter = data.default_character;
|
||||
const charNames = Object.keys(data.characters);
|
||||
this.characters = charNames.sort();
|
||||
for(const character of charNames)
|
||||
if(data.characters[character] === data.default_character) {
|
||||
this.defaultCharacter = character;
|
||||
break;
|
||||
}
|
||||
profileApiInit(data.characters);
|
||||
} catch(e) {
|
||||
this.error = l('login.error');
|
||||
if(process.env.NODE_ENV !== 'production') throw e;
|
||||
|
@ -150,4 +164,4 @@
|
|||
html, body, #page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
*.iml
|
||||
*.apk
|
||||
.gradle
|
||||
/local.properties
|
||||
.idea/*
|
||||
!.idea/modules.xml
|
||||
!.idea/misc.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/android.iml" filepath="$PROJECT_DIR$/.idea/android.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,27 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "27.0.3"
|
||||
defaultConfig {
|
||||
applicationId "net.f_list.fchat"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 4
|
||||
versionName "0.1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Android\android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="net.f_list.fchat"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Holo.NoActionBar">
|
||||
<activity android:name=".MainActivity" android:launchMode="singleInstance"
|
||||
android:configChanges="orientation|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1 @@
|
|||
../../../../../www
|
|
@ -0,0 +1,54 @@
|
|||
package net.f_list.fchat
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.JavascriptInterface
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class File(private val ctx: Context) {
|
||||
@JavascriptInterface
|
||||
fun readFile(name: String, s: Long, l: Int): String? {
|
||||
val file = File(ctx.filesDir, name)
|
||||
if(!file.exists()) return null
|
||||
FileInputStream(file).use { fs ->
|
||||
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
|
||||
fun getSize(name: String) = File(ctx.filesDir, name).length()
|
||||
|
||||
@JavascriptInterface
|
||||
fun writeFile(name: String, data: String) {
|
||||
FileOutputStream(File(ctx.filesDir, name)).use { it.write(data.toByteArray()) }
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun append(name: String, data: String) {
|
||||
FileOutputStream(File(ctx.filesDir, name), true).use { it.write(data.toByteArray()) }
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun listFiles(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isFile }.map { it.name }).toString()
|
||||
|
||||
@JavascriptInterface
|
||||
fun listDirectories(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isDirectory }.map { it.name }).toString()
|
||||
|
||||
@JavascriptInterface
|
||||
fun ensureDirectory(name: String) {
|
||||
File(ctx.filesDir, name).mkdirs()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package net.f_list.fchat
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
|
||||
class MainActivity : Activity() {
|
||||
private lateinit var webView: WebView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
webView = findViewById(R.id.webview)
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.settings.mediaPlaybackRequiresUserGesture = false
|
||||
webView.loadUrl("file:///android_asset/www/index.html")
|
||||
webView.addJavascriptInterface(File(this), "NativeFile")
|
||||
webView.addJavascriptInterface(Notifications(this), "NativeNotification")
|
||||
webView.webChromeClient = WebChromeClient()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if(intent.action == "notification") {
|
||||
val data = intent.extras.getString("data")
|
||||
webView.evaluateJavascript("document.dispatchEvent(new CustomEvent('notification-clicked',{detail:{data:'$data'}}))", {}) //TODO
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package net.f_list.fchat
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Vibrator
|
||||
import android.webkit.JavascriptInterface
|
||||
import java.net.URL
|
||||
|
||||
class Notifications(private val ctx: Context) {
|
||||
@JavascriptInterface
|
||||
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) {
|
||||
(ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(400)
|
||||
return 0
|
||||
}
|
||||
if(soundUri != null) {
|
||||
val player = MediaPlayer()
|
||||
val asset = ctx.assets.openFd("www/sounds/$sound.mp3")
|
||||
player.setDataSource(asset.fileDescriptor, asset.startOffset, asset.length)
|
||||
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION)
|
||||
player.prepare()
|
||||
player.start()
|
||||
}
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.action = "notification"
|
||||
intent.putExtra("data", data)
|
||||
val notification = Notification.Builder(ctx).setContentTitle(title).setContentText(text).setSmallIcon(R.drawable.ic_notification).setDefaults(Notification.DEFAULT_VIBRATE)
|
||||
.setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setAutoCancel(true)
|
||||
object : AsyncTask<String, Void, Bitmap>() {
|
||||
override fun doInBackground(vararg args: String): Bitmap {
|
||||
val connection = URL(args[0]).openConnection()
|
||||
return BitmapFactory.decodeStream(connection.getInputStream())
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Bitmap?) {
|
||||
notification.setLargeIcon(result)
|
||||
(ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(1, notification.build())
|
||||
}
|
||||
}.execute(icon)
|
||||
return 1
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun requestPermission() {
|
||||
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 705 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 989 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="net.f_list.fchat.MainActivity">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">F-Chat</string>
|
||||
</resources>
|
|
@ -0,0 +1,25 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.10'
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
|
@ -0,0 +1,6 @@
|
|||
#Mon Dec 28 10:00:20 PST 2015
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
|
@ -0,0 +1,90 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
|
@ -0,0 +1 @@
|
|||
include ':app'
|