Expand / minimize custom tags; better character image view; better guestbook view; mouseover preview on character profile links

This commit is contained in:
Mr. Stallion 2019-06-23 16:15:21 -05:00
parent af1960ed02
commit 7bdf9d815a
18 changed files with 516 additions and 75 deletions

View File

@ -72,6 +72,7 @@ export class CoreBBCodeParser extends BBCodeParser {
parent.appendChild(el);
return el;
}));
this.addTag(new BBCodeTextTag('url', (parser, parent, param, content) => {
const tagData = analyzeUrlTag(parser, param, content);
const element = parser.createElement('span');

View File

@ -1,7 +1,11 @@
import Vue from 'vue';
import { BBCodeElement } from '../chat/bbcode';
import {InlineImage} from '../interfaces';
import {CoreBBCodeParser} from './core';
import { analyzeUrlTag, CoreBBCodeParser } from './core';
import {InlineDisplayMode} from './interfaces';
import {BBCodeCustomTag, BBCodeSimpleTag, BBCodeTextTag} from './parser';
import UrlTagView from '../chat/UrlTagView.vue';
interface StandardParserSettings {
siteDomain: string
@ -16,6 +20,8 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
allowInlines = true;
inlines: {[key: string]: InlineImage | undefined} | undefined;
cleanup: Vue[] = [];
createInline(inline: InlineImage): HTMLElement {
const p1 = inline.hash.substr(0, 2);
const p2 = inline.hash.substr(2, 2);
@ -187,6 +193,38 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
} else parent.appendChild(element = parser.createInline(inline));
return element;
}));
this.addTag(new BBCodeTextTag(
'url',
(parser, parent, _, content) => {
const tagData = analyzeUrlTag(parser, _, content);
const root = parser.createElement('span');
// const el = parser.createElement('span');
parent.appendChild(root);
// root.appendChild(el);
if (!tagData.success) {
root.textContent = tagData.textContent;
return;
}
const view = new UrlTagView({el: root, propsData: {url: tagData.url, text: tagData.textContent, domain: tagData.domain}});
this.cleanup.push(view);
return root;
}));
}
parseEverything(input: string): BBCodeElement {
const elm = <BBCodeElement>super.parseEverything(input);
if(this.cleanup.length > 0)
elm.cleanup = ((cleanup: Vue[]) => () => {
for(const component of cleanup) component.$destroy();
})(this.cleanup);
this.cleanup = [];
return elm;
}
}

View File

@ -90,6 +90,7 @@
<report-dialog ref="reportDialog"></report-dialog>
<user-menu ref="userMenu" :reportDialog="$refs['reportDialog']"></user-menu>
<recent-conversations ref="recentDialog"></recent-conversations>
<image-preview ref="imagePreview"></image-preview>
</div>
</template>
@ -114,6 +115,7 @@
import {getStatusIcon} from './user_view';
import UserList from './UserList.vue';
import UserMenu from './UserMenu.vue';
import ImagePreview from './ImagePreview.vue';
const unreadClasses = {
[Conversation.UnreadState.None]: '',
@ -125,7 +127,8 @@
components: {
'user-list': UserList, channels: ChannelList, 'status-switcher': StatusSwitcher, 'character-search': CharacterSearch,
settings: SettingsView, conversation: ConversationView, 'report-dialog': ReportDialog, sidebar: Sidebar,
'user-menu': UserMenu, 'recent-conversations': RecentConversations
'user-menu': UserMenu, 'recent-conversations': RecentConversations,
'image-preview': ImagePreview
}
})
export default class ChatView extends Vue {

View File

@ -136,7 +136,6 @@
<settings ref="settingsDialog" :conversation="conversation"></settings>
<logs ref="logsDialog" :conversation="conversation"></logs>
<manage-channel ref="manageDialog" v-if="isChannel(conversation)" :channel="conversation.channel"></manage-channel>
<image-preview ref="imagePreview"></image-preview>
</div>
</template>
@ -151,7 +150,6 @@
import { characterImage, getByteLength, getKey } from "./common";
import ConversationSettings from './ConversationSettings.vue';
import core from './core';
import ImagePreview from './ImagePreview.vue';
import {Channel, channelModes, Character, Conversation, Settings} from './interfaces';
import l from './localize';
import Logs from './Logs.vue';
@ -164,8 +162,7 @@
@Component({
components: {
user: UserView, 'bbcode-editor': Editor, 'manage-channel': ManageChannel, settings: ConversationSettings,
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp,
'image-preview': ImagePreview
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp
}
})
export default class ConversationView extends Vue {

View File

@ -267,4 +267,8 @@
background-repeat: no-repeat;
// background-color: black;
}
.image-preview-wrapper {
z-index: 10000;
}
</style>

View File

@ -38,7 +38,10 @@ export class ImagePreviewMutator {
this.add('e-hentai.org', this.getBaseJsMutatorScript('#img, video'));
this.add('gelbooru.com', this.getBaseJsMutatorScript('#image, video'));
this.add('chan.sankakucomplex.com', this.getBaseJsMutatorScript('#image, video'));
this.add('danbooru.donmai.us', this.getBaseJsMutatorScript('#image, video'));
this.add('gfycat.com', this.getBaseJsMutatorScript('video'));
this.add('gfycatporn.com', this.getBaseJsMutatorScript('video'));
this.add('www.youtube.com', this.getBaseJsMutatorScript('video'));
// this fixes videos only -- images are fine as is
this.add('i.imgur.com', this.getBaseJsMutatorScript('video'));
@ -46,9 +49,9 @@ export class ImagePreviewMutator {
this.add(
'imgur.com',
`
const imageCount = $('.post-image-container').length;
const imageCount = $('.post-container video, .post-container img').length;
${this.getBaseJsMutatorScript('.image.post-image img, .image.post-image video')}
${this.getBaseJsMutatorScript('.post-container video, .post-container img', true)}
if(imageCount > 1)
$('#flistWrapper').append('<div id="imageCount" style="position: absolute; bottom: 0; right: 0; background: green; border: 2px solid lightgreen; width: 5rem; height: 5rem; font-size: 2rem; font-weight: bold; color: white; border-radius: 5rem; margin: 0.75rem;"><div style="position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%);">+' + (imageCount - 1) + '</div></div>');
@ -65,28 +68,41 @@ export class ImagePreviewMutator {
);
}
getBaseJsMutatorScript(imageSelector: string): string {
getBaseJsMutatorScript(imageSelector: string, skipElementRemove = false): string {
return `const body = document.querySelector('body');
const img = document.querySelector('${imageSelector}');
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = 'width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: 100000; '
+ 'background-color: black; background-size: contain; background-repeat: no-repeat; background-position: top left;';
img.remove();
el.append(img);
body.append(el);
body.style = 'padding: 0; margin: 0; overflow: hidden; width: 100%; height: 100%';
img.style = 'object-position: top left; object-fit: contain; width: 100%; height: 100%;'
if (img.play) { img.muted = true; img.play(); }
let removeList = [];
body.childNodes.forEach((el) => { if(el.id !== 'flistWrapper') { removeList.push(el); } });
removeList.forEach((el) => el.remove());
removeList = [];
const img = document.querySelector('${imageSelector}');
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = 'width: 100% !important; height: 100% !important; position: absolute !important;'
+ 'top: 0 !important; left: 0 !important; z-index: 100000 !important;'
+ 'background-color: black !important; background-size: contain !important;'
+ 'background-repeat: no-repeat !important; background-position: top left !important;'
+ 'opacity: 1 !important; padding: 0 !important; border: 0 !important; margin: 0 !important;';
img.remove();
el.append(img);
body.append(el);
body.class = '';
body.style = 'border: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'top: 0 !important; left: 0 !important;';
img.style = 'object-position: top left !important; object-fit: contain !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'margin: 0 !imporant; border: 0 !important; padding: 0 !important;';
img.class = '';
el.class = '';
if (img.play) { img.muted = true; img.play(); }
let removeList = [];
body.childNodes.forEach((el) => { if(el.id !== 'flistWrapper') { removeList.push(el); } });
${skipElementRemove ? '' : 'removeList.forEach((el) => el.remove());'}
removeList = [];
`;
}

View File

@ -2,12 +2,13 @@ import Axios from 'axios';
import Vue from 'vue';
import Editor from '../bbcode/Editor.vue';
import {InlineDisplayMode} from '../bbcode/interfaces';
import {initParser, standardParser} from '../bbcode/standard';
import { initParser, standardParser } from '../bbcode/standard';
import CharacterLink from '../components/character_link.vue';
import CharacterSelect from '../components/character_select.vue';
import {setCharacters} from '../components/character_select/character_list';
import DateDisplay from '../components/date_display.vue';
import SimplePager from '../components/simple_pager.vue';
import {
Character as CharacterInfo, CharacterImage, CharacterImageOld, CharacterInfotag, CharacterSettings, KinkChoice
} from '../interfaces';
@ -180,6 +181,7 @@ async function kinksGet(id: number): Promise<CharacterKink[]> {
});
}
export function init(characters: {[key: string]: number}): void {
Utils.setDomains(parserSettings.siteDomain, parserSettings.staticDomain);
initParser(parserSettings);

View File

@ -152,11 +152,11 @@
parent.send('switch-tab', this.character);
});
if (process.env.NODE_ENV !== 'production') {
/*if (process.env.NODE_ENV !== 'production') {
const dt = require('@vue/devtools');
dt.connect();
}
}*/
}

121
electron/matcher.ts Normal file
View File

@ -0,0 +1,121 @@
/*
export enum TagId {
Age = 1,
Orientation = 2,
Gender = 3,
Build = 13,
FurryPreference = 49,
BdsmRole = 15,
Position = 41,
BodyType = 51,
ApparentAge = 64,
RelationshipStatus = 42,
Species = 9,
LanguagePreference = 49
}
export enum Orientation {
Straight = 4,
Gay = 5,
Bisexual = 6,
Asexual = 7,
Unsure = 8,
BiMalePreference = 89,
BiFemalePreference = 90,
Pansexual = 127,
BiCurious = 128
}
orientationCompatibilityMap[Orientation.Straight] = [
[Orientation.Straight, 1],
[Orientation.Gay, -1],
[Orientation.Bisexual, 1],
[Orientation.Asexual, 0],
[Orientation.Unsure, 0],
[Orientation.BiMalePreference, (c: CharacterInfo) => (isMale(c) ? 1 : 0.5)],
[Orientation.BiFemalePreference, (c: CharacterInfo) => (isFemale(c) ? 1 : 0.5)],
[Orientation.Pansexual, 1],
[Orientation.BiCurious, 0]
];
orientationCompatibilityMap[Orientation.Gay] = [
[Orientation.Straight, -1],
[Orientation.Gay, 1],
[Orientation.Bisexual, 1],
[Orientation.Asexual, 0],
[Orientation.Unsure, 0],
[Orientation.BiMalePreference, (c: CharacterInfo) => (isMale(c) ? 1 : 0.5)],
[Orientation.BiFemalePreference, (c: CharacterInfo) => (isFemale(c) ? 1 : 0.5)],
[Orientation.Pansexual, 1],
[Orientation.BiCurious, (c: CharacterInfo, t) => isSameGender(c, t) ? 0.5 : 1]
];
orientationCompatibilityMap[Orientation.Bisexual] = [
[Orientation.Straight, 1],
[Orientation.Gay, 1],
[Orientation.Bisexual, 1],
[Orientation.Asexual, 0],
[Orientation.Unsure, 0],
[Orientation.BiMalePreference, (c: CharacterInfo) => (isMale(c) ? 1 : 0.5)],
[Orientation.BiFemalePreference, (c: CharacterInfo) => (isFemale(c) ? 1 : 0.5)],
[Orientation.Pansexual, 1],
[Orientation.BiCurious, 0]
];
orientationCompatibilityMap[Orientation.Asexual] = [];
orientationCompatibilityMap[Orientation.Unsure] = [];
orientationCompatibilityMap[Orientation.BiMalePreference] = [
[Orientation.Straight, -1],
[Orientation.Gay, 1],
[Orientation.Bisexual, 1],
[Orientation.Asexual, 0],
[Orientation.Unsure, 0],
[Orientation.BiMalePreference, (c: CharacterInfo) => (isMale(c) ? 1 : 0.5)],
[Orientation.BiFemalePreference, (c: CharacterInfo) => (isFemale(c) ? 1 : 0.5)],
[Orientation.Pansexual, 1],
[Orientation.BiCurious, 0]
];
*/
export class Matcher {
static readonly TAGID_AGE = 1;
static readonly TAGID_ORIENTATION = 2;
static readonly TAGID_GENDER = 3;
static readonly TAGID_FURRY_PREFERENCE = 49;
static readonly TAGID_BUILD = 13;
static readonly TAGID_BDSM_ROLE = 15;
static readonly TAGID_POSITION = 41;
static readonly TAGID_BODY_TYPE = 51;
static readonly TAGID_APPARENT_AGE = 64;
static readonly TAGID_RELATIONSHIP = 42;
static readonly TAGID_SPECIES = 9;
static readonly TAGID_LANGUAGE_PREFERENCE = 49;
}

View File

@ -132,3 +132,16 @@ div.indentText {
.user-link {
text-shadow: none;
}
.bbcode {
.fa-link {
color: white;
margin-right: 2px;
}
a.user-link {
color: white;
}
}

View File

@ -5,7 +5,7 @@
<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">
<sidebar :character="character" @memo="memo" @bookmarked="bookmarked" :oldApi="oldApi"></sidebar>
<sidebar :character="character" :selfCharacter="selfCharacter" @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">
<div id="characterView">
@ -82,6 +82,7 @@
import InfotagsView from './infotags.vue';
import CharacterKinksView from './kinks.vue';
import Sidebar from './sidebar.vue';
import core from '../../chat/core';
interface ShowableVueTab extends Vue {
show?(): void
@ -116,6 +117,8 @@
error = '';
tab = '0';
selfCharacter: Character | undefined;
@Hook('beforeMount')
beforeMount(): void {
this.shared.authenticated = this.authenticated;
@ -123,7 +126,7 @@
@Hook('mounted')
async mounted(): Promise<void> {
if(this.character === undefined) await this._getCharacter();
await this.load(false);
}
@Watch('tab')
@ -136,9 +139,34 @@
@Watch('name')
async onCharacterSet(): Promise<void> {
this.tab = '0';
return this._getCharacter();
return this.load();
}
async load(mustLoad = true) {
this.loading = true;
try {
const due: Promise<any>[] = [];
if ((this.selfCharacter === undefined) && (Utils.Settings.defaultCharacter >= 0)) {
due.push(this.loadSelfCharacter());
}
if((mustLoad === true) || (this.character === undefined)) {
due.push(this._getCharacter());
}
await Promise.all(due);
} catch(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;
}
memo(memo: {id: number, memo: string}): void {
Vue.set(this.character!, 'memo', memo);
}
@ -147,29 +175,73 @@
Vue.set(this.character!, 'bookmarked', state);
}
protected async loadSelfCharacter(): Promise<Character> {
console.log('SELF');
const ownChar = core.characters.ownCharacter;
this.selfCharacter = await methods.characterData(ownChar.name, -1);
console.log('SELF LOADED');
return this.selfCharacter;
}
private async _getCharacter(): Promise<void> {
this.error = '';
this.character = undefined;
if(this.name === undefined || this.name.length === 0)
return;
try {
this.loading = true;
await methods.fieldsGet();
this.character = await methods.characterData(this.name, this.characterid);
standardParser.allowInlines = true;
standardParser.inlines = this.character.character.inlines;
} catch(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;
await methods.fieldsGet();
this.character = await methods.characterData(this.name, this.characterid);
standardParser.allowInlines = true;
standardParser.inlines = this.character.character.inlines;
}
}
</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 {
min-width: 200px;
margin-bottom: 0;
padding-bottom: 0;
}
p {
line-height: 125%;
}
p:last-child {
margin-bottom:0;
}
}
}
.expanded-custom-kink {
.custom-kink {
margin-top: 14px;
margin-bottom: 14px;
}
}
.custom-kink {
&:first-child {
@ -180,12 +252,19 @@
margin-bottom: 0;
}
font-weight: bold;
margin-top: 14px;
margin-bottom: 14px;
.kink-name {
font-weight: bold;
color: #f2cd00;
}
i {
color: #f2cd00;
}
margin-top: 7px;
margin-bottom: 7px;
margin-left: -6px;
margin-right: -6px;
color: #f2cd00;
border: 1px rgba(255, 255, 255, 0.1) solid;
border-radius: 2px;
/* border-collapse: collapse; */
@ -193,6 +272,14 @@
}
.stock-kink {
.kink-name, i {
color: #ededf6;
font-weight: normal;
}
}
.kink-custom-desc {
display: block;
font-weight: normal;
@ -239,13 +326,98 @@
color: rgba(255, 255, 255, 0.7);
}
img.character-image {
max-width: 33% !important;
width: 33% !important;
height: auto !important;
object-fit: contain;
object-position: top center;
vertical-align: top !important;
.guestbook-post {
margin-bottom: 15px;
margin-top: 15px;
background-color: rgba(0,0,0,0.15);
border-radius: 5px;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
.characterLink {
font-size: 20pt;
}
.guestbook-timestamp {
color: rgba(255, 255, 255, 0.3);
font-size: 85%
}
.guestbook-message {
margin-top: 10px;
}
.guestbook-reply {
margin-top: 20px;
background-color: rgba(0,0,0, 0.1);
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 {
.character-image-wrapper {
display: inline-block;
background-color: rgba(0,0,0, 0.2);
border-radius: 5px;
width: calc(50% - 20px);
box-sizing: border-box;
margin: 5px;
// float: left;
/* margin-bottom: auto; */
/* margin-top: auto; */
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;
}
}
}
</style>

View File

@ -12,7 +12,7 @@
<span v-show="!post.approved" class="post-unapproved"> (unapproved)</span>
<span class="guestbook-timestamp">
<character-link :character="post.character"></character-link>, posted <date-display
<character-link :character="post.character"></character-link> posted <date-display
:time="post.postedAt"></date-display>
</span>
<button class="btn btn-secondary" v-show="canEdit" @click="approve" :disabled="approving" style="margin-left:10px">

View File

@ -3,7 +3,13 @@
<div class="character-images">
<div v-show="loading" class="alert alert-info">Loading images.</div>
<template v-if="!loading">
<img :src="imageUrl(image)" :title="image.description" class="character-image" v-for="image in images" :key="image.id">
<!-- @click="handleImageClick($event, image)" -->
<div v-for="image in images" :key="image.id" class="character-image-wrapper">
<a :href="imageUrl(image)" target="_blank">
<img :src="imageUrl(image)" class="character-image">
</a>
<div class="image-description" v-if="!!image.description">{{image.description}}</div>
</div>
<!-- <div class="character-image col-6 col-sm-12 col-md-12" v-for="image in images" :key="image.id">-->
<!-- <img :src="imageUrl(image)" :title="image.description">-->

View File

@ -1,5 +1,5 @@
<template>
<div class="infotag">
<div :class="tagClasses">
<span class="infotag-label">{{label}}</span>
<span v-if="!contactLink" class="infotag-value">{{value}}</span>
<span v-if="contactLink" class="infotag-value"><a :href="contactLink">{{value}}</a></span>
@ -10,14 +10,55 @@
import {Component, Prop} from '@f-list/vue-ts';
import Vue from 'vue';
import {formatContactLink, formatContactValue} from './contact_utils';
import { Character, DisplayInfotag } from './interfaces';
// import { Character as CharacterInfo } from '../../interfaces';
import {Store} from './data_store';
import {DisplayInfotag} from './interfaces';
@Component
export default class InfotagView extends Vue {
@Prop({required: true})
private readonly infotag!: DisplayInfotag;
@Prop({required: true})
private readonly selfCharacter!: Character;
get tagClasses() {
const styles = {
infotag: true,
};
//console.log('TAG', this.label, this.value, this.infotag);
return this.getCharacterCompatibilityStyles(styles, this.selfCharacter);
}
getCharacterCompatibilityStyles(styles: any, a: Character) {
if (a.character.name) {
styles.infotag = true;
}
// const c: CharacterInfo = this.selfCharacter.character;
// const t = this.infotag;
/* console.log(this.label, this.value, this.infotag.id, this.infotag, c.infotags);
switch (t.id) {
case InfotagView.TAGID_ORIENTATION:
break;
default:
// do nothing;
break;
}*/
return styles;
}
get label(): string {
const infotag = Store.kinks.infotags[this.infotag.id];
if(typeof infotag === 'undefined')

View File

@ -3,7 +3,7 @@
<div class="infotag-group col-sm-3" v-for="group in groupedInfotags" :key="group.id" style="margin-top:5px">
<div class="infotag-title">{{group.name}}</div>
<hr>
<infotag :infotag="infotag" v-for="infotag in group.infotags" :key="infotag.id"></infotag>
<infotag :infotag="infotag" v-for="infotag in group.infotags" :key="infotag.id" :selfCharacter="selfCharacter"></infotag>
</div>
</div>
</template>
@ -30,6 +30,8 @@
export default class InfotagsView extends Vue {
@Prop({required: true})
private readonly character!: Character;
@Prop({required: true})
readonly selfCharacter!: Character;
get groupedInfotags(): DisplayInfotagGroup[] {
const groups = Store.kinks.infotag_groups;

View File

@ -4,14 +4,14 @@
<i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i>
<i v-show="!kink.hasSubkinks && kink.isCustom" class="far custom-kink-icon"></i>
<span class="kink-name">{{ kink.name }}</span>
<span class="kink-custom-desc" v-if="(kink.isCustom)">{{kink.description}}</span>
<span class="kink-custom-desc" v-if="((kink.isCustom) && (expandedCustom))">{{kink.description}}</span>
<template v-if="kink.hasSubkinks">
<div class="subkink-list" :class="{closed: this.listClosed}">
<kink v-for="subkink in kink.subkinks" :kink="subkink" :key="subkink.id" :comparisons="comparisons"
:highlights="highlights"></kink>
</div>
</template>
<div class="popover popover-top" v-if="((showTooltip) && (!kink.isCustom))" style="display:block;bottom:100%;top:initial;margin-bottom:5px">
<div class="popover popover-top" v-if="((showTooltip) && ((!kink.isCustom) || (!expandedCustom)))" style="display:block;bottom:100%;top:initial;margin-bottom:5px">
<div class="arrow" style="left:10%"></div>
<h5 class="popover-header">{{kink.name}}</h5>
<div class="popover-body"><p>{{kink.description}}</p></div>
@ -34,9 +34,17 @@
readonly highlights!: {[key: number]: boolean};
@Prop({required: true})
readonly comparisons!: {[key: number]: string | undefined};
@Prop({required: false})
expandedCustom: boolean = false;
listClosed = true;
showTooltip = false;
toggleExpandedCustoms(): void {
this.expandedCustom = !this.expandedCustom;
}
toggleSubkinks(): void {
if(!this.kink.hasSubkinks)
return;
@ -52,7 +60,8 @@
'stock-kink': !this.kink.isCustom,
'custom-kink': this.kink.isCustom,
highlighted: !this.kink.isCustom && this.highlights[this.kink.id],
subkink: this.kink.hasSubkinks
subkink: this.kink.hasSubkinks,
'expanded-custom-kink': this.expandedCustom,
};
classes[`kink-id-${this.kinkId}`] = true;
classes[`kink-group-${this.kink.group}`] = true;

View File

@ -1,12 +1,17 @@
<template>
<div class="character-kinks-block" @contextmenu="contextMenu" @touchstart="contextMenu" @touchend="contextMenu">
<div class="compare-highlight-block d-flex justify-content-between">
<div class="expand-custom-kinks-block form-inline">
<button class="btn btn-primary" @click="toggleExpandedCustomKinks" :disabled="loading">Expand Custom Kinks</button>
</div>
<div v-if="shared.authenticated" class="quick-compare-block form-inline">
<character-select v-model="characterToCompare"></character-select>
<button class="btn btn-primary" @click="compareKinks" :disabled="loading || !characterToCompare">
<button class="btn btn-outline-secondary" @click="compareKinks" :disabled="loading || !characterToCompare">
{{ compareButtonText }}
</button>
</div>
<div class="form-inline">
<select v-model="highlightGroup" class="form-control">
<option :value="undefined">None</option>
@ -21,7 +26,7 @@
<h4>Favorites</h4>
</div>
<div class="card-body">
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.id" :highlights="highlighting"
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.id" :highlights="highlighting" :expandedCustom="expandedCustoms"
:comparisons="comparison"></kink>
</div>
</div>
@ -32,7 +37,7 @@
<h4>Yes</h4>
</div>
<div class="card-body">
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.id" :highlights="highlighting"
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.id" :highlights="highlighting" :expandedCustom="expandedCustoms"
:comparisons="comparison"></kink>
</div>
</div>
@ -43,7 +48,7 @@
<h4>Maybe</h4>
</div>
<div class="card-body">
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.id" :highlights="highlighting"
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.id" :highlights="highlighting" :expandedCustom="expandedCustoms"
:comparisons="comparison"></kink>
</div>
</div>
@ -54,7 +59,7 @@
<h4>No</h4>
</div>
<div class="card-body">
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.id" :highlights="highlighting"
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.id" :highlights="highlighting" :expandedCustom="expandedCustoms"
:comparisons="comparison"></kink>
</div>
</div>
@ -91,6 +96,13 @@
highlighting: {[key: string]: boolean} = {};
comparison: {[key: string]: KinkChoice} = {};
expandedCustoms = false;
toggleExpandedCustomKinks(): void {
this.expandedCustoms = !this.expandedCustoms;
}
async compareKinks(): Promise<void> {
if(this.comparing) {
this.comparison = {};

View File

@ -38,12 +38,13 @@
<i class="far fa-envelope fa-fw"></i>Send Note</a>
<div v-if="character.character.online_chat" @click="showInChat()" class="character-page-online-chat">Online In Chat</div>
<div class="contact-block">
<contact-method v-for="method in contactMethods" :method="method" :key="method.id"></contact-method>
</div>
<div class="quick-info-block">
<infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id"></infotag-item>
<infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id" :selfCharacter="selfCharacter"></infotag-item>
<div class="contact-block">
<contact-method v-for="method in contactMethods" :method="method" :key="method.id"></contact-method>
</div>
<div class="quick-info">
<span class="quick-info-label">Created</span>
<span class="quick-info-value"><date :time="character.character.created_at"></date></span>
@ -141,6 +142,9 @@
readonly character!: Character;
@Prop()
readonly oldApi?: true;
@Prop({required: true})
readonly selfCharacter!: Character;
readonly shared: SharedStore = Store;
readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these.
readonly avatarUrl = Utils.avatarURL;