Profile analyzer
This commit is contained in:
parent
6b2d49f630
commit
21ff1d25b9
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## 1.21.0
|
||||
* Added clearer broadcast messages
|
||||
* Removed extra arrow from gallery view (credit: [@FatCatClient](https://github.com/FatCatClient))
|
||||
* Added profile analyser to help improve profile matching
|
||||
* Dictionary lookup view now has a 'open in browser' button
|
||||
* Character memos are now displayed more prominently
|
||||
* Fixed redgifs.com V3 image previews
|
||||
* Fixed rule34video.com image previews
|
||||
* 'Select all channels' for Post Ads
|
||||
|
||||
## 1.20.0
|
||||
* Kink scoring is skipped if characters have only a few shared kinks
|
||||
* Kink scoring gives more weight to 'favorite' and 'no' categories
|
||||
|
|
|
@ -4,4 +4,5 @@ This project contains contributions from:
|
|||
|
||||
* [Mr Stallion, esq.](https://github.com/mrstallion)
|
||||
* [ButterCheezii](https://github.com/ButterCheezii)
|
||||
* [FatCatClient](https://github.com/FatCatClient)
|
||||
* [F-List Team](https://github.com/f-list) (original F-Chat 3.0 client)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Download
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-win.exe) (82 MB)
|
||||
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-intel.dmg) (82 MB)
|
||||
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-m1.dmg) (84 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-linux.AppImage) (82 MB)
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-win.exe) (82 MB)
|
||||
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-macos-intel.dmg) (82 MB)
|
||||
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-macos-m1.dmg) (84 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-linux.AppImage) (82 MB)
|
||||
|
||||
|
||||
# F-Chat Rising
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div><a href="#" @click.prevent="showProfileAnalyzer()" class="btn"><span class="fas fa-user-md"></span>
|
||||
Profile Analyzer</a>
|
||||
</div>
|
||||
|
||||
<div class="list-group conversation-nav">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
|
@ -114,6 +118,15 @@
|
|||
<image-preview ref="imagePreview"></image-preview>
|
||||
<add-pm-partner ref="addPmPartnerDialog"></add-pm-partner>
|
||||
<note-status v-if="coreState.settings.risingShowUnreadOfflineCount"></note-status>
|
||||
|
||||
<modal :buttons="false" ref="profileAnalysis" dialogClass="profile-analysis" >
|
||||
<profile-analysis></profile-analysis>
|
||||
<template slot="title">
|
||||
{{ownCharacter.name}}
|
||||
<a class="btn" @click="showProfileAnalyzer"><i class="fa fa-sync" /></a>
|
||||
</template>
|
||||
</modal>
|
||||
|
||||
</div>
|
||||
</template>/me
|
||||
|
||||
|
@ -148,6 +161,8 @@
|
|||
// import { EventBus } from './preview/event-bus';
|
||||
import AdCenterDialog from './ads/AdCenter.vue';
|
||||
import AdLauncherDialog from './ads/AdLauncher.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import ProfileAnalysis from '../learn/recommend/ProfileAnalysis.vue';
|
||||
|
||||
const unreadClasses = {
|
||||
[Conversation.UnreadState.None]: '',
|
||||
|
@ -164,7 +179,9 @@
|
|||
'add-pm-partner': PmPartnerAdder,
|
||||
'note-status': NoteStatus,
|
||||
adCenter: AdCenterDialog,
|
||||
adLauncher: AdLauncherDialog
|
||||
adLauncher: AdLauncherDialog,
|
||||
modal: Modal,
|
||||
'profile-analysis': ProfileAnalysis
|
||||
}
|
||||
})
|
||||
export default class ChatView extends Vue {
|
||||
|
@ -370,6 +387,11 @@
|
|||
(<AdLauncherDialog>this.$refs['adLauncher']).show();
|
||||
}
|
||||
|
||||
showProfileAnalyzer(): void {
|
||||
(this.$refs.profileAnalysis as any).show();
|
||||
void (this.$refs.profileAnalysis as any).$children[0].analyze();
|
||||
}
|
||||
|
||||
showAddPmPartner(): void {
|
||||
(<PmPartnerAdder>this.$refs['addPmPartnerDialog']).show();
|
||||
}
|
||||
|
|
|
@ -21,10 +21,15 @@
|
|||
<a href="#" @click.prevent="showChannels()" class="btn">
|
||||
<span class="fa fa-tv"></span><span class="btn-text">Channels</span>
|
||||
</a>
|
||||
|
||||
<a href="#" @click.prevent="showMemo()" class="btn">
|
||||
<span class="fas fa-edit"></span><span class="btn-text">Memo</span>
|
||||
</a>
|
||||
</div>
|
||||
<div style="overflow:auto;overflow-x:hidden;max-height:50px;user-select:text">
|
||||
{{l('status.' + conversation.character.status)}}
|
||||
<span v-show="conversation.character.statusText"> – <bbcode :text="conversation.character.statusText"></bbcode></span>
|
||||
<div v-show="userMemo"><b>Memo:</b> {{ userMemo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -178,6 +183,10 @@
|
|||
<manage-channel ref="manageDialog" v-if="isChannel(conversation)" :channel="conversation.channel"></manage-channel>
|
||||
<ad-view ref="adViewer" v-if="isPrivate(conversation) && conversation.character" :character="conversation.character"></ad-view>
|
||||
<channel-list ref="channelList" v-if="isPrivate(conversation)" :character="conversation.character"></channel-list>
|
||||
<modal :action="l('user.memo.action')" ref="userMemoEditor" @submit="updateMemo" dialogClass="w-100">
|
||||
<div style="float:right;text-align:right;">{{getByteLength(editorMemo)}} / 1000</div>
|
||||
<textarea class="form-control" v-model="editorMemo" maxlength="1000"></textarea>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -186,12 +195,12 @@
|
|||
import Vue from 'vue';
|
||||
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
||||
import Modal, {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
||||
import {Keys} from '../keys';
|
||||
import CharacterAdView from './character/CharacterAdView.vue';
|
||||
import {Editor} from './bbcode';
|
||||
import CommandHelp from './CommandHelp.vue';
|
||||
import { characterImage, getByteLength, getKey } from './common';
|
||||
import { characterImage, errorToString, getByteLength, getKey } from './common';
|
||||
import ConversationSettings from './ConversationSettings.vue';
|
||||
import ConversationAdSettings from './ads/ConversationAdSettings.vue';
|
||||
import core from './core';
|
||||
|
@ -207,6 +216,9 @@
|
|||
import * as _ from 'lodash';
|
||||
import Dropdown from '../components/Dropdown.vue';
|
||||
import { EventBus } from './preview/event-bus';
|
||||
// import { CharacterMemo } from '../site/character_page/interfaces';
|
||||
import { MemoManager } from './character/memo';
|
||||
import { CharacterMemo } from '../site/character_page/interfaces';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
|
@ -221,7 +233,8 @@
|
|||
'ad-view': CharacterAdView,
|
||||
'channel-list': CharacterChannelList,
|
||||
dropdown: Dropdown,
|
||||
adSettings: ConversationAdSettings
|
||||
adSettings: ConversationAdSettings,
|
||||
modal: Modal
|
||||
}
|
||||
})
|
||||
export default class ConversationView extends Vue {
|
||||
|
@ -258,6 +271,10 @@
|
|||
isPrivate = Conversation.isPrivate;
|
||||
showNonMatchingAds = true;
|
||||
|
||||
userMemo: string = '';
|
||||
editorMemo: string = '';
|
||||
memoManager?: MemoManager;
|
||||
|
||||
ownName?: string;
|
||||
|
||||
@Hook('beforeMount')
|
||||
|
@ -315,10 +332,15 @@
|
|||
|
||||
this.configUpdateHook = () => this.updateOwnName();
|
||||
EventBus.$on('configuration-update', this.configUpdateHook);
|
||||
|
||||
this.memoUpdateHook = (e: any) => this.refreshMemo(e);
|
||||
EventBus.$on('character-memo', this.memoUpdateHook);
|
||||
}
|
||||
|
||||
protected configUpdateHook: any;
|
||||
|
||||
protected memoUpdateHook: any;
|
||||
|
||||
@Hook('destroyed')
|
||||
destroyed(): void {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
|
@ -329,6 +351,7 @@
|
|||
clearInterval(this.adCountdown);
|
||||
|
||||
EventBus.$off('configuration-update', this.configUpdateHook);
|
||||
EventBus.$off('character-memo', this.memoUpdateHook);
|
||||
}
|
||||
|
||||
hideSearch(): void {
|
||||
|
@ -355,13 +378,19 @@
|
|||
}
|
||||
|
||||
@Watch('conversation')
|
||||
conversationChanged(): void {
|
||||
async conversationChanged(): Promise<void> {
|
||||
this.updateOwnName();
|
||||
|
||||
if(!anyDialogsShown) (<Editor>this.$refs['textBox']).focus();
|
||||
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight));
|
||||
this.scrolledDown = true;
|
||||
this.refreshAutoPostingTimer();
|
||||
this.userMemo = '';
|
||||
|
||||
if (this.isPrivate(this.conversation)) {
|
||||
const c = await core.cache.profileCache.get(this.conversation.name);
|
||||
this.userMemo = c?.character?.memo?.memo || '';
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('conversation.messages')
|
||||
|
@ -621,6 +650,35 @@
|
|||
return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
|
||||
}
|
||||
|
||||
updateMemo(): void {
|
||||
this.memoManager?.set(this.editorMemo).catch((e: object) => alert(errorToString(e)))
|
||||
this.userMemo = this.editorMemo;
|
||||
}
|
||||
|
||||
refreshMemo(event: { character: string, memo: CharacterMemo }): void {
|
||||
this.userMemo = event.memo.memo;
|
||||
}
|
||||
|
||||
async showMemo(): Promise<void> {
|
||||
if (this.isPrivate(this.conversation)) {
|
||||
const c = this.conversation.character;
|
||||
|
||||
this.editorMemo = '';
|
||||
|
||||
(<Modal>this.$refs['userMemoEditor']).show();
|
||||
|
||||
try {
|
||||
this.memoManager = new MemoManager(c.name);
|
||||
await this.memoManager.load();
|
||||
|
||||
this.userMemo = this.memoManager.get().memo;
|
||||
this.editorMemo = this.userMemo;
|
||||
} catch(e) {
|
||||
alert(errorToString(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get characterImage(): string {
|
||||
return characterImage(this.conversation.name);
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ import ReportDialog from './ReportDialog.vue';
|
|||
import { Matcher, MatchReport } from '../learn/matcher';
|
||||
import _ from 'lodash';
|
||||
import MatchTags from './preview/MatchTags.vue';
|
||||
import { MemoManager } from './character/memo';
|
||||
|
||||
@Component({
|
||||
components: {'match-tags': MatchTags, bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
|
||||
|
@ -76,9 +77,10 @@ import MatchTags from './preview/MatchTags.vue';
|
|||
touchedElement: HTMLElement | undefined;
|
||||
channel: Channel | undefined;
|
||||
memo = '';
|
||||
memoId = 0;
|
||||
// memoId = 0;
|
||||
memoLoading = false;
|
||||
match: MatchReport | null = null;
|
||||
memoManager?: MemoManager;
|
||||
|
||||
openConversation(jump: boolean): void {
|
||||
const conversation = core.conversations.getPrivate(this.character!);
|
||||
|
@ -115,24 +117,24 @@ import MatchTags from './preview/MatchTags.vue';
|
|||
async showMemo(): Promise<void> {
|
||||
this.memoLoading = true;
|
||||
this.memo = '';
|
||||
this.memoManager = new MemoManager(this.character!.name);
|
||||
|
||||
(<Modal>this.$refs['memo']).show();
|
||||
|
||||
try {
|
||||
const memo = await core.connection.queryApi<{note: string | null, id: number}>('character-memo-get2.php',
|
||||
{target: this.character!.name});
|
||||
this.memoId = memo.id;
|
||||
this.memo = memo.note !== null ? memo.note : '';
|
||||
this.memoLoading = false;
|
||||
await this.memoManager.load();
|
||||
|
||||
this.memo = this.memoManager.get().memo;
|
||||
this.memoLoading = false;
|
||||
} catch(e) {
|
||||
alert(errorToString(e));
|
||||
}
|
||||
}
|
||||
|
||||
updateMemo(): void {
|
||||
core.connection.queryApi('character-memo-save.php', {target: this.memoId, note: this.memo})
|
||||
.catch((e: object) => alert(errorToString(e)));
|
||||
this.memoManager?.set(this.memo).catch((e: object) => alert(errorToString(e)))
|
||||
}
|
||||
|
||||
|
||||
showAdLogs(): void {
|
||||
if (!this.hasAdLogs()) {
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal action="Post Ads" @submit="submit" ref="dialog" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
|
||||
<modal action="Post Ads" @submit="submit" ref="dialog" @reopen="load" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
|
||||
<div v-if="hasAds()">
|
||||
<h4>Ad Tags</h4>
|
||||
<div class="form-group">
|
||||
|
@ -15,6 +15,13 @@
|
|||
<div class="form-group">
|
||||
<p>Serve ads on these channels:</p>
|
||||
|
||||
<p v-if="channels.length === 0">You have no channels open that support ad posting. Open some channels first.</p>
|
||||
|
||||
<label class="control-label">
|
||||
<input type="checkbox" id="ard-all-channels" @change="selectAllChannels($event)" />
|
||||
<i>Select/unselect all</i>
|
||||
</label>
|
||||
|
||||
<label class="control-label" :for="`adr-channel-${index}`" v-for="(channel,index) in channels">
|
||||
<input type="checkbox" v-model="channel.value" :id="`adr-channel-${index}`" />
|
||||
{{ channel.title }}
|
||||
|
@ -129,6 +136,15 @@ export default class AdLauncherDialog extends CustomDialog {
|
|||
(<AdCenterDialog>this.$parent.$refs['adCenter']).show();
|
||||
}
|
||||
|
||||
selectAllChannels(e: any): void {
|
||||
const newValue = e.target.checked;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
_.each(this.channels, (c) => c.value = newValue);
|
||||
}
|
||||
|
||||
submit(e: Event) {
|
||||
const tags = this.getWantedTags();
|
||||
const channelIds = this.getWantedChannels();
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import core from '../core';
|
||||
import { CharacterMemo } from '../../site/character_page/interfaces';
|
||||
import { EventBus } from '../preview/event-bus';
|
||||
|
||||
|
||||
export class MemoManager {
|
||||
memo?: CharacterMemo;
|
||||
|
||||
constructor(protected character: string) {
|
||||
|
||||
}
|
||||
|
||||
get(): CharacterMemo {
|
||||
if (!this.memo) {
|
||||
throw new Error('Missing character memo');
|
||||
}
|
||||
|
||||
return this.memo;
|
||||
}
|
||||
|
||||
async set(message: string | null): Promise<void> {
|
||||
if (!this.memo) {
|
||||
await this.load(true);
|
||||
}
|
||||
|
||||
const response = await core.connection.queryApi('character-memo-save.php', {target: this.memo!.id, note: message});
|
||||
|
||||
this.memo!.memo = (response as any).note;
|
||||
|
||||
await this.updateStores();
|
||||
}
|
||||
|
||||
protected async updateStores(): Promise<void> {
|
||||
const character = await core.cache.profileCache.get(this.character);
|
||||
|
||||
if (character && character.character?.memo?.memo !== this.memo!.memo) {
|
||||
character.character.memo = this.memo;
|
||||
|
||||
await core.cache.profileCache.register(character.character);
|
||||
}
|
||||
|
||||
EventBus.$emit('character-memo', { character: this.character, memo: this.memo! });
|
||||
}
|
||||
|
||||
async load(skipStoreUpdate: boolean = false): Promise<void> {
|
||||
const memo = await core.connection.queryApi<{note: string | null, id: number}>('character-memo-get2.php', {target: this.character});
|
||||
this.memo = { id: memo.id, memo: memo.note || '' };
|
||||
|
||||
if (!skipStoreUpdate) {
|
||||
await this.updateStores();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,6 +36,11 @@
|
|||
<!-- <span v-for="c in customs" :class="Score.getClasses(c.score)">{{c.name}}</span>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="memo" v-if="memo">
|
||||
<h4>Memo</h4>
|
||||
<div>{{ memo }}</div>
|
||||
</div>
|
||||
|
||||
<div class="status-message" v-if="statusMessage">
|
||||
<h4>Status <span v-if="latestAd && (statusMessage === latestAd.message)">& Latest Ad</span></h4>
|
||||
<bbcode :text="statusMessage"></bbcode>
|
||||
|
@ -116,6 +121,7 @@ export default class CharacterPreview extends Vue {
|
|||
statusClasses?: StatusClasses;
|
||||
latestAd?: AdCachedPosting;
|
||||
statusMessage?: string;
|
||||
memo?: string;
|
||||
|
||||
smartFilterIsFiltered?: boolean;
|
||||
smartFilterDetails?: string[];
|
||||
|
@ -199,6 +205,7 @@ export default class CharacterPreview extends Vue {
|
|||
this.match = undefined;
|
||||
this.character = undefined;
|
||||
this.customs = undefined;
|
||||
this.memo = undefined;
|
||||
this.ownCharacter = core.characters.ownProfile;
|
||||
|
||||
this.conversation = undefined;
|
||||
|
@ -218,6 +225,7 @@ export default class CharacterPreview extends Vue {
|
|||
this.updateSmartFilterReport();
|
||||
this.updateCustoms();
|
||||
this.updateDetails();
|
||||
this.updateMemo();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
@ -293,6 +301,9 @@ export default class CharacterPreview extends Vue {
|
|||
this.latestAd = cache.posts[cache.posts.length - 1];
|
||||
}
|
||||
|
||||
updateMemo(): void {
|
||||
this.memo = this.character?.memo?.memo;
|
||||
}
|
||||
|
||||
updateCustoms(): void {
|
||||
this.customs = _.orderBy(
|
||||
|
@ -469,7 +480,8 @@ export default class CharacterPreview extends Vue {
|
|||
|
||||
.status-message,
|
||||
.latest-ad-message,
|
||||
.conversation {
|
||||
.conversation,
|
||||
.memo {
|
||||
display: block;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
padding: 10px;
|
||||
|
|
|
@ -58,6 +58,10 @@ const previewInitiationTime = Date.now();
|
|||
return;
|
||||
}
|
||||
|
||||
if (window.location.href.match(/^https?:\/\/(www\.)?rule34video\.com/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.href.match(/^https?:\/\/[a-zA-Z0-9-]+\.tumblr\.com/)) {
|
||||
// Because Tumblr sucks with their iframes
|
||||
const og = document.querySelectorAll('meta[property="og:image"]:not([content=""])');
|
||||
|
|
|
@ -19,7 +19,8 @@ import { NoteCheckerCount } from '../../site/note-checker';
|
|||
* 'channel-ad': {message: Message, channel: Conversation, profile: ComplexCharacter | undefined}
|
||||
* 'channel-message': {message: Message, channel: Conversation}
|
||||
* 'select-conversation': { conversation: Conversation }
|
||||
* 'note-counts-update': {}
|
||||
* 'note-counts-update': {},
|
||||
* 'character-memo': { character: string, memo: CharacterMemo }
|
||||
*/
|
||||
|
||||
|
||||
|
|
|
@ -174,6 +174,7 @@ export class ImageDomMutator {
|
|||
this.add('xhamster.com', this.getBaseJsMutatorScript(['#photo_slider video', '#photo_slider img', 'video', 'img']));
|
||||
this.add('shadbase.com', this.getBaseJsMutatorScript(['#comic video', '#comic img', 'video', 'img']));
|
||||
this.add('instagram.com', this.getBaseJsMutatorScript(['article video', 'article img', 'video', 'img']));
|
||||
this.add('rule34video.com', this.getBaseJsMutatorScript(['video'], true, [], false, true));
|
||||
|
||||
this.add(
|
||||
'pornhub.com',
|
||||
|
@ -237,7 +238,6 @@ export class ImageDomMutator {
|
|||
'dom-ready'
|
||||
);
|
||||
|
||||
|
||||
this.add(
|
||||
'hentai-foundry.com',
|
||||
this.getBaseJsMutatorScript(['#picBox video', '#picBox img']),
|
||||
|
|
|
@ -38,6 +38,15 @@ export class ImageUrlMutator {
|
|||
// async(): Promise<string> => 'https://i.imgur.com/ScNLbsp.png'
|
||||
// );
|
||||
|
||||
this.add(
|
||||
/^https?:\/\/rule34video.com\/videos\/([0-9a-zA-Z-_]+)/,
|
||||
async(_url: string, match: RegExpMatchArray): Promise<string> => {
|
||||
const videoId = match[1];
|
||||
|
||||
return `https://rule34video.com/embed/${videoId}`;
|
||||
}
|
||||
);
|
||||
|
||||
this.add(
|
||||
/^https?:\/\/(www.)?pornhub.com\/view_video.php\?viewkey=([a-z0-9A-Z]+)/,
|
||||
async(_url: string, match: RegExpMatchArray): Promise<string> => {
|
||||
|
@ -60,7 +69,7 @@ export class ImageUrlMutator {
|
|||
);
|
||||
|
||||
this.add(
|
||||
/^https?:\/\/(www.)?redgifs.com\/watch\/([a-z0-9A-Z]+)/,
|
||||
/^https?:\/\/(www.|v3.)?redgifs.com\/watch\/([a-z0-9A-Z]+)/,
|
||||
async(_url: string, match: RegExpMatchArray): Promise<string> => {
|
||||
const redgifId = match[2];
|
||||
|
||||
|
|
|
@ -130,3 +130,10 @@
|
|||
[url=https://www.redgifs.com/watch/blissfulhandywhoopingcrane]Redgifs.com[/url]
|
||||
|
||||
[url=https://media.giphy.com/media/P7hen0jkFBud3EZ6ez/giphy.gif]Giphy[/url]
|
||||
|
||||
[url=https://v3.redgifs.com/watch/bruisedinconsequentialseabird#rel=niche%3Afit-girls;order=trending]Redgifs V3[/url]
|
||||
|
||||
[url=https://rule34video.com/videos/3087711/bonding-ritual-4k-no-wm/]Rule34video[/url]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -85,7 +85,10 @@
|
|||
|
||||
show(keepOpen: boolean = false): void {
|
||||
this.keepOpen = keepOpen;
|
||||
if(this.isShown) return;
|
||||
if(this.isShown) {
|
||||
this.$emit('reopen');
|
||||
return;
|
||||
}
|
||||
this.isShown = true;
|
||||
dialogStack.push(this);
|
||||
this.$emit('open');
|
||||
|
|
|
@ -50,7 +50,7 @@ theme: jekyll-theme-slate
|
|||
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
|
||||
|
||||
download:
|
||||
version: 1.20.0
|
||||
version: 1.21.0
|
||||
|
||||
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%
|
||||
|
||||
|
|
|
@ -97,13 +97,15 @@
|
|||
<word-definition :expression="wordDefinitionLookup" ref="wordDefinitionLookup"></word-definition>
|
||||
<template slot="title">
|
||||
{{wordDefinitionLookup}}
|
||||
|
||||
<a class="btn wordDefBtn dictionary" @click="openDefinitionWithDictionary"><i>D</i></a>
|
||||
<a class="btn wordDefBtn thesaurus" @click="openDefinitionWithThesaurus"><i>T</i></a>
|
||||
<a class="btn wordDefBtn urbandictionary" @click="openDefinitionWithUrbanDictionary"><i>UD</i></a>
|
||||
<a class="btn wordDefBtn wikipedia" @click="openDefinitionWithWikipedia"><i>W</i></a>
|
||||
|
||||
<a class="btn" @click="openWordDefinitionInBrowser"><i class="fa fa-external-link-alt"/></a>
|
||||
</template>
|
||||
</modal>
|
||||
|
||||
<logs ref="logsDialog"></logs>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -133,6 +135,7 @@
|
|||
// import { Sqlite3Store } from '../learn/store/sqlite3';
|
||||
import CharacterPage from '../site/character_page/character_page.vue';
|
||||
import WordDefinition from '../learn/dictionary/WordDefinition.vue';
|
||||
import ProfileAnalysis from '../learn/recommend/ProfileAnalysis.vue';
|
||||
import {defaultHost, GeneralSettings, nativeRequire} from './common';
|
||||
import { fixLogs /*SettingsStore, Logs as FSLogs*/ } from './filesystem';
|
||||
import * as SlimcatImporter from './importer';
|
||||
|
@ -200,7 +203,8 @@
|
|||
logs: Logs,
|
||||
'word-definition': WordDefinition,
|
||||
BBCodeTester: BBCodeTester,
|
||||
bbcode: BBCodeView(core.bbCodeParser)
|
||||
bbcode: BBCodeView(core.bbCodeParser),
|
||||
'profile-analysis': ProfileAnalysis
|
||||
}
|
||||
})
|
||||
export default class Index extends Vue {
|
||||
|
@ -601,6 +605,14 @@
|
|||
}
|
||||
|
||||
|
||||
async openWordDefinitionInBrowser(): Promise<void> {
|
||||
await remote.shell.openExternal((this.$refs.wordDefinitionLookup as any).getWebUrl());
|
||||
|
||||
// tslint:disable-next-line: no-any no-unsafe-any
|
||||
(this.$refs.wordDefinitionViewer as any).hide();
|
||||
}
|
||||
|
||||
|
||||
unpinUrlPreview(e: Event): void {
|
||||
const imagePreview = (this.$refs['chat'] as Chat)?.getChatView()?.getImagePreview();
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"author": "The F-List Team and Mister Stallion (Esq.)",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -524,6 +524,10 @@ export class Matcher {
|
|||
return this.formatScoring(score, postLengthPreferenceMapping[theirLength]);
|
||||
}
|
||||
|
||||
static getSpeciesName(species: Species): string {
|
||||
return speciesNames[species] || `${Species[species].toLowerCase()}s`;
|
||||
}
|
||||
|
||||
private resolveSpeciesScore(): Score {
|
||||
const you = this.you;
|
||||
const theirAnalysis = this.theirAnalysis;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="profile-analysis-wrapper" ref="profileAnalysisWrapper">
|
||||
<div v-if="!analyzing && !recommendations.length">
|
||||
<h3>Looking good!</h3>
|
||||
<p>The profile analyzer could not find any improvement recommendations for your profile.</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="analyzing && !recommendations.length">
|
||||
<p>Having problems with finding good matches?</p>
|
||||
<p> </p>
|
||||
<p>The profile analyzer will identify if your profile could benefit from adjustments.</p>
|
||||
<p> </p>
|
||||
<h3>Analyzing...</h3>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p>Having problems with finding good matches?</p>
|
||||
<p> </p>
|
||||
<p>The profile analyzer recommends the following adjustments to your profile:</p>
|
||||
|
||||
<ul>
|
||||
<li v-for="r in recommendations" class="recommendation" :class="r.level">
|
||||
<h3>{{r.title}}</h3>
|
||||
<p>{{r.desc}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component } from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import core from '../../chat/core';
|
||||
import { ProfileRecommendation, ProfileRecommendationAnalyzer } from './profile-recommendation';
|
||||
import { CharacterAnalysis } from '../matcher';
|
||||
import { methods } from '../../site/character_page/data_store';
|
||||
|
||||
@Component({})
|
||||
export default class ProfileAnalysis extends Vue {
|
||||
recommendations: ProfileRecommendation[] = [];
|
||||
analyzing = false;
|
||||
|
||||
async analyze() {
|
||||
this.analyzing = true;
|
||||
this.recommendations = [];
|
||||
|
||||
const char = await methods.characterData(core.characters.ownProfile.character.name, core.characters.ownProfile.character.id, true);
|
||||
const profile = new CharacterAnalysis(char.character);
|
||||
const analyzer = new ProfileRecommendationAnalyzer(profile);
|
||||
|
||||
this.analyzing = false;
|
||||
|
||||
this.recommendations = analyzer.analyze();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
.profile-analysis-wrapper {
|
||||
h3 {
|
||||
font-size: 130%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 95%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
line-height: 120%;
|
||||
border-radius: 3px;
|
||||
|
||||
&.critical {
|
||||
background-color: var(--scoreMismatchBg);
|
||||
}
|
||||
|
||||
&.note {
|
||||
background-color: var(--scoreWeakMismatchBg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,203 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { CharacterAnalysis, Matcher } from '../matcher';
|
||||
import { FurryPreference, Kink, mammalSpecies, Species } from '../matcher-types';
|
||||
|
||||
export enum ProfileRecommendationLevel {
|
||||
INFO = 'info',
|
||||
NOTE = 'note',
|
||||
CRITICAL = 'critical'
|
||||
}
|
||||
|
||||
export interface ProfileRecommendationUrlParams {
|
||||
// TBD
|
||||
}
|
||||
|
||||
export interface ProfileRecommendation {
|
||||
code: string;
|
||||
level: ProfileRecommendationLevel;
|
||||
title: string;
|
||||
desc: string;
|
||||
urlParams?: ProfileRecommendationUrlParams
|
||||
}
|
||||
|
||||
export class ProfileRecommendationAnalyzer {
|
||||
protected recommendations: ProfileRecommendation[] = [];
|
||||
|
||||
constructor(protected readonly profile: CharacterAnalysis) {
|
||||
//
|
||||
}
|
||||
|
||||
protected add(code: string, level: ProfileRecommendationLevel, title: string, desc: string, urlParams?: ProfileRecommendationUrlParams): void {
|
||||
this.recommendations.push({ code, level, title, desc, urlParams });
|
||||
}
|
||||
|
||||
analyze(): ProfileRecommendation[] {
|
||||
this.recommendations = [];
|
||||
|
||||
this.checkMissingProperties();
|
||||
this.checkSpeciesPreferences();
|
||||
this.checkKinkCounts();
|
||||
this.checkCustomKinks();
|
||||
|
||||
this.checkPortrait();
|
||||
this.checkImages();
|
||||
this.checkInlineImage();
|
||||
this.checkDescriptionLength();
|
||||
|
||||
return this.recommendations;
|
||||
}
|
||||
|
||||
protected checkPortrait(): void {
|
||||
// this.profile.character.
|
||||
// do nothing
|
||||
}
|
||||
|
||||
protected checkImages(): void {
|
||||
if (!this.profile.character.image_count) {
|
||||
this.add(`ADD_IMAGE`, ProfileRecommendationLevel.CRITICAL, 'Add a profile image', 'Profiles with images are more attractive to other players.');
|
||||
} else if (this.profile.character.image_count > 1 && this.profile.character.image_count < 3) {
|
||||
this.add(`ADD_MORE_IMAGES`, ProfileRecommendationLevel.NOTE, 'Add more profile images', 'Profiles with images are more attractive – try to have at least 3 images in your profile.');
|
||||
}
|
||||
}
|
||||
|
||||
protected checkInlineImage(): void {
|
||||
if (_.keys(this.profile.character.inlines).length < 1) {
|
||||
this.add(`ADD_INLINE_IMAGE`, ProfileRecommendationLevel.NOTE, 'Add an inline image', 'Profiles with inline images are more engaging to other players.');
|
||||
}
|
||||
}
|
||||
|
||||
protected checkDescriptionLength(): void {
|
||||
const desc = this.profile.character.description.trim();
|
||||
|
||||
if (desc.length < 20) {
|
||||
this.add(`ADD_DESCRIPTION`, ProfileRecommendationLevel.CRITICAL, 'Add description', 'Profiles with descriptions are more likely to draw attention from other players.');
|
||||
} else if (desc.length < 400) {
|
||||
this.add(`EXPAND_DESCRIPTION`, ProfileRecommendationLevel.NOTE, 'Extend your description', 'Long descriptions are more attractive to other players. Try expanding your description to at least 400 characters.');
|
||||
}
|
||||
}
|
||||
|
||||
protected checkCustomKinks(): void {
|
||||
const counts = _.reduce(this.profile.character.customs, (accum, kink) => {
|
||||
if (kink) {
|
||||
accum.total += 1;
|
||||
|
||||
if (kink.description) {
|
||||
accum.filled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, { filled: 0, total: 0 });
|
||||
|
||||
if (counts.total === 0) {
|
||||
this.add(`ADD_CUSTOM_KINK`, ProfileRecommendationLevel.CRITICAL, 'Add custom kinks', `Custom kinks will help your profile stand out. Try adding at least 5 custom kinks.`);
|
||||
} else if (counts.total < 5) {
|
||||
this.add(`ADD_MORE_CUSTOM_KINKS`, ProfileRecommendationLevel.NOTE, 'Add more custom kinks', `Players pay a lot of attention to custom kinks. Try adding at least 5 custom kinks.`);
|
||||
}
|
||||
|
||||
if (counts.filled < counts.total && counts.total > 0) {
|
||||
this.add(`ADD_MORE_CUSTOM_KINK_DESCRIPTIONS`, ProfileRecommendationLevel.NOTE, 'Add descriptions to custom kinks', `Some or all of your custom kinks are missing descriptions. Add descriptions to your custom kinks to attract more players.`);
|
||||
}
|
||||
}
|
||||
|
||||
protected checkKinkCounts(): void {
|
||||
const counts = _.reduce(this.profile.character.kinks, (accum, kinkLevel) => {
|
||||
if (_.isString(kinkLevel) && kinkLevel) {
|
||||
accum[kinkLevel as keyof typeof accum] += 1;
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, { favorite: 0, yes: 0, maybe: 0, no: 0 });
|
||||
|
||||
const minCountPerType = 5;
|
||||
const totalCount = counts.favorite + counts.yes + counts.maybe + counts.no;
|
||||
|
||||
if (totalCount < 10) {
|
||||
this.add(`ADD_MORE_KINKS`, ProfileRecommendationLevel.CRITICAL, `Add more kinks`, `You should have at least 10 kinks for the matching algorithm to work well.`);
|
||||
} else {
|
||||
_.each(counts, (count, key) => {
|
||||
if (count < minCountPerType) {
|
||||
this.add(`ADD_MORE_KINKS_${key.toString().toUpperCase()}`, ProfileRecommendationLevel.CRITICAL, `Add more '${key}' kinks`, `You should have at least ${minCountPerType} '${key}' kinks for the matching algorithm to work well.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected checkMissingProperties(): void {
|
||||
const p = this.profile;
|
||||
|
||||
if (p.age === null) {
|
||||
this.add('AGE', ProfileRecommendationLevel.CRITICAL, 'Enter age', 'Specifying the age of your character will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.orientation === null) {
|
||||
this.add('ORIENTATION', ProfileRecommendationLevel.CRITICAL, 'Enter sexual orientation', 'Specifying the sexual orientation of your character will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.species === null) {
|
||||
this.add('SPECIES', ProfileRecommendationLevel.CRITICAL, 'Enter species', 'Specifying the species of your character – even if it\'s \'human\' – will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.furryPreference === null) {
|
||||
this.add('FURRY_PREFERENCE', ProfileRecommendationLevel.CRITICAL, 'Enter furry preference', 'Specifying whether you like to play with anthro characters will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.subDomRole === null) {
|
||||
this.add('SUB_DOM_ROLE', ProfileRecommendationLevel.CRITICAL, 'Enter sub/dom role', 'Specifying your preferred sub/dom role will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.position === null) {
|
||||
this.add('POSITION', ProfileRecommendationLevel.CRITICAL, 'Enter position', 'Specifying your preferred position (e.g. "top", "bottom") will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.postLengthPreference === null) {
|
||||
this.add('POST_LENGTH', ProfileRecommendationLevel.CRITICAL, 'Enter post length preference', 'Specifying your post length preference will improve your matches with other players.');
|
||||
}
|
||||
|
||||
if (p.bodyType === null) {
|
||||
this.add('BODY_TYPE', ProfileRecommendationLevel.CRITICAL, 'Enter body type', 'Specifying your character\'s body type will improve your matches with other players.');
|
||||
}
|
||||
}
|
||||
|
||||
protected checkSpeciesPreferences(): void {
|
||||
const p = this.profile;
|
||||
const c = this.profile.character;
|
||||
|
||||
if (p.furryPreference === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.furryPreference === FurryPreference.FurriesOnly) {
|
||||
if (Matcher.getKinkPreference(c, Kink.Humans)! > 0) {
|
||||
this.add('KINK_MISMATCH_FURRIES_ONLY_HUMAN', ProfileRecommendationLevel.NOTE, 'Inconsistent kink', 'Your "furries-only" profile has a positive "humans" kink. If you are open to playing with humans, consider updating your preference from "furries only" to "furs and humans".');
|
||||
}
|
||||
}
|
||||
|
||||
if (p.furryPreference === FurryPreference.HumansOnly) {
|
||||
if (Matcher.getKinkPreference(c, Kink.AnimalsFerals)! >= 0 || Matcher.getKinkPreference(c, Kink.Zoophilia)! >= 0) {
|
||||
// do nothing
|
||||
} else {
|
||||
const likedAnthros = this.getLikedAnimals();
|
||||
|
||||
_.each(likedAnthros, (species) => {
|
||||
this.add('KINK_MISMATCH_HUMANS_ONLY_ANTHRO', ProfileRecommendationLevel.NOTE, 'Inconsistent kink', `Your "humans-only" profile has a positive "furry" kink (${Matcher.getSpeciesName(species)}). If you are open to playing with anthros, consider updating your preference from "humans only" to "furs and humans"`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (p.furryPreference !== FurryPreference.HumansOnly) {
|
||||
const likedAnthros = this.getLikedAnimals();
|
||||
|
||||
if (likedAnthros && !_.difference(likedAnthros, [Kink.AnthroCharacters, Kink.Mammals, Kink.Humans] as any as Species[])) {
|
||||
this.add('KINK_NO_SPECIES', ProfileRecommendationLevel.NOTE, 'Add preferred species', 'Specifying which anthro species you like (e.g. "equines", or "canines") in your kinks can improve your matches.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getLikedAnimals(): Species[] {
|
||||
const c = this.profile.character;
|
||||
|
||||
return _.filter(mammalSpecies, (species) => Matcher.getKinkPreference(c, species)! > 0);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "f-list-rising",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"author": "The F-List Team and and Mister Stallion (Esq.)",
|
||||
"description": "A heavily modded F-Chat 3.0 client for F-List",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -340,10 +340,14 @@
|
|||
|
||||
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> {
|
||||
|
|
|
@ -96,15 +96,17 @@ export interface CharacterGroup {
|
|||
owner: boolean
|
||||
}
|
||||
|
||||
export interface CharacterMemo {
|
||||
id: number;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
readonly is_self: boolean
|
||||
character: CharacterInfo
|
||||
readonly settings: CharacterSettings
|
||||
readonly badges?: string[]
|
||||
memo?: {
|
||||
id: number
|
||||
memo: string
|
||||
}
|
||||
memo?: CharacterMemo;
|
||||
readonly character_list?: {
|
||||
id: number
|
||||
name: string
|
||||
|
@ -158,4 +160,4 @@ export interface FriendsByCharacter {
|
|||
pending: FriendRequest[]
|
||||
incoming: FriendRequest[]
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
import Modal from '../../components/Modal.vue';
|
||||
import {SimpleCharacter} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
// import {methods} from './data_store';
|
||||
import { MemoManager } from '../../chat/character/memo';
|
||||
|
||||
export interface Memo {
|
||||
id: number
|
||||
|
@ -61,8 +62,11 @@
|
|||
async save(): Promise<void> {
|
||||
try {
|
||||
this.saving = true;
|
||||
const memoReply = await methods.memoUpdate(this.character.id, this.message);
|
||||
this.$emit('memo', this.message !== '' ? memoReply : undefined);
|
||||
|
||||
const memoManager = new MemoManager(this.character.name);
|
||||
await memoManager.set(this.message);
|
||||
|
||||
this.$emit('memo', memoManager.get());
|
||||
this.hide();
|
||||
} catch(e) {
|
||||
Utils.ajaxError(e, 'Unable to set memo.');
|
||||
|
@ -70,4 +74,4 @@
|
|||
this.saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue