<template> <div class="row character-page" id="pageBody" ref="pageBody"> <div class="col-12" style="min-height:0"> <div class="alert alert-info" v-show="loading">Loading character information.</div> <div class="alert alert-danger" v-show="error">{{error}}</div> </div> <div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character && character.character && characterMatch && selfCharacter"> <sidebar :character="character" :characterMatch="characterMatch" @memo="memo" @bookmarked="bookmarked" :oldApi="oldApi"></sidebar> </div> <div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character && character.character && characterMatch && selfCharacter"> <div id="characterView"> <div> <div v-if="character.ban_reason" id="headerBanReason" class="alert alert-warning"> This character has been banned and is not visible to the public. Reason: <br/> {{ character.ban_reason }} <template v-if="character.timeout"><br/>Timeout expires: <date :time="character.timeout"></date> </template> </div> <div v-if="character.block_reason" id="headerBlocked" class="alert alert-warning"> This character has been blocked and is not visible to the public. Reason: <br/> {{ character.block_reason }} </div> <div v-if="character.memo" id="headerCharacterMemo" class="alert alert-info">Memo: {{ character.memo.memo }}</div> <div class="card bg-light"> <div class="card-header character-card-header"> <tabs class="card-header-tabs" v-model="tab"> <span>Overview</span> <span>Info</span> <span v-if="!oldApi">Groups <span class="tab-count" v-if="groups !== null">({{ groups.length }})</span></span> <span>Images <span class="tab-count">({{ character.character.image_count }})</span></span> <span v-if="character.settings.guestbook">Guestbook <span class="tab-count" v-if="guestbook !== null">({{ guestbook.posts.length }})</span></span> <span v-if="character.is_self || character.settings.show_friends">Friends <span class="tab-count" v-if="friends !== null">({{ friends.length }})</span></span> <span>Recon</span> </tabs> </div> <div class="card-body"> <div class="tab-content"> <div role="tabpanel" v-show="tab === '0'" id="overview"> <match-report :characterMatch="characterMatch" v-if="shouldShowMatch()"></match-report> <div style="margin-bottom:10px" class="character-description"> <bbcode :text="character.character.description"></bbcode> </div> <character-kinks :character="character" :oldApi="oldApi" ref="tab0" :autoExpandCustoms="autoExpandCustoms"></character-kinks> </div> <div role="tabpanel" v-show="tab === '1'" id="infotags"> <character-infotags :character="character" ref="tab1" :characterMatch="characterMatch"></character-infotags> </div> <div role="tabpanel" v-show="tab === '2'" v-if="!oldApi"> <character-groups :character="character" ref="tab2"></character-groups> </div> <div role="tabpanel" v-show="tab === '3'"> <character-images :character="character" ref="tab3" :use-preview="imagePreview" :injected-images="images"></character-images> </div> <div v-if="character.settings.guestbook" role="tabpanel" v-show="tab === '4'" id="guestbook"> <character-guestbook :character="character" :oldApi="oldApi" ref="tab4"></character-guestbook> </div> <div v-if="character.is_self || character.settings.show_friends" role="tabpanel" v-show="tab === '5'" id="friends"> <character-friends :character="character" ref="tab5"></character-friends> </div> <div role="tabpanel" v-show="tab === '6'"> <character-recon :character="character" ref="tab6"></character-recon> </div> </div> </div> </div> </div> </div> </div> </div> </template> <script lang="ts"> import * as _ from 'lodash'; import {Component, Hook, Prop, Watch} from '@f-list/vue-ts'; import Vue from 'vue'; import log from 'electron-log'; //tslint:disable-line:match-default-export-name import {StandardBBCodeParser} from '../../bbcode/standard'; import {BBCodeView} from '../../bbcode/view'; import { CharacterCacheRecord } from '../../learn/profile-cache'; import * as Utils from '../utils'; import {methods, Store} from './data_store'; import {Character, CharacterGroup, Guestbook, SharedStore} from './interfaces'; import DateDisplay from '../../components/date_display.vue'; import Tabs from '../../components/tabs'; import FriendsView from './friends.vue'; import GroupsView from './groups.vue'; import GuestbookView from './guestbook.vue'; import ImagesView from './images.vue'; import InfotagsView from './infotags.vue'; import CharacterKinksView from './kinks.vue'; import ReconView from './recon.vue'; import Sidebar from './sidebar.vue'; import core from '../../chat/core'; import { Matcher, MatchReport } from '../../learn/matcher'; import MatchReportView from './match-report.vue'; import { CharacterImage, SimpleCharacter } from '../../interfaces'; const CHARACTER_CACHE_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7 days (milliseconds) const CHARACTER_META_CACHE_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7 days (milliseconds) interface ShowableVueTab extends Vue { show?(): void } const standardParser = new StandardBBCodeParser(); @Component({ components: { sidebar: Sidebar, date: DateDisplay, tabs: Tabs, 'character-friends': FriendsView, 'character-guestbook': GuestbookView, 'character-groups': GroupsView, 'character-infotags': InfotagsView, 'character-images': ImagesView, 'character-kinks': CharacterKinksView, 'character-recon': ReconView, 'match-report': MatchReportView, bbcode: BBCodeView(standardParser) } }) export default class CharacterPage extends Vue { @Prop readonly name?: string; @Prop readonly id?: number; @Prop({required: true}) readonly authenticated!: boolean; @Prop readonly oldApi?: true; @Prop readonly imagePreview?: true; shared: SharedStore = Store; character: Character | undefined; loading = true; refreshing = false; error = ''; tab = '0'; autoExpandCustoms = false; /* guestbookPostCount: number | null = null; friendCount: number | null = null; groupCount: number | null = null; */ guestbook: Guestbook | null = null; friends: SimpleCharacter[] | null = null; groups: CharacterGroup[] | null = null; images: CharacterImage[] | null = null; selfCharacter: Character | undefined; characterMatch: MatchReport | undefined; @Hook('beforeMount') beforeMount(): void { this.shared.authenticated = this.authenticated; // console.log('Beforemount'); } @Hook('mounted') async mounted(): Promise<void> { await this.load(false); // console.log('mounted'); } @Watch('tab') switchTabHook(): void { const target = <ShowableVueTab>this.$refs[`tab${this.tab}`]; //tslint:disable-next-line:no-unbound-method if(typeof target.show === 'function') target.show(); } @Watch('name') async onCharacterSet(): Promise<void> { this.tab = '0'; this.autoExpandCustoms = core.state.settings.risingAutoExpandCustomKinks; await this.load(); // Kludge kluge this.$nextTick( () => { const el = document.querySelector('.modal .profile-viewer .modal-body'); if (!el) { console.error('Could not find modal body for profile view'); return; } el.scrollTo(0, 0); } ); } shouldShowMatch(): boolean { if (this.character?.character.name === 'YiffBot 4000') { return false; } return core.state.settings.risingAdScore; } async reload(): Promise<void> { await this.load(true, true); const target = <ShowableVueTab>this.$refs[`tab${this.tab}`]; //tslint:disable-next-line:no-unbound-method if(typeof target.show === 'function') target.show(); } async load(mustLoad: boolean = true, skipCache: boolean = false): Promise<void> { this.loading = true; this.refreshing = false; this.error = ''; try { const due: Promise<void>[] = []; if(this.name === undefined || this.name.length === 0) return; await methods.fieldsGet(); if ( ((this.selfCharacter === undefined) && (Utils.settings.defaultCharacter >= 0)) || (_.get(this.selfCharacter, 'character.name') !== core.characters.ownCharacter.name) ) { due.push(this.loadSelfCharacter()); } if((mustLoad) || (this.character === undefined)) due.push(this._getCharacter(skipCache)); await Promise.all(due); } catch(e) { console.error(e); this.error = Utils.isJSONError(e) ? <string>e.response.data.error : (<Error>e).message; Utils.ajaxError(e, 'Failed to load character information.'); } this.loading = false; } async updateGuestbook(): Promise<void> { try { if ((!this.character) || (!_.get(this.character, 'settings.guestbook'))) { this.guestbook = null; return; } this.guestbook = await methods.guestbookPageGet(this.character.character.id, 1); } catch (err) { console.error(err); this.guestbook = null; } } async updateGroups(): Promise<void> { try { if ((!this.character) || (this.oldApi)) { this.groups = null; return; } this.groups = await methods.groupsGet(this.character.character.id); } catch (err) { console.error('Update groups', err); this.groups = null; } } async updateFriends(): Promise<void> { try { if ( (!this.character) || (!this.character.is_self) && (!this.character.settings.show_friends) ) { this.friends = null; return; } this.friends = await methods.friendsGet(this.character.character.id); } catch (err) { console.error('Update friends', err); this.friends = null; } } async updateImages(): Promise<void> { try { if (!this.character) { this.images = null; return; } this.images = await methods.imagesGet(this.character.character.id); } catch (err) { console.error('Update images', err); this.images = null; } } async updateMeta(name: string): Promise<void> { await Promise.all( [ this.updateImages(), this.updateGuestbook(), this.updateFriends(), this.updateGroups() ] ); await core.cache.profileCache.registerMeta( name, { lastMetaFetched: new Date(), groups: this.groups, friends: this.friends, guestbook: this.guestbook, images: this.images } ); } memo(memo: {id: number, memo: string}): void { Vue.set(this.character!, 'memo', memo); void core.cache.profileCache.register(this.character!); } bookmarked(state: boolean): void { Vue.set(this.character!, 'bookmarked', state); void core.cache.profileCache.register(this.character!); } protected async loadSelfCharacter(): Promise<void> { // console.log('SELF'); // const ownChar = core.characters.ownCharacter; // this.selfCharacter = await methods.characterData(ownChar.name, -1); this.selfCharacter = core.characters.ownProfile; // console.log('SELF LOADED'); this.updateMatches(); } private async fetchCharacterCache(): Promise<CharacterCacheRecord | null> { if (!this.name) { throw new Error('A man must have a name'); } // tslint:disable-next-line: await-promise return (await core.cache.profileCache.get(this.name)) || null; } private async _getCharacter(skipCache: boolean = false): Promise<void> { log.debug('profile.getCharacter', { name: this.name } ); this.character = undefined; this.friends = null; this.groups = null; this.guestbook = null; this.images = null; if (!this.name) { return; } const cache = await this.fetchCharacterCache(); this.character = (cache && !skipCache) ? cache.character : await methods.characterData(this.name, this.id, false); standardParser.inlines = this.character.character.inlines; if ((cache) && (cache.meta)) { this.guestbook = cache.meta.guestbook; this.friends = cache.meta.friends; this.groups = cache.meta.groups; this.images = cache.meta.images; } if ( (cache && !skipCache) && (cache.meta) && (cache.meta.lastMetaFetched) && (Date.now() - cache.meta.lastMetaFetched.getTime() < CHARACTER_META_CACHE_EXPIRE) ) { // do nothing } else { log.debug('profile.updateMeta', { timestamp: cache?.meta?.lastMetaFetched, diff: Date.now() - (cache?.meta?.lastMetaFetched?.getTime() || 0) }); // No await on purpose: // tslint:disable-next-line no-floating-promises this.updateMeta(this.name).catch(err => console.error('profile.updateMeta', err)); } // console.log('LoadChar', this.name, this.character); this.updateMatches(); // old profile cache, let's refresh if ((cache) && (cache.lastFetched)) { if (Date.now() - cache.lastFetched.getTime() >= CHARACTER_CACHE_EXPIRE) { // No await on purpose: // tslint:disable-next-line no-floating-promises this.refreshCharacter(); } } } private async refreshCharacter(): Promise<void> { this.refreshing = true; try { const character = await methods.characterData(this.name, this.id, false); if ((!this.refreshing) || (this.name !== character.character.name)) { return; } this.character = character; this.updateMatches(); // No awaits on these on purpose: // tslint:disable-next-line no-floating-promises this.updateMeta(this.name); } finally { this.refreshing = false; } } private updateMatches(): void { if ((!this.selfCharacter) || (!this.character)) return; this.characterMatch = Matcher.identifyBestMatchReport(this.selfCharacter.character, this.character.character); // console.log('Match', this.selfCharacter.character.name, this.character.character.name, this.characterMatch); } } </script> <style lang="scss"> .compare-highlight-block { margin-bottom: 3px; .quick-compare-block button { margin-left: 2px; } } .character-kinks-block { i.fa { margin-right: 0.25rem; } .character-kink { .popover { display:block; bottom:100%; top:initial; // margin-bottom:5px; min-width: 200px; margin-bottom: 0; padding-bottom: 0; opacity: 1; } p { line-height: 125%; } p:last-child { margin-bottom:0; } &.comparison-result { margin: -4px; padding: 4px; padding-top: 2px; padding-bottom: 2px; margin-top: 1px; margin-bottom: 1px; border-radius: 3px; } } } .expanded-custom-kink { .custom-kink { margin-top: 14px; margin-bottom: 14px; } } .custom-kink { &:first-child { margin-top: 0; } &:last-child { margin-bottom: 0; } .kink-name { font-weight: bold; color: var(--characterKinkCustomColor); } i { color: var(--characterKinkCustomColor); } margin-top: 7px; margin-bottom: 7px; margin-left: -6px; margin-right: -6px; border: 1px var(--characterKinkCustomBorderColor) solid; border-radius: 2px; /* border-collapse: collapse; */ padding: 5px; } .stock-kink { .kink-name, i { color: var(--characterKinkStockColor); font-weight: normal; } &.highlighted { .kink-name, i { font-weight: bold; color: var(--characterKinkStockHighlightedColor); } } } .character-kinks-block { .highlighting { .character-kink.stock-kink { .kink-name { opacity: 0.4; } &.highlighted { .kink-name { opacity: 1; } } } } } .kink-custom-desc { display: block; font-weight: normal; font-size: 0.9rem; color: var(--characterInfotagColor); line-height: 125%; } .infotag-label { display: block; /* margin-bottom: 1rem; */ font-weight: normal !important; line-height: 120%; font-size: 85%; color: var(--characterInfotagColor); } .infotag-value { display: block; margin-bottom: 1rem; font-weight: bold; line-height: 120%; } .quick-info-value { display: block; font-weight: bold; } .quick-info-label { display: block; /* margin-bottom: 1rem; */ font-weight: normal !important; line-height: 120%; font-size: 85%; color: var(--characterInfotagColor); } .quick-info { margin-bottom: 1rem; font-size: 0.9rem; color: var(--characterInfotagColor); } .guestbook-post { margin-bottom: 15px; margin-top: 15px; background-color: var(--characterGuestbookPostBg); border-radius: 5px; padding: 15px; border: 1px solid var(--characterGuestbookPostBorderColor); .characterLink { font-size: 20pt; } .guestbook-timestamp { color: var(--characterGuestbookTimestampFg); font-size: 85% } .guestbook-message { margin-top: 10px; display: block; } .guestbook-reply { margin-top: 20px; background-color: var(--characterGuestbookReplyBg); padding: 15px; border-radius: 4px; } } .contact-block { margin-top: 25px !important; margin-bottom: 25px !important; .contact-method { font-size: 80%; display: block; margin-bottom: 2px; img { border-radius: 2px; } } } #character-page-sidebar .character-list-block { .character-avatar.icon { height: 43px !important; width: 43px !important; border-radius: 3px; } .characterLink { font-size: 85%; padding-left: 3px; } } .character-images { column-width: auto; column-count: 2; column-gap: 0.5rem; .character-image-wrapper { display: inline-block; background-color: var(--characterImageWrapperBg); border-radius: 5px; box-sizing: border-box; margin: 5px; a { border: none; img { max-width: 100% !important; width: 100% !important; height: auto !important; object-fit: contain; object-position: top center; vertical-align: top !important; border-radius: 6px; } } .image-description { font-size: 85%; padding-top: 5px; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; } } } .infotag { &.match-score { padding-top: 2px; padding-left: 4px; padding-right: 4px; margin-left: -4px; margin-right: -4px; border-radius: 2px; padding-bottom: 2px; margin-bottom: 1rem; .infotag-value { margin-bottom: 0; } } } .match-report { display: flex; flex-direction: row; background-color: var(--scoreReportBg); /* width: 100%; */ margin-top: -1.2rem; margin-left: -1.2rem; margin-right: -1.2rem; padding: 1rem; margin-bottom: 1rem; padding-bottom: 0; padding-top: 0.5rem; .thumbnail { width: 50px; height: 50px; } &.minimized { height: 0; overflow: hidden; background-color: transparent; .vs, .scores { display: none; } .minimize-btn { opacity: 0.6; } } h3 { font-size: 1.25rem; } .minimize-btn { position: absolute; display: block; right: 0.5rem; background-color: var(--scoreMinimizeButtonBg); padding: 0.4rem; padding-top: 0.2rem; padding-bottom: 0.2rem; font-size: 0.8rem; color: var(--scoreMinimizeButtonFg); border-radius: 4px; z-index: 1000; } .scores { float: left; flex: 1; margin: 0; max-width: 25rem; &.you { margin-right: 1rem; } &.them { margin-left: 1rem; } .species { display: inline-block; color: var(--characterInfotagColor); // opacity: 0.7; } ul { padding: 0; list-style: none; } .match-score { font-size: 0.85rem; border-radius: 2px; margin-bottom: 4px; padding: 2px; padding-left: 4px; padding-right: 4px; span { color: var(--scoreTitleColor); font-weight: bold; } } } .vs { margin-left: 1rem; margin-right: 1rem; text-align: center; font-size: 5rem; line-height: 0; color: rgba(255, 255, 255, 0.3); margin-top: auto; margin-bottom: auto; font-style: italic; font-family: 'Times New Roman', Georgia, serif; } } .character-kinks-block .character-kink.comparison-favorite, .match-report .scores .match-score.match, .infotag.match { background-color: var(--scoreMatchBg); border: solid 1px var(--scoreMatchFg); } .character-kinks-block .character-kink.comparison-yes, .match-report .scores .match-score.weak-match, .infotag.weak-match { background-color: var(--scoreWeakMatchBg); border: 1px solid var(--scoreWeakMatchFg); } .character-kinks-block .character-kink.comparison-maybe, .match-report .scores .match-score.weak-mismatch, .infotag.weak-mismatch { background-color: var(--scoreWeakMismatchBg); border: 1px solid var(--scoreWeakMismatchFg); } .character-kinks-block .character-kink.comparison-no, .match-report .scores .match-score.mismatch, .infotag.mismatch { background-color: var(--scoreMismatchBg); border: 1px solid var(--scoreMismatchFg); } .character-kinks-block .highlighting { .character-kink { &.comparison-favorite { background-color: var(--scoreFadedMatchBg); border-color: var(--scoreFadedMatchFg); &.highlighted { background-color: var(--scoreMatchBg); border-color: var(--scoreMatchFg); } } &.comparison-yes { background-color: var(--scoreWeakMatchBg); border-color: var(--scoreWeakMatchFg); &.highlighted { background-color: var(--scoreWeakMatchBg); border-color: var(--scoreWeakMatchFg); } } &.comparison-maybe { background-color: var(--scoreWeakMismatchBg); border-color: var(--scoreWeakMismatchFg); &.highlighted { background-color: var(--scoreWeakMismatchBg); border-color: var(--scoreWeakMismatchFg); } } &.comparison-no { background-color: var(--scoreMismatchBg); border-color: var(--scoreMismatchFg); &.highlighted { background-color: var(--scoreMismatchBg); border-color: var(--scoreMismatchFg); } } } } .tab-count { color: var(--tabSecondaryFgColor); } .character-card-header { position: sticky; top: -1rem; z-index: 10000; background: var(--headerBackgroundMaskColor) !important; } .character-description .bbcode { white-space: pre-line !important; blockquote { margin: 0; background-color: var(--characterImageWrapperBg); padding: 1em; border-radius: 3px; .quoteHeader { border-bottom: 1px solid; text-transform: uppercase; font-weight: bold; font-size: 80%; opacity: 0.7; } } } </style>