From af1960ed024f7c2f12d6cbeaa451f53662d6a7e2 Mon Sep 17 00:00:00 2001 From: "Mr. Stallion" <mrstallion@nobody.nowhere.fauxemail.ext> Date: Sun, 9 Jun 2019 18:33:52 -0500 Subject: [PATCH] Friendlier UX for image previews; profile kinks now inlined; character info sidebar cleaner; full size images on character images page --- chat/ImagePreview.vue | 173 ++++++++++++++++--------- chat/UrlTagView.vue | 21 ++- chat/image-preview-mutator.ts | 42 +++--- electron/Window.vue | 3 + electron/main.ts | 1 + readme.md | 25 ++++ site/character_page/character_page.vue | 84 +++++++++++- site/character_page/images.vue | 19 ++- site/character_page/infotag.vue | 2 +- site/character_page/kink.vue | 5 +- site/character_page/kinks.vue | 7 +- site/character_page/sidebar.vue | 10 +- 12 files changed, 273 insertions(+), 119 deletions(-) diff --git a/chat/ImagePreview.vue b/chat/ImagePreview.vue index 4d72e4d..aff0580 100644 --- a/chat/ImagePreview.vue +++ b/chat/ImagePreview.vue @@ -1,7 +1,7 @@ <template> <!-- hiding elements instead of using 'v-if' is used here as an optimization --> <div class="image-preview-wrapper" :style="{display: visible ? 'block' : 'none'}"> - <webview webpreferences="allowRunningInsecureContent" id="image-preview-ext" ref="imagePreviewExt" class="image-preview-external" :src="externalUrl" :style="{display: externalUrlVisible ? 'flex' : 'none'}"></webview> + <webview webpreferences="allowRunningInsecureContent, autoplayPolicy=no-user-gesture-required" id="image-preview-ext" ref="imagePreviewExt" class="image-preview-external" :src="externalUrl" :style="{display: externalUrlVisible ? 'flex' : 'none'}"></webview> <div class="image-preview-local" :style="{backgroundImage: `url(${internalUrl})`, display: internalUrlVisible ? 'block' : 'none'}" @@ -16,31 +16,40 @@ import {EventBus} from './event-bus'; import {domain} from '../bbcode/core'; import {ImagePreviewMutator} from './image-preview-mutator'; + import {Point, screen} from 'electron'; @Component export default class ImagePreview extends Vue { + private readonly MinTimePreviewVisible = 500; + public visible: boolean = false; public externalUrlVisible: boolean = false; public internalUrlVisible: boolean = false; - public externalUrl: string|null = null; - public internalUrl: string|null = null; + public externalUrl: string | null = null; + public internalUrl: string | null = null; - public url: string|null = null; - public domain: string|undefined; + public url: string | null = null; + public domain: string | undefined; private jsMutator = new ImagePreviewMutator(); private interval: any = null; private exitInterval: any = null; - private exitUrl: string|null = null; + private exitUrl: string | null = null; + + private initialCursorPosition: Point | null = null; + private shouldDismiss = false; + private visibleSince = 0; + @Hook('mounted') - onMounted() { + onMounted(): void { EventBus.$on( 'imagepreview-dismiss', (eventData: any) => { + // console.log('Event dismiss', eventData.url); this.dismiss(eventData.url); } ); @@ -48,6 +57,7 @@ EventBus.$on( 'imagepreview-show', (eventData: any) => { + // console.log('Event show', eventData.url); this.show(eventData.url); } ); @@ -57,6 +67,8 @@ webview.addEventListener( 'dom-ready', () => { + // webview.openDevTools(); + const url = webview.getURL(); const js = this.jsMutator.getMutatorJsForSite(url); @@ -64,67 +76,92 @@ if (js) { webview.executeJavaScript(js); } - - // webview.openDevTools(); - - /* webview.executeJavaScript( - "(() => {" - + "$('#topbar').hide();" - + "$('.post-header').hide();" - + "$('#inside').css({padding: 0, margin: 0, width: '100%'});" - + "$('#right-content').hide();" - + "$('.post-container').css({width: '100%'});" - + "$('.post-image img').css({width: '100%', 'min-height': 'unset'});" - + "$('#recommendations').hide();" - + "$('.left').css({float: 'none'});" - + "})()" - );*/ } ); + + webview.getWebContents().on( + 'did-finish-load', + ()=> { + webview.getWebContents().session.on( + 'will-download', + (e: any) => { + e.preventDefault(); + } + ); + } + ); + + setInterval( + () => { + if (((this.visible) && (!this.exitInterval) && (!this.shouldDismiss)) || (this.interval)) + this.initialCursorPosition = screen.getCursorScreenPoint(); + + if ((this.visible) && (this.shouldDismiss) && (this.hasMouseMovedSince()) && (!this.exitInterval) && (!this.interval)) + this.hide(); + }, + 10 + ); } - dismiss(url: string) { - if (this.url !== url) { - // simply ignore - return; - } + private hide(): void { + this.cancelExitTimer(); - let due = this.visible ? 1000 : 0; + this.url = null; + this.visible = false; + + this.internalUrlVisible = false; + this.externalUrlVisible = false; + + this.externalUrl = 'about:blank'; + this.internalUrl = 'about:blank'; + + this.exitUrl = null; + this.exitInterval = null; + + this.shouldDismiss = false; + } + + + dismiss(url: string): void { + if (this.url !== url) + return; // simply ignore + + // console.log('DISMISS'); + + const due = this.visible ? this.MinTimePreviewVisible - Math.min(this.MinTimePreviewVisible, (Date.now() - this.visibleSince)) : 0; this.cancelTimer(); - if (this.exitInterval) { + if (this.exitInterval) return; - } this.exitUrl = this.url; + this.shouldDismiss = true; + if (!this.hasMouseMovedSince()) + return; + + // This timeout makes the preview window disappear with a slight delay, which helps UX + // when dealing with situations such as quickly scrolling text that moves the cursor away + // from the link this.exitInterval = setTimeout( - () => { - this.url = null; - this.visible = false; - - this.internalUrlVisible = false; - this.externalUrlVisible = false; - - this.externalUrl = 'about:blank'; - this.internalUrl = 'about:blank'; - - this.exitUrl = null; - this.exitInterval = null; - }, + () => this.hide(), due ); } - show(url: string) { - // url = 'https://imgur.com/a/2uzWx'; - // url = 'http://lodash.com'; - // url = 'https://rule34.xxx/index.php?page=post&s=view&id=3254983'; + show(url: string): void { + // console.log('SHOW'); - let due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 100; + if ((this.visible) && (!this.hasMouseMovedSince())) + return; + + if ((this.url === url) && ((this.visible) || (this.interval))) + return; + + const due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 100; this.url = url; this.domain = domain(url); @@ -132,6 +169,8 @@ this.cancelExitTimer(); this.cancelTimer(); + // This timer makes sure that just by accidentally brushing across a link won't show (blink) the preview + // -- you actually have to pause on it this.interval = setTimeout( () => { const isInternal = this.isInternalUrl(); @@ -145,46 +184,56 @@ this.externalUrl = this.url; this.visible = true; + this.visibleSince = Date.now(); + + this.initialCursorPosition = screen.getCursorScreenPoint(); }, due ); } + hasMouseMovedSince(): boolean { + if (!this.initialCursorPosition) + return true; - cancelTimer() { - if (this.interval) { - clearTimeout(this.interval); + try { + const p = screen.getCursorScreenPoint(); + + return ((p.x !== this.initialCursorPosition.x) || (p.y !== this.initialCursorPosition.y)); + } catch (err) { + console.error(err); + return true; } + } + + cancelTimer(): void { + if (this.interval) + clearTimeout(this.interval); this.interval = null; } - - cancelExitTimer() { - if (this.exitInterval) { + cancelExitTimer(): void { + if (this.exitInterval) clearTimeout(this.exitInterval); - } this.exitInterval = null; } - - isVisible() { + isVisible(): boolean { return this.visible; } - - getUrl() { + getUrl(): string | null { return this.url; } - - isExternalUrl() { + isExternalUrl(): boolean { return !((this.domain === 'f-list.net') || (this.domain === 'static.f-list.net')); } - isInternalUrl() { + isInternalUrl(): boolean { return !this.isExternalUrl(); } } diff --git a/chat/UrlTagView.vue b/chat/UrlTagView.vue index dd5a0e1..8f2e693 100644 --- a/chat/UrlTagView.vue +++ b/chat/UrlTagView.vue @@ -1,15 +1,15 @@ <template> - <span - @mouseover="show()" - @mouseleave="dismiss()" - > + <span> <i class="fa fa-link"></i> <a :href="url" rel="nofollow noreferrer noopener" target="_blank" class="user-link" - :title="url" + @mouseover="show()" + @mouseenter="show()" + @mouseleave="dismiss()" + @mouseout="dismiss()" >{{text}}</a> <span class="link-domain bbcode-pseudo" @@ -34,24 +34,21 @@ @Prop({required: true}) readonly domain!: string; - @Prop() - hover!: boolean = false; - @Hook("beforeDestroy") - beforeDestroy() { + beforeDestroy(): void { this.dismiss(); } @Hook("deactivated") - deactivate() { + deactivate(): void { this.dismiss(); } - dismiss() { + dismiss(): void { EventBus.$emit('imagepreview-dismiss', {url: this.url}); } - show() { + show(): void { EventBus.$emit('imagepreview-show', {url: this.url}); } } diff --git a/chat/image-preview-mutator.ts b/chat/image-preview-mutator.ts index 1726b67..9095c21 100644 --- a/chat/image-preview-mutator.ts +++ b/chat/image-preview-mutator.ts @@ -34,17 +34,14 @@ export class ImagePreviewMutator { } protected init() { - this.add('e621.net', this.getBaseJsMutatorScript('#image')); - this.add('e-hentai.org', this.getBaseJsMutatorScript('#img')); - this.add('gelbooru.com', this.getBaseJsMutatorScript('#image')); - this.add('chan.sankakucomplex.com', this.getBaseJsMutatorScript('#image')); + this.add('e621.net', this.getBaseJsMutatorScript('#image, video')); + 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('gfycat.com', this.getBaseJsMutatorScript('video')); - this.add( - 'gfycat.com', - `${this.getBaseJsMutatorScript('video')} - document.querySelector('video').play(); - ` - ); + // this fixes videos only -- images are fine as is + this.add('i.imgur.com', this.getBaseJsMutatorScript('video')); this.add( 'imgur.com', @@ -56,25 +53,12 @@ export class ImagePreviewMutator { 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>'); ` - - // "$('#topbar').hide();" - // + "$('.post-header').hide();" - // + "$('#inside').css({padding: 0, margin: 0, width: '100%'});" - // + "$('#right-content').hide();" - // + "$('.post-container').css({width: '100%'});" - // + "$('.post-image img').css({width: 'auto', 'min-height': 'unset', 'max-height': '100vh'});" - // + "$('#recommendations').hide();" - // + "$('.left').css({float: 'none'});" - // + "$('body').css({overflow: 'hidden'});" - // + "const imageCount = $('.post-image-container').length;" - // + "if(imageCount > 1) {" - // + "$('body').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>');" - // + "}" ); + this.add( 'rule34.xxx', - `${this.getBaseJsMutatorScript('#image')} + `${this.getBaseJsMutatorScript('#image, video')} const content = document.querySelector('#content'); content.remove(); ` @@ -95,6 +79,14 @@ export class ImagePreviewMutator { 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 = []; + `; } diff --git a/electron/Window.vue b/electron/Window.vue index dd43d47..f76ada3 100644 --- a/electron/Window.vue +++ b/electron/Window.vue @@ -84,7 +84,10 @@ @Hook('mounted') mounted(): void { + // browserWindow.webContents.openDevTools(); + this.addTab(); + electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings); electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow); electron.ipcRenderer.on('open-tab', () => this.addTab()); diff --git a/electron/main.ts b/electron/main.ts index 049ce70..abc0fdf 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -142,6 +142,7 @@ function createWindow(): Electron.BrowserWindow | undefined { window.show(); if(lastState.maximized) window.maximize(); }); + return window; } diff --git a/readme.md b/readme.md index 8e98543..c995d05 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,28 @@ +# F-Chat Rising + +This repository contains a modified version of the mainline F-Chat 3.0 client. + + +## Key Differences + +* Ad auto-posting + * Manage channel's ad settings in "Tab Settings" + * Automatically repost ads every 11-18 minutes (randomized) + * Auto-posting can rotate through multiple ads +* Link previews + * Hover cursor over any `[url]` to see a preview of it +* Profile + * Kinks are auto-compared when profile is loaded + * Custom kink explanations are shown inline + * Custom kinks are highlighted + * Gender, fur/human status, age, and sexual preference are highlighted if compatible or incompatible + * Guestbook, friend, and group counts are visible on tabs + * Character pictures can be expanded inline + * Cleaner presentation for the side bar details (age, etc.), sorted in most relevant order + * Less informative side bar details (views, contact) are separated and shown in a less prominent way + + + # F-List Exported This repository contains the open source parts of F-list and F-Chat 3.0. All necessary files to build F-Chat 3.0 as an Electron, mobile or web application are included. diff --git a/site/character_page/character_page.vue b/site/character_page/character_page.vue index 4142a5f..e21c617 100644 --- a/site/character_page/character_page.vue +++ b/site/character_page/character_page.vue @@ -166,4 +166,86 @@ this.loading = false; } } -</script> \ No newline at end of file +</script> + + +<style lang="scss"> + + .custom-kink { + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + font-weight: bold; + margin-top: 14px; + margin-bottom: 14px; + margin-left: -6px; + margin-right: -6px; + color: #f2cd00; + border: 1px rgba(255, 255, 255, 0.1) solid; + border-radius: 2px; + /* border-collapse: collapse; */ + padding: 5px; + } + + + .kink-custom-desc { + display: block; + font-weight: normal; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); + line-height: 125%; + } + + + .infotag-label { + display: block; + /* margin-bottom: 1rem; */ + font-weight: normal !important; + line-height: 120%; + font-size: 85%; + color: rgba(255, 255, 255, 0.7); + } + + + .infotag-value { + display: block; + margin-bottom: 1rem; + font-weight: bold; + line-height: 120%; + } + + .quick-info-value { + display: block; + font-weight: bold; + } + + .quick-info-label { + display: block; + /* margin-bottom: 1rem; */ + font-weight: normal !important; + line-height: 120%; + font-size: 85%; + color: rgba(255, 255, 255, 0.7); + } + + .quick-info { + margin-bottom: 1rem; + font-size: 0.9rem; + 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; + } + +</style> \ No newline at end of file diff --git a/site/character_page/images.vue b/site/character_page/images.vue index 873727d..42950dc 100644 --- a/site/character_page/images.vue +++ b/site/character_page/images.vue @@ -1,12 +1,19 @@ <template> - <div class="character-images row"> +<!-- <div class="character-images row">--> + <div class="character-images"> <div v-show="loading" class="alert alert-info">Loading images.</div> <template v-if="!loading"> - <div class="character-image col-6 col-sm-4 col-md-2" v-for="image in images" :key="image.id"> - <a :href="imageUrl(image)" target="_blank" @click="handleImageClick($event, image)"> - <img :src="thumbUrl(image)" :title="image.description"> - </a> - </div> + <img :src="imageUrl(image)" :title="image.description" class="character-image" v-for="image in images" :key="image.id"> + +<!-- <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">--> +<!-- </div>--> +<!-- --> + +<!-- <a :href="imageUrl(image)" target="_blank" @click="handleImageClick($event, image)">--> +<!-- <img :src="thumbUrl(image)" :title="image.description">--> +<!-- </a>--> +<!-- </div>--> </template> <div v-if="!loading && !images.length" class="alert alert-info">No images.</div> <div class="image-preview" v-show="previewImage" @click="previewImage = ''"> diff --git a/site/character_page/infotag.vue b/site/character_page/infotag.vue index 473c12a..113ae9e 100644 --- a/site/character_page/infotag.vue +++ b/site/character_page/infotag.vue @@ -1,6 +1,6 @@ <template> <div class="infotag"> - <span class="infotag-label">{{label}}: </span> + <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> </div> diff --git a/site/character_page/kink.vue b/site/character_page/kink.vue index 1a7b4a1..9896f07 100644 --- a/site/character_page/kink.vue +++ b/site/character_page/kink.vue @@ -2,15 +2,16 @@ <div class="character-kink" :class="kinkClasses" :id="kinkId" @click="toggleSubkinks" :data-custom="customId" @mouseover.stop="showTooltip = true" @mouseout.stop="showTooltip = false"> <i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i> - <i v-show="!kink.hasSubkinks && kink.isCustom" class="far fa-dot-circle custom-kink-icon"></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> <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" style="display:block;bottom:100%;top:initial;margin-bottom:5px"> + <div class="popover popover-top" v-if="((showTooltip) && (!kink.isCustom))" 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> diff --git a/site/character_page/kinks.vue b/site/character_page/kinks.vue index 403034c..cc97baf 100644 --- a/site/character_page/kinks.vue +++ b/site/character_page/kinks.vue @@ -129,19 +129,16 @@ this.highlighting = toAssign; } - @Hook('mounted') async mounted(): Promise<void> { await this.compareKinks(); } - @Watch('character') - characterChanged(): void { - this.compareKinks(); + async characterChanged(): Promise<void> { + await this.compareKinks(); } - get kinkGroups(): {[key: string]: KinkGroup | undefined} { return this.shared.kinks.kink_groups; } diff --git a/site/character_page/sidebar.vue b/site/character_page/sidebar.vue index 4b4c582..91bca7d 100644 --- a/site/character_page/sidebar.vue +++ b/site/character_page/sidebar.vue @@ -45,23 +45,23 @@ <div class="quick-info-block"> <infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id"></infotag-item> <div class="quick-info"> - <span class="quick-info-label">Created: </span> + <span class="quick-info-label">Created</span> <span class="quick-info-value"><date :time="character.character.created_at"></date></span> </div> <div class="quick-info"> - <span class="quick-info-label">Last updated: </span> + <span class="quick-info-label">Last Updated </span> <span class="quick-info-value"><date :time="character.character.updated_at"></date></span> </div> <div class="quick-info" v-if="character.character.last_online_at"> - <span class="quick-info-label">Last online:</span> + <span class="quick-info-label">Last Online</span> <span class="quick-info-value"><date :time="character.character.last_online_at"></date></span> </div> <div class="quick-info"> - <span class="quick-info-label">Views: </span> + <span class="quick-info-label">Views</span> <span class="quick-info-value">{{character.character.views}}</span> </div> <div class="quick-info" v-if="character.character.timezone != null"> - <span class="quick-info-label">Timezone:</span> + <span class="quick-info-label">Timezone</span> <span class="quick-info-value"> UTC{{character.character.timezone > 0 ? '+' : ''}}{{character.character.timezone != 0 ? character.character.timezone : ''}} </span>