820 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			820 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|     <!-- hiding elements instead of using 'v-if' is used here as an optimization -->
 | |
|     <div class="image-preview-wrapper" :class="{interactive: sticky, visible: visible}">
 | |
|         <div class="image-preview-toolbar" v-show="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="reset()" title="Reset Image Viewer"><i class="fa fa-recycle"></i></a>
 | |
|             <a @click="toggleStickyMode()" :class="{toggled: sticky}" title="Toggle Stickyness"><i class="fa fa-thumbtack"></i></a>
 | |
|         </div>
 | |
| 
 | |
|         <!-- note: preload requires a webpack config CopyPlugin configuration -->
 | |
|         <webview
 | |
|             preload="./preview/assets/browser.pre.js"
 | |
|             src="about:blank"
 | |
|             webpreferences="autoplayPolicy=no-user-gesture-required,contextIsolation,sandbox,disableDialogs,disableHtmlFullScreenWindowResize,webSecurity,enableWebSQL=no,nodeIntegration=no,nativeWindowOpen=no,nodeIntegrationInWorker=no,nodeIntegrationInSubFrames=no,webviewTag=no"
 | |
|             enableremotemodule="false"
 | |
|             allowpopups="false"
 | |
|             nodeIntegration="false"
 | |
|             partition="persist:adblocked"
 | |
| 
 | |
|             id="image-preview-ext"
 | |
|             ref="imagePreviewExt"
 | |
|             class="image-preview-external"
 | |
|             :style="previewStyles.ExternalImagePreviewHelper">
 | |
|         </webview>
 | |
| 
 | |
|         <div
 | |
|             class="image-preview-local"
 | |
|             :style="previewStyles.LocalImagePreviewHelper"
 | |
|         >
 | |
|         </div>
 | |
| 
 | |
|         <character-preview
 | |
|             :style="previewStyles.CharacterPreviewHelper"
 | |
|             ref="characterPreview"
 | |
|         ></character-preview>
 | |
| 
 | |
|         <i id="preview-spinner" class="fas fa-circle-notch fa-spin" v-show="shouldShowSpinner"></i>
 | |
|         <i id="preview-error" class="fas fa-times" v-show="shouldShowError"></i>
 | |
|     </div>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts">
 | |
|     import * as _ from 'lodash';
 | |
|     import {Component, Hook} from '@f-list/vue-ts';
 | |
|     import Vue from 'vue';
 | |
|     import core from '../core';
 | |
|     import { EventBus, EventBusEvent } from './event-bus';
 | |
|     import {domain} from '../../bbcode/core';
 | |
|     import {ImageDomMutator} from './image-dom-mutator';
 | |
| 
 | |
|     import {
 | |
|       ExternalImagePreviewHelper,
 | |
|       LocalImagePreviewHelper,
 | |
|       PreviewManager,
 | |
|       CharacterPreviewHelper, RenderStyle
 | |
|     } from './helper';
 | |
| 
 | |
|     import {Point} from 'electron';
 | |
|     import * as remote from '@electron/remote';
 | |
| 
 | |
|     import Timer = NodeJS.Timer;
 | |
|     import IpcMessageEvent = Electron.IpcMessageEvent;
 | |
|     import CharacterPreview from './CharacterPreview.vue';
 | |
| 
 | |
|     const screen = remote.screen;
 | |
| 
 | |
|     const FLIST_PROFILE_MATCH = _.cloneDeep(/https?:\/\/(www.)?f-list.net\/c\/([a-zA-Z0-9+%_.!~*'()]+)\/?/);
 | |
| 
 | |
|     interface DidFailLoadEvent extends Event {
 | |
|         errorCode: number;
 | |
|         errorDescription: string;
 | |
|     }
 | |
| 
 | |
|     interface DidNavigateEvent extends Event {
 | |
|         httpResponseCode: number;
 | |
|         httpStatusText: string;
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|         components: {
 | |
|           'character-preview': CharacterPreview
 | |
|         }
 | |
|     })
 | |
|     export default class ImagePreview extends Vue {
 | |
|         private readonly MinTimePreviewVisible = 100;
 | |
| 
 | |
|         visible = false;
 | |
| 
 | |
|         previewManager = new PreviewManager(
 | |
|           this,
 | |
|           [
 | |
|             new ExternalImagePreviewHelper(this),
 | |
|             new LocalImagePreviewHelper(this),
 | |
|             new CharacterPreviewHelper(this)
 | |
|             // new ChannelPreviewHelper(this)
 | |
|           ]
 | |
|         );
 | |
| 
 | |
|         // externalPreviewHelper = new ExternalImagePreviewHelper(this);
 | |
|         // localPreviewHelper = new LocalImagePreviewHelper(this);
 | |
|         // externalPreviewStyle: Record<string, any> = {};
 | |
|         // localPreviewStyle: Record<string, any> = {};
 | |
| 
 | |
|         url: string | null = null;
 | |
|         domain: string | undefined;
 | |
| 
 | |
|         sticky = false;
 | |
|         runJs = true;
 | |
|         debug = false;
 | |
| 
 | |
|         jsMutator = new ImageDomMutator(this.debug);
 | |
| 
 | |
|         state = 'hidden';
 | |
| 
 | |
|         shouldShowSpinner = false;
 | |
|         shouldShowError = true;
 | |
| 
 | |
|         private interval: Timer | null = null;
 | |
| 
 | |
|         private exitInterval: Timer | null = null;
 | |
|         private exitUrl: string | null = null;
 | |
| 
 | |
|         private initialCursorPosition: Point | null = null;
 | |
|         private shouldDismiss = false;
 | |
|         private visibleSince = 0;
 | |
| 
 | |
|         previewStyles: Record<string, RenderStyle> = {};
 | |
| 
 | |
| 
 | |
|         @Hook('mounted')
 | |
|         async onMounted(): Promise<void> {
 | |
|             console.info('Mounted ImagePreview');
 | |
| 
 | |
|             // tslint:disable-next-line:no-floating-promises
 | |
|             this.jsMutator.init();
 | |
| 
 | |
|             EventBus.$on(
 | |
|                 'imagepreview-dismiss',
 | |
|                 (eventData: EventBusEvent) => {
 | |
|                     // console.log('Event dismiss', eventData.url);
 | |
|                     this.dismiss(this.negotiateUrl(eventData.url as string || ''), eventData.force as boolean);
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             EventBus.$on(
 | |
|                 'imagepreview-show',
 | |
|                 (eventData: EventBusEvent) => {
 | |
|                     // console.log('Event show', eventData.url);
 | |
| 
 | |
|                     const url = this.negotiateUrl(eventData.url as string || '');
 | |
|                     const isInternalPreview = CharacterPreviewHelper.FLIST_CHARACTER_PROTOCOL_TESTER.test(url);
 | |
| 
 | |
|                     if (
 | |
|                       ((!core.state.settings.risingCharacterPreview) && (isInternalPreview))
 | |
|                       || ((!core.state.settings.risingLinkPreview) && (!isInternalPreview))
 | |
|                     ) {
 | |
|                       return;
 | |
|                     }
 | |
| 
 | |
|                     this.show(url);
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             EventBus.$on(
 | |
|                 'imagepreview-toggle-stickyness',
 | |
|                 (eventData: EventBusEvent) => {
 | |
|                     if (!core.state.settings.risingLinkPreview) {
 | |
|                         return;
 | |
|                     }
 | |
| 
 | |
|                     const eventUrl = this.jsMutator.mutateUrl(this.negotiateUrl(eventData.url as string || ''));
 | |
| 
 | |
|                     if (
 | |
|                       ((eventData.force === true) || (this.url === eventUrl))
 | |
|                       && (this.visible)
 | |
|                     ) {
 | |
|                         this.sticky = !this.sticky;
 | |
| 
 | |
|                         if (eventData.force) {
 | |
|                           this.hide();
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             const webview = this.getWebview();
 | |
| 
 | |
|             // clear preview cache, particularly cookies
 | |
|             // setInterval(
 | |
|             //     () => remote.webContents.fromId(webview.getWebContentsId()).session.clearStorageData({storages: ['cookies', 'indexdb']}),
 | |
|             //     5000
 | |
|             // );
 | |
| 
 | |
|             webview.addEventListener(
 | |
|                 'update-target-url', // 'did-navigate', // 'dom-ready',
 | |
|                 (event: EventBusEvent) => {
 | |
|                     const url = webview.getURL();
 | |
|                     const js = this.jsMutator.getMutatorJsForSite(url, 'update-target-url');
 | |
| 
 | |
|                     // tslint:disable-next-line
 | |
|                     this.executeJavaScript(js, 'update-target-url', event);
 | |
|                 }
 | |
|             );
 | |
| 
 | |
| 
 | |
|             webview.addEventListener(
 | |
|                 'dom-ready', // 'did-navigate', // 'dom-ready',
 | |
|                 (event: EventBusEvent) => {
 | |
|                     const url = webview.getURL();
 | |
|                     const js = this.jsMutator.getMutatorJsForSite(url, 'dom-ready');
 | |
| 
 | |
|                     // tslint:disable-next-line
 | |
|                     this.executeJavaScript(js, 'dom-ready', event);
 | |
| 
 | |
|                     this.setState('loaded');
 | |
|                 }
 | |
|             );
 | |
| 
 | |
| 
 | |
|             webview.addEventListener(
 | |
|                 'did-fail-load',
 | |
|                 (event: Event) => {
 | |
| 
 | |
|                     const e = event as DidFailLoadEvent;
 | |
| 
 | |
|                     if (e.errorCode !== -3) {
 | |
|                         this.setState('error'); // -3 is a weird error code, not sure why it occurs
 | |
|                     }
 | |
| 
 | |
| 
 | |
|                     if (e.errorCode < 0) {
 | |
|                         const url = webview.getURL();
 | |
| 
 | |
|                         if (url.match(/^https?:\/\/(www.)?pornhub.com/)) {
 | |
|                             const qjs = this.jsMutator.getMutatorJsForSite(url, 'update-target-url')
 | |
|                                 || this.jsMutator.getMutatorJsForSite(url, 'dom-ready');
 | |
| 
 | |
|                             // tslint:disable-next-line
 | |
|                             this.executeJavaScript(qjs, 'did-fail-load-but-still-loading', event);
 | |
|                             return;
 | |
|                         }
 | |
| 
 | |
|                         // console.error('DID FAIL LOAD', event);
 | |
|                         // const url = this.getUrl() || '';
 | |
|                         //
 | |
|                         // const qjs = this.jsMutator.getMutatorJsForSite(url, 'update-target-url')
 | |
|                         //   || this.jsMutator.getMutatorJsForSite(url, 'dom-ready');
 | |
|                         //
 | |
|                         // // tslint:disable-next-line
 | |
|                         // this.executeJavaScript(qjs, 'did-fail-load-but-still-loading', event);
 | |
|                         return;
 | |
|                     }
 | |
| 
 | |
|                     // if (e.errorCode < 100) {
 | |
|                     //   const url = webview.getURL();
 | |
|                     //   const js = this.jsMutator.getMutatorJsForSite(url, 'update-target-url');
 | |
|                     //
 | |
|                     //   this.executeJavaScript(js, 'did-fail-load-but-still-loading', event);
 | |
|                     //
 | |
|                     //   return;
 | |
|                     // }
 | |
| 
 | |
|                     const js = this.jsMutator.getErrorMutator(e.errorCode, e.errorDescription);
 | |
| 
 | |
|                     // tslint:disable-next-line
 | |
|                     this.executeJavaScript(js, 'did-fail-load', event);
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             webview.addEventListener(
 | |
|                 'did-navigate',
 | |
|                 (event: Event) => {
 | |
|                     const e = event as DidNavigateEvent;
 | |
| 
 | |
|                     if (e.httpResponseCode >= 400) {
 | |
|                         const js = this.jsMutator.getErrorMutator(e.httpResponseCode, e.httpStatusText);
 | |
| 
 | |
|                         // tslint:disable-next-line
 | |
|                         this.executeJavaScript(js, 'did-navigate', event);
 | |
|                     }
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             // webview.getWebContents().on(
 | |
|             webview.addEventListener(
 | |
|                 'did-finish-load',
 | |
|                 (event: Event) => {
 | |
|                     this.debugLog('ImagePreview did-finish-load', event);
 | |
|                 }
 | |
|             );
 | |
| 
 | |
| 
 | |
|             webview.addEventListener(
 | |
|                 'ipc-message',
 | |
|                 (event: IpcMessageEvent) => {
 | |
|                     this.debugLog('ImagePreview ipc-message', event);
 | |
| 
 | |
|                     if (event.channel === 'webview.img') {
 | |
|                         // tslint:disable-next-line:no-unsafe-any
 | |
|                         this.updatePreviewSize(parseInt(event.args[0], 10), parseInt(event.args[1], 10));
 | |
|                     }
 | |
|                 }
 | |
|             );
 | |
| 
 | |
| 
 | |
|             // const webContentsId = webview.getWebContentsId();
 | |
|             //
 | |
|             // remote.webContents.fromId(webContentsId).session.on(
 | |
|             //     'will-download',
 | |
|             //     (e: Event) => {
 | |
|             //         e.preventDefault();
 | |
|             //     }
 | |
|             // );
 | |
| 
 | |
| 
 | |
|             _.each(
 | |
|                 ['did-start-loading', 'load-commit', 'dom-ready', 'will-navigate', 'did-navigate', 'did-navigate-in-page', 'update-target-url', 'ipc-message'],
 | |
|                 (en: string) => {
 | |
|                     webview.addEventListener(
 | |
|                         en,
 | |
|                         (event: Event) => {
 | |
|                             this.debugLog(`ImagePreview ${en} ${Date.now()}`, event);
 | |
|                         }
 | |
|                     );
 | |
|                 }
 | |
|             );
 | |
| 
 | |
| 
 | |
|             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.debugLog('ImagePreview: call hide from interval');
 | |
| 
 | |
|                         this.hide();
 | |
|                     }
 | |
| 
 | |
|                     this.shouldShowSpinner = this.testSpinner();
 | |
|                     this.shouldShowError = this.testError();
 | |
|                 },
 | |
|                 50
 | |
|             );
 | |
|         }
 | |
| 
 | |
| 
 | |
|         reRenderStyles(): void {
 | |
|             this.previewStyles = this.previewManager.renderStyles();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         negotiateUrl(url: string): string {
 | |
|           const match = url.match(FLIST_PROFILE_MATCH);
 | |
| 
 | |
|           if (!match) {
 | |
|             return url;
 | |
|           }
 | |
| 
 | |
|           return `flist-character://${decodeURI(match[2])}`;
 | |
|         }
 | |
| 
 | |
|         updatePreviewSize(width: number, height: number): void {
 | |
|             const helper = this.previewManager.getVisiblePreview();
 | |
| 
 | |
|             if ((!helper) || (!helper.reactsToSizeUpdates())) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if ((width) && (height)) {
 | |
|                 this.debugLog('ImagePreview: updatePreviewSize', width, height, width / height);
 | |
| 
 | |
|                 helper.setRatio(width / height);
 | |
|                 this.reRenderStyles();
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         hide(): void {
 | |
|             this.cancelExitTimer();
 | |
| 
 | |
|             this.url = null;
 | |
|             this.visible = false;
 | |
| 
 | |
|             this.previewManager.hide();
 | |
| 
 | |
|             this.exitUrl = null;
 | |
|             this.exitInterval = null;
 | |
| 
 | |
|             this.shouldDismiss = false;
 | |
| 
 | |
|             this.sticky = false;
 | |
| 
 | |
|             this.setState('hidden');
 | |
| 
 | |
|             this.reRenderStyles();
 | |
|         }
 | |
| 
 | |
|         dismiss(initialUrl: string, force: boolean = false): void {
 | |
|             const url = this.jsMutator.mutateUrl(initialUrl);
 | |
| 
 | |
|             this.debugLog('ImagePreview: dismiss', url);
 | |
| 
 | |
|             if (this.url !== url)
 | |
|                 return; // simply ignore
 | |
| 
 | |
|             // if (this.debug)
 | |
|             //    return;
 | |
| 
 | |
|             if (this.sticky)
 | |
|                 return;
 | |
| 
 | |
|             // console.log('DISMISS');
 | |
| 
 | |
|             const due = this.visible ? this.MinTimePreviewVisible - Math.min(this.MinTimePreviewVisible, (Date.now() - this.visibleSince)) : 0;
 | |
| 
 | |
|             this.cancelTimer();
 | |
| 
 | |
|             if (this.exitInterval)
 | |
|                 return;
 | |
| 
 | |
|             this.exitUrl = this.url;
 | |
|             this.shouldDismiss = true;
 | |
| 
 | |
|             if ((!this.hasMouseMovedSince()) && (!force))
 | |
|                 return;
 | |
| 
 | |
|             this.debugLog('ImagePreview: dismiss.exec', due, this.previewManager.getVisibilityStatus(), url);
 | |
| 
 | |
|             // 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
 | |
|             // tslint:disable-next-line no-unnecessary-type-assertion
 | |
|             this.exitInterval = setTimeout(
 | |
|                 () => this.hide(),
 | |
|                 due
 | |
|             ) as Timer;
 | |
|         }
 | |
| 
 | |
|         show(initialUrl: string): void {
 | |
|             const url = this.jsMutator.mutateUrl(initialUrl);
 | |
| 
 | |
|             this.debugLog('ImagePreview: show', this.previewManager.getVisibilityStatus(),
 | |
|                 this.visible, this.hasMouseMovedSince(), !!this.interval, this.sticky, url);
 | |
| 
 | |
|             // console.log('SHOW');
 | |
| 
 | |
|             if ((this.visible) && (!this.exitInterval) && (!this.hasMouseMovedSince())) {
 | |
|                 this.debugLog('ImagePreview: show cancel: visible & not moved');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ((this.url === url) && ((this.visible) || (this.interval))) {
 | |
|                 this.debugLog('ImagePreview: same url', url, this.url);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ((this.url) && (this.sticky) && (this.visible)) {
 | |
|                 this.debugLog('ImagePreview: sticky visible');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             this.debugLog('ImagePreview: show.exec', url);
 | |
| 
 | |
|             const due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 200;
 | |
| 
 | |
|             this.url = url;
 | |
|             this.domain = domain(url);
 | |
| 
 | |
|             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
 | |
|             // tslint:disable-next-line no-unnecessary-type-assertion
 | |
|             this.interval = setTimeout(
 | |
|                 () => {
 | |
|                     this.debugLog('ImagePreview: show.timeout', this.url);
 | |
| 
 | |
|                     const helper = this.previewManager.show(this.url || undefined, this.domain);
 | |
| 
 | |
|                     this.interval = null;
 | |
|                     this.visible = true;
 | |
|                     this.visibleSince = Date.now();
 | |
|                     this.shouldDismiss = false;
 | |
| 
 | |
|                     this.initialCursorPosition = screen.getCursorScreenPoint();
 | |
| 
 | |
|                     this.reRenderStyles();
 | |
| 
 | |
|                     if (helper) {
 | |
|                       this.setState(helper.shouldTrackLoading() ? 'loading' : 'loaded');
 | |
|                     } else {
 | |
|                       this.setState('loaded');
 | |
|                     }
 | |
|                 },
 | |
|                 due
 | |
|             ) as Timer;
 | |
|         }
 | |
| 
 | |
|         hasMouseMovedSince(): boolean {
 | |
|             if (!this.initialCursorPosition)
 | |
|                 return true;
 | |
| 
 | |
|             try {
 | |
|                 const p = screen.getCursorScreenPoint();
 | |
| 
 | |
|                 return ((p.x !== this.initialCursorPosition.x) || (p.y !== this.initialCursorPosition.y));
 | |
|             } catch (err) {
 | |
|                 console.error('ImagePreview', err);
 | |
|                 return true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         cancelTimer(): void {
 | |
|             if (this.interval)
 | |
|                 clearTimeout(this.interval);
 | |
| 
 | |
|             this.interval = null;
 | |
|         }
 | |
| 
 | |
|         cancelExitTimer(): void {
 | |
|             if (this.exitInterval)
 | |
|                 clearTimeout(this.exitInterval);
 | |
| 
 | |
|             this.exitInterval = null;
 | |
|         }
 | |
| 
 | |
|         isVisible(): boolean {
 | |
|             return this.visible;
 | |
|         }
 | |
| 
 | |
|         getUrl(): string | null {
 | |
|             return this.url;
 | |
|         }
 | |
| 
 | |
|         /* 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'));
 | |
|         }
 | |
| 
 | |
|         isInternalUrl(): boolean {
 | |
|             return !this.isExternalUrl();
 | |
|         }*/
 | |
| 
 | |
|         toggleDevMode(): void {
 | |
|             this.debug = !this.debug;
 | |
| 
 | |
|             this.jsMutator.setDebug(this.debug);
 | |
|             this.previewManager.setDebug(this.debug);
 | |
| 
 | |
|             if (this.debug) {
 | |
|                 const webview = this.getWebview();
 | |
| 
 | |
|                 webview.openDevTools();
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         async executeJavaScript(js: string | undefined, context: string = 'unknown', logDetails?: any): Promise<any> {
 | |
|             // console.log('EXECUTE JS', js);
 | |
| 
 | |
|             if (!this.runJs) return;
 | |
| 
 | |
|             const webview = this.getWebview();
 | |
| 
 | |
|             if (!js) {
 | |
|                 this.debugLog(`ImagePreview ${context}: No JavaScript to execute`, logDetails);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             this.debugLog(`ImagePreview execute-${context}`, js, logDetails);
 | |
| 
 | |
|             try {
 | |
|                 const result = await (webview.executeJavaScript(js) as unknown as Promise<any>);
 | |
| 
 | |
|                 this.debugLog(`ImagePreview result-${context}`, result);
 | |
| 
 | |
|                 return result;
 | |
|             } catch (err) {
 | |
|                 this.debugLog(`ImagePreview error-${context}`, err);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         debugLog(...args: any[]): void {
 | |
|             if (this.debug) {
 | |
|                 console.log(...args);
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         toggleStickyMode(): void {
 | |
|             this.sticky = !this.sticky;
 | |
| 
 | |
|             if (!this.sticky)
 | |
|                 this.hide();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         toggleJsMode(): void {
 | |
|             this.runJs = !this.runJs;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         reloadUrl(): void {
 | |
|             const helper = this.previewManager.getVisiblePreview();
 | |
| 
 | |
|             if ((!helper) || (!helper.usesWebView())) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             // helper.reload();
 | |
|             this.getWebview().reload();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         getWebview(): Electron.WebviewTag {
 | |
|             return this.$refs.imagePreviewExt as Electron.WebviewTag;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         getCharacterPreview(): CharacterPreview {
 | |
|           return this.$refs.characterPreview as CharacterPreview;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         reset(): void {
 | |
|             this.previewManager = new PreviewManager(
 | |
|               this,
 | |
|               [
 | |
|                 new ExternalImagePreviewHelper(this),
 | |
|                 new LocalImagePreviewHelper(this),
 | |
|                 new CharacterPreviewHelper(this)
 | |
|                 // new ChannelPreviewHelper(this)
 | |
|               ]
 | |
|             );
 | |
| 
 | |
|             this.url = null;
 | |
|             this.domain = undefined;
 | |
| 
 | |
|             this.sticky = false;
 | |
|             this.runJs = true;
 | |
|             this.debug = false;
 | |
| 
 | |
|             this.jsMutator = new ImageDomMutator(this.debug);
 | |
| 
 | |
|             this.cancelExitTimer();
 | |
|             this.cancelTimer();
 | |
| 
 | |
|             this.exitUrl = null;
 | |
| 
 | |
|             this.initialCursorPosition = null;
 | |
|             this.shouldDismiss = false;
 | |
|             this.visibleSince = 0;
 | |
|             this.shouldShowSpinner = false;
 | |
|             this.shouldShowError = false;
 | |
| 
 | |
|             this.setState('hidden');
 | |
| 
 | |
|             this.reRenderStyles();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         setState(state: string): void {
 | |
|             this.debugLog('ImagePreview set-state', state, (this.visibleSince > 0) ? `${(Date.now() - this.visibleSince) / 1000}s` : '');
 | |
| 
 | |
|             this.state = state;
 | |
|             this.shouldShowSpinner = this.testSpinner();
 | |
|             this.shouldShowError = this.testError();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         testSpinner(): boolean {
 | |
|             return (this.visibleSince > 0)
 | |
|                 ? ((this.state === 'loading') && (Date.now() - this.visibleSince > 1000))
 | |
|                 : false;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         testError(): boolean {
 | |
|             const helper = this.previewManager.getVisiblePreview();
 | |
| 
 | |
|             if ((!helper) || (!helper.usesWebView())) {
 | |
|               return false;
 | |
|             }
 | |
| 
 | |
|             return (this.state === 'error');
 | |
|         }
 | |
|     }
 | |
| </script>
 | |
| 
 | |
| 
 | |
| <style lang="scss">
 | |
|     @import "../../node_modules/bootstrap/scss/functions";
 | |
|     @import "../../node_modules/bootstrap/scss/variables";
 | |
|     @import "../../node_modules/bootstrap/scss/mixins/breakpoints";
 | |
| 
 | |
|     .image-preview-wrapper {
 | |
|         z-index: 10000;
 | |
|         display: none;
 | |
|         position: absolute;
 | |
|         left: 0;
 | |
|         top: 0;
 | |
|         width: 50%;
 | |
|         height: 70%;
 | |
|         pointer-events: none;
 | |
|         overflow: visible;
 | |
| 
 | |
|         &.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);
 | |
|             z-index: 1000;
 | |
| 
 | |
|             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);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         #preview-spinner {
 | |
|             color: white;
 | |
|             opacity: 0.5;
 | |
|             transition: visibility 0.25s, opacity 0.25s;
 | |
|             font-size: 30pt;
 | |
|             position: absolute;
 | |
|             left: 1rem;
 | |
|             top: 1rem;
 | |
|             transform: translateX(-50%), translateY(-50%);
 | |
|             text-shadow: 0 0 2px #b3b3b3;
 | |
|         }
 | |
| 
 | |
|         #preview-error {
 | |
|             color: red;
 | |
|             transition: all 0.25s;
 | |
|             font-size: 180pt;
 | |
|             position: absolute;
 | |
|             left: 2rem;
 | |
|             top: 0;
 | |
|             opacity: 0.8;
 | |
|         }
 | |
|     }
 | |
| </style>
 |