785 lines
26 KiB
Vue
785 lines
26 KiB
Vue
<template>
|
|
<modal :action="l('characterSearch.action')" @submit.prevent="submit()" dialogClass="w-100"
|
|
:buttonText="state === 'results' ? l('characterSearch.again') : undefined" class="character-search">
|
|
<div v-if="options && state === 'search'">
|
|
<div v-show="error" class="alert alert-danger">{{error}}</div>
|
|
<filterable-select v-model="data.kinks" :multiple="true" :placeholder="l('filter')"
|
|
:title="l('characterSearch.kinks')" :filterFunc="filterKink" :options="options.kinks">
|
|
<template slot-scope="s">{{s.option.name}}</template>
|
|
</filterable-select>
|
|
<filterable-select v-for="item in listItems" :multiple="true"
|
|
v-model="data[item]" :placeholder="l('filter')" :title="l('characterSearch.' + item)" :options="options[item]" :key="item">
|
|
</filterable-select>
|
|
|
|
<filterable-select class="species-filter" v-model="data.species" :filterFunc="filterSpecies" :multiple="true" :placeholder="l('filter')"
|
|
:title="l('characterSearch.species')" :options="options.species">
|
|
<template slot-scope="s">{{s.option.shortName}} <small>{{s.option.details}}</small></template>
|
|
</filterable-select>
|
|
|
|
<div v-if="searchString" class="search-string">
|
|
Searching for <span>{{searchString}}</span>
|
|
</div>
|
|
|
|
<div class="btn-group">
|
|
<button class="btn btn-outline-secondary" @click.prevent="showHistory()">History</button>
|
|
<button class="btn btn-outline-secondary" @click.prevent="reset()">Reset</button>
|
|
</div>
|
|
|
|
<search-history ref="searchHistory" :callback="updateSearch" :curSearch="data"></search-history>
|
|
</div>
|
|
<div v-else-if="state === 'results'" class="results">
|
|
<div class="debug" v-show="false">
|
|
<textarea v-model="debugSearchJson"></textarea>
|
|
<button class="btn" @click.prevent="debugUpdateResults()">Update</button>
|
|
</div>
|
|
|
|
<h4 v-if="hasReceivedResults">
|
|
{{results.length}} {{l('characterSearch.results')}}
|
|
|
|
<span v-if="resultsPending > 0" class="pending">Scoring {{resultsPending}}... <i class="fas fa-circle-notch fa-spin search-spinner"></i></span>
|
|
</h4>
|
|
<h4 v-else>Searching...</h4>
|
|
|
|
<div v-for="record in results" :key="record.character.name" class="search-result" :class="'status-' + record.character.status">
|
|
<template v-if="record.character.status === 'looking'" v-once>
|
|
<img :src="characterImage(record.character.name)" v-if="showAvatars"/>
|
|
<user :character="record.character" :showStatus="true" :match="shouldShowMatch" :avatar="false"></user>
|
|
<bbcode :text="record.character.statusText" class="status-text"></bbcode>
|
|
</template>
|
|
<template v-else v-once>
|
|
<user :character="record.character" :showStatus="true" :match="shouldShowMatch" :avatar="shouldShowAvatar"></user>
|
|
<bbcode :text="record.character.statusText" v-if="!!record.character.statusText" class="status-text"></bbcode>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="search-yiffbot-suggestion" v-if="isYiffBot4000Online()" @click.prevent="showYiffBot4000()"><div class="btn">No luck? Try AI play with <span>YiffBot 4000</span></div></div>
|
|
</modal>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Hook, Watch } from '@f-list/vue-ts';
|
|
import Axios from 'axios';
|
|
import {BBCodeView} from '../bbcode/view';
|
|
import CustomDialog from '../components/custom_dialog';
|
|
import FilterableSelect from '../components/FilterableSelect.vue';
|
|
import Modal from '../components/Modal.vue';
|
|
import {characterImage} from './common';
|
|
import core from './core';
|
|
import { Character, Connection, ExtendedSearchData, SearchData, SearchKink, SearchSpecies } from './interfaces';
|
|
import l from './localize';
|
|
import UserView from './UserView.vue';
|
|
import * as _ from 'lodash';
|
|
import {EventBus} from './preview/event-bus';
|
|
import CharacterSearchHistory from './CharacterSearchHistory.vue';
|
|
import { Matcher } from '../learn/matcher';
|
|
import {
|
|
Gender,
|
|
kinkMatchScoreMap,
|
|
kinkMatchWeights,
|
|
nonAnthroSpecies, Orientation,
|
|
Species,
|
|
speciesMapping,
|
|
speciesNames, TagId
|
|
} from '../learn/matcher-types';
|
|
import { CharacterCacheRecord } from '../learn/profile-cache';
|
|
import Bluebird from 'bluebird';
|
|
|
|
type Options = {
|
|
kinks: SearchKink[],
|
|
listitems: {id: string, name: string, value: string}[]
|
|
};
|
|
|
|
let options: Options | undefined;
|
|
|
|
interface SearchResult {
|
|
character: Character;
|
|
profile: CharacterCacheRecord | null;
|
|
}
|
|
|
|
function sort(resultX: SearchResult, resultY: SearchResult): number {
|
|
const x = resultX.character;
|
|
const y = resultY.character;
|
|
|
|
if(x.status === 'looking' && y.status !== 'looking') return -1;
|
|
if(x.status !== 'looking' && y.status === 'looking') return 1;
|
|
|
|
const xc = core.cache.profileCache.getSync(x.name);
|
|
const yc = core.cache.profileCache.getSync(y.name);
|
|
|
|
if(xc && !yc) {
|
|
return -1;
|
|
}
|
|
|
|
if(!xc && yc) {
|
|
return 1;
|
|
}
|
|
|
|
if(xc && yc) {
|
|
if(xc.match.matchScore > yc.match.matchScore)
|
|
return -1;
|
|
|
|
if(xc.match.matchScore < yc.match.matchScore)
|
|
return 1;
|
|
|
|
if(xc.match.searchScore > yc.match.searchScore)
|
|
return -1;
|
|
|
|
if(xc.match.searchScore < yc.match.searchScore)
|
|
return 1;
|
|
}
|
|
|
|
if(x.name < y.name)
|
|
return -1;
|
|
|
|
if(x.name > y.name)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
@Component({
|
|
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView(core.bbCodeParser), 'search-history': CharacterSearchHistory}
|
|
})
|
|
export default class CharacterSearch extends CustomDialog {
|
|
l = l;
|
|
kinksFilter = '';
|
|
error = '';
|
|
results: SearchResult[] = [];
|
|
resultsPending = 0;
|
|
characterImage = characterImage;
|
|
options!: ExtendedSearchData;
|
|
shouldShowMatch = true;
|
|
state = 'search';
|
|
hasReceivedResults = false;
|
|
shouldShowAvatar = false;
|
|
|
|
debugSearchJson = JSON.stringify(
|
|
{
|
|
scoreMap: kinkMatchScoreMap,
|
|
weights: kinkMatchWeights
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
|
|
private countUpdater?: ResultCountUpdater;
|
|
|
|
data: ExtendedSearchData = {
|
|
kinks: [],
|
|
genders: [],
|
|
orientations: [],
|
|
languages: [],
|
|
furryprefs: [],
|
|
roles: [],
|
|
positions: [],
|
|
species: [],
|
|
bodytypes: []
|
|
};
|
|
|
|
listItems: ReadonlyArray<keyof SearchData> = [
|
|
'genders', 'orientations', 'languages', 'furryprefs', 'roles', 'positions', 'bodytypes'
|
|
]; // SearchData is correct
|
|
|
|
searchString = '';
|
|
|
|
// tslint:disable-next-line no-any
|
|
scoreWatcher: ((event: any) => void) | null = null;
|
|
|
|
isYiffBot4000Online(): boolean {
|
|
return core.characters.get('YiffBot 4000').status !== 'offline';
|
|
}
|
|
|
|
showYiffBot4000(): void {
|
|
const character = core.characters.get('YiffBot 4000');
|
|
|
|
if (character.status === 'offline') {
|
|
return;
|
|
}
|
|
|
|
const conversation = core.conversations.getPrivate(character);
|
|
|
|
conversation.show();
|
|
this.hide();
|
|
|
|
const last = _.last(conversation.messages);
|
|
|
|
if (!last || last.time.getTime() < Date.now() - 1000 * 60 * 30) {
|
|
conversation.enteredText = 'Hello!';
|
|
conversation.send();
|
|
}
|
|
}
|
|
|
|
@Hook('created')
|
|
async created(): Promise<void> {
|
|
if(options === undefined)
|
|
options = <Options | undefined>(await Axios.get('https://www.f-list.net/json/api/mapping-list.php')).data;
|
|
if(options === undefined) return;
|
|
this.options = Object.freeze({
|
|
kinks: options.kinks.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0))),
|
|
genders: options.listitems.filter((x) => x.name === 'gender').map((x) => x.value),
|
|
orientations: options.listitems.filter((x) => x.name === 'orientation').map((x) => x.value),
|
|
languages: options.listitems.filter((x) => x.name === 'languagepreference').map((x) => x.value),
|
|
furryprefs: options.listitems.filter((x) => x.name === 'furrypref').map((x) => x.value),
|
|
roles: options.listitems.filter((x) => x.name === 'subdom').map((x) => x.value),
|
|
positions: options.listitems.filter((x) => x.name === 'position').map((x) => x.value),
|
|
species: this.getSpeciesOptions(),
|
|
bodytypes: options.listitems.filter((x) => x.name === 'bodytype').map((x) => x.value)
|
|
});
|
|
|
|
|
|
this.countUpdater = new ResultCountUpdater(
|
|
(names: string[]) => {
|
|
this.resultsPending = this.countPendingResults(names);
|
|
|
|
if (this.resultsPending === 0) {
|
|
this.countUpdater?.stop();
|
|
}
|
|
|
|
this.resort();
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
async debugUpdateResults(): Promise<void> {
|
|
if (this.state !== 'results') {
|
|
return;
|
|
}
|
|
|
|
const data = JSON.parse(this.debugSearchJson);
|
|
|
|
_.assign(kinkMatchScoreMap, data.scoreMap);
|
|
_.assign(kinkMatchWeights, data.weights);
|
|
|
|
core.cache.profileCache.clear();
|
|
|
|
const results = this.results;
|
|
|
|
this.results = [];
|
|
|
|
await Bluebird.delay(10);
|
|
|
|
// pre-warm cache
|
|
await Bluebird.mapSeries(
|
|
results,
|
|
(c) => core.cache.profileCache.get(c.character.name)
|
|
);
|
|
|
|
this.resultsPending = this.countPendingResults(undefined, results);
|
|
|
|
this.countUpdater?.start();
|
|
this.resort(results);
|
|
|
|
console.log('Done!');
|
|
}
|
|
|
|
getYiffBotCompatibleGender(): Character.Gender {
|
|
const g = Matcher.getTagValueList(TagId.Gender, core.characters.ownProfile.character);
|
|
const o = Matcher.getTagValueList(TagId.Orientation, core.characters.ownProfile.character);
|
|
|
|
if (o === Orientation.Straight || o === Orientation.Unsure || _.isNil(o)) {
|
|
if (g === Gender.Male) {
|
|
return 'Female';
|
|
}
|
|
|
|
if (g === Gender.Female) {
|
|
return 'Male';
|
|
}
|
|
}
|
|
|
|
if (o === Orientation.Gay && g) {
|
|
return g === Gender.Male ? 'Male' : 'Female';
|
|
}
|
|
|
|
if (o === Orientation.BiFemalePreference) {
|
|
return 'Female';
|
|
}
|
|
|
|
if (o === Orientation.BiMalePreference) {
|
|
return 'Male';
|
|
}
|
|
|
|
return _.sample(['Male', 'Female']) as Character.Gender;
|
|
}
|
|
|
|
@Hook('mounted')
|
|
mounted(): void {
|
|
core.connection.onMessage('ERR', (data) => {
|
|
this.state = 'search';
|
|
|
|
switch(data.number) {
|
|
case 18:
|
|
this.error = l('characterSearch.error.noResults');
|
|
break;
|
|
case 50:
|
|
this.error = l('characterSearch.error.throttle');
|
|
break;
|
|
case 72:
|
|
this.error = l('characterSearch.error.tooManyResults');
|
|
}
|
|
});
|
|
|
|
core.connection.onMessage('FKS', async (data) => {
|
|
const results = data.characters.map((x) => ({ character: core.characters.get(x), profile: null }))
|
|
.filter((x) => core.state.hiddenUsers.indexOf(x.character.name) === -1 && !x.character.isIgnored)
|
|
.filter((x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x))
|
|
.sort(sort);
|
|
|
|
// pre-warm cache
|
|
await Bluebird.mapSeries(
|
|
results,
|
|
(c) => core.cache.profileCache.get(c.character.name)
|
|
);
|
|
|
|
this.resultsPending = this.countPendingResults(undefined, results);
|
|
|
|
this.countUpdater?.start();
|
|
|
|
// this is done LAST to force Vue to wait with rendering
|
|
this.hasReceivedResults = true;
|
|
this.results = results;
|
|
|
|
if (this.isYiffBot4000Online()) {
|
|
const char = core.characters.get('YiffBot 4000');
|
|
|
|
(char as any).status = 'looking';
|
|
(char as any).gender = this.getYiffBotCompatibleGender();
|
|
(char as any).statusText = 'Try AI play with any gender, orientation & kink!';
|
|
|
|
this.results.push({ character: char, profile: core.cache.profileCache.getSync('YiffBot 4000') });
|
|
}
|
|
|
|
this.resort(results);
|
|
});
|
|
|
|
if (this.scoreWatcher) {
|
|
EventBus.$off('character-score', this.scoreWatcher);
|
|
}
|
|
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
this.scoreWatcher = (event: any): void => {
|
|
// console.log('scoreWatcher', event);
|
|
|
|
if (
|
|
(this.state === 'results')
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
&& (event.character)
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
&& (_.find(this.results, (s: SearchResult) => s.character.name === event.character.character.name))
|
|
) {
|
|
this.countUpdater?.requestUpdate(event.character.character.name);
|
|
}
|
|
};
|
|
|
|
EventBus.$on(
|
|
'character-score',
|
|
this.scoreWatcher
|
|
);
|
|
}
|
|
|
|
|
|
@Hook('beforeDestroy')
|
|
beforeDestroy(): void {
|
|
if (this.scoreWatcher) {
|
|
EventBus.$off(
|
|
'character-score',
|
|
this.scoreWatcher
|
|
);
|
|
|
|
this.scoreWatcher = null;
|
|
}
|
|
|
|
this.countUpdater?.stop();
|
|
}
|
|
|
|
|
|
@Watch('data', { deep: true })
|
|
onDataChange(): void {
|
|
this.searchString = _.join(
|
|
_.map(
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
_.flatten(_.map(this.data as any)),
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
(v) => _.get(v, 'name', v)
|
|
),
|
|
', '
|
|
);
|
|
}
|
|
|
|
|
|
private resort(results = this.results) {
|
|
this.results = (_.filter(
|
|
results,
|
|
(x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x)
|
|
) as SearchResult[]).sort(sort);
|
|
}
|
|
|
|
|
|
isSpeciesMatch(result: SearchResult): boolean {
|
|
if (this.data.species.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const knownCharacter = core.cache.profileCache.getSync(result.character.name);
|
|
|
|
if (!knownCharacter) {
|
|
return true;
|
|
}
|
|
|
|
// optimization
|
|
result.profile = knownCharacter;
|
|
|
|
const isSearchingForAnthro = (!!_.find(this.data.species, (s) => s.id === Species.Anthro));
|
|
const isSearchingForHuman = (!!_.find(this.data.species, (s) => s.id === Species.Human));
|
|
|
|
const species = Matcher.species(knownCharacter.character.character);
|
|
|
|
if (!species) {
|
|
// returns TRUE if we're only searching for humans -- we suck at identifying humans
|
|
return ((isSearchingForHuman) && (this.data.species.length === 1));
|
|
}
|
|
|
|
return ((isSearchingForAnthro) && (_.indexOf(nonAnthroSpecies, species) < 0))
|
|
// || ((isSearchingForMammal) && (_.indexOf(mammalSpecies, s.id) >= 0))
|
|
|| !!_.find(this.data.species, (s: SearchSpecies) => (
|
|
(s.id === species)
|
|
));
|
|
}
|
|
|
|
isBodyTypeMatch(result: SearchResult) {
|
|
if (this.data.bodytypes.length === 0) return true
|
|
|
|
const knownCharacter = core.cache.profileCache.getSync(result.character.name)
|
|
if (!knownCharacter) return false
|
|
|
|
result.profile = knownCharacter
|
|
|
|
const bodytypeId = result.profile.character.character.infotags[51]?.list
|
|
if (bodytypeId === undefined) return false
|
|
|
|
const bodytype = options!.listitems.filter(x => x.name === 'bodytype').find(x => +x.id === bodytypeId)
|
|
return this.data.bodytypes.indexOf(bodytype!.value) > -1
|
|
}
|
|
|
|
isSmartFiltered(result: SearchResult) {
|
|
if (!core.state.settings.risingFilter.hideSearchResults) {
|
|
return false;
|
|
}
|
|
|
|
return !!result.profile?.match.isFiltered;
|
|
}
|
|
|
|
getSpeciesOptions(): SearchSpecies[] {
|
|
const species = _.map(
|
|
speciesMapping,
|
|
(keywords: string[], speciesIdStr: Species): SearchSpecies => {
|
|
// const speciesId: number = Species[speciesName];
|
|
const keywordsStr = `${keywords.join(', ')}`;
|
|
const details = `${keywordsStr.substr(0, 24)}...`;
|
|
const speciesId = parseInt(speciesIdStr as any, 10);
|
|
|
|
if (speciesId in speciesNames) {
|
|
const name = `${speciesNames[speciesId].substr(0, 1).toUpperCase()}${speciesNames[speciesId].substr(1)}`;
|
|
|
|
return {
|
|
details,
|
|
keywords: `${name}: ${keywordsStr}`,
|
|
name: `${name} (species)`,
|
|
shortName: name,
|
|
id: speciesId
|
|
};
|
|
}
|
|
|
|
const speciesName = Species[speciesId];
|
|
|
|
return {
|
|
details,
|
|
keywords: `${speciesName}s: ${keywordsStr}`,
|
|
name: `${speciesName}s (species)`,
|
|
shortName: `${speciesName}s`,
|
|
id: speciesId
|
|
};
|
|
}
|
|
) as unknown[] as SearchSpecies[];
|
|
|
|
// console.log('SPECIES', species);
|
|
|
|
return _.sortBy(species, 'name');
|
|
}
|
|
|
|
|
|
countPendingResults(names?: string[], results = this.results): number {
|
|
// console.log('COUNTPENDINGRESULTS', names);
|
|
|
|
return _.reduce(
|
|
results,
|
|
(accum: number, result: SearchResult) => {
|
|
if (!!result.profile) {
|
|
return accum;
|
|
}
|
|
|
|
if ((_.isUndefined(names)) || (_.indexOf(names, result.character.name) >= 0)) {
|
|
result.profile = core.cache.profileCache.getSync(result.character.name);
|
|
}
|
|
|
|
return !!result.profile ? accum : accum + 1;
|
|
},
|
|
0
|
|
);
|
|
}
|
|
|
|
|
|
filterKink(filter: RegExp, kink: SearchKink): boolean {
|
|
if(this.data.kinks.length >= 5)
|
|
return this.data.kinks.indexOf(kink) !== -1;
|
|
return filter.test(kink.name);
|
|
}
|
|
|
|
|
|
filterSpecies(filter: RegExp, species: SearchSpecies): boolean {
|
|
return filter.test(species.keywords);
|
|
}
|
|
|
|
get showAvatars(): boolean {
|
|
return core.state.settings.showAvatars;
|
|
}
|
|
|
|
|
|
reset(): void {
|
|
this.data = {kinks: [], genders: [], orientations: [], languages: [], furryprefs: [], roles: [], positions: [], species: [], bodytypes: []};
|
|
}
|
|
|
|
|
|
updateSearch(data?: ExtendedSearchData): void {
|
|
if (data) {
|
|
// this.data = {kinks: [], genders: [], orientations: [], languages: [], furryprefs: [], roles: [], positions: []};
|
|
// this.data = data;
|
|
|
|
this.data = _.mapValues(
|
|
data,
|
|
(category, categoryName) => (
|
|
_.map(
|
|
category,
|
|
(selection) => {
|
|
const jsonSelection = JSON.stringify(selection);
|
|
const v = _.find((this.options as any)[categoryName], (op) => (JSON.stringify(op) === jsonSelection));
|
|
|
|
return v || selection;
|
|
}
|
|
)
|
|
)
|
|
) as ExtendedSearchData;
|
|
}
|
|
}
|
|
|
|
|
|
submit(): void {
|
|
if(this.state === 'results') {
|
|
this.results = [];
|
|
this.hasReceivedResults = false;
|
|
this.countUpdater?.stop();
|
|
this.state = 'search';
|
|
return;
|
|
}
|
|
|
|
this.shouldShowMatch = core.state.settings.risingComparisonInSearch;
|
|
this.shouldShowAvatar = core.state.settings.risingShowPortraitInMessage;
|
|
|
|
this.results = [];
|
|
|
|
this.state = 'results';
|
|
|
|
this.error = '';
|
|
|
|
const data: Connection.ClientCommands['FKS'] & {[key: string]: (string | number)[]} = {kinks: []};
|
|
|
|
for(const key in this.data) {
|
|
const item = this.data[<keyof SearchData>key]; // SearchData is correct
|
|
if(item.length > 0 && key !== 'bodytypes')
|
|
data[key] = key === 'kinks' ? (<SearchKink[]>item).map((x) => x.id) : (<string[]>item);
|
|
}
|
|
|
|
core.connection.send('FKS', data);
|
|
|
|
// tslint:disable-next-line
|
|
this.updateSearchHistory(this.data);
|
|
}
|
|
|
|
|
|
showHistory(): void {
|
|
(<CharacterSearchHistory>this.$refs.searchHistory).show();
|
|
}
|
|
|
|
|
|
async updateSearchHistory(data: ExtendedSearchData): Promise<void> {
|
|
const history = (await core.settingsStore.get('searchHistory')) || [];
|
|
const dataStr = JSON.stringify(data, null, 0);
|
|
|
|
const filteredHistory = _.map(
|
|
_.reject(history, (h: SearchData) => (JSON.stringify(h, null, 0) === dataStr)),
|
|
(h) => _.merge({ species: [], bodytypes: [] }, h)
|
|
) as ExtendedSearchData[];
|
|
|
|
const newHistory: ExtendedSearchData[] = _.take(_.concat([data], filteredHistory), 15);
|
|
|
|
await core.settingsStore.set('searchHistory', newHistory);
|
|
}
|
|
}
|
|
|
|
|
|
class ResultCountUpdater {
|
|
// @ts-ignore
|
|
private _isVue = true;
|
|
|
|
private updatedNames: string[] = [];
|
|
|
|
private timerId?: NodeJS.Timeout;
|
|
|
|
constructor(private callback: (names: string[]) => void) {
|
|
|
|
}
|
|
|
|
|
|
requestUpdate(name: string): void {
|
|
this.updatedNames.push(name);
|
|
}
|
|
|
|
|
|
start() {
|
|
const schedule = () => {
|
|
this.timerId = setTimeout(
|
|
() => {
|
|
if (this.updatedNames.length > 0) {
|
|
this.callback(this.updatedNames);
|
|
this.updatedNames = [];
|
|
}
|
|
|
|
schedule();
|
|
},
|
|
250
|
|
);
|
|
};
|
|
|
|
schedule();
|
|
}
|
|
|
|
|
|
stop() {
|
|
if (this.timerId) {
|
|
clearTimeout(this.timerId);
|
|
delete this.timerId;
|
|
}
|
|
}
|
|
}
|
|
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.character-search {
|
|
.species-filter {
|
|
small {
|
|
color: var(--tabSecondaryFgColor)
|
|
}
|
|
}
|
|
|
|
.dropdown {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.results {
|
|
.user-view {
|
|
// display: block;
|
|
}
|
|
& > .search-result {
|
|
clear: both;
|
|
}
|
|
& > .status-looking {
|
|
margin-bottom: 5px;
|
|
min-height: 50px;
|
|
|
|
.status-text {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
& > .status-offline,
|
|
& > .status-online,
|
|
& > .status-away,
|
|
& > .status-idle,
|
|
& > .status-busy,
|
|
& > .status-dnd,
|
|
& > .status-crown {
|
|
overflow: hidden;
|
|
width: 100%;
|
|
height: 2em;
|
|
padding-top: 5px;
|
|
|
|
.user-avatar {
|
|
max-width: 2em;
|
|
max-height: 2em;
|
|
min-width: 2em;
|
|
min-height: 2em;
|
|
margin-top: -5px;
|
|
}
|
|
|
|
.status-text {
|
|
opacity: 0.75;
|
|
padding-left: 4px;
|
|
display: inline-flex;
|
|
}
|
|
}
|
|
|
|
img {
|
|
float: left;
|
|
margin-right: 5px;
|
|
width: 50px;
|
|
}
|
|
|
|
.search-result:nth-child(2n) {
|
|
background-color: rgba(0,0,0, 0.15);
|
|
}
|
|
}
|
|
|
|
.search-string {
|
|
margin-bottom: 1rem;
|
|
margin-top: 1rem;
|
|
margin-left: 0.5rem;
|
|
font-size: 80%;
|
|
}
|
|
|
|
.search-string span {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.pending {
|
|
float: right;
|
|
color: var(--gray);
|
|
font-size: 80%;
|
|
}
|
|
|
|
.search-spinner {
|
|
// float: right;
|
|
}
|
|
|
|
.search-yiffbot-suggestion .btn {
|
|
padding-left: 5px;
|
|
padding-right: 5px;
|
|
margin-top: 1em;
|
|
margin-bottom: 0;
|
|
padding-top: 0;
|
|
padding-bottom: 0;
|
|
background-color: var(--secondary);
|
|
|
|
&:hover {
|
|
background-color: var(--blue);
|
|
}
|
|
|
|
span {
|
|
color: var(--yellow);
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
}
|
|
</style>
|