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