Image preview toolbar and sticky mode

This commit is contained in:
Mr. Stallion 2019-07-04 14:34:21 -05:00
parent 49e58b27c7
commit 093ab4e7eb
7 changed files with 400 additions and 101 deletions

View File

@ -10,6 +10,7 @@
@mouseenter="show()"
@mouseleave="dismiss()"
@mouseout="dismiss()"
@click.middle.prevent="toggleStickyness()"
>{{text}}</a>
<span
class="link-domain bbcode-pseudo"
@ -51,5 +52,10 @@
show(): void {
EventBus.$emit('imagepreview-show', {url: this.url});
}
toggleStickyness(): void {
EventBus.$emit('imagepreview-toggle-stickyness', {url: this.url});
}
}
</script>

View File

@ -1,6 +1,13 @@
<template>
<!-- hiding elements instead of using 'v-if' is used here as an optimization -->
<div class="image-preview-wrapper" :style="{display: visible ? 'block' : 'none'}">
<div class="image-preview-wrapper" :class="{visible: visible, interactive: sticky}">
<div class="image-preview-toolbar" v-if="sticky || debug">
<a @click="toggleDevMode()" :class="{toggled: debug}" title="Debug Mode"><i class="fa fa-terminal"></i></a>
<a @click="toggleJsMode()" :class="{toggled: runJs}" title="Expand Images"><i class="fa fa-magic"></i></a>
<a @click="reloadUrl()" title="Reload Image"><i class="fa fa-redo-alt"></i></a>
<a @click="toggleStickyMode()" :class="{toggled: sticky}" title="Toggle Stickyness"><i class="fa fa-thumbtack"></i></a>
</div>
<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"
@ -43,7 +50,9 @@
private shouldDismiss = false;
private visibleSince = 0;
private debug = false;
sticky = false;
runJs = true;
debug = false;
@Hook('mounted')
@ -52,6 +61,7 @@
'imagepreview-dismiss',
(eventData: any) => {
// console.log('Event dismiss', eventData.url);
this.dismiss(eventData.url);
}
);
@ -60,31 +70,74 @@
'imagepreview-show',
(eventData: any) => {
// console.log('Event show', eventData.url);
this.show(eventData.url);
}
);
EventBus.$on(
'imagepreview-toggle-stickyness',
(eventData: any) => {
if ((this.url === eventData.url) && (this.visible))
this.sticky = !this.sticky;
}
);
const webview = this.$refs.imagePreviewExt as any;
webview.addEventListener(
'dom-ready',
() => {
if (this.debug)
webview.openDevTools();
(event: any) => {
const url = webview.getURL();
const js = this.jsMutator.getMutatorJsForSite(url);
if (js) {
if (this.debug) {
console.log('ImagePreview dom-ready', event, js);
// webview.openDevTools();
}
if ((js) && (this.runJs))
webview.executeJavaScript(js);
}
);
webview.addEventListener(
'did-fail-load',
(event: any) => {
const js = this.jsMutator.getErrorMutator(event.errorCode, event.errorDescription);
if (this.debug) {
console.log('ImagePreview did-fail-load', event, js);
}
if ((js) && (this.runJs) && (event.errorCode >= 400))
webview.executeJavaScript(js);
}
);
webview.addEventListener(
'did-navigate',
(event: any) => {
if (event.httpResponseCode >= 400) {
const js = this.jsMutator.getErrorMutator(event.httpResponseCode, event.httpStatusText);
if (this.debug) {
console.log('ImagePreview did-navigate', event, js);
}
if ((js) && (this.runJs))
webview.executeJavaScript(js);
}
}
);
webview.getWebContents().on(
// webview.getWebContents().on(
webview.addEventListener(
'did-finish-load',
()=> {
(event: any)=> {
if (this.debug)
console.log('ImagePreview did-finish-load', event);
webview.getWebContents().session.on(
'will-download',
(e: any) => {
@ -94,6 +147,7 @@
}
);
setInterval(
() => {
if (((this.visible) && (!this.exitInterval) && (!this.shouldDismiss)) || (this.interval))
@ -107,7 +161,7 @@
}
private hide(): void {
hide(): void {
this.cancelExitTimer();
this.url = null;
@ -123,6 +177,8 @@
this.exitInterval = null;
this.shouldDismiss = false;
this.sticky = false;
}
@ -130,7 +186,10 @@
if (this.url !== url)
return; // simply ignore
if (this.debug)
// if (this.debug)
// return;
if (this.sticky)
return;
// console.log('DISMISS');
@ -167,6 +226,9 @@
if ((this.url === url) && ((this.visible) || (this.interval)))
return;
if ((this.url) && (this.sticky) && (this.visible))
return;
const due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 100;
this.url = url;
@ -235,6 +297,7 @@
}
isExternalUrl(): boolean {
// 'f-list.net' is tested here on purpose, because keeps the character URLs from being previewed
return !((this.domain === 'f-list.net') || (this.domain === 'static.f-list.net'));
}
@ -242,6 +305,39 @@
isInternalUrl(): boolean {
return !this.isExternalUrl();
}
toggleDevMode(): void {
this.debug = !this.debug;
if (this.debug) {
const webview = this.$refs.imagePreviewExt as any;
webview.openDevTools();
}
}
toggleStickyMode(): void {
this.sticky = !this.sticky;
if (!this.sticky)
this.hide();
}
toggleJsMode(): void {
this.runJs = !this.runJs;
}
reloadUrl(): void {
if (this.externalUrlVisible) {
const webview = this.$refs.imagePreviewExt as any;
webview.reload();
}
}
}
</script>
@ -251,30 +347,99 @@
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins/breakpoints";
.image-preview-external {
position: absolute;
width: 50%;
height: 70%;
top: 0;
left: 0;
pointer-events: none;
background-color: black;
}
.image-preview-local {
position: absolute;
width: 50%;
height: 70%;
top: 0;
left: 0;
pointer-events: none;
background-size: contain;
background-position: top left;
background-repeat: no-repeat;
// background-color: black;
}
.image-preview-wrapper {
z-index: 10000;
display: none;
position: absolute;
left: 0;
top: 0;
width: 50%;
height: 70%;
pointer-events: none;
&.visible {
display: block;
}
&.interactive {
pointer-events: auto;
.image-preview-local,
.image-preview-auto {
// pointer-events: auto;
}
}
.image-preview-external {
/* position: absolute;
width: 50%;
height: 70%;
top: 0;
left: 0; */
width: 100%;
height: 100%;
// pointer-events: none;
background-color: black;
}
.image-preview-local {
/* position: absolute;
width: 50%;
height: 70%;
top: 0;
left: 0; */
width: 100%;
height: 100%;
// pointer-events: none;
background-size: contain;
background-position: top left;
background-repeat: no-repeat;
// background-color: black;
}
.image-preview-toolbar {
position: absolute;
/* background-color: green; */
left: 0;
top: 0;
margin: 1rem;
height: 3.5rem;
display: flex;
-webkit-backdrop-filter: blur(10px);
flex-direction: row;
width: 15rem;
flex-wrap: nowrap;
background-color: rgba(77, 76, 116, 0.92);
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem;
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2);
a i.fa {
font-size: 1.25rem;
top: 50%;
position: relative;
transform: translateY(-50%);
}
a {
flex: 1;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 3px;
margin-right: 0.5rem;
background-color: rgba(0, 0, 0, 0.1);
}
a:last-child {
margin-right: 0;
}
.toggled {
background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 1px 0px rgba(255, 255, 255, 0.6);
}
}
}
</style>

View File

@ -1,36 +1,77 @@
/* tslint:disable:quotemark */
import * as _ from 'lodash';
import { domain } from '../bbcode/core';
export interface PreviewMutator {
match: string|RegExp;
injectJs: string;
}
export interface ImagePreviewMutatorCollection {
[key: string]: string;
[key: string]: PreviewMutator;
}
export class ImagePreviewMutator {
private mutators: ImagePreviewMutatorCollection = {};
private hostMutators: ImagePreviewMutatorCollection = {};
private regexMutators: PreviewMutator[] = [];
constructor() {
this.init();
}
getMutatorJsForSite(url: string): string | undefined {
let mutator = this.matchMutator(url);
if (!mutator)
mutator = this.hostMutators['default'];
return this.wrapJs(mutator.injectJs);
}
matchMutator(url: string): PreviewMutator | undefined {
const urlDomain = domain(url);
if (!urlDomain)
return;
// console.log('Domain is', urlDomain);
if (urlDomain in this.hostMutators)
return this.hostMutators[urlDomain];
let mutatorJs = this.mutators[urlDomain];
return _.find(
this.regexMutators,
(m: PreviewMutator) => {
const match = m.match;
if (!mutatorJs)
mutatorJs = this.mutators['default'];
return `(() => { try { ${mutatorJs} } catch (err) { console.error(err); } })()`;
return (match instanceof RegExp) ? (urlDomain.match(match) !== null) : (match === urlDomain);
}
);
}
protected add(domain: string, mutatorJs: string) {
this.mutators[domain] = mutatorJs;
protected wrapJs(mutatorJs: string) {
return `(() => { try { ${mutatorJs} } catch (err) { console.error('Mutator error', err); } })();`;
}
protected add(domain: string|RegExp, mutatorJs: string) {
if (domain instanceof RegExp) {
this.regexMutators.push(
{
match: domain,
injectJs: mutatorJs
}
);
return;
}
this.hostMutators[domain] = {
match: domain,
injectJs: mutatorJs
};
}
protected init() {
@ -42,16 +83,19 @@ export class ImagePreviewMutator {
this.add('danbooru.donmai.us', this.getBaseJsMutatorScript('#image, video'));
this.add('gfycat.com', this.getBaseJsMutatorScript('video'));
this.add('gfycatporn.com', this.getBaseJsMutatorScript('video'));
this.add('www.youtube.com', this.getBaseJsMutatorScript('video'));
this.add('youtube.com', this.getBaseJsMutatorScript('video'));
this.add('instantfap.com', this.getBaseJsMutatorScript('#post img, #post video'));
this.add('www.webmshare.com', this.getBaseJsMutatorScript('video'));
this.add('webmshare.com', this.getBaseJsMutatorScript('video'));
this.add('pornhub.com', this.getBaseJsMutatorScript('.mainPlayerDiv video, .photoImageSection img'));
this.add('www.sex.com', this.getBaseJsMutatorScript('.image_frame img, .image_frame video'));
this.add('sex.com', this.getBaseJsMutatorScript('.image_frame img, .image_frame video'));
this.add('redirect.media.tumblr.com', this.getBaseJsMutatorScript('picture img, picture video'));
this.add('i.imgur.com', this.getBaseJsMutatorScript('video, img'));
this.add('postimg.cc', this.getBaseJsMutatorScript('#main-image, video'));
this.add('gifsauce.com', this.getBaseJsMutatorScript('video'));
this.add('motherless.com', this.getBaseJsMutatorScript('.content video, .content img'));
// this.add('beeg.com', this.getBaseJsMutatorScript('video'));
this.add(/^media[0-9]\.giphy\.com$/, this.getBaseJsMutatorScript('video, img[alt]'));
this.add(
'imgur.com',
@ -65,7 +109,6 @@ export class ImagePreviewMutator {
`
);
this.add(
'rule34.xxx',
`${this.getBaseJsMutatorScript('#image, video')}
@ -75,49 +118,108 @@ export class ImagePreviewMutator {
);
}
getBaseJsMutatorScript(imageSelector: string, skipElementRemove = false): string {
getBaseJsMutatorScript(imageSelector: string, skipElementRemove: boolean = false): string {
return `const body = document.querySelector('body');
const img = Array.from(document.querySelectorAll('${imageSelector}')).filter((i) => ((i.width !== 1) && (i.height !== 1))).shift()
if (!img) { return; }
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = 'width: 100% !important; height: 100% !important; position: absolute !important;'
+ 'top: 0 !important; left: 0 !important; z-index: 100000 !important;'
+ 'background-color: black !important; background-size: contain !important;'
+ 'background-repeat: no-repeat !important; background-position: top left !important;'
+ 'opacity: 1 !important; padding: 0 !important; border: 0 !important; margin: 0 !important;';
img.remove();
el.append(img);
body.append(el);
body.class = '';
console.log(el);
console.log(img);
console.log('${imageSelector}');
body.style = 'border: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'top: 0 !important; left: 0 !important;';
img.style = 'object-position: top left !important; object-fit: contain !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'margin: 0 !imporant; border: 0 !important; padding: 0 !important;';
img.class = '';
el.class = '';
if (img.play) { img.muted = true; img.play(); }
let removeList = [];
body.childNodes.forEach((el) => { if(el.id !== 'flistWrapper') { removeList.push(el); } });
${skipElementRemove ? '' : 'removeList.forEach((el) => el.remove());'}
removeList = [];
const img = Array.from(document.querySelectorAll('${imageSelector}'))
.filter((i) => ((i.width !== 1) && (i.height !== 1))).shift()
if (!img) { return; }
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = 'width: 100% !important; height: 100% !important; position: absolute !important;'
+ 'top: 0 !important; left: 0 !important; z-index: 100000 !important;'
+ 'background-color: black !important; background-size: contain !important;'
+ 'background-repeat: no-repeat !important; background-position: top left !important;'
+ 'opacity: 1 !important; padding: 0 !important; border: 0 !important; margin: 0 !important;';
img.remove();
el.append(img);
body.append(el);
body.class = '';
// console.log(el);
// console.log(img);
// console.log('${imageSelector}');
body.style = 'border: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'top: 0 !important; left: 0 !important;';
img.style = 'object-position: top left !important; object-fit: contain !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'margin: 0 !imporant; border: 0 !important; padding: 0 !important;';
img.class = '';
el.class = '';
if (img.play) { img.muted = true; img.play(); }
let removeList = [];
body.childNodes.forEach((el) => { if((el.id !== 'flistWrapper') && (el.id !== 'flistError')) { removeList.push(el); } });
// ${skipElementRemove ? '' : 'removeList.forEach((el) => el.remove());'}
removeList = [];
`;
}
getErrorMutator(code: number, description: string): string {
const errorHtml = `
<div id="flistError" style="
width: 100% !important;
height: 100% !important;
background-color: black !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 200000 !important;
margin: 0 !important;
padding: 0 !important;
line-height: 100% !important;
border: 0 !important;
opacity: 1 !important;
text-align: center !important;
"><h1 style="
color: #FF4444 !important;
font-size: 45pt !important;
margin: 0 !important;
margin-top: 10pt !important;
line-height: 100% !important;
padding: 0 !important;
border: 0 !important;
background: none !important;
font-family: Helvetica, Arial, sans-serif !important;
font-weight: bold !important;
">${code}</h1><p style="
max-width: 400px !important;
color: #ededed !important;
display: inline-block !important;
font-size: 15pt !important;
font-family: Helvetica, Arial, sans-serif !important;
font-weight: 300 !important;
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
margin-top: 5pt !important;
line-height: 130% !important;
">${description}</p></div>
`;
return this.wrapJs(`
const range = document.createRange();
range.selectNode(document.body);
const error = range.createContextualFragment(\`${errorHtml}\`);
document.body.appendChild(error);
`);
}
}

View File

@ -11,6 +11,7 @@ This repository contains a modified version of the mainline F-Chat 3.0 client.
* Rotate multiple ads on a single channel
* Link previews
* Hover cursor over any `[url]` to see a preview of it
* Middle click any `[url]` to turn the preview into a sticky / interactive mode
* Profile
* Kinks are auto-compared when profile is loaded
* Custom kink explanations can be expanded inline
@ -23,6 +24,9 @@ This repository contains a modified version of the mainline F-Chat 3.0 client.
* Cleaner guestbook view
## Todo / Ideas
* Preview mode should allow detaching from the main window
# F-List Exported
This repository contains the open source parts of F-list and F-Chat 3.0.

View File

@ -36,7 +36,7 @@
<div class="card-body">
<div class="tab-content">
<div role="tabpanel" class="tab-pane" :class="{active: tab === '0'}" id="overview">
<match-report :characterMatch="characterMatch"></match-report>
<match-report :characterMatch="characterMatch" :minimized="character.is_self"></match-report>
<div v-bbcode="character.character.description" style="margin-bottom: 10px"></div>
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
</div>
@ -198,8 +198,6 @@
const guestbookState = await methods.guestbookPageGet(this.character.character.id, 1, false);
console.log('GUESTBOOK', guestbookState.posts);
this.guestbookPostCount = `${guestbookState.posts.length}${guestbookState.nextPage ? '+' : ''}`;
} catch (err) {
console.error(err);
@ -227,7 +225,10 @@
async countFriends() {
try {
if (!this.character) {
if (
(!this.character)
|| (!this.character.is_self) && (!this.character.settings.show_friends)
) {
this.friendCount = null;
return;
}
@ -274,12 +275,14 @@
standardParser.allowInlines = true;
standardParser.inlines = this.character.character.inlines;
console.log('LoadChar', this.name, this.character);
this.updateMatches();
// no awaits on these on purpose
this.countGuestbookPosts();
this.countGroups();
this.countFriends();
this.updateMatches();
}

View File

@ -143,11 +143,19 @@
@Hook('mounted')
async mounted(): Promise<void> {
if ((this.character) && (this.character.is_self)) {
return;
}
await this.compareKinks();
}
@Watch('character')
async characterChanged(): Promise<void> {
if ((this.character) && (this.character.is_self)) {
return;
}
await this.compareKinks();
}

View File

@ -1,6 +1,6 @@
<template>
<div id="match-report" :class="{'match-report': true, minimized: minimized}" v-if="(haveScores(characterMatch.you) || haveScores(characterMatch.them))">
<a class="minimize-btn" @click="toggleMinimize()"><i :class="{fa: true, 'fa-plus': minimized, 'fa-minus': !minimized}"></i></a>
<div id="match-report" :class="{'match-report': true, minimized: isMinimized}" v-if="(haveScores(characterMatch.you) || haveScores(characterMatch.them))">
<a class="minimize-btn" @click="toggleMinimize()"><i :class="{fa: true, 'fa-plus': isMinimized, 'fa-minus': !isMinimized}"></i></a>
<div class="scores you">
<h3>
@ -31,7 +31,7 @@
</template>
<script lang="ts">
import { Component, Prop } from '@f-list/vue-ts';
import { Component, Prop, Watch } from '@f-list/vue-ts';
import Vue from 'vue';
import { MatchReport, MatchResult, Score, Scoring } from './matcher';
import * as _ from 'lodash';
@ -42,9 +42,20 @@
@Prop({required: true})
readonly characterMatch!: MatchReport;
@Prop({required: true})
minimized = false;
readonly avatarUrl = Utils.avatarURL;
minimized = false;
isMinimized = false;
@Watch('isMinimized')
onMinimizedChange(): void {
this.isMinimized = this.minimized;
}
getScoreClass(score: Score) {
const classes: any = {};
@ -74,7 +85,7 @@
toggleMinimize() {
this.minimized = !this.minimized;
this.isMinimized = !this.isMinimized;
}
}