Files
fnx_web/svelte/src/file_viewer/FileViewer.svelte

749 lines
18 KiB
Svelte

<script>
import { onMount, tick } from "svelte";
import { copy_text } from "../util/Util.svelte";
import { file_struct, list_struct, file_set_href } from "./FileUtilities.svelte";
import Modal from "../util/Modal.svelte";
import PixeldrainLogo from "../util/PixeldrainLogo.svelte";
import DetailsWindow from "./DetailsWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import ListNavigator from "./ListNavigator.svelte";
import FileStats from "./FileStats.svelte";
import EditWindow from "./EditWindow.svelte";
import EmbedWindow from "./EmbedWindow.svelte";
import ReportWindow from "./ReportWindow.svelte";
import IntroPopup from "./IntroPopup.svelte";
import AdHead from "./AdHead.svelte";
import AdLeaderboard from "./AdLeaderboard.svelte";
import AdSkyscraper from "./AdSkyscraper.svelte";
import Sharebar from "./Sharebar.svelte";
import GalleryView from "./GalleryView.svelte";
import Downloader from "./Downloader.svelte";
import CustomBanner from "./CustomBanner.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
let loading = true
let embedded = false
let view_token = ""
let ads_enabled = false
let view = "" // file or gallery
let file = file_struct
let list = list_struct
let is_list = false
let file_preview
let button_home
let list_navigator
let list_shuffle = false
let toggle_shuffle = () => {
list_shuffle = !list_shuffle
}
let sharebar
let sharebar_visible = false
let toggle_sharebar = () => {
if (navigator.share) {
let name = file.name
if (is_list) {
name = list.title
}
navigator.share({
title: name,
text: "I would like to share '" + name + "' with you",
url: window.location.href
})
return
}
sharebar_visible = !sharebar_visible
if (sharebar_visible) {
sharebar.show()
} else {
sharebar.hide()
}
}
let toolbar_visible = (window.innerWidth > 600)
let toolbar_toggle = () => {
toolbar_visible = !toolbar_visible
if (!toolbar_visible && sharebar_visible) {
toggle_sharebar()
}
}
let downloader
let details_window
let details_visible = false
let qr_window
let qr_visible = false
let edit_window
let edit_visible = false
let report_window
let report_visible = false
let embed_window
let embed_visible = false
let skyscraper_visible = false
onMount(() => {
let viewer_data = window.viewer_data
embedded = viewer_data.embedded
if (embedded) {
toolbar_visible = false
}
view_token = viewer_data.view_token
if (viewer_data.type === "list") {
open_list(viewer_data.api_response)
} else {
list.files = [viewer_data.api_response]
open_file_index(0)
}
ads_enabled = list.files[0].show_ads
loading = false
})
const reload = async () => {
loading = true
if (is_list) {
try {
const resp = await fetch(list.info_href);
if (resp.status >= 400) {
throw (await resp.json()).message
}
open_list(await resp.json())
} catch (err) {
alert(err)
}
} else {
try {
const resp = await fetch(file.info_href);
if (resp.status >= 400) {
throw (await resp.json()).message
}
list.files = [await resp.json()]
open_file_index(0)
} catch (err) {
alert(err)
}
}
loading = false
}
const open_list = l => {
l.download_href = window.api_endpoint+"/list/"+l.id+"/zip"
l.info_href = window.api_endpoint+"/list/"+l.id
l.files.forEach(f => {
file_set_href(f)
})
list = l
// Setting is_list to true activates the ListNavgator, which makes sure the
// correct file is opened
is_list = true
if (l.files.length !== 0) {
apply_customizations(l.files[0])
}
hash_change()
}
const hash_change = () => {
// Skip to the file defined in the link hash
let matches = location.hash.match(/item=([\d]+)/)
let index = parseInt(matches ? matches[1] : null)
if (Number.isInteger(index)) {
// The URL contains an item number. Navigate to that item
open_file_index(index)
return
}
// If the hash does not contain a file ID we open the gallery
if (view !== "gallery") {
view = "gallery"
file = file_struct // Empty the file struct
document.title = list.title+" ~ pixeldrain"
}
}
const open_file_index = async index => {
if (index >= list.files.length) {
index = 0
} else if (index < 0) {
index = list.files.length - 1
}
if (list.files[index] === file) {
console.debug("ignoring request to load the same file that is currently loaded")
return
}
console.debug("received request to open file", index)
file_set_href(list.files[index])
file = list.files[index]
// Switch from gallery view to file view if it's not already so
if (view !== "file") {
view = "file"
await tick() // Wait for the file_preview and list_navigator to render
}
// Tell the preview window to start rendering the file
file_preview.set_file(file)
// Tell the list_navigator to highlight the loaded file
if (is_list) {
// Update the URL. This triggers the hash_change again, but it gets
// ignored because the file is already loaded
window.location.hash = "#item=" + index
document.title = file.name+" ~ "+list.title+" ~ pixeldrain"
list_navigator.set_item(index)
} else {
document.title = file.name+" ~ pixeldrain"
}
apply_customizations(file)
// Register a file view
fetch(window.api_endpoint + "/file/" + file.id + "/view", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "token=" + view_token
})
}
const toggle_gallery = () => {
if (view === "gallery") {
window.location.hash = "#item=0"
} else {
window.location.hash = ""
}
}
// Premium page customizations. In the gallery view we will use the
// customizations for the first file in the list, else we simply use the
// selected file. In most cases they are all the same so the user won't notice
// any change
let file_preview_background
let custom_header = ""
let custom_header_link = ""
let custom_background = ""
let custom_footer = ""
let custom_footer_link = ""
const apply_customizations = file => {
if (!file.branding) {
return
}
if (file.branding.header_image) {
custom_header = window.api_endpoint+"/file/"+file.branding.header_image
}
if (file.branding.header_link) {
custom_header_link = file.branding.header_link
}
if (file.branding.footer_image) {
custom_footer = window.api_endpoint+"/file/"+file.branding.footer_image
}
if (file.branding.footer_link) {
custom_footer_link = file.branding.footer_link
}
if (file.branding.background_image) {
custom_background = window.api_endpoint+"/file/"+file.branding.background_image
file_preview_background.style.backgroundImage = "url('"+custom_background+"')"
} else {
file_preview_background.style.backgroundImage = ""
}
}
let supports_fullscreen = !!document.documentElement.requestFullscreen
let fullscreen = false
const toggle_fullscreen = () => {
if (!fullscreen) {
document.documentElement.requestFullscreen()
document.getElementById("btn_fullscreen_icon").innerText = "fullscreen_exit"
fullscreen = true
} else {
document.exitFullscreen()
document.getElementById("btn_fullscreen_icon").innerText = "fullscreen"
fullscreen = false
}
}
let copy_url_status = "" // empty, copied, or error
const copy_url = () => {
if (copy_text(window.location.href)) {
copy_url_status = "copied"
} else {
copy_url_status = "error"
alert("Your browser does not support copying text.")
}
setTimeout(() => { copy_url_status = "" }, 60000)
}
const grab_file = async () => {
if (!window.user_authenticated || file.can_edit) {
return
}
const form = new FormData()
form.append("grab_file", file.id)
try {
const resp = await fetch(
window.api_endpoint + "/file",
{ method: "POST", body: form },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
window.open("/u/" + (await resp.json()).id, "_blank")
} catch (err) {
alert("Failed to grab file: " + err)
return
}
}
const keyboard_event = evt => {
if (evt.ctrlKey || evt.altKey || evt.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (
document.activeElement.type && (
document.activeElement.type === "text" ||
document.activeElement.type === "textarea"
)
) {
return // Prevent shortcuts from interfering with input fields
}
console.debug("Key pressed: " + evt.key)
switch (evt.key) {
case "a": // A or left arrow key go to previous file
case "ArrowLeft":
if (list_navigator) {
list_navigator.prev()
}
break
case "d": // D or right arrow key go to next file
case "ArrowRight":
if (list_navigator) {
list_navigator.next()
}
break
case "s":
case "S":
if (evt.shiftKey) {
downloader.download_list() // SHIFT + S downloads all files in list
} else {
downloader.download_file() // S to download the current file
}
break
case "r": // R to toggle list shuffle
if (list_navigator) {
toggle_shuffle()
}
break
case "c": // C to copy to clipboard
copy_url()
break
case "i": // I to open the details window
details_window.toggle()
break
case "e": // E to open the edit window
if (file.can_edit || list.can_edit) {
edit_window.toggle()
}
break
case "m": // M to open the embed window
embed_window.toggle()
break
case "g": // G to grab this file
this.grabFile()
break
case "q": // Q to close the window
window.close()
break
}
}
</script>
<svelte:window on:keydown={keyboard_event} on:hashchange={hash_change}/>
<div id="file_viewer" class="file_viewer">
<!-- Head elements for the ads -->
<AdHead></AdHead>
<LoadingIndicator loading={loading}/>
<div id="headerbar" class="headerbar">
<button
on:click={toolbar_toggle}
class="button_toggle_toolbar round"
class:button_highlight={toolbar_visible}
title="Open or close the toolbar">
<i class="icon">menu</i>
</button>
<a
href="/"
bind:this={button_home}
class="button button_home round"
target={embedded ? "_blank" : ""}
title="Go to the pixeldrain home page">
<PixeldrainLogo style="height: 1.6em; width: 1.6em; margin: 0 4px 0 0;"></PixeldrainLogo>
</a>
<div id="file_viewer_headerbar_title" class="file_viewer_headerbar_title">
{#if list.title !== ""}{list.title}<br/>{/if}
{#if file.name !== ""}{file.name}{/if}
</div>
{#if embedded && supports_fullscreen}
<button
class="round"
on:click={toggle_fullscreen}
title="Open this page in full-screen mode">
<i class="icon" id="btn_fullscreen_icon">fullscreen</i>
</button>
{/if}
</div>
{#if is_list && view === "file"}
<ListNavigator
bind:this={list_navigator}
files={list.files}
shuffle={list_shuffle}
on:set_file={e => open_file_index(e.detail)}
on:toggle_gallery={toggle_gallery}
>
</ListNavigator>
{/if}
<CustomBanner src={custom_header} link={custom_header_link} border_top={true}></CustomBanner>
<div class="file_preview_row">
<div id="toolbar" class="toolbar" class:toolbar_visible>
{#if view === "file"}
<FileStats file={file}></FileStats>
<div class="separator"></div>
{/if}
{#if file.abuse_type === "" && view === "file"}
<button
on:click={downloader.download_file}
class="toolbar_button"
title="Save this file to your computer">
<i class="icon">download</i>
<span>Download</span>
</button>
{/if}
{#if file.abuse_type === "" && is_list}
<button
on:click={downloader.download_list}
class="toolbar_button"
title="Download all files in this album as a zip archive">
<i class="icon">download</i>
<span>DL all files</span>
</button>
{/if}
<button
on:click={copy_url}
class="toolbar_button"
class:button_highlight={copy_url_status === "copied"}
class:button_red={copy_url_status === "error"}
title="Copy a link to this page to your clipboard">
<i class="icon">content_copy</i>
<span>
{#if copy_url_status === "copied"}
Copied!
{:else if copy_url_status === "error"}
Error!
{:else}
<u>C</u>opy link
{/if}
</span>
</button>
<button
on:click={toggle_sharebar}
class="toolbar_button"
class:button_highlight={sharebar_visible}
title="Share this file on social media">
<i class="icon">share</i>
<span>Share</span>
</button>
<button
class="toolbar_button"
on:click={qr_window.toggle}
class:button_highlight={qr_visible}
title="Show a QR code with a link to this page. Useful for sharing files in-person">
<i class="icon">qr_code</i>
<span>QR code</span>
</button>
{#if is_list}
<button
class="toolbar_button"
title="Go to a random file when pressing → or clicking the next file button"
class:button_highlight={list_shuffle}
on:click={toggle_shuffle}>
<i class="icon">shuffle</i>
{#if list_shuffle}
<span>Shuffle&nbsp;&#x2611;</span>
{:else}
<span>Shuffle&nbsp;&#x2610;</span>
{/if}
</button>
{/if}
{#if view === "file"}
<button
class="toolbar_button"
on:click={details_window.toggle}
class:button_highlight={details_visible}
title="Information and statistics about this file">
<i class="icon">help</i>
<span>Deta<u>i</u>ls</span>
</button>
{/if}
<div class="separator"></div>
{#if file.can_edit || list.can_edit}
<button
class="toolbar_button"
on:click={edit_window.toggle}
class:button_highlight={edit_visible}
title="Edit or delete this file or album">
<i class="icon">edit</i>
<span><u>E</u>dit</span>
</button>
{/if}
{#if view === "file" && window.user_authenticated && !file.can_edit}
<button
on:click={grab_file}
class="toolbar_button"
title="Copy this file to your own pixeldrain account">
<i class="icon">save_alt</i>
<span><u>G</u>rab file</span>
</button>
{/if}
<button
class="toolbar_button"
title="Include this file in your own webpages"
on:click={embed_window.toggle}
class:button_highlight={embed_visible}>
<i class="icon">code</i>
<span>E<u>m</u>bed</span>
</button>
{#if view === "file"}
<button
class="toolbar_button"
title="Report this file as abusive"
on:click={report_window.toggle}
class:button_highlight={report_visible}>
<i class="icon">flag</i>
<span>Report</span>
</button>
{/if}
<br/>
</div>
<div bind:this={file_preview_background}
class="file_preview"
class:checkers={!custom_background}
class:custom_background={!!custom_background}
class:toolbar_visible
class:skyscraper_visible
>
{#if view === "file"}
<FilePreview
bind:this={file_preview}
is_list={is_list}
on:download={downloader.download_file}
on:prev={() => { if (list_navigator) { list_navigator.prev() }}}
on:next={() => { if (list_navigator) { list_navigator.next() }}}
on:loading={e => {loading = e.detail}}
on:reload={reload}>
</FilePreview>
{:else if view === "gallery"}
<GalleryView
list={list}
on:reload={reload}
on:loading={e => {loading = e.detail}}>
</GalleryView>
{/if}
</div>
<Sharebar bind:this={sharebar}></Sharebar>
{#if ads_enabled}
<AdSkyscraper on:visibility={e => {skyscraper_visible = e.detail}}></AdSkyscraper>
{/if}
</div>
{#if ads_enabled}
<AdLeaderboard></AdLeaderboard>
{:else if custom_footer}
<CustomBanner src={custom_footer} link={custom_footer_link}></CustomBanner>
{/if}
<Modal bind:this={details_window} on:is_visible={e => {details_visible = e.detail}} title="File details" width="1000px">
<DetailsWindow file={file}></DetailsWindow>
</Modal>
<Modal bind:this={qr_window} on:is_visible={e => {qr_visible = e.detail}} title="QR code" width="500px">
<img src="{window.api_endpoint}/misc/qr?text={encodeURIComponent(window.location.href)}" alt="QR code" style="display: block; width: 100%;"/>
</Modal>
<Modal bind:this={edit_window} on:is_visible={e => {edit_visible = e.detail}} title={"Editing "+file.name}>
<EditWindow file={file} list={list} on:reload={reload}></EditWindow>
</Modal>
<Modal bind:this={embed_window} on:is_visible={e => {embed_visible = e.detail}} title="Embed file" width="820px">
<EmbedWindow file={file} list={list}></EmbedWindow>
</Modal>
<Modal bind:this={report_window} on:is_visible={e => {report_visible = e.detail}} title="Report abuse" width="800px">
<ReportWindow file={file} list={list}></ReportWindow>
</Modal>
{#if ads_enabled}
<IntroPopup target={button_home}></IntroPopup>
{/if}
<Downloader bind:this={downloader} file={file} list={list}></Downloader>
</div>
<style>
.file_viewer {
position: absolute;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--body_background);
}
/* Headerbar (row 1) */
.headerbar {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
text-align: left;
padding: 4px;
}
@media(max-height: 600px) {
.headerbar {
padding: 1px;
}
}
/* Headerbar components */
.headerbar > * {
flex-grow: 0;
flex-shrink: 0;
margin-left: 4px;
margin-right: 4px;
display: inline;
}
.headerbar > .file_viewer_headerbar_title {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
overflow: hidden;
line-height: 1.2em; /* When the page is a list there will be two lines. Dont's want to stretch the container*/
white-space: nowrap;
text-overflow: ellipsis;
justify-content: center;
}
.headerbar > button > .icon {
font-size: 1.6em;
}
.headerbar > .button_home::after {
content: "pixeldrain";
}
@media (max-width: 600px) {
.headerbar > .button_home::after {
content: "pd";
}
}
/* File preview area (row 3) */
.file_preview_row {
flex-grow: 1;
flex-shrink: 1;
position: relative;
display: block;
}
.file_preview {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: block;
min-height: 100px;
min-width: 100px;
transition: left 0.5s;
overflow: auto;
text-align: center;
border-radius: 12px;
border: 2px solid var(--separator);
}
.file_preview.toolbar_visible { left: 8.2em; }
.file_preview.skyscraper_visible { right: 160px; }
.file_preview.custom_background {
background-size: cover;
background-position: center;
}
/* Toolbars */
.toolbar {
position: absolute;
width: 8.2em;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
left: -8.2em;
bottom: 0;
top: 0;
padding: 0;
text-align: left;
transition: left 0.5s, right 0.5s;
z-index: 1;
}
.toolbar::-webkit-scrollbar {
display: none;
}
.toolbar.toolbar_visible { left: 0; }
.toolbar_button {
text-align: left;
margin: 4px;
width: calc(100% - 8px);
}
.toolbar_button > span {
vertical-align: middle;
}
.toolbar > .separator {
height: 2px;
width: 100%;
margin: 4px 0;
background-color: var(--separator);
}
</style>