0.2.9 - Hide ads, image viewer, bugfixes
This commit is contained in:
parent
ebf7cb43c5
commit
b1a63ab6fb
|
@ -1,7 +1,7 @@
|
|||
import {BBCodeCustomTag, BBCodeParser, BBCodeSimpleTag} from './parser';
|
||||
|
||||
const urlFormat = '((?:(?:https?|ftps?|irc):)?\\/\\/[^\\s\\/$.?#"\']+\\.[^\\s"]*)';
|
||||
export const findUrlRegex = new RegExp(`((?!\\[url(?:\\]|=))(?:.{4}[^\\s])\\s+|^.{0,4}\\s|^)${urlFormat}`, 'g');
|
||||
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
|
||||
export const urlRegex = new RegExp(`^${urlFormat}$`);
|
||||
|
||||
function domain(url: string): string | undefined {
|
||||
|
@ -83,7 +83,8 @@ export class CoreBBCodeParser extends BBCodeParser {
|
|||
}
|
||||
|
||||
parseEverything(input: string): HTMLElement {
|
||||
if(this.makeLinksClickable && input.length > 0) input = input.replace(findUrlRegex, '$1[url]$2[/url]');
|
||||
if(this.makeLinksClickable && input.length > 0)
|
||||
input = input.replace(findUrlRegex, (match, tag) => tag === undefined ? `[url]${match}[/url]` : match);
|
||||
return super.parseEverything(input);
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ export let defaultButtons: ReadonlyArray<EditorButton> = [
|
|||
key: 's'
|
||||
},
|
||||
{
|
||||
title: 'Color (Ctrl+Q)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
|
||||
title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
|
||||
tag: 'color',
|
||||
startText: '[color=]',
|
||||
icon: 'fa-eyedropper',
|
||||
|
|
|
@ -112,7 +112,8 @@
|
|||
this.error = l('characterSearch.error.tooManyResults');
|
||||
}
|
||||
});
|
||||
core.connection.onMessage('FKS', (data) => this.results = data.characters.map((x: string) => 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -242,7 +242,8 @@
|
|||
}
|
||||
} 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') {
|
||||
if(e.shiftKey) return;
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
<li><a tabindex="-1" href="#" @click.prevent="setIgnored">
|
||||
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}
|
||||
</a></li>
|
||||
<li><a tabindex="-1" href="#" @click.prevent="setHidden">
|
||||
<span class="fa fa-fw fa-eye-slash"></span>{{l('user.' + (isHidden ? 'unhide' : 'hide'))}}
|
||||
</a></li>
|
||||
<li><a tabindex="-1" href="#" @click.prevent="report">
|
||||
<span class="fa fa-fw fa-exclamation-triangle"></span>{{l('user.report')}}</a></li>
|
||||
<li v-show="isChannelMod"><a tabindex="-1" href="#" @click.prevent="channelKick">
|
||||
|
@ -89,6 +92,12 @@
|
|||
.catch((e: object) => alert(errorToString(e)));
|
||||
}
|
||||
|
||||
setHidden(): void {
|
||||
const index = core.state.hiddenUsers.indexOf(this.character!.name);
|
||||
if(index !== -1) core.state.hiddenUsers.splice(index, 1);
|
||||
else core.state.hiddenUsers.push(this.character!.name);
|
||||
}
|
||||
|
||||
report(): void {
|
||||
this.reportDialog.report(this.character!);
|
||||
}
|
||||
|
@ -128,6 +137,10 @@
|
|||
return member !== undefined && member.rank > Channel.Rank.Member;
|
||||
}
|
||||
|
||||
get isHidden(): boolean {
|
||||
return core.state.hiddenUsers.indexOf(this.character!.name) !== -1;
|
||||
}
|
||||
|
||||
get isChatOp(): boolean {
|
||||
return core.characters.ownCharacter.isChatOp;
|
||||
}
|
||||
|
|
|
@ -505,7 +505,7 @@ export default function(this: void): Interfaces.State {
|
|||
});
|
||||
connection.onMessage('LRP', (data, time) => {
|
||||
const char = core.characters.get(data.character);
|
||||
if(char.isIgnored) return;
|
||||
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));
|
||||
|
|
|
@ -12,6 +12,7 @@ function createBBCodeParser(): BBCodeParser {
|
|||
|
||||
class State implements StateInterface {
|
||||
_settings: Settings | undefined = undefined;
|
||||
hiddenUsers: string[] = [];
|
||||
|
||||
get settings(): Settings {
|
||||
if(this._settings === undefined) throw new Error('Settings load failed.');
|
||||
|
@ -41,6 +42,12 @@ const vue = <Vue & VueState>new Vue({
|
|||
characters: undefined,
|
||||
conversations: undefined,
|
||||
state
|
||||
},
|
||||
watch: {
|
||||
'state.hiddenUsers': (newValue: string[]) => {
|
||||
//tslint:disable-next-line:no-floating-promises
|
||||
if(data.settingsStore !== undefined) data.settingsStore.set('hiddenUsers', newValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -68,6 +75,8 @@ const data = {
|
|||
if(loadedSettings !== undefined)
|
||||
for(const key in loadedSettings) settings[<keyof Settings>key] = loadedSettings[<keyof Settings>key];
|
||||
state._settings = settings;
|
||||
const hiddenUsers = await core.settingsStore.get('hiddenUsers');
|
||||
state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : [];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -142,6 +142,7 @@ export namespace Settings {
|
|||
pinned: {channels: string[], private: string[]},
|
||||
conversationSettings: {[key: string]: Conversation.Settings}
|
||||
recent: Conversation.RecentConversation[]
|
||||
hiddenUsers: string[]
|
||||
};
|
||||
|
||||
export interface Store {
|
||||
|
@ -180,4 +181,5 @@ export interface Notifications {
|
|||
|
||||
export interface State {
|
||||
settings: Settings
|
||||
hiddenUsers: string[]
|
||||
}
|
|
@ -74,6 +74,8 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'user.unbookmark': 'Unbookmark',
|
||||
'user.ignore': 'Ignore',
|
||||
'user.unignore': 'Unignore',
|
||||
'user.hide': 'Hide ads',
|
||||
'user.unhide': 'Unhide ads',
|
||||
'user.memo': 'View memo',
|
||||
'user.memo.action': 'Update memo',
|
||||
'user.report': 'Report user',
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<div tabindex="-1" class="modal flex-modal" :style="isShown ? 'display:flex' : ''"
|
||||
style="align-items: flex-start; padding: 30px; justify-content: center;">
|
||||
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">{{action}}</h4>
|
||||
<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>
|
||||
|
@ -28,7 +30,7 @@
|
|||
|
||||
@Component
|
||||
export default class Modal extends Vue {
|
||||
@Prop({required: true})
|
||||
@Prop({default: ''})
|
||||
readonly action: string;
|
||||
@Prop()
|
||||
readonly dialogClass?: {string: boolean};
|
||||
|
|
|
@ -40,8 +40,9 @@
|
|||
<div class="progress-bar" :style="{width: importProgress * 100 + '%'}"></div>
|
||||
</div>
|
||||
</modal>
|
||||
<modal action="Profile" :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||
<character-page :authenticated="false" :hideGroups="true" :name="profileName"></character-page>
|
||||
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
|
||||
<character-page :authenticated="false" :hideGroups="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>
|
||||
</template>
|
||||
|
@ -355,6 +356,10 @@
|
|||
preview.style.display = 'none';
|
||||
}
|
||||
|
||||
openProfileInBrowser(): void {
|
||||
electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
|
||||
}
|
||||
|
||||
get styling(): string {
|
||||
try {
|
||||
return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.currentSettings.theme}.css`))}</style>`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -115,8 +115,10 @@ function createWindow(): void {
|
|||
if(process.env.NODE_ENV === 'production') runUpdater();
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
app.makeSingleInstance(() => {
|
||||
const running = app.makeSingleInstance(() => {
|
||||
if(windows.length < 3) createWindow();
|
||||
return true;
|
||||
});
|
||||
if(running) app.quit();
|
||||
else app.on('ready', createWindow);
|
||||
app.on('window-all-closed', () => app.quit());
|
|
@ -1,12 +1,13 @@
|
|||
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://github.com/wooorm/dictionaries/raw/master/dictionaries/';
|
||||
const dir = `${__dirname}/spellchecker`;
|
||||
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<{
|
||||
|
@ -18,30 +19,35 @@ const sc = nativeRequire<{
|
|||
}
|
||||
}
|
||||
}>('spellchecker/build/Release/spellchecker.node');
|
||||
let availableDictionaries: string[] | undefined;
|
||||
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) return availableDictionaries;
|
||||
const dicts = (<{name: string}[]>(await Axios.get('https://api.github.com/repos/wooorm/dictionaries/contents/dictionaries')).data)
|
||||
.map((x: {name: string}) => x.name);
|
||||
availableDictionaries = dicts;
|
||||
return dicts;
|
||||
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 dictName = lang !== undefined ? lang.replace('-', '_') : undefined;
|
||||
if(dictName !== undefined) {
|
||||
const dicPath = path.join(dir, `${dictName}.dic`);
|
||||
if(!fs.existsSync(dicPath)) {
|
||||
await writeFile(dicPath, new Buffer(<string>(await Axios.get(`${downloadUrl}${lang}/index.dic`, requestConfig)).data));
|
||||
await writeFile(path.join(dir, `${dictName}.aff`),
|
||||
new Buffer(<string>(await Axios.get(`${downloadUrl}${lang}/index.aff`, requestConfig)).data));
|
||||
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(dictName, dir);
|
||||
spellchecker.setDictionary(lang, dir);
|
||||
}
|
||||
|
||||
export function getCorrections(word: string): ReadonlyArray<string> {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import {decodeHTML} from './common';
|
||||
import {Channel as Interfaces, Character, Connection} from './interfaces';
|
||||
|
||||
interface SortableMember extends Interfaces.Member {
|
||||
rank: Interfaces.Rank,
|
||||
key: string
|
||||
}
|
||||
|
||||
export function queuedJoin(this: void, channels: string[]): void {
|
||||
const timer: NodeJS.Timer = setInterval(() => {
|
||||
const channel = channels.shift();
|
||||
|
@ -9,8 +14,7 @@ export function queuedJoin(this: void, channels: string[]): void {
|
|||
}, 100);
|
||||
}
|
||||
|
||||
function sortMember(this: void | never, array: Interfaces.Member[], member: Interfaces.Member): void {
|
||||
const name = member.character.name;
|
||||
function sortMember(this: void | never, array: SortableMember[], member: SortableMember): void {
|
||||
let i = 0;
|
||||
for(; i < array.length; ++i) {
|
||||
const other = array[i];
|
||||
|
@ -22,7 +26,7 @@ function sortMember(this: void | never, array: Interfaces.Member[], member: Inte
|
|||
if(member.character.isFriend && !other.character.isFriend) break;
|
||||
if(other.character.isBookmarked && !member.character.isBookmarked) continue;
|
||||
if(member.character.isBookmarked && !other.character.isBookmarked) break;
|
||||
if(name < other.character.name) break;
|
||||
if(member.key < other.key) break;
|
||||
}
|
||||
array.splice(i, 0, member);
|
||||
}
|
||||
|
@ -32,13 +36,13 @@ class Channel implements Interfaces.Channel {
|
|||
opList: string[];
|
||||
owner = '';
|
||||
mode: Interfaces.Mode = 'both';
|
||||
members: {[key: string]: {character: Character, rank: Interfaces.Rank} | undefined} = {};
|
||||
sortedMembers: Interfaces.Member[] = [];
|
||||
members: {[key: string]: SortableMember | undefined} = {};
|
||||
sortedMembers: SortableMember[] = [];
|
||||
|
||||
constructor(readonly id: string, readonly name: string) {
|
||||
}
|
||||
|
||||
addMember(member: Interfaces.Member): void {
|
||||
addMember(member: SortableMember): void {
|
||||
this.members[member.character.name] = member;
|
||||
sortMember(this.sortedMembers, member);
|
||||
for(const handler of state.handlers) handler('join', this, member);
|
||||
|
@ -53,16 +57,17 @@ class Channel implements Interfaces.Channel {
|
|||
}
|
||||
}
|
||||
|
||||
reSortMember(member: Interfaces.Member): void {
|
||||
reSortMember(member: SortableMember): void {
|
||||
this.sortedMembers.splice(this.sortedMembers.indexOf(member), 1);
|
||||
sortMember(this.sortedMembers, member);
|
||||
}
|
||||
|
||||
createMember(character: Character): {character: Character, rank: Interfaces.Rank} {
|
||||
createMember(character: Character): SortableMember {
|
||||
return {
|
||||
character,
|
||||
rank: this.owner === character.name ? Interfaces.Rank.Owner :
|
||||
this.opList.indexOf(character.name) !== -1 ? Interfaces.Rank.Op : Interfaces.Rank.Member
|
||||
this.opList.indexOf(character.name) !== -1 ? Interfaces.Rank.Op : Interfaces.Rank.Member,
|
||||
key: character.name.toLowerCase()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -173,8 +178,8 @@ export default function(this: void, connection: Connection, characters: Characte
|
|||
const channel = state.getChannel(data.channel);
|
||||
if(channel === undefined) return state.leave(data.channel);
|
||||
channel.mode = data.mode;
|
||||
const members: {[key: string]: Interfaces.Member} = {};
|
||||
const sorted: Interfaces.Member[] = [];
|
||||
const members: {[key: string]: SortableMember} = {};
|
||||
const sorted: SortableMember[] = [];
|
||||
for(const user of data.users) {
|
||||
const name = user.identity;
|
||||
const member = channel.createMember(characters.get(name));
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
color: @white-color;
|
||||
}
|
||||
|
||||
.blackColor {
|
||||
.blackText {
|
||||
color: @black-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.badges-block,.contact-block,.quick-info-block,.character-list-block {
|
||||
.badges-block, .contact-block, .quick-info-block, .character-list-block {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
|||
background-color: @character-badge-bg;
|
||||
border: 1px solid @character-badge-border;
|
||||
border-radius: @border-radius-base;
|
||||
.box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
|
||||
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .05));
|
||||
|
||||
&.character-badge-subscription-lifetime {
|
||||
background-color: @character-badge-subscriber-bg;
|
||||
|
@ -196,3 +196,21 @@
|
|||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
padding: 5px;
|
||||
background: white;
|
||||
z-index: 1100;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
@gray-darker: lighten(@gray-base, 4%);
|
||||
@gray-dark: lighten(@gray-base, 20%);
|
||||
@gray: lighten(@gray-base, 55%);
|
||||
@gray-light: lighten(@gray-base, 85%);
|
||||
@gray-light: lighten(@gray-base, 80%);
|
||||
@gray-lighter: lighten(@gray-base, 95%);
|
||||
|
||||
@body-bg: @gray-darker;
|
||||
|
@ -19,6 +19,7 @@
|
|||
@brand-success: #080;
|
||||
@brand-info: #13b;
|
||||
@brand-primary: @brand-info;
|
||||
@blue-color: #36f;
|
||||
|
||||
@state-info-bg: darken(@brand-info, 15%);
|
||||
@state-info-text: lighten(@brand-info, 30%);
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
@gray-darker: lighten(@gray-base, 15%);
|
||||
@gray-dark: lighten(@gray-base, 25%);
|
||||
@gray: lighten(@gray-base, 55%);
|
||||
@gray-light: lighten(@gray-base, 76.7%);
|
||||
@gray-lighter: lighten(@gray-base, 93.5%);
|
||||
@gray-light: lighten(@gray-base, 73%);
|
||||
@gray-lighter: lighten(@gray-base, 95%);
|
||||
|
||||
// @body-bg: #262626;
|
||||
@body-bg: darken(@text-background-color-disabled, 3%);
|
||||
|
@ -20,6 +20,7 @@
|
|||
@brand-success: #009900;
|
||||
@brand-info: #0447af;
|
||||
@brand-primary: @brand-info;
|
||||
@blue-color: #36f;
|
||||
|
||||
@state-info-bg: darken(@brand-info, 15%);
|
||||
@state-info-text: lighten(@brand-info, 30%);
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
<li role="presentation" class="active"><a href="#overview" aria-controls="overview" role="tab" data-toggle="tab">Overview</a>
|
||||
</li>
|
||||
<li role="presentation"><a href="#infotags" aria-controls="infotags" role="tab" data-toggle="tab">Info</a></li>
|
||||
<li role="presentation" v-if="!hideGroups"><a href="#groups" aria-controls="groups" role="tab" data-toggle="tab">Groups</a></li>
|
||||
<li role="presentation" v-if="!hideGroups"><a href="#groups" aria-controls="groups" role="tab" data-toggle="tab">Groups</a>
|
||||
</li>
|
||||
<li role="presentation"><a href="#images" aria-controls="images" role="tab"
|
||||
data-toggle="tab">Images ({{ character.character.image_count }})</a></li>
|
||||
<li v-if="character.settings.guestbook" role="presentation"><a href="#guestbook" aria-controls="guestbook"
|
||||
|
@ -45,7 +46,7 @@
|
|||
<character-groups :character="character" ref="groups"></character-groups>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="images" aria-labeledby="images-tab">
|
||||
<character-images :character="character" ref="images"></character-images>
|
||||
<character-images :character="character" ref="images" :use-preview="imagePreview"></character-images>
|
||||
</div>
|
||||
<div v-if="character.settings.guestbook" role="tabpanel" class="tab-pane" id="guestbook"
|
||||
aria-labeledby="guestbook-tab">
|
||||
|
@ -106,6 +107,8 @@
|
|||
private readonly authenticated: boolean;
|
||||
@Prop()
|
||||
readonly hideGroups?: true;
|
||||
@Prop()
|
||||
readonly imagePreview?: true;
|
||||
private shared: SharedStore = Store;
|
||||
private character: Character | null = null;
|
||||
loading = true;
|
||||
|
|
|
@ -3,12 +3,16 @@
|
|||
<div v-show="loading" class="alert alert-info">Loading images.</div>
|
||||
<template v-if="!loading">
|
||||
<div class="character-image" v-for="image in images" :key="image.id">
|
||||
<a :href="imageUrl(image)" target="_blank">
|
||||
<a :href="imageUrl(image)" target="_blank" @click="handleImageClick($event, image)">
|
||||
<img :src="thumbUrl(image)" :title="image.description">
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!loading && !images.length" class="alert alert-info">No images.</div>
|
||||
<div class="image-preview" v-show="previewImage" @click="previewImage = ''">
|
||||
<img :src="previewImage" />
|
||||
<div class="modal-backdrop in"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -24,7 +28,10 @@
|
|||
export default class ImagesView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly character: Character;
|
||||
@Prop()
|
||||
private readonly usePreview?: boolean;
|
||||
private shown = false;
|
||||
previewImage = '';
|
||||
images: CharacterImage[] = [];
|
||||
loading = true;
|
||||
error = '';
|
||||
|
@ -47,5 +54,12 @@
|
|||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
handleImageClick(e: MouseEvent, image: CharacterImage): void {
|
||||
if(this.usePreview) {
|
||||
this.previewImage = methods.imageUrl(image);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue