Character previews

This commit is contained in:
Mr. Stallion 2020-10-25 17:55:21 -05:00
parent ef123fdddc
commit 61b4976763
15 changed files with 829 additions and 203 deletions

View File

@ -1,7 +1,8 @@
# Changelog
## Canary
* More conifugrable settings for F-Chat Rising
* More configurable settings for F-Chat Rising
* Hover mouse over a character name to see a character preview
## 1.3.0

View File

@ -55,7 +55,6 @@
EventBus.$emit('imagepreview-show', {url: this.url});
}
toggleStickyness(): void {
EventBus.$emit('imagepreview-toggle-stickyness', {url: this.url});
}

View File

@ -10,9 +10,7 @@
<bbcode id="userMenuStatus" :text="character.statusText" v-show="character.statusText" class="list-group-item"
style="max-height:200px;overflow:auto;clear:both"></bbcode>
<div v-if="match" class="list-group-item menu-character-score">
<span v-for="(score, key) in match" :class="score.getRecommendedClass()"><i :class="score.getRecommendedIcon()"></i> {{getTagDesc(key)}}</span>
</div>
<match-tags v-if="match" :match="match" class="list-group-item"></match-tags>
<a tabindex="-1" :href="profileLink" target="_blank" v-if="showProfileFirst" class="list-group-item list-group-item-action">
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
@ -59,12 +57,12 @@ import core from './core';
import { Channel, Character } from './interfaces';
import l from './localize';
import ReportDialog from './ReportDialog.vue';
import { Matcher, MatchResultScores } from '../learn/matcher';
import { TagId } from '../learn/matcher-types';
import { Matcher, MatchReport } from '../learn/matcher';
import _ from 'lodash';
import MatchTags from './preview/MatchTags.vue';
@Component({
components: {bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
components: {'match-tags': MatchTags, bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
})
export default class UserMenu extends Vue {
@Prop({required: true})
@ -80,7 +78,7 @@ import _ from 'lodash';
memo = '';
memoId = 0;
memoLoading = false;
match: MatchResultScores | null = null;
match: MatchReport | null = null;
openConversation(jump: boolean): void {
const conversation = core.conversations.getPrivate(this.character!);
@ -226,9 +224,6 @@ import _ from 'lodash';
this.showContextMenu = false;
}
getTagDesc(key: any): any {
return TagId[key].toString().replace(/([A-Z])/g, ' $1').trim();
}
private async openMenu(touch: MouseEvent | Touch, character: Character, channel: Channel | undefined): Promise<void> {
this.channel = channel;
@ -246,7 +241,7 @@ import _ from 'lodash';
const match = Matcher.identifyBestMatchReport(myProfile.character, theirProfile.character.character);
if (_.keys(match.merged).length > 0) {
this.match = match.merged;
this.match = match;
}
}
}
@ -272,44 +267,6 @@ import _ from 'lodash';
border-top-width: 0;
z-index: -1;
}
#userMenu {
.menu-character-score {
span {
padding-left: 3px;
padding-right: 3px;
margin-bottom: 3px;
margin-right: 3px;
display: inline-block;
border: 1px solid;
border-radius: 3px;
i {
color: white;
}
&.match {
background-color: var(--scoreMatchBg);
border: solid 1px var(--scoreMatchFg);
}
&.weak-match {
background-color: var(--scoreWeakMatchBg);
border: 1px solid var(--scoreWeakMatchFg);
}
&.weak-mismatch {
background-color: var(--scoreWeakMismatchBg);
border: 1px solid var(--scoreWeakMismatchFg);
}
&.mismatch {
background-color: var(--scoreMismatchBg);
border: 1px solid var(--scoreMismatchFg);
}
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<!-- Linebreaks inside this template will break BBCode views -->
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @mouseout.prevent="dismiss()" @click.middle.prevent="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
<script lang="ts">
@ -33,6 +33,70 @@ export function getStatusIcon(status: Character.Status): string {
}
export interface StatusClasses {
rankIcon: string | null;
statusClass: string | null;
matchClass: string | null;
matchScore: number | null;
userClass: string;
isBookmark: boolean;
}
export function getStatusClasses(
character: Character,
channel: Channel | undefined,
showStatus: boolean,
showBookmark: boolean,
showMatch: boolean
): StatusClasses {
let rankIcon: string | null = null;
let statusClass = null;
let matchClass = null;
let matchScore = null;
if(character.isChatOp) {
rankIcon = 'far fa-gem';
} else if(channel !== undefined) {
rankIcon = (channel.owner === character.name)
? 'fa fa-key'
: channel.opList.indexOf(character.name) !== -1
? (channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star')
: null;
}
if ((showStatus) || (character.status === 'crown'))
statusClass = `fa-fw ${getStatusIcon(character.status)}`;
if ((core.state.settings.risingAdScore) && (showMatch)) {
const cache = core.cache.profileCache.getSync(character.name);
if (cache) {
matchClass = `match-found ${Score.getClasses(cache.matchScore)}`;
matchScore = cache.matchScore;
} else {
/* tslint:disable-next-line no-floating-promises */
core.cache.addProfile(character.name);
}
}
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
((character.isFriend) || (character.isBookmarked));
const userClass = `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`;
return {
rankIcon,
statusClass,
matchClass,
matchScore,
userClass,
isBookmark
};
}
@Component({
components: {
@ -54,6 +118,9 @@ export default class UserView extends Vue {
@Prop()
readonly match?: boolean = false;
@Prop({default: true})
readonly preview: boolean = true;
userClass = '';
rankIcon: string | null = null;
@ -100,8 +167,14 @@ export default class UserView extends Vue {
onBeforeDestroy(): void {
if (this.scoreWatcher)
EventBus.$off('character-score', this.scoreWatcher);
this.dismiss();
}
@Hook('deactivated')
deactivate(): void {
this.dismiss();
}
@Hook('beforeUpdate')
onBeforeUpdate(): void {
@ -114,55 +187,13 @@ export default class UserView extends Vue {
}
update(): void {
this.rankIcon = null;
this.statusClass = null;
this.matchClass = null;
this.matchScore = null;
const res = getStatusClasses(this.character, this.channel, !!this.showStatus, !!this.bookmark, !!this.match);
// if (this.match) console.log('Update', this.character.name);
if(this.character.isChatOp) {
this.rankIcon = 'far fa-gem';
} else if(this.channel !== undefined) {
this.rankIcon = (this.channel.owner === this.character.name)
? 'fa fa-key'
: this.channel.opList.indexOf(this.character.name) !== -1
? (this.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star')
: null;
}
if ((this.showStatus) || (this.character.status === 'crown'))
this.statusClass = `fa-fw ${getStatusIcon(this.character.status)}`;
// if (this.match) console.log('Update prematch', this.character.name);
if ((core.state.settings.risingAdScore) && (this.match)) {
const cache = core.cache.profileCache.getSync(this.character.name);
if (cache) {
this.matchClass = `match-found ${Score.getClasses(cache.matchScore)}`;
this.matchScore = cache.matchScore;
// console.log('Found match data', this.character.name, cache.matchScore);
} else {
// console.log('Need match data', this.character.name);
/* tslint:disable-next-line no-floating-promises */
core.cache.addProfile(this.character.name);
}
}
// if (this.match) console.log('Update post match', this.character.name);
const gender = this.character.gender !== undefined ? this.character.gender.toLowerCase() : 'none';
const isBookmark = (this.bookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
((this.character.isFriend) || (this.character.isBookmarked));
this.userClass = `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`;
// if (this.match) console.log('Update done');
this.rankIcon = res.rankIcon;
this.statusClass = res.statusClass;
this.matchClass = res.matchClass;
this.matchScore = res.matchScore;
this.userClass = res.userClass;
}
@ -183,47 +214,37 @@ export default class UserView extends Vue {
return '';
}
}
//tslint:disable-next-line:variable-name
/* const UserView = Vue.extend({
functional: true,
render(this: void | Vue, createElement: CreateElement, context?: RenderContext): VNode {
const props = <{character: Character, channel?: Channel, showStatus?: true, bookmark?: false, match?: false}>(
context !== undefined ? context.props : (<Vue>this).$options.propsData);
const character = props.character;
getCharacterUrl(): string {
return `flist-character://${this.character.name}`;
}
let matchClasses: string | undefined;
if (props.match) {
const cache = core.cache.profileCache.getSync(character.name);
if (cache) {
matchClasses = Score.getClasses(cache.matchScore);
}
dismiss(force: boolean = false): void {
if (!this.preview) {
return;
}
let rankIcon;
if(character.isChatOp) rankIcon = 'far fa-gem';
else if(props.channel !== undefined)
rankIcon = props.channel.owner === character.name ? 'fa fa-key' : props.channel.opList.indexOf(character.name) !== -1 ?
(props.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star') : '';
else rankIcon = '';
const children: (VNode | string)[] = [character.name];
if(rankIcon !== '') children.unshift(createElement('span', {staticClass: rankIcon}));
if(props.showStatus !== undefined || character.status === 'crown')
children.unshift(createElement('span', {staticClass: `fa-fw ${getStatusIcon(character.status)}`}));
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
const isBookmark = props.bookmark !== false && core.connection.isOpen && core.state.settings.colorBookmarks &&
(character.isFriend || character.isBookmarked);
return createElement('span', {
attrs: {class: `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''} ${matchClasses}`},
domProps: {character, channel: props.channel, bbcodeTag: 'user'}
}, children);
EventBus.$emit('imagepreview-dismiss', {url: this.getCharacterUrl(), force});
}
});
export default UserView;
*/
show(): void {
if (!this.preview) {
return;
}
EventBus.$emit('imagepreview-show', {url: this.getCharacterUrl()});
}
toggleStickyness(): void {
if (!this.preview) {
return;
}
EventBus.$emit('imagepreview-toggle-stickyness', {url: this.getCharacterUrl()});
}
}
</script>

View File

@ -0,0 +1,290 @@
<template>
<div class="character-preview">
<div v-if="match && character" class="row">
<div class="col-2">
<img :src="avatarUrl(character.character.name)" class="character-avatar">
</div>
<div class="col-8">
<h1><span class="character-name" :class="(statusClasses || {}).userClass">{{ character.character.name }}</span></h1>
<h3>{{ getOnlineStatus() }}</h3>
<div class="summary">
<span class="uc">
<span v-if="age" :class="byScore(TagId.Age)">{{age}}-years-old </span>
<span v-if="sexualOrientation" :class="byScore(TagId.Orientation)">{{sexualOrientation}} </span>
<span v-if="gender" :class="byScore(TagId.Gender)">{{gender}} </span>
<span v-if="species" :class="byScore(TagId.Species)">{{species}} </span>
</span>
<span v-if="furryPref" :class="byScore(TagId.FurryPreference)"><br /><span class="uc">{{furryPref}}</span></span>
<span v-if="subDomRole" :class="byScore(TagId.SubDomRole)"><br /><span class="uc">{{subDomRole}}</span></span>
</div>
<match-tags v-if="match" :match="match"></match-tags>
<div v-if="latestAd">
<h4>Latest Ad <span class="message-time">{{formatTime(latestAd.datePosted)}}</span></h4>
<bbcode :text="latestAd.message"></bbcode>
</div>
</div>
</div>
<div v-else>
Loading...
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop } from '@f-list/vue-ts';
import Vue from 'vue';
import core from '../core';
import { methods } from '../../site/character_page/data_store';
import {Character as ComplexCharacter} from '../../site/character_page/interfaces';
import { Matcher, MatchReport } from '../../learn/matcher';
import { Character as CharacterStatus } from '../../fchat';
import { getStatusClasses, StatusClasses } from '../UserView.vue';
import * as _ from 'lodash';
import { AdCachedPosting } from '../../learn/ad-cache';
import {formatTime} from '../common';
import * as Utils from '../../site/utils';
import MatchTags from './MatchTags.vue';
import {
furryPreferenceMapping,
Gender,
Orientation,
Species,
SubDomRole,
TagId
} from '../../learn/matcher-types';
import { BBCodeView } from '../../bbcode/view';
@Component({
components: {
'match-tags': MatchTags,
bbcode: BBCodeView(core.bbCodeParser)
}
})
export default class CharacterPreview extends Vue {
@Prop
readonly id?: number;
characterName?: string;
character?: ComplexCharacter;
match?: MatchReport;
ownCharacter?: ComplexCharacter;
onlineCharacter?: CharacterStatus;
statusClasses?: StatusClasses;
latestAd?: AdCachedPosting;
age?: string;
sexualOrientation?: string;
species?: string;
gender?: string;
furryPref?: string;
subDomRole?: string;
formatTime = formatTime;
readonly avatarUrl = Utils.avatarURL;
TagId = TagId;
async load(characterName: string): Promise<void> {
if (
(this.characterName === characterName)
&& (this.match)
&& (this.character)
&& (this.ownCharacter)
&& (this.ownCharacter.character.name === core.characters.ownProfile.character.name)
) {
this.updateOnlineStatus();
this.updateAdStatus();
return;
}
this.characterName = characterName;
this.match = undefined;
this.character = undefined;
this.ownCharacter = core.characters.ownProfile;
this.updateOnlineStatus();
this.updateAdStatus();
this.character = await this.getCharacterData(characterName);
this.match = Matcher.identifyBestMatchReport(this.ownCharacter.character, this.character.character);
this.updateDetails();
}
updateOnlineStatus(): void {
this.onlineCharacter = core.characters.get(this.characterName!);
if (!this.onlineCharacter) {
this.statusClasses = undefined;
return;
}
this.statusClasses = getStatusClasses(this.onlineCharacter, undefined, true, true, false);
}
updateAdStatus(): void {
const cache = core.cache.adCache.get(this.characterName!);
if ((!cache) || (cache.posts.length === 0)) {
this.latestAd = undefined;
return;
}
this.latestAd = cache.posts[cache.posts.length - 1];
}
updateDetails(): void {
if (!this.match) {
this.age = undefined;
this.species = undefined;
this.gender = undefined;
this.furryPref = undefined;
this.subDomRole = undefined;
this.sexualOrientation = undefined;
return;
}
const a = this.match.them.yourAnalysis;
const c = this.match.them.you;
const rawSpecies = Matcher.getTagValue(TagId.Species, c);
const rawAge = Matcher.getTagValue(TagId.Age, c);
if ((a.species) && (!Species[a.species])) {
console.log('SPECIES', a.species, rawSpecies);
}
this.age = a.age ? this.readable(`${a.age}`) : (rawAge && rawAge.string) || undefined;
this.species = a.species ? this.readable(Species[a.species]) : (rawSpecies && rawSpecies.string) || undefined;
this.gender = a.gender ? this.readable(Gender[a.gender]) : undefined;
this.furryPref = a.furryPreference ? this.readable(furryPreferenceMapping[a.furryPreference]) : undefined;
this.subDomRole = a.subDomRole ? this.readable(SubDomRole[a.subDomRole]) : undefined;
this.sexualOrientation = a.orientation ? this.readable(Orientation[a.orientation]) : undefined;
}
readable(s: string): string {
return s.replace(/([A-Z])/g, ' $1').trim().toLowerCase()
.replace(/(always|usually) (submissive|dominant)/, '$2')
.replace(/bi (fe)?male preference/, 'bisexual');
}
byScore(_tagId: any): string {
return '';
// too much
// if (!this.match) {
// return '';
// }
//
// const score = this.match.merged[tagId];
//
// if (!score) {
// return '';
// }
//
// return score.getRecommendedClass();
}
getOnlineStatus(): string {
if (!this.onlineCharacter) {
return 'Offline';
}
const s = this.onlineCharacter.status as string;
return `${s.substr(0, 1).toUpperCase()}${s.substr(1)}`;
}
async getCharacterData(characterName: string): Promise<ComplexCharacter> {
const cache = await core.cache.profileCache.get(characterName);
if (cache) {
return cache.character;
}
return methods.characterData(characterName, this.id, false);
}
}
</script>
<style lang="scss">
.character-preview {
padding: 10px;
background-color: var(--input-bg);
.summary {
font-size: 125%;
.uc {
display: inline-block;
&::first-letter {
text-transform: capitalize;
}
}
.match {
background-color: var(--scoreMatchBg);
border: solid 1px var(--scoreMatchFg);
}
.weak-match {
background-color: var(--scoreWeakMatchBg);
border: 1px solid var(--scoreWeakMatchFg);
}
.weak-mismatch {
background-color: var(--scoreWeakMismatchBg);
border: 1px solid var(--scoreWeakMismatchFg);
}
.mismatch {
background-color: var(--scoreMismatchBg);
border: 1px solid var(--scoreMismatchFg);
}
}
.matched-tags {
margin-top: 1rem;
}
h1 {
line-height: 100%;
margin-bottom: 0;
font-size: 2em;
}
h3 {
font-size: 1.1rem;
color: var(--dark);
}
h4 {
font-size: 1rem;
margin-top: 1rem;
.message-time {
font-size: 80%;
font-weight: normal;
color: var(--messageTimeFgColor);
margin-left: 2px;
}
}
.character-avatar {
width: 100%;
height: auto;
}
}
</style>

View File

@ -21,15 +21,20 @@
id="image-preview-ext"
ref="imagePreviewExt"
class="image-preview-external"
:style="externalPreviewStyle">
:style="previewStyles.ExternalImagePreviewHelper">
</webview>
<div
class="image-preview-local"
:style="localPreviewStyle"
:style="previewStyles.LocalImagePreviewHelper"
>
</div>
<character-preview
:style="previewStyles.CharacterPreviewHelper"
ref="characterPreview"
></character-preview>
<i id="preview-spinner" class="fas fa-circle-notch fa-spin" v-show="shouldShowSpinner"></i>
<i id="preview-error" class="fas fa-times" v-show="shouldShowError"></i>
</div>
@ -44,12 +49,17 @@
import {domain} from '../../bbcode/core';
import {ImageDomMutator} from './image-dom-mutator';
import { ExternalImagePreviewHelper, LocalImagePreviewHelper } from './helper';
import {
ExternalImagePreviewHelper,
LocalImagePreviewHelper,
PreviewManager,
CharacterPreviewHelper, RenderStyle
} from './helper';
import {Point, WebviewTag, remote} from 'electron';
import Timer = NodeJS.Timer;
import IpcMessageEvent = Electron.IpcMessageEvent;
import CharacterPreview from './CharacterPreview.vue';
const screen = remote.screen;
@ -63,15 +73,30 @@
httpStatusText: string;
}
@Component
@Component({
components: {
'character-preview': CharacterPreview
}
})
export default class ImagePreview extends Vue {
private readonly MinTimePreviewVisible = 100;
visible = false;
externalPreviewHelper = new ExternalImagePreviewHelper(this);
localPreviewHelper = new LocalImagePreviewHelper(this);
previewManager = new PreviewManager(
this,
[
new ExternalImagePreviewHelper(this),
new LocalImagePreviewHelper(this),
new CharacterPreviewHelper(this)
// new ChannelPreviewHelper(this)
]
);
// externalPreviewHelper = new ExternalImagePreviewHelper(this);
// localPreviewHelper = new LocalImagePreviewHelper(this);
// externalPreviewStyle: Record<string, any> = {};
// localPreviewStyle: Record<string, any> = {};
url: string | null = null;
domain: string | undefined;
@ -82,15 +107,11 @@
jsMutator = new ImageDomMutator(this.debug);
externalPreviewStyle: Record<string, any> = {};
localPreviewStyle: Record<string, any> = {};
state = 'hidden';
shouldShowSpinner = false;
shouldShowError = true;
private interval: Timer | null = null;
private exitInterval: Timer | null = null;
@ -100,6 +121,9 @@
private shouldDismiss = false;
private visibleSince = 0;
previewStyles: Record<string, RenderStyle> = {};
@Hook('mounted')
onMounted(): void {
console.warn('Mounted ImagePreview');
@ -299,43 +323,33 @@
reRenderStyles(): void {
// tslint:disable-next-line:no-unsafe-any
this.externalPreviewStyle = this.externalPreviewHelper.renderStyle();
// tslint:disable-next-line:no-unsafe-any
this.localPreviewStyle = this.localPreviewHelper.renderStyle();
this.debugLog(
'ImagePreview: reRenderStyles', 'external',
JSON.parse(JSON.stringify(this.externalPreviewStyle)),
'local', JSON.parse(JSON.stringify(this.localPreviewStyle))
);
this.previewStyles = this.previewManager.renderStyles();
}
updatePreviewSize(width: number, height: number): void {
if (!this.externalPreviewHelper.isVisible()) {
return;
const helper = this.previewManager.getVisiblePreview();
if ((!helper) || (!helper.reactsToSizeUpdates())) {
return;
}
if ((width) && (height)) {
this.debugLog('ImagePreview: updatePreviewSize', width, height, width / height);
this.externalPreviewHelper.setRatio(width / height);
helper.setRatio(width / height);
this.reRenderStyles();
}
}
hide(): void {
this.debugLog('ImagePreview: hide', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible());
this.cancelExitTimer();
this.url = null;
this.visible = false;
this.localPreviewHelper.hide();
this.externalPreviewHelper.hide();
this.previewManager.hide();
this.exitUrl = null;
this.exitInterval = null;
@ -378,7 +392,7 @@
if ((!this.hasMouseMovedSince()) && (!force))
return;
this.debugLog('ImagePreview: dismiss.exec', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible(), url);
this.debugLog('ImagePreview: dismiss.exec', this.previewManager.getVisibilityStatus(), url);
// This timeout makes the preview window disappear with a slight delay, which helps UX
// when dealing with situations such as quickly scrolling text that moves the cursor away
@ -393,7 +407,7 @@
show(initialUrl: string): void {
const url = this.jsMutator.mutateUrl(initialUrl);
this.debugLog('ImagePreview: show', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible(),
this.debugLog('ImagePreview: show', this.previewManager.getVisibilityStatus(),
this.visible, this.hasMouseMovedSince(), !!this.interval, this.sticky, url);
// console.log('SHOW');
@ -430,15 +444,7 @@
() => {
this.debugLog('ImagePreview: show.timeout', this.url);
const isLocal = this.localPreviewHelper.match(this.domain as string);
isLocal
? this.localPreviewHelper.show(this.url as string)
: this.localPreviewHelper.hide();
this.externalPreviewHelper.match(this.domain as string)
? this.externalPreviewHelper.show(this.url as string)
: this.externalPreviewHelper.hide();
const helper = this.previewManager.show(this.url || undefined, this.domain);
this.interval = null;
this.visible = true;
@ -449,7 +455,11 @@
this.reRenderStyles();
this.setState(isLocal ? 'loaded' : 'loading');
if (helper) {
this.setState(helper.shouldTrackLoading() ? 'loading' : 'loaded');
} else {
this.setState('loaded');
}
},
due
) as Timer;
@ -504,8 +514,7 @@
this.debug = !this.debug;
this.jsMutator.setDebug(this.debug);
this.localPreviewHelper.setDebug(this.debug);
this.externalPreviewHelper.setDebug(this.debug);
this.previewManager.setDebug(this.debug);
if (this.debug) {
const webview = this.getWebview();
@ -550,26 +559,44 @@
this.hide();
}
toggleJsMode(): void {
this.runJs = !this.runJs;
}
reloadUrl(): void {
if (this.externalPreviewHelper.isVisible()) {
const webview = this.getWebview();
webview.reload();
reloadUrl(): void {
const helper = this.previewManager.getVisiblePreview();
if ((!helper) || (!helper.usesWebView())) {
return;
}
// helper.reload();
this.getWebview().reload();
}
getWebview(): WebviewTag {
return this.$refs.imagePreviewExt as WebviewTag;
}
getCharacterPreview(): CharacterPreview {
return this.$refs.characterPreview as CharacterPreview;
}
reset(): void {
this.externalPreviewHelper = new ExternalImagePreviewHelper(this);
this.localPreviewHelper = new LocalImagePreviewHelper(this);
this.previewManager = new PreviewManager(
this,
[
new ExternalImagePreviewHelper(this),
new LocalImagePreviewHelper(this),
new CharacterPreviewHelper(this)
// new ChannelPreviewHelper(this)
]
);
this.url = null;
this.domain = undefined;
@ -612,8 +639,15 @@
: false;
}
testError(): boolean {
return ((this.state === 'error') && (this.externalPreviewHelper.isVisible()));
const helper = this.previewManager.getVisiblePreview();
if ((!helper) || (!helper.usesWebView())) {
return false;
}
return (this.state === 'error');
}
}
</script>
@ -693,6 +727,7 @@
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem;
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2);
z-index: 1000;
a i.fa {
font-size: 1.25rem;

View File

@ -0,0 +1,80 @@
<template>
<div class="matched-tags">
<span v-for="(score, key) in merged" :class="score.getRecommendedClass()"><i :class="score.getRecommendedIcon()"></i> {{getTagDesc(key)}}</span>
</div>
</template>
<script lang="ts">
import { Component, Hook, Prop } from '@f-list/vue-ts';
import Vue from 'vue';
import { MatchReport, MatchResultScores } from '../../learn/matcher';
import { TagId } from '../../learn/matcher-types';
@Component({
components: {
}
})
export default class MatchTags extends Vue {
@Prop({required: true})
readonly match!: MatchReport;
merged!: MatchResultScores;
@Hook('mounted')
onMounted(): void {
this.merged = this.match.merged;
}
// @Watch('match', { deep: true })
// onMatchUpdate(match: MatchReport): void {
// // console.log('ON UPDATED ETA', match);
// this.merged = match.merged;
// }
getTagDesc(key: any): any {
return TagId[key].toString().replace(/([A-Z])/g, ' $1').trim();
}
}
</script>
<style lang="scss">
.matched-tags {
span {
padding-left: 3px;
padding-right: 3px;
margin-bottom: 3px;
margin-right: 3px;
display: inline-block;
border: 1px solid;
border-radius: 3px;
i {
color: white;
}
&.match {
background-color: var(--scoreMatchBg);
border: solid 1px var(--scoreMatchFg);
}
&.weak-match {
background-color: var(--scoreWeakMatchBg);
border: 1px solid var(--scoreWeakMatchFg);
}
&.weak-mismatch {
background-color: var(--scoreWeakMismatchBg);
border: 1px solid var(--scoreWeakMismatchFg);
}
&.mismatch {
background-color: var(--scoreMismatchBg);
border: 1px solid var(--scoreMismatchFg);
}
}
}
</style>

View File

@ -0,0 +1,68 @@
import { ImagePreviewHelper } from './helper';
export class CharacterPreviewHelper extends ImagePreviewHelper {
static readonly FLIST_CHARACTER_PROTOCOL_TESTER = /^flist-character:\/\/(.+)/;
hide(): void {
this.visible = false;
this.url = undefined;
}
show(url: string | undefined): void {
this.visible = true;
this.url = url;
if (!url) {
return;
}
const match = url.match(CharacterPreviewHelper.FLIST_CHARACTER_PROTOCOL_TESTER);
if (!match) {
return;
}
const characterName = match[1];
// tslint:disable-next-line no-floating-promises
this.parent.getCharacterPreview().load(characterName);
}
setRatio(_ratio: number): void {
// do nothing
}
reactsToSizeUpdates(): boolean {
return false;
}
shouldTrackLoading(): boolean {
return false;
}
usesWebView(): boolean {
return false;
}
match(_domainName: string | undefined, url: string | undefined): boolean {
if (!url) {
return false;
}
return CharacterPreviewHelper.FLIST_CHARACTER_PROTOCOL_TESTER.test(url);
}
renderStyle(): Record<string, any> {
return this.isVisible()
? { display: 'block' }
: { display: 'none' };
}
}

View File

@ -3,7 +3,7 @@ import { ImagePreviewHelper } from './helper';
import * as _ from 'lodash';
export class ExternalImagePreviewHelper extends ImagePreviewHelper {
protected lastExternalUrl: string | null = null;
protected lastExternalUrl: string | undefined = undefined;
protected allowCachedUrl = true;
@ -47,6 +47,21 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
}
reactsToSizeUpdates(): boolean {
return true;
}
shouldTrackLoading(): boolean {
return true;
}
usesWebView(): boolean {
return true;
}
setDebug(debug: boolean): void {
this.debug = debug;
@ -54,7 +69,7 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
}
show(url: string): void {
show(url: string | undefined): void {
const webview = this.parent.getWebview();
if (!this.parent) {
@ -65,6 +80,10 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
throw new Error('Empty webview!');
}
if (!url) {
throw new Error('Empty URL!');
}
// const oldUrl = this.url;
// const oldLastExternalUrl = this.lastExternalUrl;
@ -113,8 +132,13 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
}
match(domainName: string): boolean {
return !((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
match(domainName: string | undefined, url: string | undefined): boolean {
if ((!domainName) || (!url)) {
return false;
}
return (ImagePreviewHelper.HTTP_TESTER.test(url))
&& (!((domainName === 'f-list.net') || (domainName === 'static.f-list.net')));
}
@ -152,6 +176,7 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
}
}
renderStyle(): Record<string, any> {
return this.isVisible()
? _.merge({ display: 'flex' }, this.determineScalingRatio())

View File

@ -1,16 +1,23 @@
import ImagePreview from '../ImagePreview.vue';
export abstract class ImagePreviewHelper {
static readonly HTTP_TESTER = /^https?:\/\//;
protected visible = false;
protected url: string | null = 'about:blank';
protected url: string | undefined = 'about:blank';
protected parent: ImagePreview;
protected debug: boolean;
abstract show(url: string): void;
abstract show(url: string | undefined): void;
abstract hide(): void;
abstract match(domainName: string): boolean;
abstract match(domainName: string | undefined, url: string | undefined): boolean;
abstract renderStyle(): Record<string, any>;
abstract reactsToSizeUpdates(): boolean;
abstract setRatio(ratio: number): void;
abstract shouldTrackLoading(): boolean;
abstract usesWebView(): boolean;
constructor(parent: ImagePreview) {
if (!parent) {
throw new Error('Empty parent!');
@ -24,7 +31,7 @@ export abstract class ImagePreviewHelper {
return this.visible;
}
getUrl(): string | null {
getUrl(): string | undefined {
return this.url;
}

View File

@ -1,4 +1,6 @@
export * from './helper';
export * from './character';
export * from './external';
export * from './helper';
export * from './local';
export * from './manager';

View File

@ -1,20 +1,46 @@
import { ImagePreviewHelper } from './helper';
export class LocalImagePreviewHelper extends ImagePreviewHelper {
hide(): void {
this.visible = false;
this.url = null;
this.url = undefined;
}
show(url: string): void {
show(url: string | undefined): void {
this.visible = true;
this.url = url;
}
match(domainName: string): boolean {
return ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
setRatio(_ratio: number): void {
// do nothing
}
reactsToSizeUpdates(): boolean {
return false;
}
shouldTrackLoading(): boolean {
return false;
}
usesWebView(): boolean {
return false;
}
match(domainName: string | undefined, url: string | undefined): boolean {
if ((!domainName) || (!url)) {
return false;
}
return (ImagePreviewHelper.HTTP_TESTER.test(url))
&& ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
}

View File

@ -0,0 +1,107 @@
import _ from 'lodash';
import { ImagePreviewHelper } from './helper';
import ImagePreview from '../ImagePreview.vue';
export type RenderStyle = Record<string, any>;
export interface PreviewManagerHelper {
helper: ImagePreviewHelper;
renderStyle: RenderStyle;
}
export class PreviewManager {
private parent: ImagePreview;
private helpers: PreviewManagerHelper[];
private debugMode = false;
constructor(parent: ImagePreview, helperInstances: ImagePreviewHelper[]) {
this.parent = parent;
this.helpers = _.map(helperInstances, (helper) => ({ helper, renderStyle: {}}));
}
match(domain: string | undefined, url: string | undefined): PreviewManagerHelper | undefined {
return _.find(this.helpers, (h) => h.helper.match(domain, url));
}
matchIndex(domain: string | undefined, url: string | undefined): number {
return _.findIndex(this.helpers, (h) => h.helper.match(domain, url));
}
renderStyles(): Record<string, RenderStyle> {
_.each(
this.helpers,
(h) => {
h.renderStyle = h.helper.renderStyle();
this.debugLog('ImagePreview: pm.renderStyles()', h.helper.constructor.name, JSON.parse(JSON.stringify(h.renderStyle)));
}
);
return _.fromPairs(
_.map(
this.helpers, (h) => ([h.helper.constructor.name, h.renderStyle])
)
);
}
getVisiblePreview(): ImagePreviewHelper | undefined {
const found = _.find(this.helpers, (h) => h.helper.isVisible());
return found ? found.helper : undefined;
}
show(url: string | undefined, domain: string | undefined): ImagePreviewHelper | undefined {
const matchedHelper = this.match(domain, url);
_.each(
_.filter(this.helpers, (h) => (h !== matchedHelper)),
(h) => h.helper.hide()
);
if (!matchedHelper) {
this.debugLog('ImagePreview: pm.show()', 'Unmatched helper', url, domain);
return undefined;
}
matchedHelper.helper.show(url);
return matchedHelper.helper;
}
hide(): void {
_.each(
this.helpers,
(h) => {
this.debugLog('ImagePreview: pm.hide()', h.helper.constructor.name, h.helper.isVisible());
h.helper.hide();
}
);
}
getVisibilityStatus(): Record<string, boolean> {
return _.fromPairs(
_.map(
this.helpers, (h) => [h.helper.constructor.name, h.helper.isVisible()]
)
);
}
setDebug(debugMode: boolean): void {
_.each(this.helpers, (h) => h.helper.setDebug(debugMode));
this.debugMode = debugMode;
}
debugLog(...messages: any[]): void {
if (this.debugMode) {
this.parent.debugLog(...messages);
}
}
}

View File

@ -92,6 +92,15 @@ export enum FurryPreference {
FurriesPreferredHumansOk = 149
}
export const furryPreferenceMapping = {
[FurryPreference.FurriesOnly]: 'furries only',
[FurryPreference.FursAndHumans]: 'loves furries and humans',
[FurryPreference.HumansOnly]: 'humans only',
[FurryPreference.HumansPreferredFurriesOk]: 'loves humans, likes furries',
[FurryPreference.FurriesPreferredHumansOk]: 'loves furries, likes humans'
};
export interface GenderKinkIdMap {
[key: number]: Kink
}

View File

@ -1,13 +1,12 @@
<template>
<div id="character-page-sidebar" class="card bg-light">
<div class="card-header">
<span class="character-name">{{ character.character.name }}</span>
<div class="card-body">
<img :src="avatarUrl(character.character.name)" class="character-avatar" style="margin-right:10px">
<div v-if="character.character.title" class="character-title">{{ character.character.title }}</div>
<character-action-menu :character="character" @rename="showRename()" @delete="showDelete()"
@block="showBlock()"></character-action-menu>
</div>
<div class="card-body">
<img :src="avatarUrl(character.character.name)" class="character-avatar" style="margin-right:10px">
<div v-if="authenticated" class="d-flex justify-content-between flex-wrap character-links-block">
<template v-if="character.is_self">
<a :href="editUrl" class="edit-link"><i class="fa fa-fw fa-pencil-alt"></i>Edit</a>