404 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			404 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|     <modal :action="l('characterSearch.action')" @submit.prevent="submit()" dialogClass="w-100"
 | |
|         :buttonText="results ? l('characterSearch.again') : undefined" class="character-search">
 | |
|         <div v-if="options && !results">
 | |
|             <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 v-model="data.species" :multiple="true" :placeholder="l('filter')"
 | |
|                 :title="l('characterSearch.species')" :options="options.species">
 | |
|                 <template slot-scope="s">{{s.option.name}}</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="results" class="results">
 | |
|             <h4>
 | |
|                 {{l('characterSearch.results')}}
 | |
|                 <i class="fas fa-circle-notch fa-spin search-spinner" v-if="!resultsComplete"></i>
 | |
|             </h4>
 | |
| 
 | |
|             <div v-for="character in results" :key="character.name" class="search-result" :class="'status-' + character.status">
 | |
|                 <template v-if="character.status === 'looking'" v-once>
 | |
|                     <img :src="characterImage(character.name)" v-if="showAvatars"/>
 | |
|                     <user :character="character" :showStatus="true" :match="true"></user>
 | |
|                     <bbcode :text="character.statusText"></bbcode>
 | |
|                 </template>
 | |
|                 <user v-else :character="character" :showStatus="true" :match="true" v-once></user>
 | |
|             </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, Species, speciesNames } from '../learn/matcher';
 | |
| 
 | |
|     type Options = {
 | |
|         kinks: SearchKink[],
 | |
|         listitems: {id: string, name: string, value: string}[]
 | |
|     };
 | |
| 
 | |
|     let options: Options | undefined;
 | |
| 
 | |
|     function sort(x: Character, y: Character): number {
 | |
|         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.matchScore > yc.matchScore)
 | |
|                 return -1;
 | |
| 
 | |
|             if (xc.matchScore < yc.matchScore)
 | |
|                 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: Character[] | undefined;
 | |
|         resultsComplete = false;
 | |
|         characterImage = characterImage;
 | |
|         options!: ExtendedSearchData;
 | |
| 
 | |
|         data: ExtendedSearchData = {
 | |
|             kinks: [],
 | |
|             genders: [],
 | |
|             orientations: [],
 | |
|             languages: [],
 | |
|             furryprefs: [],
 | |
|             roles: [],
 | |
|             positions: [],
 | |
|             species: []
 | |
|         };
 | |
| 
 | |
|         listItems: ReadonlyArray<keyof SearchData> = [
 | |
|             'genders', 'orientations', 'languages', 'furryprefs', 'roles', 'positions'
 | |
|         ]; // SearchData is correct
 | |
| 
 | |
|         searchString = '';
 | |
| 
 | |
|         // tslint:disable-next-line no-any
 | |
|         scoreWatcher: ((event: any) => void) | null = null;
 | |
| 
 | |
| 
 | |
|         @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()
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         @Hook('mounted')
 | |
|         mounted(): void {
 | |
|             core.connection.onMessage('ERR', (data) => {
 | |
|                 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', (data) => {
 | |
|                 this.results = data.characters.map((x) => core.characters.get(x))
 | |
|                     .filter((x) => core.state.hiddenUsers.indexOf(x.name) === -1 && !x.isIgnored)
 | |
|                     .filter((x) => this.isSpeciesMatch(x))
 | |
|                     .sort(sort);
 | |
| 
 | |
|                 this.resultsComplete = this.checkResultCompletion();
 | |
|             });
 | |
| 
 | |
|             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.results)
 | |
|                     // tslint:disable-next-line no-unsafe-any no-any
 | |
|                     && (event.character)
 | |
|                     // tslint:disable-next-line no-unsafe-any no-any
 | |
|                     && (_.find(this.results, (c: Character) => c.name === event.character.character.name))
 | |
|                 ) {
 | |
|                     this.results = (_.filter(
 | |
|                         this.results,
 | |
|                         (x) => this.isSpeciesMatch(x)
 | |
|                     ) as Character[]).sort(sort);
 | |
| 
 | |
|                     this.resultsComplete = this.checkResultCompletion();
 | |
|                 }
 | |
|             };
 | |
| 
 | |
|             EventBus.$on(
 | |
|                 'character-score',
 | |
|                 this.scoreWatcher
 | |
|             );
 | |
|         }
 | |
| 
 | |
| 
 | |
|         @Hook('beforeDestroy')
 | |
|         beforeDestroy(): void {
 | |
|             if (this.scoreWatcher) {
 | |
|                 EventBus.$off(
 | |
|                     'character-score',
 | |
|                     this.scoreWatcher
 | |
|                 );
 | |
| 
 | |
|                 delete this.scoreWatcher;
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         @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)
 | |
|                 ),
 | |
|                 ', '
 | |
|             );
 | |
|         }
 | |
| 
 | |
| 
 | |
|         isSpeciesMatch(c: Character): boolean {
 | |
|           if (this.data.species.length === 0) {
 | |
|             return true;
 | |
|           }
 | |
| 
 | |
|           const knownCharacter = core.cache.profileCache.getSync(c.name);
 | |
| 
 | |
|           if (!knownCharacter) {
 | |
|             return true;
 | |
|           }
 | |
| 
 | |
|           const species = Matcher.species(knownCharacter.character.character);
 | |
| 
 | |
|           if (!species) {
 | |
|             return false;
 | |
|           }
 | |
| 
 | |
|           return !!_.find(this.data.species, (s: SearchSpecies) => (s.id === species));
 | |
|         }
 | |
| 
 | |
| 
 | |
|         getSpeciesOptions(): SearchSpecies[] {
 | |
|             const species = _.map(
 | |
|                 _.filter(Species, (s) => (_.isString(s))) as unknown[] as string[],
 | |
|                 (speciesName: keyof typeof Species): SearchSpecies => {
 | |
|                     const speciesId: number = Species[speciesName];
 | |
| 
 | |
|                     if (speciesId in speciesNames) {
 | |
|                         return {
 | |
|                             name: `${speciesNames[speciesId].substr(0, 1).toUpperCase()}${speciesNames[speciesId].substr(1)} (species)`,
 | |
|                             id: speciesId
 | |
|                         };
 | |
|                     }
 | |
| 
 | |
|                     return {
 | |
|                         name: `${speciesName}s (species)`,
 | |
|                         id: speciesId
 | |
|                     };
 | |
|                 }
 | |
|             ) as unknown[] as SearchSpecies[];
 | |
| 
 | |
|             return _.sortBy(species, 'name');
 | |
|         }
 | |
| 
 | |
| 
 | |
|         checkResultCompletion(): boolean {
 | |
|             return _.every(
 | |
|                 this.results,
 | |
|                 (c: Character) => (!!core.cache.profileCache.getSync(c.name))
 | |
|             );
 | |
|         }
 | |
| 
 | |
| 
 | |
|         filterKink(filter: RegExp, kink: SearchKink): boolean {
 | |
|             if(this.data.kinks.length >= 5)
 | |
|                 return this.data.kinks.indexOf(kink) !== -1;
 | |
|             return filter.test(kink.name);
 | |
|         }
 | |
| 
 | |
|         get showAvatars(): boolean {
 | |
|             return core.state.settings.showAvatars;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         reset(): void {
 | |
|             this.data = {kinks: [], genders: [], orientations: [], languages: [], furryprefs: [], roles: [], positions: [], species: []};
 | |
|         }
 | |
| 
 | |
| 
 | |
|         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.results !== undefined) {
 | |
|                 this.results = undefined;
 | |
|                 return;
 | |
|             }
 | |
|             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)
 | |
|                     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: [] }, h)
 | |
|             ) as ExtendedSearchData[];
 | |
| 
 | |
|             const newHistory: ExtendedSearchData[] = _.take(_.concat([data], filteredHistory), 15);
 | |
| 
 | |
|             await core.settingsStore.set('searchHistory', newHistory);
 | |
|         }
 | |
|     }
 | |
| </script>
 | |
| 
 | |
| <style lang="scss">
 | |
|     .character-search {
 | |
|         .dropdown {
 | |
|             margin-bottom: 10px;
 | |
|         }
 | |
| 
 | |
|         .results {
 | |
|             .user-view {
 | |
|                 display: block;
 | |
|             }
 | |
|             & > .search-result {
 | |
|                 clear: both;
 | |
|             }
 | |
|             & > .status-looking {
 | |
|                 margin-bottom: 5px;
 | |
|                 min-height: 50px;
 | |
|             }
 | |
|             img {
 | |
|                 float: left;
 | |
|                 margin-right: 5px;
 | |
|                 width: 50px;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         .search-string {
 | |
|             margin-bottom: 1rem;
 | |
|             margin-top: 1rem;
 | |
|             margin-left: 0.5rem;
 | |
|             font-size: 80%;
 | |
|         }
 | |
| 
 | |
|         .search-string span {
 | |
|             font-weight: bold;
 | |
|         }
 | |
| 
 | |
|         .search-spinner {
 | |
|             float: right;
 | |
|         }
 | |
|     }
 | |
| </style>
 |