Friendlier UX for image previews; profile kinks now inlined; character info sidebar cleaner; full size images on character images page

This commit is contained in:
Mr. Stallion 2019-06-09 18:33:52 -05:00
parent cb81610515
commit af1960ed02
12 changed files with 273 additions and 119 deletions

View File

@ -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();
}
}

View File

@ -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});
}
}

View File

@ -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 = [];
`;
}

View File

@ -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());

View File

@ -142,6 +142,7 @@ function createWindow(): Electron.BrowserWindow | undefined {
window.show();
if(lastState.maximized) window.maximize();
});
return window;
}

View File

@ -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.

View File

@ -166,4 +166,86 @@
this.loading = false;
}
}
</script>
</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>

View File

@ -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 = ''">

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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>