This commit is contained in:
Mr. Stallion 2024-01-26 19:45:59 -08:00
parent e5243efef4
commit 3bd1a84350
19 changed files with 218 additions and 19 deletions

View File

@ -1,15 +1,34 @@
# Changelog # Changelog
## Canary
* High-quality portraits
* Add the following anywhere in your character's profile to enable high-quality portrait:
* `[url=https://some.domain.ext/path/to/image.png]Rising Portrait[/url]`
* Replace `https://some.domain.ext/path/to/image.png` with the URL to your portrait.
* The URL must point directly to an image resource, such as PNG, GIF, or JPG (think of it as `<img src="YOUR URL" />`).
* Yes, animations are supported! (GIF, APNG, AVIF, WebP)
* The image must be hosted on one of the following services:
* `f-list.net` (profile images and inline images are supported)
* [e621.net](https://e621.net)
* [imgur.com](https://imgur.com)
* [freeimage.host](https://freeimage.host)
* [redgifs.com](https://redgifs.com)
* High-quality portraits are only visible to other F-Chat Rising users; users on other clients will see your regular portrait.
* If your image is not a square, [you're gonna have a bad time](https://www.youtube.com/watch?v=6Ls5j5iz2eA).
* [YiffBot 4000](https://www.f-list.net/c/YiffBot%204000) integration
* Fix "select/unselect all" behavior in Post Ads (credit: [@FatCatClient](https://github.com/FatCatClient))
* Extended emoji support (credit: [@FatCatClient](https://github.com/FatCatClient))
## 1.25.1 ## 1.25.1
* Shift-clicking in eicon selector adds the icon without closing the selector * Shift-clicking in eicon selector adds the icon without closing the selector
* Minor updates to browser switching * Minor updates to browser switching
## 1.25.0 ## 1.25.0
* Added option for switching browsers (Credit: [@greyhoof](https://github.com/greyhoof)) * Added option for switching browsers (credit: [@greyhoof](https://github.com/greyhoof))
* Fixed broken adblocker * Fixed broken adblocker
* Fixed incorrect BBCode rendering of `[collapse=[hr]test[hr]]` (Credit: [@Abeehiltz](https://github.com/Abeehiltz)) * Fixed incorrect BBCode rendering of `[collapse=[hr]test[hr]]` (credit: [@Abeehiltz](https://github.com/Abeehiltz))
* Fixed TikTok previews * Fixed TikTok previews
* Switched `node-sass` to `sass` for ARM64 compatibility (Credit: [@WhiteHusky](https://github.com/WhiteHusky)) * Switched `node-sass` to `sass` for ARM64 compatibility (credit: [@WhiteHusky](https://github.com/WhiteHusky))
## 1.24.2 ## 1.24.2
* Hotfix to address connectivity issues * Hotfix to address connectivity issues

View File

@ -12,3 +12,8 @@ This project contains contributions from:
* [Abeehiltz](https://github.com/Abeehiltz) * [Abeehiltz](https://github.com/Abeehiltz)
* [greyhoof](https://github.com/greyhoof) * [greyhoof](https://github.com/greyhoof)
* [F-List Team](https://github.com/f-list) (original F-Chat 3.0 client) * [F-List Team](https://github.com/f-list) (original F-Chat 3.0 client)
## Acknowledgements
Some emojis designed by [OpenMoji](https://openmoji.org/) the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/#)

View File

@ -26,6 +26,17 @@ When the 'Link Preview' feature is used, F-Chat Rising will connect to the URL b
* Twitter previews are proxied through `api.fxtwitter.com` * Twitter previews are proxied through `api.fxtwitter.com`
* YouTube previews are proxied through `yewtu.be` * YouTube previews are proxied through `yewtu.be`
## High-Quality Portraits
When 'High-Quality Portraits' feature is used, F-Chat Rising may connect to the following additional domains:
* iili.io
* e621.net
* imgur.com
* freeimage.host
* redgifs.com
If you are concerned about your security or privacy, consider disabling the high quality portraits feature in F-Chat Rising settings.
## Locally Stored Data ## Locally Stored Data
F-Chat Rising stores data on your computer. This data contains conversation logs, settings, cache, and other F-Chat Rising stores data on your computer. This data contains conversation logs, settings, cache, and other
information such as custom dictionary words. By default, the data is stored in: information such as custom dictionary words. By default, the data is stored in:

View File

@ -8,7 +8,7 @@
@click.middle.prevent.stop="toggleStickyness()" @click.middle.prevent.stop="toggleStickyness()"
@click.right.passive="dismiss(true)" @click.right.passive="dismiss(true)"
@click.left.passive="dismiss(true)" @click.left.passive="dismiss(true)"
><img :src="`${Utils.staticDomain}images/avatar/${character.toLowerCase()}.png`" class="character-avatar icon" :title="character" :alt="character" v-once></a> ><img :src="getAvatarUrl()" class="character-avatar icon" :title="character" :alt="character" v-once></a>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -16,6 +16,7 @@ import {Component, Hook, Prop} from '@f-list/vue-ts';
import Vue from 'vue'; import Vue from 'vue';
import { EventBus } from '../chat/preview/event-bus'; import { EventBus } from '../chat/preview/event-bus';
import * as Utils from '../site/utils'; import * as Utils from '../site/utils';
import { characterImage } from '../chat/common';
@Component @Component
export default class IconView extends Vue { export default class IconView extends Vue {
@ -45,6 +46,10 @@ export default class IconView extends Vue {
return `flist-character://${this.character}`; return `flist-character://${this.character}`;
} }
getAvatarUrl(): string {
return characterImage(this.character);
}
dismiss(force: boolean = false): void { dismiss(force: boolean = false): void {
// if (!this.preview) { // if (!this.preview) {

View File

@ -2,7 +2,7 @@
<div style="height:100%; display: flex; position: relative;" id="chatView" @click="userMenuHandle" @contextmenu="userMenuHandle" @touchstart.passive="userMenuHandle" <div style="height:100%; display: flex; position: relative;" id="chatView" @click="userMenuHandle" @contextmenu="userMenuHandle" @touchstart.passive="userMenuHandle"
@touchend="userMenuHandle"> @touchend="userMenuHandle">
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars"> <sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;margin-top:5px;width:70px"/> <img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;margin-top:5px;width:70px; height: 70px;"/>
<a target="_blank" :href="ownCharacterLink" class="btn" style="display:block">{{ownCharacter.name}}</a> <a target="_blank" :href="ownCharacterLink" class="btn" style="display:block">{{ownCharacter.name}}</a>
<a href="#" @click.prevent="logOut()" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/> <a href="#" @click.prevent="logOut()" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
<div> <div>
@ -543,6 +543,7 @@ import { Component, Hook, Watch } from '@f-list/vue-ts';
} }
img { img {
height: 40px; height: 40px;
width: 40px;
margin: -1px 5px -1px -1px; margin: -1px 5px -1px -1px;
} }
&:first-child img { &:first-child img {

View File

@ -95,7 +95,8 @@ export function getStatusClasses(
smartFilterIcon = 'user-filter fas fa-filter'; smartFilterIcon = 'user-filter fas fa-filter';
} }
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none'; const baseGender = character.overrides.gender || character.gender;
const gender = baseGender !== undefined ? baseGender.toLowerCase() : 'none';
const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) && const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
((character.isFriend) || (character.isBookmarked)); ((character.isFriend) || (character.isBookmarked));
@ -208,6 +209,11 @@ export default class UserView extends Vue {
this.update(); this.update();
} }
@Watch('character.overrides.avatarUrl')
onAvatarUrlUpdate(): void {
this.update();
}
update(): void { update(): void {
// console.log('user.view.update', this.character.name); // console.log('user.view.update', this.character.name);
@ -219,7 +225,7 @@ export default class UserView extends Vue {
this.matchClass = res.matchClass; this.matchClass = res.matchClass;
this.matchScore = res.matchScore; this.matchScore = res.matchScore;
this.userClass = res.userClass; this.userClass = res.userClass;
this.avatarUrl = characterImage(this.character.name); this.avatarUrl = this.character.overrides.avatarUrl || characterImage(this.character.name);
} }

View File

@ -1,12 +1,19 @@
import {isToday} from 'date-fns'; import {isToday} from 'date-fns';
import {Keys} from '../keys'; import {Keys} from '../keys';
import {Character, Conversation, Settings as ISettings} from './interfaces'; import {Character, Conversation, Settings as ISettings} from './interfaces';
import core from './core';
export function profileLink(this: any | never, character: string): string { export function profileLink(this: any | never, character: string): string {
return `https://www.f-list.net/c/${character}`; return `https://www.f-list.net/c/${character}`;
} }
export function characterImage(this: any | never, character: string): string { export function characterImage(this: any | never, character: string): string {
const c = core.characters.get(character);
if (c.overrides.avatarUrl) {
return c.overrides.avatarUrl;
}
return `https://static.f-list.net/images/avatar/${character.toLowerCase()}.png`; return `https://static.f-list.net/images/avatar/${character.toLowerCase()}.png`;
} }

View File

@ -10,6 +10,7 @@ import { AdCenter } from './ads/ad-center';
import { GeneralSettings } from '../electron/common'; import { GeneralSettings } from '../electron/common';
import { SiteSession } from '../site/site-session'; import { SiteSession } from '../site/site-session';
import _ from 'lodash'; import _ from 'lodash';
import { initYiffbot4000Integration } from '../learn/yiffbot';
function createBBCodeParser(): BBCodeParser { function createBBCodeParser(): BBCodeParser {
const parser = new BBCodeParser(); const parser = new BBCodeParser();
@ -115,6 +116,8 @@ export function init(
if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue); if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue);
}); });
initYiffbot4000Integration();
connection.onEvent('connecting', async() => { connection.onEvent('connecting', async() => {
await data.reloadSettings(); await data.reloadSettings();
data.bbCodeParser = createBBCodeParser(); data.bbCodeParser = createBBCodeParser();

View File

@ -2,7 +2,7 @@
<div class="character-preview"> <div class="character-preview">
<div v-if="match && character" class="row"> <div v-if="match && character" class="row">
<div class="col-2"> <div class="col-2">
<img :src="avatarUrl(character.character.name)" class="character-avatar"> <img :src="getAvatarUrl()" class="character-avatar">
</div> </div>
<div class="col-10"> <div class="col-10">
@ -140,7 +140,7 @@ export default class CharacterPreview extends Vue {
subDomRole?: string; subDomRole?: string;
formatTime = formatTime; formatTime = formatTime;
readonly avatarUrl = Utils.avatarURL; // readonly avatarUrl = Utils.avatarURL;
TagId = TagId; TagId = TagId;
Score = Score; Score = Score;
@ -150,6 +150,13 @@ export default class CharacterPreview extends Vue {
conversation?: Conversation.Message[]; conversation?: Conversation.Message[];
getAvatarUrl(): string {
if (this.onlineCharacter && this.onlineCharacter.overrides.avatarUrl) {
return this.onlineCharacter.overrides.avatarUrl;
}
return Utils.avatarURL(this.characterName || this.character?.character.name || '');
}
@Hook('mounted') @Hook('mounted')
mounted(): void { mounted(): void {

View File

@ -10,7 +10,7 @@
<li v-for="(tab,index) in tabs" :key="'tab-' + index" class="nav-item" @click.middle="remove(tab)"> <li v-for="(tab,index) in tabs" :key="'tab-' + index" class="nav-item" @click.middle="remove(tab)">
<a href="#" @click.prevent="show(tab)" class="nav-link tab" <a href="#" @click.prevent="show(tab)" class="nav-link tab"
:class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}"> :class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}">
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/> <img v-if="tab.user || tab.avatarUrl" :src="getAvatarImage(tab)"/>
<span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span> <span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span>
<a href="#" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit;text-decoration:none" <a href="#" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit;text-decoration:none"
@click.stop="remove(tab)"><i class="fa fa-times"></i> @click.stop="remove(tab)"><i class="fa fa-times"></i>
@ -108,6 +108,7 @@
view: Electron.BrowserView view: Electron.BrowserView
hasNew: boolean hasNew: boolean
tray: Electron.Tray tray: Electron.Tray
avatarUrl?: string;
} }
// console.log(require('./build/tray.png').default); // console.log(require('./build/tray.png').default);
@ -192,6 +193,16 @@
menu.unshift({label: tab.user, enabled: false}, {type: 'separator'}); menu.unshift({label: tab.user, enabled: false}, {type: 'separator'});
tab.tray.setContextMenu(remote.Menu.buildFromTemplate(menu)); tab.tray.setContextMenu(remote.Menu.buildFromTemplate(menu));
}); });
electron.ipcRenderer.on('update-avatar-url', (_e: Event, characterName: string, url: string) => {
const tab = this.tabs.find((tab) => tab.user === characterName);
if (!tab) {
return;
}
Vue.set(tab, 'avatarUrl', url);
// tab.avatarUrl = url;
});
electron.ipcRenderer.on('disconnect', (_e: Event, id: number) => { electron.ipcRenderer.on('disconnect', (_e: Event, id: number) => {
const tab = this.tabMap[id]; const tab = this.tabMap[id];
if(tab.hasNew) { if(tab.hasNew) {
@ -270,6 +281,14 @@
log.debug('init.window.mounted'); log.debug('init.window.mounted');
} }
getAvatarImage(tab: Tab) {
if (tab.avatarUrl) {
return tab.avatarUrl;
}
return 'https://static.f-list.net/images/avatar/' + (tab.user || '').toLowerCase() + '.png';
}
destroyAllTabs(): void { destroyAllTabs(): void {
browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword
this.tabs.forEach(destroyTab); this.tabs.forEach(destroyTab);
@ -481,6 +500,7 @@
img { img {
height: 28px; height: 28px;
width: 28px;
margin: -5px 3px -5px -5px; margin: -5px 3px -5px -5px;
} }
} }

View File

@ -135,7 +135,7 @@ export function fixLogs(character: string): void {
fs.readSync(fd, buffer, 0, 50100, pos); fs.readSync(fd, buffer, 0, 50100, pos);
const deserialized = deserializeMessage(buffer, 0, (name) => ({ const deserialized = deserializeMessage(buffer, 0, (name) => ({
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false, gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
isIgnored: false, name isIgnored: false, name, overrides: {}
})); }));
const time = deserialized.message.time; const time = deserialized.message.time;
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440); const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src file: data: https://static.f-list.net http://static.f-list.net; connect-src *"> <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src file: data: https://static.f-list.net http://static.f-list.net https://iili.io https://static1.e621.net https://i.imgur.com https://freeimage.host https://v3.redgifs.com; connect-src *">
<title>F-Chat</title> <title>F-Chat</title>
<link href="fa.css" rel="stylesheet"> <link href="fa.css" rel="stylesheet">
</head> </head>

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src https://static.f-list.net"> <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src https://static.f-list.net https://iili.io https://static1.e621.net https://i.imgur.com https://freeimage.host https://v3.redgifs.com">
<title>F-Chat</title> <title>F-Chat</title>
<link href="fa.css" rel="stylesheet"> <link href="fa.css" rel="stylesheet">
</head> </head>

View File

@ -12,11 +12,18 @@ class Character implements Interfaces.Character {
isBookmarked = false; isBookmarked = false;
isChatOp = false; isChatOp = false;
isIgnored = false; isIgnored = false;
overrides: CharacterOverrides = {};
constructor(public name: string) { constructor(public name: string) {
} }
} }
export interface CharacterOverrides {
avatarUrl?: string;
gender?: Interfaces.Gender;
status?: Interfaces.Status;
}
class State implements Interfaces.State { class State implements Interfaces.State {
characters: {[key: string]: Character | undefined} = {}; characters: {[key: string]: Character | undefined} = {};
@ -56,6 +63,14 @@ class State implements Interfaces.State {
character.statusText = decodeHTML(text); character.statusText = decodeHTML(text);
} }
setOverride(name: string, type: 'avatarUrl', value: string | undefined): void;
setOverride(name: string, type: 'gender', value: Interfaces.Gender | undefined): void;
setOverride(name: string, type: 'status', value: Interfaces.Status | undefined): void;
setOverride(name: string, type: keyof CharacterOverrides, value: any): void {
const char = this.get(name);
char.overrides[type] = value;
}
async resolveOwnProfile(): Promise<void> { async resolveOwnProfile(): Promise<void> {
await methods.fieldsGet(); await methods.fieldsGet();

View File

@ -1,4 +1,5 @@
import { Character as CharacterProfile } from '../site/character_page/interfaces'; import { Character as CharacterProfile } from '../site/character_page/interfaces';
import { CharacterOverrides } from './characters';
//tslint:disable:no-shadowed-variable //tslint:disable:no-shadowed-variable
export namespace Connection { export namespace Connection {
@ -170,7 +171,8 @@ export namespace Character {
readonly ownProfile: CharacterProfile; readonly ownProfile: CharacterProfile;
get(name: string): Character get(name: string): Character;
setOverride(name: string, type: keyof CharacterOverrides, value: any): void;
} }
export interface Character { export interface Character {
@ -182,6 +184,7 @@ export namespace Character {
readonly isBookmarked: boolean readonly isBookmarked: boolean
readonly isChatOp: boolean readonly isChatOp: boolean
readonly isIgnored: boolean readonly isIgnored: boolean
readonly overrides: CharacterOverrides
} }
} }

View File

@ -8,6 +8,7 @@ import { PermanentIndexedStore } from './store/types';
import { CharacterImage, SimpleCharacter } from '../interfaces'; import { CharacterImage, SimpleCharacter } from '../interfaces';
import { Scoring } from './matcher-types'; import { Scoring } from './matcher-types';
import { matchesSmartFilters } from './filter/smart-filter'; import { matchesSmartFilters } from './filter/smart-filter';
import * as remote from '@electron/remote';
export interface MetaRecord { export interface MetaRecord {
@ -148,6 +149,54 @@ export class ProfileCache extends AsyncCache<CharacterCacheRecord> {
} }
} }
isSafeImageURL(url: string): boolean {
if (url.match(/^https?:\/\/static\.f-list\.net\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?imgur\.com\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?freeimage\.host\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?iili\.io\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?redgifs\.com\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?e621\.net\//i)) {
return true;
}
return false;
}
updateOverrides(c: ComplexCharacter): void {
const match = c.character.description.match(/\[url=(.*?)]\s*?Rising\s*?Portrait\s*?\[\/url]/i);
if (match && match[1]) {
const avatarUrl = match[1].trim();
if (!this.isSafeImageURL(avatarUrl)) {
return;
}
if (c.character.name === core.characters.ownCharacter.name) {
const parent = remote.getCurrentWindow().webContents;
parent.send('update-avatar-url', c.character.name, avatarUrl);
}
core.characters.setOverride(c.character.name, 'avatarUrl', avatarUrl);
}
}
async register(c: ComplexCharacter, skipStore: boolean = false): Promise<CharacterCacheRecord> { async register(c: ComplexCharacter, skipStore: boolean = false): Promise<CharacterCacheRecord> {
const k = AsyncCache.nameKey(c.character.name); const k = AsyncCache.nameKey(c.character.name);
@ -158,6 +207,8 @@ export class ProfileCache extends AsyncCache<CharacterCacheRecord> {
console.log(`Storing score 0 for character ${c.character.name}`); console.log(`Storing score 0 for character ${c.character.name}`);
} }
this.updateOverrides(c);
// const totalScoreDimensions = match ? Matcher.countScoresTotal(match) : 0; // const totalScoreDimensions = match ? Matcher.countScoresTotal(match) : 0;
// const dimensionsAtScoreLevel = match ? (Matcher.countScoresAtLevel(match, score) || 0) : 0; // const dimensionsAtScoreLevel = match ? (Matcher.countScoresAtLevel(match, score) || 0) : 0;
// const dimensionsAboveScoreLevel = match ? (Matcher.countScoresAboveLevel(match, Math.max(score, Scoring.WEAK_MATCH))) : 0; // const dimensionsAboveScoreLevel = match ? (Matcher.countScoresAboveLevel(match, Math.max(score, Scoring.WEAK_MATCH))) : 0;

27
learn/yiffbot.ts Normal file
View File

@ -0,0 +1,27 @@
import { EventBus } from '../chat/preview/event-bus';
import { Message } from '../chat/common';
import core from '../chat/core';
export function initYiffbot4000Integration() {
EventBus.$on('private-message', ({ message }: { message: Message }) => {
if (message.sender.name === 'YiffBot 4000') {
const match = message.text.match(/\[spoiler](.*?FChatRisingBotManifest.*?)\[\/spoiler]/i);
if (match && match[1]) {
try {
const manifest = JSON.parse(match[1]);
if (manifest.type === 'FChatRisingBotManifest' && manifest.version >= 1) {
const char = core.characters.get('YiffBot 4000');
char.overrides.avatarUrl = manifest.avatarUrl;
char.overrides.gender = manifest.gender;
}
} catch (err) {
console.error('FChatRisingBotManifest.error', err);
}
}
}
});
}

View File

@ -4,7 +4,7 @@
<div class="scores you"> <div class="scores you">
<h3> <h3>
<img :src="avatarUrl(characterMatch.you.you.name)" class="thumbnail"/> <img :src="getAvatarUrl(characterMatch.you.you.name)" class="thumbnail"/>
{{characterMatch.you.you.name}} {{characterMatch.you.you.name}}
<small v-if="characterMatch.youMultiSpecies" class="species">as {{getSpeciesStr(characterMatch.you)}}</small> <small v-if="characterMatch.youMultiSpecies" class="species">as {{getSpeciesStr(characterMatch.you)}}</small>
</h3> </h3>
@ -20,7 +20,7 @@
<div class="scores them"> <div class="scores them">
<h3> <h3>
<img :src="avatarUrl(characterMatch.them.you.name)" class="thumbnail" /> <img :src="getAvatarUrl(characterMatch.them.you.name)" class="thumbnail" />
{{characterMatch.them.you.name}} {{characterMatch.them.you.name}}
<small v-if="characterMatch.themMultiSpecies" class="species">as {{getSpeciesStr(characterMatch.them)}}</small> <small v-if="characterMatch.themMultiSpecies" class="species">as {{getSpeciesStr(characterMatch.them)}}</small>
</h3> </h3>
@ -54,8 +54,6 @@
// @Prop({required: true}) // @Prop({required: true})
// readonly minimized = false; // readonly minimized = false;
readonly avatarUrl = Utils.avatarURL;
isMinimized = false; isMinimized = false;
@ -70,6 +68,16 @@
// this.isMinimized = this.minimized; // this.isMinimized = this.minimized;
// } // }
getAvatarUrl(name: string) {
const c = core.characters.get(name);
if (c.overrides.avatarUrl) {
return c.overrides.avatarUrl;
}
return Utils.avatarURL(name);
}
getScoreClass(score: Score): CssClassMap { getScoreClass(score: Score): CssClassMap {
const classes: CssClassMap = {}; const classes: CssClassMap = {};

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="character-page-sidebar" class="card bg-light"> <div id="character-page-sidebar" class="card bg-light">
<div class="card-body"> <div class="card-body">
<img :src="avatarUrl(character.character.name)" class="character-avatar" style="width: 100%; height: auto;"> <img :src="getAvatarUrl()" class="character-avatar" style="width: 100%; height: auto;">
<div v-if="character.character.title" class="character-title">{{ character.character.title }}</div> <div v-if="character.character.title" class="character-title">{{ character.character.title }}</div>
<character-action-menu :character="character" @rename="showRename()" @delete="showDelete()" <character-action-menu :character="character" @rename="showRename()" @delete="showDelete()"
@ -106,6 +106,7 @@
import { MatchReport } from '../../learn/matcher'; import { MatchReport } from '../../learn/matcher';
import MemoDialog from './memo_dialog.vue'; import MemoDialog from './memo_dialog.vue';
import ReportDialog from './report_dialog.vue'; import ReportDialog from './report_dialog.vue';
import core from '../../chat/core';
interface ShowableVueDialog extends Vue { interface ShowableVueDialog extends Vue {
show(): void show(): void
@ -152,6 +153,16 @@
readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these. readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these.
readonly avatarUrl = Utils.avatarURL; readonly avatarUrl = Utils.avatarURL;
getAvatarUrl(): string {
const onlineCharacter = core.characters.get(this.character.character.name);
if (onlineCharacter && onlineCharacter.overrides.avatarUrl) {
return onlineCharacter.overrides.avatarUrl;
}
return Utils.avatarURL(this.character.character.name);
}
badgeClass(badgeName: string): string { badgeClass(badgeName: string): string {
return `character-badge-${badgeName.replace('.', '-')}`; return `character-badge-${badgeName.replace('.', '-')}`;
} }