Fork pd_web, remove everything we don't need

This commit is contained in:
2025-09-24 15:37:57 +02:00
parent 9dcdd94b3a
commit fd5cd0bfd1
415 changed files with 146269 additions and 120786 deletions

View File

@@ -1,285 +0,0 @@
<script>
import FilePicker from "file_viewer/FilePicker.svelte";
import CustomBanner from "file_viewer/CustomBanner.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import SuccessMessage from "util/SuccessMessage.svelte";
import ThemePicker from "util/ThemePicker.svelte";
import { onMount } from "svelte";
import Persistence from "icons/Persistence.svelte";
import ToggleButton from "layout/ToggleButton.svelte";
let loading = false
let success_message
let file_picker
let currently_selecting = "" // header, background or footer
let theme = ""
let header_image = ""
let header_link = ""
let background_image = ""
let footer_image = ""
let footer_link = ""
let affiliate_prompt = false
let disable_download_button = false
let disable_share_button = false
let select_file = t => {
currently_selecting = t
file_picker.open()
}
let add_file = files => {
let type = files[0].type
if (type != "image/png" && type != "image/jpeg" && type != "image/gif" && type != "image/webp") {
success_message.set(false, "File must be an image type")
return
}
if (files[0].size > 10e6) {
success_message.set(false, "Files larger than 10 MB are not allowed. Recommended size is below 1 MB")
return
}
if (currently_selecting === "header") {
header_image = files[0].id
} else if (currently_selecting === "background") {
background_image = files[0].id
} else if (currently_selecting === "footer") {
footer_image = files[0].id
}
save()
}
let save = async () => {
loading = true
const form = new FormData()
form.append("theme", theme)
form.append("header_image", header_image)
form.append("header_link", header_link)
form.append("background_image", background_image)
form.append("footer_image", footer_image)
form.append("footer_link", footer_link)
form.append("disable_download_button", disable_download_button)
form.append("disable_share_button", disable_share_button)
if (affiliate_prompt) {
form.append("affiliate_prompt", window.user.username)
}
try {
const resp = await fetch(
window.api_endpoint+"/user/file_customization",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
}
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
} finally {
loading = false
}
}
onMount(() => {
// The fields are undefined when they're empty. So we need to check if each
// field is defined before converting to a string
if (window.user.file_viewer_branding) {
let b = window.user.file_viewer_branding
theme = b.theme ? b.theme : ""
header_image = b.header_image ? b.header_image : ""
header_link = b.header_link ? b.header_link : ""
background_image = b.background_image ? b.background_image : ""
footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : ""
affiliate_prompt = b.affiliate_prompt === window.user.username ? true : false
disable_download_button = b.disable_download_button ? b.disable_download_button : false
disable_share_button = b.disable_share_button ? b.disable_share_button : false
}
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2><Persistence/>File viewer branding</h2>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_yellow">
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{/if}
<SuccessMessage bind:this={success_message}></SuccessMessage>
<p>
You can change the appearance of your file viewer pages. The images you
choose here will be loaded each time someone visits one of your files.
The data usage will also be subtracted from your account's data cap.
Keep in mind that large images can take a very long time to load over
cellular connections. I recommend keeping the header and footer images
below 100 kB, and the background image below 1 MB. Allowed image types
are PNG, JPEG, GIF and WebP. If you want to use an animated banner you
should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load.
</p>
<fieldset>
<legend>Theme</legend>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
the viewer choose their own theme.
</p>
<ThemePicker
theme={theme}
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
</fieldset>
<fieldset>
<legend>Header image</legend>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
image. The link needs to start with 'https://'.
</p>
<button on:click={() => {select_file("header")}}>
<i class="icon">add_photo_alternate</i>
Select header image
</button>
<button on:click={() => {header_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if header_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Background image</legend>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Footer image</legend>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Affiliate prompt</legend>
<p>
When this is enabled premium users on your download pages will be
asked to support you through pixeldrain's <a
href="/about#toc_12">affiliate program</a>.
</p>
<ToggleButton bind:on={affiliate_prompt} action={save}>
Enable affiliate prompt
</ToggleButton>
</fieldset>
<fieldset>
<legend>Toolbar buttons</legend>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
<ToggleButton bind:on={disable_download_button} action={save}>
Disable download button
</ToggleButton>
<br/>
<ToggleButton bind:on={disable_share_button} action={save}>
Disable share button
</ToggleButton>
</fieldset>
</section>
<FilePicker
bind:this={file_picker}
on:files={e => {add_file(e.detail)}}
multi_select={false}
title="Select image file"
/>
<style>
.background_preview {
max-height: 200px;
max-width: 100%;
display: block;
margin: auto;
}
.form_row {
display: inline-flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.grow {
flex: 1 1 auto;
}
.shrink {
flex: 0 0 auto;
}
</style>

View File

@@ -9,10 +9,8 @@ import DepositCredit from "./DepositCredit.svelte";
import TabMenu, { type Tab } from "util/TabMenu.svelte";
import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte";
import Dashboard from "./dashboard/Dashboard.svelte";
import AffiliatePrompt from "./AffiliatePrompt.svelte";
import FileManager from "./filemanager/FileManager.svelte";
import { onMount } from "svelte";
import { get_user, type User } from "lib/PixeldrainAPI";
@@ -23,13 +21,6 @@ let pages: Tab[] = [
icon: "dashboard",
component: Dashboard,
hide_background: true,
}, {
path: "/user/filemanager",
title: "My Files",
icon: "",
component: FileManager,
hidden: true,
hide_frame: true,
}, {
path: "/user/settings",
title: "Settings",
@@ -67,11 +58,6 @@ let pages: Tab[] = [
title: "Sharing settings",
icon: "share",
component: BandwidthSharing,
}, {
path: "/user/sharing/branding",
title: "Page Branding",
icon: "palette",
component: PageBranding,
}, {
path: "/user/sharing/embedding",
title: "Embedding Controls",

View File

@@ -146,7 +146,6 @@ const update = async (plan) => {
<li>4 TB transfer limit (higher plans available)</li>
<li>Access to the <a href="/filesystem">filesystem</a></li>
<li>2 TB filesytem storage limit (higher plans available)</li>
<li>File expire after 240 days for Pro, and never on the other plans</li>
</ul>
</div>
</div>
@@ -185,12 +184,6 @@ const update = async (plan) => {
href="/user/sharing/bandwidth">hotlinking</a>
enabled)
</li>
<li>Access to the <a href="/filesystem">filesystem</a></li>
<li>Files never expire as long as subscription is active</li>
<li>
Download page <a href="/user/sharing/branding">branding
options</a>
</li>
<li>
File <a href="/user/sharing/embedding">embedding
control</a> options

View File

@@ -20,14 +20,6 @@
<h3>Quick navigation</h3>
<div class="button_row">
<a href="/user/filemanager#files" class="button">
<i class="icon">image</i>
My Files
</a>
<a href="/user/filemanager#lists" class="button">
<i class="icon">photo_library</i>
My Albums
</a>
{#if window.user.subscription.filesystem_access}
<a href="/d/me" class="button">
<i class="icon">folder</i>
@@ -40,19 +32,6 @@
</a>
</div>
<h3>Exports</h3>
<div class="button_row">
<a href="/api/user/files?format=csv" class="button">
<i class="icon">list</i>
Export files to CSV
</a>
<a href="/api/user/lists?format=csv" class="button">
<i class="icon">list</i>
Export albums to CSV
</a>
</div>
<style>
.button_row {
display: flex;

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte";
import { FSNavigator } from "filesystem/FSNavigator.ts"
import { fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI";
import Button from "layout/Button.svelte";
import CreateDirectory from "filesystem/filemanager/CreateDirectory.svelte";
import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
@@ -66,7 +66,7 @@ onMount(() => nav.navigate("/me", false))
{child.name}
</div>
{#if child.id}
{#if node_is_shared(child)}
<a href="/d/{child.id}" class="button action_button">
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>

View File

@@ -47,14 +47,12 @@ import { formatDataVolume } from "util/Formatting";
No data transfer limit
{/if}
</li>
<li>
{#if window.user.subscription.file_expiry_days > 0}
Files expire after {window.user.subscription.file_expiry_days} days
{:else}
Files never expire
{/if}
</li>
{#if window.user.subscription.id !== ""}
<li>
Support: For questions related to your account you can send a
message to support@pixeldrain.com
</li>
{/if}
</ul>
<style>

View File

@@ -1,47 +0,0 @@
<script>
import UploadLib from "./UploadLib.svelte";
import { drop_target } from "lib/DropTarget.ts"
let upload_widget
</script>
<div class="wrapper" use:drop_target={{upload: (files) => upload_widget.upload_files(files)}}>
<div class="upload_buttons">
<button on:click={() => upload_widget.pick_files() } class="big_button button_highlight">
<i class="icon small">cloud_upload</i>
<span><u>U</u>pload Files</span>
</button>
<a href="/t" id="upload_text_button" class="button big_button button_highlight">
<i class="icon small">text_fields</i>
<span>Upload <u>T</u>ext</span>
</a>
</div>
<div class="center">
<UploadLib bind:this={upload_widget}/>
</div>
</div>
<style>
.wrapper {
border-radius: 4px;
}
.center {
text-align: center;
}
.upload_buttons {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
}
.upload_buttons > * {
flex: 1 1 auto;
}
.big_button {
margin: 0;
max-width: 300px;
font-size: 1.4em;
justify-content: center;
}
</style>

View File

@@ -60,22 +60,3 @@ Total storage space used:
Premium data transfer:
(<a href="/user/sharing/bandwidth">set custom limit</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
<br/>
File count (does not apply to filesystem)
<ProgressBar total={10000} used={window.user.file_count}></ProgressBar>
<div class="gauge_labels">
<div>{window.user.file_count}</div>
<div>10000</div>
</div>
<style>
.gauge_labels {
display: flex;
justify-content: space-between;
line-height: 1em;
}
.gauge_labels > div {
flex: 0 0 auto;
}
</style>

View File

@@ -6,7 +6,6 @@ import CardStatistics from "./CardStatistics.svelte";
import CardSubscription from "./CardSubscription.svelte";
import CardUsage from "./CardUsage.svelte";
import CardActivity from "./CardActivity.svelte";
import CardUpload from "./CardUpload.svelte";
import CardPrepaidTransactions from "./CardPrepaidTransactions.svelte";
import CardFsHome from "./CardFSHome.svelte";
import AddressReputation from "home_page/AddressReputation.svelte";
@@ -62,12 +61,6 @@ const swap_card = (idx1, idx2) => {
onMount(() => {
cards = []
cards.push({
id: "upload",
elem: CardUpload,
title: "Quick upload",
link: "/home",
})
if (window.user.subscription.filesystem_access === true) {
cards.push({
id: "filesystem_home",

View File

@@ -1,442 +0,0 @@
<script>
import { formatDataVolume, formatDate } from "util/Formatting";
// Main elements
let directoryArea
let nodeContainer
let statusBar = "Loading..."
// Internal state, contains a list of all files in the directory, visible
// files in the directory and the last scroll position. These are used for
// rendering the file list correctly
// type: {icon, name, href, type, size, sizeLabel, dateCreated, selected}
let allFiles = []
export const reset = () => {
allFiles = []
}
export const addFile = (id, icon, name, href, type, size, sizeLabel, dateCreated) => {
allFiles.push({
id: id,
icon: icon,
name: name,
href: href,
type: type,
size: size,
sizeLabel: sizeLabel,
dateCreated: dateCreated,
selected: false,
filtered: false,
visible: false,
})
}
export const renderFiles = () => {
search(lastSearchTerm)
}
export const getSelectedFiles = () => {
let selectedFiles = []
for (let i in allFiles) {
if (allFiles[i].selected) {
selectedFiles.push(allFiles[i])
}
}
return selectedFiles
}
// search filters the allFiles array on a search term. All files which match the
// search term will be put into visibleFiles. The visibleFiles array will then
// be rendered by render_visible_files
let lastSearchTerm = ""
export const search = (term) => {
term = term.toLowerCase()
lastSearchTerm = term
if (term === "") {
for (let i in allFiles) {
allFiles[i].filtered = false
}
sortBy("")
render_visible_files()
return
}
let fileName = ""
for (let i in allFiles) {
fileName = allFiles[i].name.toLowerCase()
if (fileName.includes(term)) {
// If a file name contains the search term we include it in the results
allFiles[i].filtered = false
} else {
allFiles[i].filtered = true
}
}
sortBy("")
render_visible_files()
}
// searchSubmit opens the first file in the search results
export const searchSubmit = () => {
for (let i in allFiles) {
if (allFiles[i].visible && !allFiles[i].filtered) {
window.open(allFiles[i].href, "_blank")
break
}
}
}
// Sorting internal state. By default we sort by dateCreated in descending
// order (new to old)
let currentSortField = "dateCreated"
let currentSortAscending = false
let tableColumns = [
{ name: "Name", field: "name", width: "" },
{ name: "Creation date", field: "dateCreated", width: "160px" },
{ name: "Size", field: "size", width: "90px" },
{ name: "Type", field: "type", width: "200px" },
]
const sortBy = (field) => {
if (field === "") {
// If no sort field is provided we use the last used sort field
field = currentSortField
} else {
// If a sort field is provided we check in which direction we have to
// sort
if (currentSortField !== field) {
// If this field is a different field than before we sort it in
// ascending order
currentSortAscending = true
currentSortField = field
} else if (currentSortField === field) {
// If it is the same field as before we reverse the sort order
currentSortAscending = !currentSortAscending
}
}
// Add the arrow to the sort label. First remove the arrow from all sort
// labels
let colIdx = 0
for (let i in tableColumns) {
if (tableColumns[i].field == field) {
colIdx = i
}
tableColumns[i].name = tableColumns[i].name.replace("▲ ", "").replace("▼ ", "")
}
// Then prepend the arrow to the current sort label
if (currentSortAscending) {
tableColumns[colIdx].name = "▼ " + tableColumns[colIdx].name
} else {
tableColumns[colIdx].name = "▲ " + tableColumns[colIdx].name
}
tableColumns = tableColumns
let fieldA, fieldB
allFiles.sort((a, b) => {
fieldA = a[currentSortField]
fieldB = b[currentSortField]
if (typeof (fieldA) === "number") {
if (currentSortAscending) {
return fieldA - fieldB
} else {
return fieldB - fieldA
}
} else {
if (currentSortAscending) {
return fieldA.localeCompare(fieldB, undefined, {numeric: true})
} else {
return fieldB.localeCompare(fieldA, undefined, {numeric: true})
}
}
})
render_visible_files()
}
// Scroll event for rendering new file nodes when they become visible. For
// performance reasons the files will only be rendered once every 100ms. If a
// scroll event comes in and we're not done with the previous frame yet the
// event will be ignored
let render_timeout = false;
const onScroll = (e) => {
if (render_timeout) {
return
}
render_timeout = true
setTimeout(() => {
render_visible_files()
render_timeout = false
}, 100)
}
const render_visible_files = () => {
const fileHeight = 40
let paddingTop = directoryArea.scrollTop - directoryArea.scrollTop % fileHeight
let start = Math.floor(paddingTop / fileHeight) - 5
if (start < 0) { start = 0 }
let end = Math.ceil((paddingTop + directoryArea.clientHeight) / fileHeight) + 5
if (end > allFiles.length) { end = allFiles.length - 1 }
nodeContainer.style.paddingTop = (start * fileHeight) + "px"
// All files which have not been filtered out by the search function. We
// pretend that files with filtered == true do not exist
let totalFiles = 0
let totalSize = 0
let selectedFiles = 0
let selectedSize = 0
for (let i in allFiles) {
if (totalFiles >= start && totalFiles <= end && !allFiles[i].filtered) {
allFiles[i].visible = true
} else {
allFiles[i].visible = false
}
if (!allFiles[i].filtered) {
totalFiles++
totalSize += allFiles[i].size
if (allFiles[i].selected) {
selectedFiles++
selectedSize += allFiles[i].size
}
}
}
nodeContainer.style.height = (totalFiles * fileHeight) + "px"
statusBar = totalFiles + " items ("+formatDataVolume(totalSize, 4)+")"
if (selectedFiles !== 0) {
statusBar += ", "+selectedFiles+" selected ("+formatDataVolume(selectedSize, 4)+")"
}
}
let selectionMode = false
export const setSelectionMode = (s) => {
selectionMode = s
// When selection mode is disabled we automatically deselect all files
if (!s) {
for (let i in allFiles) {
allFiles[i].selected = false
}
render_visible_files()
}
}
let shift_pressed = false
const detect_shift = (e) => {
if (e.key !== "Shift") {
return
}
shift_pressed = e.type === "keydown"
}
export let multi_select = true
let last_selected_node = -1
const node_click = (index) => {
if (selectionMode) {
if (multi_select && shift_pressed && last_selected_node != -1) {
let id_low = last_selected_node
let id_high = last_selected_node
if (last_selected_node < index) {
id_high = index
} else {
id_low = index
}
for (let i = id_low; i <= id_high && !allFiles[i].filtered; i++) {
if (i != last_selected_node) {
allFiles[i].selected = !allFiles[i].selected
}
}
} else {
// If multi select is disabled we deselect all other files before
// selecting this one
if (!multi_select) {
for (let i in allFiles) {
allFiles[i].selected = false
}
}
allFiles[index].selected = !allFiles[index].selected
}
last_selected_node = index
render_visible_files()
} else {
window.open(allFiles[index].href, "_blank")
}
}
</script>
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} />
<div id="directory_element">
<div class="directory_sorters">
{#each tableColumns as col}
<button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button">
{col.name}
</button>
{/each}
</div>
<div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area">
<div bind:this={nodeContainer} id="node_container" class="directory_node_container">
{#each allFiles as file, index}
{#if file.visible && !file.filtered}
<a class="node"
href={file.href}
target="_blank"
rel="noreferrer"
title="{file.name}"
class:node_selected={file.selected}
on:click|preventDefault={() => {node_click(index)}}
>
<div>
<img src={file.icon} alt="thumbnail" />
<span>{file.name}</span>
</div>
<div style="width: {tableColumns[1].width}">
<span>{formatDate(new Date(file.dateCreated), true, true, false)}</span>
</div>
<div style="width: {tableColumns[2].width}">
<span>{file.sizeLabel}</span>
</div>
<div style="width: {tableColumns[3].width}">
<span>{file.type}</span>
</div>
</a>
{/if}
{/each}
</div>
</div>
<div id="footer">
{statusBar}
</div>
</div>
<style>
#directory_element {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: auto;
text-align: left;
}
.directory_sorters {
flex: 0 0 auto;
display: flex;
flex-direction: row;
overflow: hidden;
background: var(--body_background);
min-width: 850px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom: 1px solid var(--separator);
}
.sorter_button {
display: inline-block;
margin: 4px 10px;
text-align: initial;
background: none;
box-shadow: none;
}
.sorter_button:hover {
background: var(--input_hover_background);
}
.directory_sorters > :first-child,
.node > :first-child {
flex-shrink: 1;
flex-grow: 1;
}
.directory_sorters > :not(:first-child),
.node > :not(:first-child) {
flex-shrink: 0;
flex-grow: 0;
}
#directory_area {
flex: 1 1 auto;
margin: 0;
padding: 0;
overflow-x: auto;
background: var(--body_background);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
#node_container {
display: block;
min-width: 850px;
}
#footer {
flex-shrink: 0;
color: var(--background_text_color);
padding: 4px;
}
.node {
display: flex;
flex-direction: row;
position: static;
height: 40px;
overflow: hidden;
/* I use padding instead of margin here because it goves me more precise
control over the size.
Check out https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing */
margin: 0;
color: var(--body_text_color);
text-decoration: none;
transition: background 0.2s;
}
.node:hover:not(.node_selected) {
background: var(--input_hover_background);
color: var(--input_text);
text-decoration: none;
}
.node_selected {
background: var(--highlight_background);
color: var(--highlight_text_color);
}
.node > div {
height: 100%;
overflow: hidden;
margin: auto 10px;
padding: 4px;
display: inline-block;
text-overflow: ellipsis;
white-space: nowrap;
}
.node > div > span {
margin: auto;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
}
.node > div > img {
max-height: 100%;
margin-right: 6px;
width: auto;
min-width: auto;
float: left;
display: block;
}
</style>

View File

@@ -1,393 +0,0 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume } from "util/Formatting";
import Modal from "util/Modal.svelte";
import Spinner from "util/Spinner.svelte";
import UploadWidget from "util/upload_widget/UploadWidget.svelte";
import DirectoryElement from "./DirectoryElement.svelte"
let loading = true
let contentType = "" // files or lists
let inputSearch
let directoryElement
let downloadFrame
let help_modal
let help_modal_visible = false
let upload_widget
let getUserFiles = () => {
loading = true
fetch(window.api_endpoint + "/user/files").then(resp => {
if (!resp.ok) { Promise.reject("yo") }
return resp.json()
}).then(resp => {
directoryElement.reset()
for (let i in resp.files) {
directoryElement.addFile(
resp.files[i].id,
window.api_endpoint + "/file/" + resp.files[i].id + "/thumbnail?width=32&height=32",
resp.files[i].name,
"/u/" + resp.files[i].id,
resp.files[i].mime_type,
resp.files[i].size,
formatDataVolume(resp.files[i].size, 4),
resp.files[i].date_upload,
)
}
directoryElement.renderFiles()
}).catch((err) => {
throw (err)
}).finally(() => {
loading = false
})
}
let getUserLists = () => {
loading = true
fetch(window.api_endpoint + "/user/lists").then(resp => {
if (!resp.ok) { Promise.reject("yo") }
return resp.json()
}).then(resp => {
directoryElement.reset()
for (let i in resp.lists) {
directoryElement.addFile(
resp.lists[i].id,
window.api_endpoint + "/list/" + resp.lists[i].id + "/thumbnail?width=32&height=32",
resp.lists[i].title,
"/l/" + resp.lists[i].id,
"list",
resp.lists[i].file_count,
resp.lists[i].file_count + " files",
resp.lists[i].date_created,
)
}
directoryElement.renderFiles()
}).catch((err) => {
throw (err)
}).finally(() => {
loading = false
})
}
const searchHandler = (e) => {
if (e.keyCode === 27) { // Escape
e.preventDefault()
inputSearch.value = ""
inputSearch.blur()
} else if (e.keyCode === 13) { // Enter
e.preventDefault()
directoryElement.searchSubmit()
return
}
requestAnimationFrame(() => {
directoryElement.search(inputSearch.value)
})
}
let initialized = false
let hashChange = () => {
if (!initialized) {
return
}
if (window.location.hash === "#lists") {
contentType = "lists"
document.title = "My Albums"
getUserLists()
resetMenu()
} else {
contentType = "files"
document.title = "My Files"
getUserFiles()
resetMenu()
}
}
let selecting = false
const toggleSelecting = () => {
selecting = !selecting
directoryElement.setSelectionMode(selecting)
}
const bulkDelete = async () => {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
if (contentType === "lists") {
if (!confirm(
"You are about to delete "+selected.length+" lists. "+
"This is not reversible!\n"+
"Are you sure?"
)){ return }
} else {
if (!confirm(
"You are about to delete "+selected.length+" files. "+
"This is not reversible!\n"+
"Are you sure?"
)){ return }
}
loading = true
let endpoint = window.api_endpoint+"/file/"
if (contentType === "lists") {
endpoint = window.api_endpoint+"/list/"
}
for (let i in selected) {
try {
const resp = await fetch(
endpoint+encodeURIComponent(selected[i].id),
{ method: "DELETE" }
);
if(resp.status >= 400) {
throw new Error(resp.text())
}
} catch (err) {
alert("Delete failed: "+err)
}
}
hashChange()
}
function createList() {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
let title = prompt(
"You are creating a list containing " + selected.length + " files.\n"
+ "What do you want to call it?", "My New Album"
)
if (title === null) {
return
}
let files = selected.reduce(
(acc, curr) => {
acc.push({"id": curr.id})
return acc
},
[],
)
fetch(
window.api_endpoint+"/list",
{
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({
"title": title,
"files": files
})
}
).then(resp => {
if (!resp.ok) {
return Promise.reject("HTTP error: " + resp.status)
}
return resp.json()
}).then(resp => {
window.open('/l/' + resp.id, '_blank')
}).catch(err => {
alert("Failed to create list. Server says this:\n"+err)
})
}
function downloadFiles() {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
// Create a list of file ID's separated by commas
let ids = selected.reduce((acc, curr) => acc + curr.id + ",", "")
// Remove the last comma
ids = ids.slice(0, -1)
downloadFrame.src = window.api_endpoint+"/file/"+ids+"?download"
}
const keydown = (e) => {
if (e.ctrlKey && e.key === "f" || !e.ctrlKey && e.keyCode === 191) {
e.preventDefault()
inputSearch.focus()
}
if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (document.activeElement.type && document.activeElement.type === "text") {
return // Prevent shortcuts from interfering with input fields
}
if (e.key === "i") {
help_modal.toggle()
} else if (e.key === "/") {
inputSearch.focus()
} else {
return
}
// This will only be run if a custom shortcut was triggered
e.preventDefault()
}
onMount(() => {
initialized = true
hashChange()
})
</script>
<svelte:window on:keydown={keydown} on:hashchange={hashChange} />
<div id="file_manager" class="file_manager page_margins">
<div id="nav_bar" class="nav_bar">
<button id="btn_menu" onclick="toggleMenu()"><i class="icon">menu</i></button>
<button on:click={toggleSelecting} id="btn_select" class:button_highlight={selecting}>
<i class="icon">select_all</i> Select
</button>
<input
bind:this={inputSearch}
on:keyup={searchHandler}
id="input_search"
class="input_search"
type="text"
placeholder="press / to search"
/>
{#if contentType === "files"}
<button on:click={upload_widget.pick_files} id="btn_upload" title="Upload files">
<i class="icon">cloud_upload</i>
</button>
{/if}
<button on:click={hashChange} id="btn_reload" title="Refresh file list">
<i class="icon">refresh</i>
</button>
<button on:click={() => help_modal.toggle()} class:button_highlight={help_modal_visible} title="Help">
<i class="icon">info</i>
</button>
</div>
{#if selecting}
<div class="nav_bar">
{#if contentType === "files"}
<button on:click={createList}><i class="icon">list</i> Make album</button>
<button on:click={downloadFiles}><i class="icon">download</i> Download</button>
{/if}
<button on:click={bulkDelete}><i class="icon">delete</i> Delete</button>
</div>
{/if}
{#if loading}
<div class="spinner">
<Spinner></Spinner>
</div>
{/if}
<DirectoryElement bind:this={directoryElement}></DirectoryElement>
<Modal
bind:this={help_modal}
title="File manager help"
width="600px"
on:is_visible={e => {help_modal_visible = e.detail}}
>
<div class="indent">
<p>
In the file manager you can see the files you have uploaded and
the lists you have created.
</p>
<h3>Searching</h3>
<p>
By clicking the search bar or pressing the / button you can
search through your files or lists. Only the entries matching
your search term will be shown. Pressing Enter will open the
first search result in a new tab. Pressing Escape will cancel
the search and all files will be shown again.
</p>
<h3>Bulk actions</h3>
<p>
With the Select button you can click files to select them. Once
you have made a selection you can use the buttons on the toolbar
to either create a list containing the selected files or delete
them.
</p>
<p>
Holding Shift while selecting a file will select all the files
between the file you last selected and the file you just
clicked.
</p>
</div>
</Modal>
<!-- This frame will load the download URL when a download button is pressed -->
<iframe bind:this={downloadFrame} title="File download frame" style="display: none; width: 1px; height: 1px;"></iframe>
</div>
<UploadWidget bind:this={upload_widget} drop_upload on:uploads_finished={hashChange}/>
<style>
:global(#page_body) {
height: 100vh;
padding: 0;
background: none;
}
/* Override the menu button so it doesn't overlap the file manager when the menu
is collapsed */
:global(#button_toggle_navigation) {
display: none;
}
#file_manager {
position: absolute;
padding: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
}
.nav_bar {
flex-shrink: 0;
display: flex;
flex-direction: row;
padding: 2px;
}
.nav_bar > button {
flex-shrink: 0;
}
.input_search {
flex: 1 1 auto;
min-width: 100px;
}
.spinner {
position: absolute;
display: block;
margin: auto;
max-width: 100%;
max-height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
}
</style>