Update filesystem

This commit is contained in:
2020-11-17 23:39:27 +01:00
parent 09fa952ce3
commit d8236ce0f9
14 changed files with 3233 additions and 1128 deletions

View File

@@ -1,8 +1,11 @@
.file_viewer.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{position:absolute;display:flex;flex-direction:column;top:0;right:0;bottom:0;left:0;overflow:hidden}.file_viewer.svelte-15t34ei>.file_viewer_headerbar.svelte-15t34ei.svelte-15t34ei{flex-grow:0;flex-shrink:0;display:flex;flex-direction:row;text-align:left;z-index:10;box-shadow:none;padding:4px}.file_viewer.svelte-15t34ei>.file_viewer_headerbar.svelte-15t34ei>.svelte-15t34ei{flex-grow:0;flex-shrink:0;margin-left:4px;margin-right:4px;display:inline;align-self:center}.file_viewer.svelte-15t34ei>.file_viewer_headerbar.svelte-15t34ei>.file_viewer_headerbar_title.svelte-15t34ei{flex-grow:1;flex-shrink:1;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;flex-direction:row}.breadcrumb.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{border-radius:1em;min-width:1em;text-align:center;line-height:1.2em;padding:3px 8px;margin:2px 6px}.breadcrumb_button.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{cursor:pointer;background-color:var(--layer_2_color);transition:0.2s background-color}.breadcrumb_button.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei:hover,.breadcrumb_button.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei:focus,.breadcrumb_button.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei:active{background-color:var(--input_color)}.breadcrumb_last.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{background-color:var(--highlight_color);color:var(--highlight_text_color)}.button_home.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei::after{content:"pixeldrain"}@media(max-width: 600px){.button_home.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei::after{content:"pd"}}.file_viewer.svelte-15t34ei>.list_navigator.svelte-15t34ei.svelte-15t34ei{flex-grow:0;flex-shrink:0;position:relative;display:none;width:100%;background-color:var(--layer_1_color);text-align:center;line-height:1em;overflow-x:auto;overflow-y:hidden;z-index:50;white-space:nowrap}.file_viewer.svelte-15t34ei>.file_viewer_window.svelte-15t34ei.svelte-15t34ei{flex-grow:1;flex-shrink:1;position:relative;display:inline-block;width:auto;height:auto;margin:0;z-index:9}.file_viewer.svelte-15t34ei>.file_viewer_window.svelte-15t34ei>.file_viewer_file_preview.svelte-15t34ei{position:absolute;left:0;right:0;top:0;bottom:0;display:inline-block;min-height:100px;min-width:100px;text-align:center;vertical-align:middle;transition:left 0.5s;overflow:hidden;box-shadow:inset 2px 2px 8px var(--shadow_color)}.toolbar.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{position:absolute;width:8em;z-index:49;overflow:hidden;float:left;background-color:var(--layer_1_color);left:-8em;bottom:0;top:0;padding:0;text-align:left;transition:left 0.5s}.toolbar.toolbar_visible.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{left:0}.file_viewer.svelte-15t34ei>.file_viewer_window.svelte-15t34ei>.file_viewer_file_preview.toolbar_visible.svelte-15t34ei{left:8em}.toolbar.svelte-15t34ei>div.svelte-15t34ei.svelte-15t34ei{position:absolute;left:0;top:0;bottom:0;right:-30px;overflow-y:scroll;overflow-x:hidden}.toolbar.svelte-15t34ei>div.svelte-15t34ei>div.svelte-15t34ei{position:absolute;left:0;top:0;width:8em;height:auto;text-align:center}.toolbar_button.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{text-align:left}.toolbar_label.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{text-align:left;padding-left:10px;font-size:0.8em;line-height:0.7em;margin-top:0.5em}.toolbar_statistic.svelte-15t34ei.svelte-15t34ei.svelte-15t34ei{text-align:center}
.file_viewer.svelte-in5te4.svelte-in5te4.svelte-in5te4{position:absolute;display:flex;flex-direction:column;top:0;right:0;bottom:0;left:0;overflow:hidden}.file_viewer.svelte-in5te4>.file_viewer_headerbar.svelte-in5te4.svelte-in5te4{flex-grow:0;flex-shrink:0;display:flex;flex-direction:row;text-align:left;z-index:10;box-shadow:none;padding:4px}.file_viewer.svelte-in5te4>.file_viewer_headerbar.svelte-in5te4>.svelte-in5te4{flex-grow:0;flex-shrink:0;margin-left:4px;margin-right:4px;display:inline;align-self:center}.file_viewer.svelte-in5te4>.file_viewer_headerbar.svelte-in5te4>.file_viewer_headerbar_title.svelte-in5te4{flex-grow:1;flex-shrink:1;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;flex-direction:row}.breadcrumb.svelte-in5te4.svelte-in5te4.svelte-in5te4{border-radius:1em;min-width:1em;text-align:center;line-height:1.2em;padding:3px 8px;margin:2px 6px;word-break:break-all}.breadcrumb_button.svelte-in5te4.svelte-in5te4.svelte-in5te4{cursor:pointer;background-color:var(--layer_2_color);transition:0.2s background-color, 0.2s color}.breadcrumb_button.svelte-in5te4.svelte-in5te4.svelte-in5te4:hover,.breadcrumb_button.svelte-in5te4.svelte-in5te4.svelte-in5te4:focus,.breadcrumb_button.svelte-in5te4.svelte-in5te4.svelte-in5te4:active{color:var(--input_text_color);background-color:var(--input_color)}.breadcrumb_last.svelte-in5te4.svelte-in5te4.svelte-in5te4{color:var(--highlight_text_color);background-color:var(--highlight_color)}.button_home.svelte-in5te4.svelte-in5te4.svelte-in5te4::after{content:"pixeldrain"}@media(max-width: 600px){.button_home.svelte-in5te4.svelte-in5te4.svelte-in5te4::after{content:"pd"}}.file_viewer.svelte-in5te4>.list_navigator.svelte-in5te4.svelte-in5te4{flex-grow:0;flex-shrink:0;position:relative;display:none;width:100%;background-color:var(--layer_1_color);text-align:center;line-height:1em;overflow-x:auto;overflow-y:hidden;z-index:50;white-space:nowrap}.file_viewer.svelte-in5te4>.file_viewer_window.svelte-in5te4.svelte-in5te4{flex-grow:1;flex-shrink:1;position:relative;display:inline-block;width:auto;height:auto;margin:0;z-index:9}.file_viewer.svelte-in5te4>.file_viewer_window.svelte-in5te4>.file_viewer_file_preview.svelte-in5te4{position:absolute;left:0;right:0;top:0;bottom:0;display:inline-block;min-height:100px;min-width:100px;text-align:center;vertical-align:middle;transition:left 0.5s;overflow:hidden;box-shadow:inset 2px 2px 8px var(--shadow_color)}.toolbar.svelte-in5te4.svelte-in5te4.svelte-in5te4{position:absolute;width:8em;z-index:49;overflow:hidden;float:left;background-color:var(--layer_1_color);left:-8em;bottom:0;top:0;padding:0;text-align:left;transition:left 0.5s}.toolbar.toolbar_visible.svelte-in5te4.svelte-in5te4.svelte-in5te4{left:0}.file_viewer.svelte-in5te4>.file_viewer_window.svelte-in5te4>.file_viewer_file_preview.toolbar_visible.svelte-in5te4{left:8em}.toolbar.svelte-in5te4>div.svelte-in5te4.svelte-in5te4{position:absolute;left:0;top:0;bottom:0;right:-30px;overflow-y:scroll;overflow-x:hidden}.toolbar.svelte-in5te4>div.svelte-in5te4>div.svelte-in5te4{position:absolute;left:0;top:0;width:8em;height:auto;text-align:center}.toolbar_button.svelte-in5te4.svelte-in5te4.svelte-in5te4{text-align:left}.toolbar_label.svelte-in5te4.svelte-in5te4.svelte-in5te4{text-align:left;padding-left:10px;font-size:0.8em;line-height:0.7em;margin-top:0.5em}.toolbar_statistic.svelte-in5te4.svelte-in5te4.svelte-in5te4{text-align:center}
.sharebar.svelte-gnq1s2{position:absolute;width:7em;left:-8em;bottom:0;top:0;overflow-y:scroll;overflow-x:hidden;float:left;background-color:var(--layer_1_color);box-shadow:inset 1px 1px var(--layer_1_shadow) var(--shadow_color);text-align:center;z-index:48;overflow:hidden;transition:left 0.5s}.visible.svelte-gnq1s2{left:8em}
.hidden.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{visibility:hidden}.container.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{height:100%;width:100%;padding:0;overflow-y:auto;text-align:center}.width_container.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{position:relative;display:inline-block;max-width:94%;width:1000px;margin:0;padding:0}.toolbar.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{position:relative;display:inline-flex;flex-direction:row;width:100%;margin:16px 0 0 0;padding:0;box-sizing:border-box;justify-content:center}.toolbar.svelte-1bqdpyt>.svelte-1bqdpyt.svelte-1bqdpyt{flex:0 0 auto}.toolbar_spacer.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{flex:1 1 auto}@media(max-width: 800px){.toolbar_delete.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{flex-direction:column}}.directory.svelte-1bqdpyt.svelte-1bqdpyt.svelte-1bqdpyt{display:table;position:relative;overflow-x:auto;overflow-y:hidden;width:100%;margin:16px 0 16px 0;text-align:left;background-color:var(--layer_2_color);box-shadow:1px 1px var(--layer_2_shadow) var(--shadow_color);box-sizing:border-box;border-collapse:collapse}.directory.svelte-1bqdpyt>.svelte-1bqdpyt.svelte-1bqdpyt{display:table-row}.directory.svelte-1bqdpyt>.svelte-1bqdpyt>.svelte-1bqdpyt{display:table-cell}.directory.svelte-1bqdpyt .node{display:table-row;text-decoration:none;color:var(--text-color);padding:6px;box-sizing:border-box}.directory.svelte-1bqdpyt .node:not(:last-child){border-bottom:1px solid var(--layer_3_color)}.directory.svelte-1bqdpyt .node:hover:not(.node_selected){background-color:var(--input_color_dark);color:var(--input_text_color);text-decoration:none}.directory.svelte-1bqdpyt .node.node_selected{background-color:var(--highlight_color) !important;color:var(--highlight_text_color)}.directory.svelte-1bqdpyt .node.node_delete{background-color:var(--danger_color) !important;color:var(--highlight_text_color)}.directory.svelte-1bqdpyt td{padding:4px;vertical-align:middle}.directory.svelte-1bqdpyt .node_icon{height:32px;width:auto;vertical-align:middle}.directory.svelte-1bqdpyt .node_name{width:100%;overflow:hidden;line-height:1.2em;word-break:break-all}.directory.svelte-1bqdpyt .node_size{min-width:50px;white-space:nowrap}
.hidden.svelte-2zhofy{display:none}.file_input.svelte-2zhofy{display:block;visibility:hidden;width:0;height:0}.file_upload.svelte-2zhofy{display:block;text-align:left;width:100%;margin:6px 0 0 0;padding:0;background-color:var(--layer_2_color);box-shadow:1px 1px var(--layer_2_shadow) var(--shadow_color)}.upload_progress_bar.svelte-2zhofy{width:100%;height:2px}.upload_progress.svelte-2zhofy{background-color:var(--highlight_color);height:100%}
.hidden.svelte-1342op5{display:none}.file_upload.svelte-1342op5{display:inline-block;width:100%;background-color:var(--layer_2_color)}.file_upload.file_upload_active.svelte-1342op5{background-color:var(--highlight_color)}
.container.svelte-11r8rw7{height:100%;width:100%;margin:50px 0 0 0;padding:0;overflow-y:auto;text-align:center}.player.svelte-11r8rw7{width:90%}
.hidden.svelte-6g6w4.svelte-6g6w4{visibility:hidden}.container.svelte-6g6w4.svelte-6g6w4{height:100%;width:100%;padding:0;overflow-y:auto;text-align:center}.width_container.svelte-6g6w4.svelte-6g6w4{position:relative;display:inline-block;max-width:94%;width:1000px;margin:0;padding:0}.toolbar.svelte-6g6w4.svelte-6g6w4{position:relative;display:inline-flex;flex-direction:row;width:100%;margin:16px 0 0 0;padding:0;box-sizing:border-box}.toolbar.svelte-6g6w4>.svelte-6g6w4{flex:0 0 auto}.toolbar_spacer.svelte-6g6w4.svelte-6g6w4{flex:1 1 auto}.directory.svelte-6g6w4.svelte-6g6w4{position:relative;overflow-x:auto;overflow-y:hidden;width:100%;margin:16px 0 16px 0;text-align:left;background-color:var(--layer_2_color);box-shadow:1px 1px var(--layer_2_shadow) var(--shadow_color);box-sizing:border-box}.node.svelte-6g6w4.svelte-6g6w4{display:table-row;text-decoration:none;color:var(--text-color);padding:6px;box-sizing:border-box}.node.svelte-6g6w4.svelte-6g6w4:not(:last-child){border-bottom:1px solid var(--layer_3_color)}.node.svelte-6g6w4.svelte-6g6w4:hover:not(.node_selected){background-color:var(--input_color_dark);color:var(--input_text_color);text-decoration:none}.node.node_selected.svelte-6g6w4.svelte-6g6w4{background-color:var(--highlight_color);color:var(--highlight_text_color)}td.svelte-6g6w4.svelte-6g6w4{padding:4px;vertical-align:middle}.node_icon.svelte-6g6w4.svelte-6g6w4{height:32px;width:auto;vertical-align:middle}.node_name.svelte-6g6w4.svelte-6g6w4{width:100%;overflow:hidden;line-height:1.2em}.node_size.svelte-6g6w4.svelte-6g6w4{min-width:50px;white-space:nowrap}
.container.svelte-xjzx7h{position:relative;display:block;height:100%;width:100%;text-align:center;overflow:hidden}.container.zoom.svelte-xjzx7h{overflow:auto}.image.svelte-xjzx7h{position:relative;display:block;margin:auto;max-width:100%;max-height:100%;top:50%;cursor:pointer;transform:translateY(-50%);box-shadow:1px 1px var(--layer_3_shadow) var(--shadow_color)}.image.zoom.svelte-xjzx7h{max-width:none;max-height:none;top:0;cursor:move;transform:none}
.container.svelte-1iaovvu{position:relative;display:block;height:100%;width:100%;text-align:center;overflow:hidden}.player.svelte-1iaovvu{position:relative;display:block;margin:auto;max-width:100%;max-height:100%;top:50%;transform:translateY(-50%);box-shadow:1px 1px var(--layer_3_shadow) var(--shadow_color)}
.background.svelte-tirhyp{position:fixed;text-align:center;top:0;right:0;bottom:0;left:0;background-color:rgba(0, 0, 0, 0.5)}.window.svelte-tirhyp{position:absolute;z-index:inherit;display:flex;flex-direction:column;background-color:var(--layer_2_color);max-height:100%;max-width:100%;margin:0 auto;top:20%;left:50%;transform:translate(-50%, -20%);padding:0;box-sizing:border-box;text-align:left;box-shadow:var(--shadow_color) 0px 0px 50px}.header.svelte-tirhyp{flex-grow:0;flex-shrink:0;display:flex;flex-direction:row;padding:1px}.title.svelte-tirhyp{flex-grow:1;flex-shrink:1;display:flex;flex-direction:row;align-items:center;justify-content:center;font-size:1.2em;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.button_close.svelte-tirhyp{flex-grow:0;flex-shrink:0}.body.svelte-tirhyp{flex-grow:1;flex-shrink:1;overflow:auto;padding:10px}
svg.svelte-1pco739{color:var(--highlight_color);fill:currentColor}

File diff suppressed because it is too large Load Diff

View File

@@ -90,7 +90,9 @@
scrolling="no">
</iframe>
<br/>
<a href="https://a-ads.com/campaigns/new?selected_ad_unit_id=73974&selected_source_type=ad_unit&partner=73974">Put your own advertisement here</a>
<a class="button" href="https://a-ads.com/campaigns/new?selected_ad_unit_id=73974&selected_source_type=ad_unit&partner=73974">
Put your own advertisement here
</a>
</div>
{{ end }}

View File

@@ -22,7 +22,7 @@
{{.OGData}}
<script>
const initialNode = {{.Other}};
window.apiEndpoint = '{{.APIEndpoint}}';
window.api_endpoint = '{{.APIEndpoint}}';
</script>
<link rel='stylesheet' href='/res/svelte/filesystem.css'>

View File

@@ -1,12 +1,15 @@
<script>
import { onMount } from 'svelte';
import { formatDate, formatDataVolume, formatThousands } from '../util/Formatting.svelte'
import { formatDate, formatDataVolume, formatThousands, formatNumber } from '../util/Formatting.svelte'
import { fs_get_file_url, fs_get_node } from './FilesystemAPI.svelte'
import Sharebar from './Sharebar.svelte'
import Spinner from '../util/Spinner.svelte'
import Modal from '../util/Modal.svelte'
import Directory from './viewers/Directory.svelte';
import FileManager from './filemanager/FileManager.svelte';
import Audio from './viewers/Audio.svelte';
import Image from './viewers/Image.svelte';
import Video from './viewers/Video.svelte';
import { current_component } from 'svelte/internal';
// Elements
let file_viewer
@@ -22,29 +25,41 @@ let toolbar_toggle = () => {
let sharebar
let sharebar_visible = false
let details;
let details
let details_visible = false
let preview
let download_frame
// State
let currentNode = initialNode
let path_base = "/d/"+currentNode.bucket.id
let loading = true
let viewer_type = ""
let state = {
bucket: initialNode.bucket,
parents: initialNode.parents,
base: initialNode.base,
window.onpopstate = (e) => {
if(e.state){
let locsplit = document.location.pathname.split(currentNode.bucket.id+"/", 2)
navigate(decodeURIComponent(locsplit[1]))
}
};
path_root: "/d/"+initialNode.bucket.id,
loading: true,
viewer_type: ""
}
// Tallys
$: total_directories = state.base.children.reduce((acc, cur) => {
if (cur.type === "dir") {
acc++
}
return acc
}, 0)
$: total_files = state.base.children.reduce((acc, cur) => {
if (cur.type === "file") {
acc++
}
return acc
}, 0)
$: total_file_size = state.base.children.reduce((acc, cur) => acc + cur.file_size, 0)
const navigate = (path, pushHist) => {
loading = true
fetch(
window.apiEndpoint+"/filesystem/"+currentNode.bucket.id+"/"+encodeURIComponent(path)+"?stat",
).then(
resp => resp.json()
state.loading = true
fs_get_node(
state.bucket.id, path,
).then(resp => {
window.document.title = resp.base.name+" ~ pixeldrain"
if (pushHist) {
@@ -52,31 +67,64 @@ const navigate = (path, pushHist) => {
{}, window.document.title, "/d/"+resp.bucket.id+resp.base.path,
)
}
currentNode = resp
openPath()
openNode(resp)
}).catch(err => {
loading = false
console.error(err)
alert(err)
}).finally(() => {
state.loading = false
})
}
const openPath = () => {
console.log(currentNode.base.type)
if (currentNode.base.type === "bucket" || currentNode.base.type === "dir") {
viewer_type = "dir"
const openNode = (node) => {
// Sort directory children
node.base.children.sort((a, b) => {
// Sort directories before files
console.log(a)
if (a.type !== b.type) {
return a.type === "file" ? 1 : -1
}
return a.name.localeCompare(b.name)
})
// Update shared state
state.bucket = node.bucket
state.parents = node.parents
state.base = node.base
// Update the viewer area with the right viewer type
if (state.base.type === "bucket" || state.base.type === "dir") {
state.viewer_type = "dir"
} else if (state.base.file_type.startsWith("image")) {
state.viewer_type = "image"
} else if (
currentNode.base.file_type.startsWith("image")
state.base.file_type.startsWith("audio") ||
state.base.file_type === "application/ogg" ||
state.base.name.endsWith(".mp3")
) {
viewer_type = "image"
state.viewer_type = "audio"
} else if (
currentNode.base.file_type.startsWith("audio") ||
currentNode.base.file_type === "application/ogg" ||
currentNode.base.name.endsWith(".mp3")
state.base.file_type.startsWith("video") ||
state.base.file_type === "application/matroska" ||
state.base.file_type === "application/x-matroska"
) {
viewer_type = "audio"
state.viewer_type = "video"
} else {
state.viewer_type = ""
}
loading = false
// Remove spinner
state.loading = false
}
onMount(() => openNode(initialNode))
window.onpopstate = (e) => {
if(e.state){
let locsplit = document.location.pathname.split(state.bucket.id+"/", 2)
navigate(decodeURIComponent(locsplit[1]))
}
};
const keydown = e => {
switch (e.key) {
@@ -86,16 +134,18 @@ const keydown = e => {
case 'i':
details_window.toggle()
}
console.log(e.key)
};
onMount(openPath)
const download = () => {
download_frame.src = fs_get_file_url(state.bucket.id, state.base.path) + "?attach"
}
</script>
<svelte:window on:keydown={keydown}/>
<div bind:this={file_viewer} class="file_viewer">
{#if loading}
{#if state.loading}
<div style="position: absolute; right: 0; top: 0; height: 48px; width: 48px; z-index: 100;">
<Spinner></Spinner>
</div>
@@ -107,21 +157,32 @@ onMount(openPath)
</button>
<a href="/" id="button_home" class="button button_home"><i class="icon">home</i></a>
<div class="file_viewer_headerbar_title">
{#each currentNode.parents as parent}
{#each state.parents as parent}
<div class="breadcrumb breadcrumb_button" on:click={() => {navigate(parent.path, true)}}>{parent.name}</div> /
{/each}
<div class="breadcrumb breadcrumb_last">{currentNode.base.name}</div>
<div class="breadcrumb breadcrumb_last">{state.base.name}</div>
</div>
</div>
<div class="list_navigator"></div>
<div class="file_viewer_window">
<div class="toolbar" class:toolbar_visible><div><div>
{#if state.base.type === "file"}
<div class="toolbar_label">Size</div>
<div class="toolbar_statistic">{formatDataVolume(currentNode.base.file_size, 3)}</div>
<div class="toolbar_statistic">{formatDataVolume(state.base.file_size, 3)}</div>
{:else if state.base.type === "dir" || state.base.type === "bucket"}
<div class="toolbar_label">Directories</div>
<div class="toolbar_statistic">{formatThousands(total_directories, 3)}</div>
<div class="toolbar_label">Files</div>
<div class="toolbar_statistic">{formatThousands(total_files, 3)}</div>
<div class="toolbar_label">Total size</div>
<div class="toolbar_statistic">{formatDataVolume(total_file_size, 3)}</div>
{/if}
<button class="toolbar_button button_full_width">
{#if state.base.type === "file"}
<button on:click={download} class="toolbar_button button_full_width">
<i class="icon">save</i> Download
</button>
{/if}
<button id="btn_download_list" class="toolbar_button button_full_width" style="display: none;">
<i class="icon">save</i> DL all files
</button>
@@ -140,37 +201,39 @@ onMount(openPath)
</div></div></div>
<Sharebar bind:this={sharebar}></Sharebar>
<div bind:this={preview} class="file_viewer_file_preview" class:toolbar_visible>
{#if viewer_type === "dir"}
<Directory bind:this={preview} node={currentNode} path_base={path_base} on:navigate={e => {navigate(e.detail, true)}}></Directory>
{:else if viewer_type === "audio"}
<Audio bind:this={preview} node={currentNode}></Audio>
{:else if viewer_type === "image"}
<Image bind:this={preview} node={currentNode}></Image>
<div class="file_viewer_file_preview" class:toolbar_visible>
{#if state.viewer_type === "dir"}
<FileManager state={state} on:navigate={e => {navigate(e.detail, true)}} on:loading={e => {state.loading = e.detail}}></FileManager>
{:else if state.viewer_type === "audio"}
<Audio state={state}></Audio>
{:else if state.viewer_type === "image"}
<Image state={state}></Image>
{:else if state.viewer_type === "video"}
<Video state={state}></Video>
{/if}
</div>
</div>
<!-- This frame will load the download URL when a download button is pressed -->
<iframe title="Frame for downloading files" style="display: none; width: 1px; height: 1px;"></iframe>
<iframe bind:this={download_frame} title="Frame for downloading files" style="display: none; width: 1px; height: 1px;"></iframe>
<Modal bind:this={details} title="Details" width="600px">
<table style="min-width: 100%;">
<tr><td colspan="2"><h3>Node details</h3></td></tr>
<tr><td>Name</td><td>{currentNode.base.name}</td></tr>
<tr><td>Path</td><td>{currentNode.base.path}</td></tr>
<tr><td>Type</td><td>{currentNode.base.type}</td></tr>
<tr><td>Date created</td><td>{formatDate(currentNode.base.date_created, true, true, true)}</td></tr>
<tr><td>Date modified</td><td>{formatDate(currentNode.base.date_modified, true, true, true)}</td></tr>
{#if currentNode.base.type === "file"}
<tr><td>File type</td><td>{currentNode.base.file_type}</td></tr>
<tr><td>File size</td><td>{formatDataVolume(currentNode.base.file_size)}</td></tr>
<tr><td>Name</td><td>{state.base.name}</td></tr>
<tr><td>Path</td><td>{state.base.path}</td></tr>
<tr><td>Type</td><td>{state.base.type}</td></tr>
<tr><td>Date created</td><td>{formatDate(state.base.date_created, true, true, true)}</td></tr>
<tr><td>Date modified</td><td>{formatDate(state.base.date_modified, true, true, true)}</td></tr>
{#if state.base.type === "file"}
<tr><td>File type</td><td>{state.base.file_type}</td></tr>
<tr><td>File size</td><td>{formatDataVolume(state.base.file_size)}</td></tr>
{/if}
<tr><td colspan="2"><h3>Bucket details</h3></td></tr>
<tr><td>ID</td><td>{currentNode.bucket.id}</td></tr>
<tr><td>Name</td><td>{currentNode.bucket.name}</td></tr>
<tr><td>Date created</td><td>{formatDate(currentNode.bucket.date_created, true, true, true)}</td></tr>
<tr><td>Date modified</td><td>{formatDate(currentNode.bucket.date_modified, true, true, true)}</td></tr>
<tr><td>ID</td><td>{state.bucket.id}</td></tr>
<tr><td>Name</td><td>{state.bucket.name}</td></tr>
<tr><td>Date created</td><td>{formatDate(state.bucket.date_created, true, true, true)}</td></tr>
<tr><td>Date modified</td><td>{formatDate(state.bucket.date_modified, true, true, true)}</td></tr>
</table>
</Modal>
</div>
@@ -225,18 +288,20 @@ onMount(openPath)
line-height: 1.2em;
padding: 3px 8px;
margin: 2px 6px;
word-break: break-all;
}
.breadcrumb_button {
cursor: pointer;
background-color: var(--layer_2_color);
transition: 0.2s background-color;
transition: 0.2s background-color, 0.2s color;
}
.breadcrumb_button:hover, .breadcrumb_button:focus, .breadcrumb_button:active {
color: var(--input_text_color);
background-color: var(--input_color);
}
.breadcrumb_last {
background-color: var(--highlight_color);
color: var(--highlight_text_color);
background-color: var(--highlight_color);
}
.button_home::after {

View File

@@ -0,0 +1,58 @@
<script context="module">
export const fs_create_directory = (bucket, path, dir_name) => {
if (!path.startsWith("/")) {
path = "/" + path
}
let form = new FormData()
form.append("type", "dir")
return fetch(
window.api_endpoint + "/filesystem/" + bucket + encodeURIComponent(path + "/" + dir_name),
{method: "POST", body: form},
).then(resp => {
if (resp.status >= 400) {
throw new Error(resp.text())
}
})
}
export const fs_get_node = (bucket, path) => {
if (!path.startsWith("/")) {
path = "/" + path
}
return fetch(
window.api_endpoint + "/filesystem/" + bucket + encodeURIComponent(path) + "?stat",
).then(resp => {
if (resp.status >= 400) {
throw new Error(resp.text())
}
return resp.json()
})
}
export const fs_get_file_url = (bucket, path) => {
if (!path.startsWith("/")) {
path = "/" + path
}
return window.api_endpoint + "/filesystem/" + bucket + encodeURIComponent(path)
}
export const fs_delete_node = (bucket, path) => {
if (!path.startsWith("/")) {
path = "/" + path
}
return fetch(
window.api_endpoint + "/filesystem/" + bucket + encodeURIComponent(path),
{method: "DELETE"},
).then(resp => {
if (resp.status >= 400) {
throw new Error(resp.text())
}
})
}
</script>

View File

@@ -8,7 +8,6 @@ export let file
export let sharebar
export let visible = false
export const setVisible = (v) => {
visible = v
if (!visible) {
@@ -57,56 +56,56 @@ export const toggle = () => { setVisible(!visible) }
</div></div></div>
<style>
.toolbar {
position: absolute;
width: 8em;
z-index: 49;
overflow: hidden;
float: left;
background-color: var(--layer_1_color);
left: -9em;
bottom: 0;
top: 0;
padding: 0;
text-align: left;
transition: left 0.5s;
}
.visible { left: 0; }
.toolbar {
position: absolute;
width: 8em;
z-index: 49;
overflow: hidden;
float: left;
background-color: var(--layer_1_color);
left: -9em;
bottom: 0;
top: 0;
padding: 0;
text-align: left;
transition: left 0.5s;
}
.visible { left: 0; }
/* Workaround to hide the scrollbar in non webkit browsers, it's really ugly' */
.toolbar > div {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: -30px;
overflow-y: scroll;
overflow-x: hidden;
}
.toolbar > div > div {
position: absolute;
left: 0;
top: 0;
width: 8em;
height: auto;
text-align: center;
}
/* Workaround to hide the scrollbar in non webkit browsers, it's really ugly' */
.toolbar > div {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: -30px;
overflow-y: scroll;
overflow-x: hidden;
}
.toolbar > div > div {
position: absolute;
left: 0;
top: 0;
width: 8em;
height: auto;
text-align: center;
}
.toolbar_button{
text-align: left;
}
.toolbar_button > span {
vertical-align: middle;
}
.toolbar_button{
text-align: left;
}
.toolbar_button > span {
vertical-align: middle;
}
.label {
text-align: left;
padding-left: 10px;
font-size: 0.8em;
line-height: 0.7em;
margin-top: 0.5em;
}
.statistic {
text-align: center;
}
.label {
text-align: left;
padding-left: 10px;
font-size: 0.8em;
line-height: 0.7em;
margin-top: 0.5em;
}
.statistic {
text-align: center;
}
</style>

View File

@@ -0,0 +1,33 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import { fs_create_directory } from "../FilesystemAPI.svelte";
let dispatch = createEventDispatcher()
export let state;
let name_input;
let create_dir_name = ""
let create_dir = () => {
dispatch("loading", true)
let form = new FormData()
form.append("type", "dir")
fs_create_directory(
state.bucket.id, state.base.path, create_dir_name,
).then(resp => {
create_dir_name = "" // Clear input field
}).catch(err => {
alert("Error: "+err)
}).finally(() => {
dispatch("done")
})
}
onMount(() => { name_input.focus() })
</script>
<form class="node" on:submit|preventDefault={create_dir}>
<td><img src="/res/img/mime/folder.png" class="node_icon" alt="icon"/></td>
<td><input type="text" style="width: 100%;" bind:this={name_input} bind:value={create_dir_name} /></td>
<td><input type="submit" value="create"/></td>
</form>

View File

@@ -0,0 +1,275 @@
<script>
import { formatDataVolume } from '../../util/Formatting.svelte'
import { fs_delete_node } from './../FilesystemAPI.svelte'
import { createEventDispatcher } from 'svelte'
import CreateDirectory from './CreateDirectory.svelte'
import FileUploader from './FileUploader.svelte'
let dispatch = createEventDispatcher()
export let state
let mode = "viewing"
let creating_dir = false
let uploader
const node_click = (index) => {
creating_dir = false
// We prefix our custom state properties with fm_ to not interfere with
// other modules
if (mode === "viewing") {
dispatch("navigate", state.base.children[index].path)
} else if (mode === "selecting") {
state.base.children[index].fm_selected = !state.base.children[index].fm_selected
} else if (mode === "deleting") {
state.base.children[index].fm_delete = !state.base.children[index].fm_delete
}
}
const navigate_up = () => {
creating_dir = false
// Go to the path of the last parent
if (state.parents.length !== 0) {
dispatch("navigate", state.parents[state.parents.length-1].path)
}
}
const reload = () => { dispatch("navigate", state.base.path) }
const node_icon = node => {
if (node.type === "dir") {
return "/res/img/mime/folder.png"
}
switch (node.file_type) {
case "image/gif":
return "/res/img/mime/image-gif.png"
case "image/png", "image/apng":
return "/res/img/mime/image-png.png"
case "image/jpeg":
return "/res/img/mime/image-jpeg.png"
case "application/pdf":
return "/res/img/mime/pdf.png"
case "application/ogg":
return "/res/img/mime/audio.png"
}
if (node.file_type.startsWith("audio/")) {
return "/res/img/mime/audio.png"
} else if (node.file_type.startsWith("video/")) {
return "/res/img/mime/video.png"
} else if (node.file_type.startsWith("text/")) {
return "/res/img/mime/text.png"
} else if (node.file_type.startsWith("image/")) {
return "/res/img/mime/image-png.png"
} else if (node.file_type.startsWith("application/")) {
return "/res/img/mime/archive.png"
}
return "/res/img/mime/empty.png"
}
const delete_node = () => {
if (mode !== "deleting") {
mode = "deleting"
return
}
dispatch("loading", true)
// Save all promises with deletion requests in an array
let promises = []
state.base.children.forEach(child => {
if (!child.fm_delete) { return }
promises.push(fs_delete_node(state.bucket.id, child.path))
})
// Wait for all the promises to finish
Promise.all(promises).catch((err) => {
console.error(err)
}).finally(() => {
mode = "viewing"
reload()
})
}
const delete_toggle = () => {
// Turn on deletion mode if it's not already
if (mode !== "deleting") {
mode = "deleting"
return
}
// Return to normal and unmark all the marked files
mode = "viewing"
state.base.children.forEach((child, i) => {
if (child.fm_delete) {
state.base.children[i].fm_delete = false
}
})
}
</script>
<div class="container">
<div class="width_container">
<div class="toolbar">
<button on:click={navigate_up} class:hidden={state.parents.length === 0}><i class="icon">arrow_back</i></button>
<div class="toolbar_spacer"></div>
{#if state.bucket.permissions.update}
<button on:click={uploader.picker}><i class="icon">cloud_upload</i></button>
<button on:click={() => {creating_dir = true}}><i class="icon">create_new_folder</i></button>
<button
on:click={delete_toggle}
class:button_red={mode === "deleting"}>
<i class="icon">delete</i>
</button>
{/if}
</div>
<br/>
{#if mode === "deleting"}
<div class="toolbar toolbar_delete highlight_red">
<div style="flex: 1 1 auto; justify-self: center;">
Deleting files. Click a file or directory to select it for deletion.
Click confirm to delete the files.
</div>
<div style="display: flex; flex-direction: row; justify-content: center;">
<button on:click={delete_toggle}>
<i class="icon">undo</i>
Cancel
</button>
<button on:click={delete_node} class="button_red">
<i class="icon">delete</i>
Delete selected
</button>
</div>
</div>
<br/>
{/if}
<FileUploader bind:this={uploader} bucket_id={state.bucket.id} target_dir={state.base.path} on:finished={reload}></FileUploader>
<div class="directory">
<tr>
<td></td>
<td>name</td>
<td>size</td>
</tr>
{#if creating_dir}
<CreateDirectory state={state} on:done={() => {reload(); creating_dir = false;}} on:loading></CreateDirectory>
{/if}
{#each state.base.children as child, index}
<a
href={state.path_root+child.path}
on:click|preventDefault={() => {node_click(index)}}
class="node"
class:node_selected={child.fm_selected}
class:node_delete={child.fm_delete}>
<td>
<img src={node_icon(child)} class="node_icon" alt="icon"/>
</td>
<td class="node_name">
{child.name}
</td>
<td class="node_size">
{formatDataVolume(child.file_size, 3)}
</td>
</a>
{/each}
</div>
</div>
</div>
<style>
.hidden { visibility: hidden; }
.container {
height: 100%;
width: 100%;
padding: 0;
overflow-y: auto;
text-align: center;
}
.width_container {
position: relative;
display: inline-block;
max-width: 94%;
width: 1000px;
margin: 0;
padding: 0;
}
.toolbar {
position: relative;
display: inline-flex;
flex-direction: row;
width: 100%;
margin: 16px 0 0 0;
padding: 0;
box-sizing: border-box;
justify-content: center;
}
.toolbar > * { flex: 0 0 auto; }
.toolbar_spacer { flex: 1 1 auto; }
@media(max-width: 800px) {
.toolbar_delete {
flex-direction: column;
}
}
.directory {
display: table;
position: relative;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
margin: 16px 0 16px 0;
text-align: left;
background-color: var(--layer_2_color);
box-shadow: 1px 1px var(--layer_2_shadow) var(--shadow_color);
box-sizing: border-box;
border-collapse: collapse;
}
.directory > * { display: table-row; }
.directory > * > * { display: table-cell; }
.directory :global(.node) {
display: table-row;
text-decoration: none;
color: var(--text-color);
padding: 6px;
box-sizing: border-box;
}
.directory :global(.node:not(:last-child)) {
border-bottom: 1px solid var(--layer_3_color);
}
.directory :global(.node:hover:not(.node_selected)) {
background-color: var(--input_color_dark);
color: var(--input_text_color);
text-decoration: none;
}
.directory :global(.node.node_selected) {
background-color: var(--highlight_color) !important;
color: var(--highlight_text_color);
}
.directory :global(.node.node_delete) {
background-color: var(--danger_color) !important;
color: var(--highlight_text_color);
}
.directory :global(td) {
padding: 4px;
vertical-align: middle;
}
.directory :global(.node_icon) {
height: 32px;
width: auto;
vertical-align: middle;
}
.directory :global(.node_name) {
width: 100%;
overflow: hidden;
line-height: 1.2em;
word-break: break-all;
}
.directory :global(.node_size) {
min-width: 50px;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,210 @@
<script>
import { createEventDispatcher } from "svelte";
let dispatch = createEventDispatcher()
export let bucket_id
export let target_dir
let upload_jobs = []
let upload_threads = 0
let max_upload_threads = 4
// Adds files to the upload queue. The file_list parameter needs to be of type
// FileList. Upload will also create the necessary directories to place nested
// files in.
export const upload = (file_list) => {
for (let i = 0; i < file_list.length; i++) {
upload_jobs.push({
file: file_list[i],
progress: 0,
target_dir: target_dir.valueOf(),
uploading: false,
finished: false,
tries: 0,
})
}
// This updates the UI
upload_jobs = upload_jobs
while (upload_threads <= max_upload_threads) {
upload_threads++
setTimeout(upload_file, 1)
}
}
const uploads_finished = () => {
dispatch("finished")
}
const upload_file = () => {
let job = null
for (let i = 0; i < upload_jobs.length; i++) {
// If a file is done we remove it from the array
if (upload_jobs[i].progress >= 1) {
upload_jobs.splice(i, 1)
continue
}
if (upload_jobs[i].uploading === false && upload_jobs[i].finished === false) {
job = upload_jobs[i]
job.uploading = true
upload_jobs = upload_jobs
break
}
}
if (job === null) {
upload_threads--
if (upload_threads === 0) {
uploads_finished()
}
return
}
console.log(job)
let form = new FormData();
form.append("type", "file")
form.append("file", job.file)
let xhr = new XMLHttpRequest();
xhr.open(
"POST",
"/api/filesystem/"+bucket_id+encodeURIComponent(
job.target_dir+"/"+job.file.name,
),
true,
);
xhr.timeout = 21600000; // 6 hours, to account for slow connections
// Report progress updates back to the caller
xhr.upload.addEventListener("progress", evt => {
if (evt.lengthComputable) {
job.progress = evt.loaded / evt.total
upload_jobs = upload_jobs
}
});
xhr.onreadystatechange = () => {
// readystate 4 means the upload is done
if (xhr.readyState !== 4) { return; }
if (xhr.status >= 100 && xhr.status < 400) {
// Request is a success
// Finish the upload job
job.uploading = false
job.finished = true
upload_file()
} else if (xhr.status >= 400) {
// Request failed
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
let resp = JSON.parse(xhr.response);
if (job.tries === 3) { // Upload failed
return
} else { // Try again
job.tries++;
job.uploading = false
job.finished = false
}
// Sleep the upload thread for 5 seconds
setTimeout(upload_file, 5000);
} else {
// Request did not arrive
if (job.tries === 3) { // Upload failed
alert("upload failed "+xhr.responseText)
job.uploading = false
job.finished = false
} else { // Try again
job.tries++;
}
// Sleep the upload thread for 5 seconds
setTimeout(upload_file, 5000);
}
upload_jobs = upload_jobs
};
xhr.send(form);
}
// File input dialog handling
let file_input
export const picker = () => { file_input.click() }
const file_input_change = e => {
upload(e.target.files)
file_input.nodeValue = ""
}
// Drag and drop upload
let hidden = true
const dragover = e => { hidden = false }
const dragleave = e => { hidden = true }
const drop = e => {
hidden = true
upload(e.dataTransfer.files)
}
const paste = e => {
if (e.clipboardData.files[0]) {
e.preventDefault()
e.stopPropagation()
console.log(e.clipboardData.files[0].getAsFile())
}
}
</script>
<svelte:body
on:dragover|preventDefault|stopPropagation={dragover}
on:dragleave|preventDefault|stopPropagation={dragleave}
on:drop|preventDefault|stopPropagation={drop}
on:paste={paste}
/>
<div>
<input class="file_input" bind:this={file_input} on:change={file_input_change} type="file" multiple="multiple"/>
<div class:hidden class="highlight_green">
Drop files here to upload them
</div>
{#each upload_jobs as c}
<div class="file_upload">
&nbsp;{c.file.name}&nbsp;<br/>
<div class="upload_progress_bar">
<div class="upload_progress" style="width: {c.progress*100}%"></div>
</div>
</div>
{/each}
</div>
<style>
.hidden {display: none;}
.file_input {
display: block;
visibility: hidden;
width: 0;
height: 0;
}
.file_upload {
display: block;
text-align: left;
width: 100%;
margin: 6px 0 0 0;
padding: 0;
background-color: var(--layer_2_color);
box-shadow: 1px 1px var(--layer_2_shadow) var(--shadow_color);
}
.upload_progress_bar {
width: 100%;
height: 2px;
}
.upload_progress {
background-color: var(--highlight_color);
height: 100%;
}
</style>

View File

@@ -1,24 +1,20 @@
<script>
import { fs_get_file_url } from "../FilesystemAPI.svelte";
import { createEventDispatcher } from 'svelte'
let dispatch = createEventDispatcher()
export let node;
const ended = () => {
dispatch("next")
}
export let state;
</script>
<div class="container">
{node.base.name}
{state.base.name}
<br/><br/>
<audio
class="player"
src={window.apiEndpoint+"/filesystem/"+node.bucket.id+"/"+node.base.path}
src={fs_get_file_url(state.bucket.id, state.base.path)}
autoplay="autoplay"
controls="controls"
on:ended={ended}>
on:ended={() => { dispatch("next") }}>
<track kind="captions"/>
</audio>
</div>

View File

@@ -1,181 +0,0 @@
<script>
import { formatDataVolume } from '../../util/Formatting.svelte'
import { createEventDispatcher } from 'svelte'
let dispatch = createEventDispatcher()
export let node;
export let path_base;
let mode = "viewing"
$: children = node.base.children.reduce((accum, val) => {
val["selected"] = false
accum.push(val)
return accum
}, [])
const node_click = (node, index) => {
if (mode === "viewing") {
dispatch("navigate", node.path)
} else if (mode === "selecting") {
children[index].selected = !children[index].selected
}
}
const navigate_up = () => {
// Go to the path of the last parent
if (node.parents.length !== 0) {
dispatch("navigate", node.parents[node.parents.length-1].path)
}
}
const node_icon = node => {
if (node.type === "dir") {
return "/res/img/mime/folder.png"
}
switch (node.file_type) {
case "image/gif":
return "/res/img/mime/image-gif.png"
case "image/png", "image/apng":
return "/res/img/mime/image-png.png"
case "image/jpeg":
return "/res/img/mime/image-jpeg.png"
case "application/pdf":
return "/res/img/mime/pdf.png"
}
if (node.file_type.startsWith("audio/")) {
return "/res/img/mime/audio.png"
} else if (node.file_type.startsWith("video/")) {
return "/res/img/mime/video.png"
} else if (node.file_type.startsWith("text/")) {
return "/res/img/mime/text.png"
} else if (node.file_type.startsWith("image/")) {
return "/res/img/mime/image-png.png"
} else if (node.file_type.startsWith("application/")) {
return "/res/img/mime/archive.png"
}
return "/res/img/mime/empty.png"
}
</script>
<div class="container">
<div class="width_container">
<div class="toolbar">
<!-- {#if node.parents.length !== 0} -->
<button on:click={navigate_up} class:hidden={node.parents.length === 0}><i class="icon">arrow_back</i></button>
<!-- {/if} -->
<div class="toolbar_spacer"></div>
<button on:click={navigate_up}><i class="icon">cloud_upload</i></button>
<button on:click={navigate_up}><i class="icon">create_new_folder</i></button>
<button on:click={navigate_up}><i class="icon">delete</i></button>
</div>
<br/>
<table class="directory">
<tr>
<!-- <td><input type="checkbox" /></td> -->
<td></td>
<td>name</td>
<td>size</td>
</tr>
{#each children as child, index}
<a
href={path_base+child.path}
on:click|preventDefault={() => {node_click(child, index)}}
class="node"
class:node_selected={child.selected}>
<!-- <td on:click|preventDefault class="node_checkbox">
<input type="checkbox" />
</td> -->
<td>
<img src={node_icon(child)} class="node_icon" alt="icon"/>
</td>
<td class="node_name">
{child.name}
</td>
<td class="node_size">
{formatDataVolume(child.file_size, 3)}
</td>
</a>
{/each}
</table>
</div>
</div>
<style>
.hidden { visibility: hidden; }
.container {
height: 100%;
width: 100%;
padding: 0;
overflow-y: auto;
text-align: center;
}
.width_container {
position: relative;
display: inline-block;
max-width: 94%;
width: 1000px;
margin: 0;
padding: 0;
}
.toolbar {
position: relative;
display: inline-flex;
flex-direction: row;
width: 100%;
margin: 16px 0 0 0;
padding: 0;
box-sizing: border-box;
}
.toolbar > * { flex: 0 0 auto; }
.toolbar_spacer { flex: 1 1 auto; }
.directory {
position: relative;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
margin: 16px 0 16px 0;
text-align: left;
background-color: var(--layer_2_color);
box-shadow: 1px 1px var(--layer_2_shadow) var(--shadow_color);
box-sizing: border-box;
}
.node {
display: table-row;
text-decoration: none;
color: var(--text-color);
padding: 6px;
box-sizing: border-box;
}
.node:not(:last-child) {
border-bottom: 1px solid var(--layer_3_color);
}
.node:hover:not(.node_selected) {
background-color: var(--input_color_dark);
color: var(--input_text_color);
text-decoration: none;
}
.node.node_selected {
background-color: var(--highlight_color);
color: var(--highlight_text_color);
}
td {
padding: 4px;
vertical-align: middle;
}
.node_icon {
height: 32px;
width: auto;
vertical-align: middle;
}
.node_name {
width: 100%;
overflow: hidden;
line-height: 1.2em;
}
.node_size {
min-width: 50px;
white-space: nowrap;
}
</style>

View File

@@ -1,5 +1,8 @@
<script>
export let node
import { fs_get_file_url } from "../FilesystemAPI.svelte";
export let state
let container
let zoom = false
let x, y = 0
@@ -48,7 +51,7 @@ const mouseup = (e) => {
on:doubletap={() => {zoom = !zoom}}
on:mousedown={mousedown}
class="image" class:zoom
src={window.apiEndpoint+"/filesystem/"+node.bucket.id+"/"+node.base.path}
src={fs_get_file_url(state.bucket.id, state.base.path)}
alt="no description available" />
</div>

View File

@@ -0,0 +1,39 @@
<script>
import { fs_get_file_url } from "../FilesystemAPI.svelte";
import { createEventDispatcher } from 'svelte'
let dispatch = createEventDispatcher()
export let state;
</script>
<div class="container">
<video
class="player"
src={fs_get_file_url(state.bucket.id, state.base.path)}
autoplay="autoplay"
controls="controls"
on:ended={() => { dispatch("next") }}>
<track kind="captions"/>
</video>
</div>
<style>
.container {
position: relative;
display: block;
height: 100%;
width: 100%;
text-align: center;
overflow: hidden;
}
.player {
position: relative;
display: block;
margin: auto;
max-width: 100%;
max-height: 100%;
top: 50%;
transform: translateY(-50%);
box-shadow: 1px 1px var(--layer_3_shadow) var(--shadow_color);
}
</style>