Add path to bookmarks, add logout button
This commit is contained in:
@@ -4,8 +4,9 @@ import { copy_text } from "util/Util.svelte";
|
||||
import FileStats from "./FileStats.svelte";
|
||||
import type { FSNavigator } from "./FSNavigator";
|
||||
import EditWindow from "./edit_window/EditWindow.svelte";
|
||||
import { fs_share_url } from "lib/FilesystemAPI";
|
||||
import { fs_share_url, path_is_shared } from "lib/FilesystemAPI";
|
||||
import ShareDialog from "./ShareDialog.svelte";
|
||||
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
@@ -14,10 +15,9 @@ export let details_visible = false
|
||||
export let edit_window: EditWindow
|
||||
export let edit_visible = false
|
||||
let share_dialog: ShareDialog
|
||||
|
||||
$: share_url = fs_share_url($nav.path)
|
||||
let link_copied = false
|
||||
export const copy_link = () => {
|
||||
const share_url = fs_share_url($nav.path)
|
||||
if (share_url === "") {
|
||||
edit_window.edit(nav.base, true, "share")
|
||||
return
|
||||
@@ -50,7 +50,19 @@ export const copy_link = () => {
|
||||
<span>Download</span>
|
||||
</button>
|
||||
|
||||
{#if share_url !== ""}
|
||||
{#if is_bookmark($bookmarks_store, $nav.base.id)}
|
||||
<button on:click={() => bookmark_del($nav.base.id)}>
|
||||
<i class="icon">bookmark_remove</i>
|
||||
<span>Bookmark</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => bookmark_add($nav.base)}>
|
||||
<i class="icon">bookmark_add</i>
|
||||
<span>Bookmark</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if path_is_shared($nav.path)}
|
||||
<button on:click={copy_link} class:button_highlight={link_copied}>
|
||||
<i class="icon">content_copy</i>
|
||||
<span><u>C</u>opy link</span>
|
||||
@@ -58,7 +70,7 @@ export const copy_link = () => {
|
||||
{/if}
|
||||
|
||||
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)-->
|
||||
{#if $nav.base.id !== "me" && (navigator.share !== undefined || $nav.permissions.write === true)}
|
||||
{#if navigator.share !== undefined || $nav.permissions.write === true}
|
||||
<button on:click={(e) => share_dialog.open(e, nav.path)}>
|
||||
<i class="icon">share</i>
|
||||
<span>Share</span>
|
||||
|
@@ -7,6 +7,7 @@ import FileOptions from "./FileOptions.svelte";
|
||||
import SharingOptions from "./SharingOptions.svelte";
|
||||
import AccessControl from "./AccessControl.svelte";
|
||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||
import { loading_finish, loading_start } from "lib/Loading";
|
||||
|
||||
export let nav: FSNavigator
|
||||
let file: FSNode = {} as FSNode
|
||||
@@ -64,7 +65,7 @@ const save = async (keep_editing = false) => {
|
||||
|
||||
let new_file: FSNode
|
||||
try {
|
||||
nav.set_loading(true)
|
||||
loading_start()
|
||||
options.branding_enabled = JSON.stringify(branding_enabled)
|
||||
|
||||
new_file = await fs_update(file.path, options)
|
||||
@@ -88,7 +89,7 @@ const save = async (keep_editing = false) => {
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
nav.set_loading(false)
|
||||
loading_finish()
|
||||
}
|
||||
|
||||
if (open_after_edit) {
|
||||
|
@@ -3,6 +3,7 @@ import Button from "layout/Button.svelte";
|
||||
import { fs_delete_all, type FSNode } from "lib/FilesystemAPI";
|
||||
import PathLink from "filesystem/util/PathLink.svelte";
|
||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||
import { loading_finish, loading_start } from "lib/Loading";
|
||||
|
||||
export let nav: FSNavigator
|
||||
export let file: FSNode = {} as FSNode
|
||||
@@ -16,14 +17,14 @@ const delete_file = async (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
nav.set_loading(true)
|
||||
loading_start()
|
||||
await fs_delete_all(file.path)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(err)
|
||||
return
|
||||
} finally {
|
||||
nav.set_loading(false)
|
||||
loading_finish()
|
||||
}
|
||||
|
||||
if (open_after_edit) {
|
||||
|
@@ -14,6 +14,7 @@ import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
||||
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
||||
import { FileAction, type FileEvent } from "./FileManagerLib";
|
||||
import FileMenu from "./FileMenu.svelte";
|
||||
import { loading_finish, loading_start } from "lib/Loading";
|
||||
|
||||
export let nav: FSNavigator
|
||||
export let upload_widget: FsUploadWidget
|
||||
@@ -117,9 +118,9 @@ const delete_selected = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
nav.set_loading(true)
|
||||
|
||||
try {
|
||||
loading_start()
|
||||
|
||||
// Save all promises with deletion requests in an array
|
||||
let promises = []
|
||||
nav.children.forEach(child => {
|
||||
@@ -136,7 +137,7 @@ const delete_selected = async () => {
|
||||
alert("Delete failed: " + err.message + " ("+err.value+")")
|
||||
} finally {
|
||||
viewing_mode()
|
||||
nav.reload()
|
||||
loading_finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,11 +265,10 @@ const move_start = () => {
|
||||
}
|
||||
|
||||
const move_here = async () => {
|
||||
nav.set_loading(true)
|
||||
|
||||
let target_dir = nav.base.path + "/"
|
||||
|
||||
const target_dir = nav.base.path + "/"
|
||||
try {
|
||||
loading_start()
|
||||
|
||||
let promises = []
|
||||
moving_items.forEach(item => {
|
||||
console.log("moving", item.path, "to", target_dir + item.name)
|
||||
@@ -283,6 +283,7 @@ const move_here = async () => {
|
||||
} finally {
|
||||
viewing_mode()
|
||||
nav.reload()
|
||||
loading_finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +425,7 @@ onMount(() => {
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
height: 100%; /* Used for drop target */
|
||||
}
|
||||
.width_container {
|
||||
position: sticky;
|
||||
|
@@ -3,49 +3,31 @@ import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||
import Button from "layout/Button.svelte";
|
||||
import Dialog from "layout/Dialog.svelte";
|
||||
import { bookmark_add, bookmark_del, bookmarks_store } from "lib/Bookmarks";
|
||||
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
|
||||
import { fs_download, type FSNode } from "lib/FilesystemAPI";
|
||||
import { tick } from "svelte";
|
||||
|
||||
export let nav: FSNavigator
|
||||
export let edit_window: EditWindow
|
||||
let dialog: Dialog
|
||||
let node: FSNode
|
||||
let is_bookmark: boolean = false
|
||||
let node: FSNode = null
|
||||
|
||||
export const open = async (n: FSNode, target: EventTarget) => {
|
||||
node = n
|
||||
|
||||
is_bookmark = false
|
||||
for (const bm of $bookmarks_store) {
|
||||
console.log(bm)
|
||||
if (bm.id === n.id) {
|
||||
is_bookmark = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the view to update, so the dialog gets the proper measurements
|
||||
await tick()
|
||||
dialog.open((target as Element).closest("button").getBoundingClientRect())
|
||||
}
|
||||
|
||||
const bookmark = () => {
|
||||
bookmark_add({
|
||||
id: node.id,
|
||||
icon: "folder_shared",
|
||||
label: node.name,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog bind:this={dialog}>
|
||||
<div class="menu">
|
||||
<Button click={() => {dialog.close(); fs_download(node)}} icon="save" label="Download"/>
|
||||
{#if is_bookmark}
|
||||
{#if node !== null && is_bookmark($bookmarks_store, node.id)}
|
||||
<Button click={() => {dialog.close(); bookmark_del(node.id)}} icon="bookmark_remove" label="Remove bookmark"/>
|
||||
{:else}
|
||||
<Button click={() => {dialog.close(); bookmark()}} icon="bookmark_add" label="Add bookmark"/>
|
||||
<Button click={() => {dialog.close(); bookmark_add(node)}} icon="bookmark_add" label="Add bookmark"/>
|
||||
{/if}
|
||||
{#if $nav.permissions.write}
|
||||
<Button click={() => {dialog.close(); edit_window.edit(node, false, "file")}} icon="edit" label="Edit"/>
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import { writable } from "svelte/store"
|
||||
import { fs_check_response, fs_path_url } from "./FilesystemAPI"
|
||||
import { fs_check_response, fs_path_url, type FSNode } from "./FilesystemAPI"
|
||||
import { loading_finish, loading_start } from "lib/Loading"
|
||||
import { get_user } from "./PixeldrainAPI"
|
||||
|
||||
const bookmarks_file = "/me/.fnx/bookmarks.json"
|
||||
|
||||
export type Bookmark = {
|
||||
id: string,
|
||||
path: string,
|
||||
icon: string,
|
||||
label: string,
|
||||
}
|
||||
@@ -22,25 +24,54 @@ export let bookmarks_store = writable<Bookmark[]>(
|
||||
)
|
||||
|
||||
export const bookmarks_get = async (): Promise<Bookmark[]> => {
|
||||
let bookmarks: Bookmark[] = []
|
||||
try {
|
||||
bookmarks = await fs_check_response(
|
||||
await fetch(fs_path_url(bookmarks_file), { cache: "no-store" })
|
||||
)
|
||||
} catch (err) {
|
||||
// If the bookmarks were not found when we return an empty bookmarks
|
||||
// list
|
||||
if (err.value !== "path_not_found") {
|
||||
throw err
|
||||
loading_start()
|
||||
const user = await get_user()
|
||||
if (
|
||||
user.username === undefined ||
|
||||
user.username === "" ||
|
||||
user.subscription.filesystem_access === undefined ||
|
||||
user.subscription.filesystem_access === false
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const bookmarks = await fs_check_response(
|
||||
await fetch(fs_path_url(bookmarks_file), { cache: "no-store" })
|
||||
) as Bookmark[]
|
||||
|
||||
// Sanity checks
|
||||
for (const bookmark of bookmarks) {
|
||||
if (typeof bookmark.id !== "string") { bookmark.id = "" }
|
||||
if (typeof bookmark.path !== "string") { bookmark.path = "" }
|
||||
if (typeof bookmark.icon !== "string") { bookmark.icon = "" }
|
||||
if (typeof bookmark.label !== "string") { bookmark.label = "" }
|
||||
}
|
||||
|
||||
console.debug("Fetched", bookmarks.length, "bookmarks:", bookmarks)
|
||||
bookmarks_store.set(bookmarks)
|
||||
return bookmarks
|
||||
} catch (err) {
|
||||
// If the bookmarks were not found when we return an empty bookmarks
|
||||
// list
|
||||
if (
|
||||
err.value !== "path_not_found" &&
|
||||
err.value !== "forbidden" &&
|
||||
err.value !== "authentication_required"
|
||||
) {
|
||||
throw err
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} finally {
|
||||
loading_finish()
|
||||
}
|
||||
}
|
||||
|
||||
export const bookmarks_save = async (bookmarks: Bookmark[]) => {
|
||||
try {
|
||||
loading_start()
|
||||
|
||||
await fs_check_response(
|
||||
await fetch(
|
||||
fs_path_url(bookmarks_file) + "?make_parents=true",
|
||||
@@ -49,25 +80,32 @@ export const bookmarks_save = async (bookmarks: Bookmark[]) => {
|
||||
)
|
||||
console.debug("Saved", bookmarks.length, "bookmarks:", bookmarks)
|
||||
bookmarks_store.set(bookmarks)
|
||||
} finally {
|
||||
loading_finish()
|
||||
}
|
||||
}
|
||||
|
||||
export const bookmark_add = async (bm: Bookmark): Promise<Bookmark[]> => {
|
||||
let bookmarks: Bookmark[] = []
|
||||
export const bookmark_add = async (node: FSNode): Promise<Bookmark[]> => {
|
||||
try {
|
||||
loading_start()
|
||||
|
||||
// Get bookmarks
|
||||
bookmarks = await bookmarks_get()
|
||||
const bookmarks = await bookmarks_get()
|
||||
|
||||
// Add new bookmark
|
||||
bookmarks.push(bm)
|
||||
bookmarks.push({
|
||||
id: node.id,
|
||||
path: node.path,
|
||||
icon: "bookmark",
|
||||
label: node.name,
|
||||
})
|
||||
|
||||
// Save new bookmarks
|
||||
await bookmarks_save(bookmarks)
|
||||
return bookmarks
|
||||
} finally {
|
||||
loading_finish()
|
||||
}
|
||||
return bookmarks
|
||||
}
|
||||
|
||||
export const bookmark_del = async (id: string): Promise<Bookmark[]> => {
|
||||
@@ -94,3 +132,12 @@ export const bookmark_del = async (id: string): Promise<Bookmark[]> => {
|
||||
}
|
||||
return bookmarks
|
||||
}
|
||||
|
||||
export const is_bookmark = (bookmarks: Bookmark[], id: string): boolean => {
|
||||
for (const bm of bookmarks) {
|
||||
if (bm.id === id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@@ -55,6 +55,12 @@ export const drop_target = (
|
||||
const entry: FileSystemEntry | null = e.dataTransfer.items[i].webkitGetAsEntry();
|
||||
if (entry !== null) {
|
||||
read_dir_recursive(entry);
|
||||
continue
|
||||
}
|
||||
const file: File | null = e.dataTransfer.items[i].getAsFile();
|
||||
if (file !== null) {
|
||||
args.upload([file]);
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
|
@@ -55,6 +55,14 @@ export const node_is_shared = (node: FSNode): boolean => {
|
||||
}
|
||||
return false
|
||||
}
|
||||
export const path_is_shared = (path: FSNode[]): boolean => {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
if (node_is_shared(path[i])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export type FSNodeProperties = {
|
||||
branding_enabled?: string,
|
||||
@@ -364,7 +372,7 @@ export const fs_share_path = (path: FSNode[]): string => {
|
||||
|
||||
// Find the last node in the path that has a public ID
|
||||
for (let i = path.length - 1; i >= 0; i--) {
|
||||
if (path[i].id !== undefined && path[i].id !== "me") {
|
||||
if (node_is_shared(path[i])) {
|
||||
bucket_idx = i
|
||||
break
|
||||
}
|
||||
|
@@ -132,6 +132,16 @@ export const put_user = async (data: Object) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const logout_user = async (redirect_path: string) => {
|
||||
check_response(await fetch(
|
||||
get_endpoint() + "/user/session",
|
||||
{ method: "DELETE" },
|
||||
))
|
||||
|
||||
document.cookie = "pd_auth_key=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
window.location.pathname = redirect_path
|
||||
}
|
||||
|
||||
export type VATRate = {
|
||||
name: string,
|
||||
vat: number,
|
||||
|
@@ -1,3 +1,7 @@
|
||||
<script>
|
||||
import Button from "layout/Button.svelte";
|
||||
import { logout_user } from "lib/PixeldrainAPI";
|
||||
</script>
|
||||
<ul>
|
||||
<li>Username: {window.user.username}</li>
|
||||
{#if window.user.email === ""}
|
||||
@@ -26,10 +30,10 @@
|
||||
Filesystem
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/logout" class="button">
|
||||
<button on:click={()=> logout_user("/")}>
|
||||
<i class="icon">logout</i>
|
||||
Log out
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -37,7 +41,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.button_row > a {
|
||||
.button_row > * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
ul {
|
||||
|
@@ -1,150 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { FSNavigator } from "filesystem/FSNavigator.ts"
|
||||
import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI";
|
||||
import Button from "layout/Button.svelte";
|
||||
import CreateDirectory from "filesystem/filemanager/CreateDirectory.svelte";
|
||||
import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
||||
import { drop_target } from "lib/DropTarget.ts"
|
||||
|
||||
const nav = new FSNavigator(false)
|
||||
let upload_widget
|
||||
var show_hidden = false
|
||||
var creating_dir = false
|
||||
|
||||
onMount(() => nav.navigate("/me", false))
|
||||
</script>
|
||||
|
||||
<div class="wrapper" use:drop_target={{upload: (files) => upload_widget.upload_files(files)}}>
|
||||
<div class="toolbar">
|
||||
{#if $nav.permissions.write}
|
||||
<Button
|
||||
click={() => upload_widget.pick_files()}
|
||||
icon="cloud_upload"
|
||||
title="Upload files to this directory"
|
||||
label="Upload files"
|
||||
/>
|
||||
<Button
|
||||
click={() => {creating_dir = !creating_dir}}
|
||||
highlight={creating_dir}
|
||||
icon="create_new_folder"
|
||||
title="Create folder"
|
||||
label="Create folder"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="toolbar_spacer"></div>
|
||||
|
||||
<Button
|
||||
click={() => {show_hidden = !show_hidden}}
|
||||
highlight={show_hidden}
|
||||
icon={show_hidden ? "visibility_off" : "visibility"}
|
||||
title="Show hidden files and directories"
|
||||
/>
|
||||
<Button
|
||||
click={() => nav.reload()}
|
||||
icon="refresh"
|
||||
title="Refresh directory listing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if creating_dir}
|
||||
<CreateDirectory nav={nav} />
|
||||
{/if}
|
||||
|
||||
<div class="directory">
|
||||
{#each $nav.children as child (child.path)}
|
||||
<a
|
||||
href={"/d"+fs_encode_path(child.path)}
|
||||
class="node"
|
||||
class:node_selected={child.fm_selected}
|
||||
class:hidden={child.name.startsWith(".") && show_hidden === false}
|
||||
>
|
||||
<img src={fs_node_icon(child, 64, 64)} class="node_icon" alt="icon"/>
|
||||
|
||||
<div class="node_name">
|
||||
{child.name}
|
||||
</div>
|
||||
|
||||
{#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>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FSUploadWidget nav={nav} bind:this={upload_widget} />
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.directory {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.node {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: var(--body_text-color);
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
width: 250px;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--input_background);
|
||||
}
|
||||
.node:hover {
|
||||
background: var(--input_hover_background);
|
||||
color: var(--input_text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.node > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.node_icon {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.node_name {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-all;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.action_button {
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--body_text_color);
|
||||
box-shadow: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
@@ -7,7 +7,6 @@ import CardSubscription from "./CardSubscription.svelte";
|
||||
import CardUsage from "./CardUsage.svelte";
|
||||
import CardActivity from "./CardActivity.svelte";
|
||||
import CardPrepaidTransactions from "./CardPrepaidTransactions.svelte";
|
||||
import CardFsHome from "./CardFSHome.svelte";
|
||||
import AddressReputation from "home_page/AddressReputation.svelte";
|
||||
import { flip } from "svelte/animate";
|
||||
|
||||
@@ -60,51 +59,37 @@ const swap_card = (idx1, idx2) => {
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
cards = []
|
||||
if (window.user.subscription.filesystem_access === true) {
|
||||
cards.push({
|
||||
id: "filesystem_home",
|
||||
elem: CardFsHome,
|
||||
title: "Filesystem home",
|
||||
link: "/d/me",
|
||||
})
|
||||
}
|
||||
cards.push({
|
||||
cards = [
|
||||
{
|
||||
id: "account",
|
||||
elem: CardAccount,
|
||||
title: "Account",
|
||||
link: "/user/settings",
|
||||
})
|
||||
cards.push({
|
||||
},{
|
||||
id: "subscription",
|
||||
elem: CardSubscription,
|
||||
title: "Subscription",
|
||||
link: "/user/subscription",
|
||||
})
|
||||
if (window.user.subscription.type === "prepaid") {
|
||||
cards.push({
|
||||
}, {
|
||||
id: "prepaid_transactions",
|
||||
elem: CardPrepaidTransactions,
|
||||
title: "Prepaid transactions",
|
||||
link: "/user/prepaid/transactions",
|
||||
})
|
||||
}
|
||||
cards.push({
|
||||
}, {
|
||||
id: "usage",
|
||||
elem: CardUsage,
|
||||
title: "Usage",
|
||||
})
|
||||
cards.push({
|
||||
}, {
|
||||
id: "statistics",
|
||||
elem: CardStatistics,
|
||||
title: "Statistics",
|
||||
})
|
||||
cards.push({
|
||||
}, {
|
||||
id: "activity",
|
||||
elem: CardActivity,
|
||||
title: "Activity",
|
||||
link: "/user/activity",
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// Apply the view settings from localstorage
|
||||
try {
|
||||
|
@@ -1,51 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
let dragging = false
|
||||
|
||||
const drop = (e) => {
|
||||
dragging = false;
|
||||
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dispatch("upload", e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
const paste = (e) => {
|
||||
if (e.clipboardData.files.length !== 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch("upload", e.clipboardData.files)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
|
||||
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
|
||||
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
|
||||
on:drop={drop}
|
||||
on:paste={paste}
|
||||
/>
|
||||
|
||||
{#if dragging}
|
||||
<div class="drag_target" transition:fade={{duration: 200}}>
|
||||
Drop files here to upload them
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.drag_target {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 50px;
|
||||
font-size: 2em;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
@@ -1,68 +0,0 @@
|
||||
// Uploads a file to the logged in user's pixeldrain account. If no user is
|
||||
// logged in the file is uploaded anonymously.
|
||||
//
|
||||
// on_progress reports progress on the file upload, parameter 1 is the uploaded
|
||||
// file size and parameter 2 is the total file size
|
||||
//
|
||||
// on_success is called when the upload is done, the only parameter is the file
|
||||
// ID
|
||||
//
|
||||
// on_error is called when the upload has failed. The parameters are the error
|
||||
// code and an error message
|
||||
export const upload_file = (file, name, on_progress, on_success, on_error) => {
|
||||
// Check the file size limit. For free accounts it's 20 GB
|
||||
if (window.user.subscription.file_size_limit === 0) {
|
||||
window.user.subscription.file_size_limit = 20e9
|
||||
}
|
||||
|
||||
if (file.size > window.user.subscription.file_size_limit) {
|
||||
on_failure(
|
||||
"file_too_large",
|
||||
"This file is too large. Check out the Pro subscription to increase the file size limit"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("PUT", window.api_endpoint + "/file/" + encodeURIComponent(name), true);
|
||||
xhr.timeout = 86400000; // 24 hours, to account for slow connections
|
||||
|
||||
xhr.upload.addEventListener("progress", evt => {
|
||||
if (on_progress && evt.lengthComputable) {
|
||||
on_progress(evt.loaded, evt.total)
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
on_success(JSON.parse(xhr.response).id)
|
||||
} else if (xhr.status >= 400) {
|
||||
// Request failed
|
||||
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
|
||||
|
||||
let resp;
|
||||
if (xhr.status === 429) {
|
||||
resp = {
|
||||
value: "too_many_requests",
|
||||
message: "Too many requests. Please wait a few seconds",
|
||||
}
|
||||
} else {
|
||||
resp = JSON.parse(xhr.response)
|
||||
}
|
||||
|
||||
on_error(resp.value, resp.message)
|
||||
} else if (xhr.status === 0) {
|
||||
on_error("request_failed", "Your request did not arrive, check your network connection")
|
||||
} else {
|
||||
on_error(xhr.responseText, xhr.responseText)
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(file);
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ProgressBar from "util/ProgressBar.svelte";
|
||||
import { upload_file } from "util/upload_widget/UploadFunc.js";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
export let job = {
|
||||
file: null,
|
||||
name: "",
|
||||
id: "",
|
||||
status: "",
|
||||
}
|
||||
export let total = 0
|
||||
export let loaded = 0
|
||||
let error_code = ""
|
||||
let error_message = ""
|
||||
|
||||
export const start = () => {
|
||||
upload_file(
|
||||
job.file,
|
||||
job.name,
|
||||
(prog_loaded, prog_total) => {
|
||||
loaded = prog_loaded
|
||||
total = prog_total
|
||||
},
|
||||
async (id) => {
|
||||
console.log("finsished", id)
|
||||
job.status = "finished"
|
||||
job.id = id
|
||||
dispatch("finished")
|
||||
},
|
||||
(code, message) => {
|
||||
console.log("error", code, message)
|
||||
error_code = code
|
||||
error_message = message
|
||||
job.status = "error"
|
||||
dispatch("finished")
|
||||
},
|
||||
)
|
||||
|
||||
job.status = "uploading"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="upload_progress" class:error={job.status === "error"}>
|
||||
{job.name}<br/>
|
||||
{#if error_code !== ""}
|
||||
{error_message}<br/>
|
||||
{error_code}<br/>
|
||||
{/if}
|
||||
<ProgressBar total={total} used={loaded}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.upload_progress {
|
||||
display: block;
|
||||
padding: 2px 4px 1px 4px;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.error {
|
||||
background: var(--danger_color);
|
||||
color: var(--highlight_text_color);
|
||||
}
|
||||
</style>
|
@@ -1,183 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import DropUpload from "./DropUpload.svelte";
|
||||
import UploadProgress from "./UploadProgress.svelte";
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
|
||||
let file_input_field;
|
||||
let file_input_change = e => {
|
||||
// Start uploading the files async
|
||||
upload_files(e.target.files)
|
||||
|
||||
// This resets the file input field
|
||||
file_input_field.nodeValue = ""
|
||||
}
|
||||
export const pick_files = () => {
|
||||
file_input_field.click()
|
||||
}
|
||||
|
||||
export let drop_upload = false
|
||||
let visible = false
|
||||
let upload_queue = [];
|
||||
let task_id_counter = 0
|
||||
|
||||
export const upload_files = async (files) => {
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add files to the queue
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i].type === "" && files[i].size === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
upload_queue.push({
|
||||
task_id: task_id_counter,
|
||||
file: files[i],
|
||||
name: files[i].name,
|
||||
component: null,
|
||||
id: "",
|
||||
status: "queued",
|
||||
total_size: files[i].size,
|
||||
loaded_size: 0,
|
||||
})
|
||||
task_id_counter++
|
||||
}
|
||||
|
||||
// Reassign array and wait for tick to complete. After the tick is completed
|
||||
// each upload progress bar will have bound itself to its array item
|
||||
upload_queue = upload_queue
|
||||
visible = true
|
||||
await tick()
|
||||
|
||||
start_upload()
|
||||
}
|
||||
|
||||
let active_uploads = 0
|
||||
let state = "idle"
|
||||
|
||||
const start_upload = () => {
|
||||
for (let i = 0; i < upload_queue.length && active_uploads < 3; i++) {
|
||||
if (upload_queue[i]) {
|
||||
if (upload_queue[i].status === "queued") {
|
||||
active_uploads++
|
||||
upload_queue[i].component.start()
|
||||
upload_queue[i].status = "uploading"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (active_uploads === 0) {
|
||||
state = "finished"
|
||||
|
||||
let file_ids = []
|
||||
upload_queue.forEach(job => {
|
||||
if (job.status === "finished" && job.id !== "") {
|
||||
file_ids.push(job.id)
|
||||
}
|
||||
})
|
||||
|
||||
dispatch("uploads_finished", file_ids)
|
||||
|
||||
upload_queue = []
|
||||
visible = false
|
||||
} else {
|
||||
state = "uploading"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const finish_upload = (e) => {
|
||||
active_uploads--
|
||||
upload_queue = upload_queue
|
||||
start_upload()
|
||||
}
|
||||
|
||||
const leave_confirmation = e => {
|
||||
if (state === "uploading") {
|
||||
e.preventDefault()
|
||||
e.returnValue = "If you close this page your files will stop uploading. Do you want to continue?"
|
||||
return e.returnValue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={leave_confirmation} />
|
||||
|
||||
<input
|
||||
bind:this={file_input_field}
|
||||
on:change={file_input_change}
|
||||
class="upload_input" type="file" name="file" multiple="multiple"
|
||||
/>
|
||||
|
||||
{#if visible}
|
||||
<div class="upload_widget" transition:fade={{duration: 200}}>
|
||||
<div class="header">
|
||||
{#if state === "idle"}
|
||||
Waiting for files
|
||||
{:else if state === "uploading"}
|
||||
Uploading files...
|
||||
{:else if state === "finished"}
|
||||
Done
|
||||
{/if}
|
||||
</div>
|
||||
<div class="body">
|
||||
{#each upload_queue as job}
|
||||
{#if job.status !== "finished"}
|
||||
<UploadProgress bind:this={job.component} job={job} on:finished={finish_upload}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if drop_upload}
|
||||
<DropUpload on:upload={e => upload_files(e.detail)}/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.upload_input {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.upload_widget {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
max-height: 50%;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
border-radius: 20px 20px 8px 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 1px 1px 8px var(--shadow_color);
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
background: var(--background_color);
|
||||
color: var(--background_text_color);
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
padding: 4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
background: var(--body_color);
|
||||
color: var(--body_text_color);
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { bookmark_del, bookmarks_store } from "lib/Bookmarks";
|
||||
import { fs_encode_path } from "lib/FilesystemAPI";
|
||||
import { highlight_current_page } from "lib/HighlightCurrentPage";
|
||||
</script>
|
||||
|
||||
{#each $bookmarks_store as bookmark}
|
||||
<div class="row">
|
||||
<a class="button" href="/d/{bookmark.id}" use:highlight_current_page>
|
||||
<a class="button" href="/d{fs_encode_path(bookmark.path)}" use:highlight_current_page>
|
||||
<i class="icon">{bookmark.icon}</i>
|
||||
<span class="hide_small">{bookmark.label}</span>
|
||||
</a>
|
||||
|
@@ -10,9 +10,14 @@ import { fs_get_node } from "lib/FilesystemAPI";
|
||||
import { css_from_path } from "filesystem/edit_window/Branding";
|
||||
import { loading_run, loading_store } from "lib/Loading";
|
||||
import Spinner from "util/Spinner.svelte";
|
||||
import { get_user, logout_user } from "lib/PixeldrainAPI";
|
||||
|
||||
onMount(async () => {
|
||||
await loading_run(async () => {
|
||||
const user = await get_user()
|
||||
if (user.username === undefined || user.username === "") {
|
||||
return
|
||||
}
|
||||
const root = await fs_get_node("/me")
|
||||
document.documentElement.style = css_from_path(root.path)
|
||||
})
|
||||
@@ -21,10 +26,20 @@ onMount(async () => {
|
||||
|
||||
<div class="nav_container">
|
||||
<nav class="nav">
|
||||
<a class="button" href="/" use:highlight_current_page>
|
||||
<i class="icon">home</i>
|
||||
<span class="hide_small">Home</span>
|
||||
</a>
|
||||
|
||||
{#if $user.username !== undefined && $user.username !== ""}
|
||||
<div class="separator hide_small"></div>
|
||||
|
||||
<div class="username hide_small">
|
||||
{$user.username}
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="stats_table hide_small">
|
||||
<div>Subscription</div>
|
||||
<div>{$user.subscription.name}</div>
|
||||
@@ -39,14 +54,28 @@ onMount(async () => {
|
||||
<div>Transfer used</div>
|
||||
<div>{formatDataVolume($user.monthly_transfer_used, 3)}</div>
|
||||
</div>
|
||||
<div class="separator hide_small"></div>
|
||||
{/if}
|
||||
|
||||
<a class="button" href="/" use:highlight_current_page>
|
||||
<i class="icon">home</i>
|
||||
<span class="hide_small">Home</span>
|
||||
<div class="separator hide_small"></div>
|
||||
|
||||
<a class="button" href="/d/me" use:highlight_current_page>
|
||||
<i class="icon">folder</i>
|
||||
<span class="hide_small">Filesystem</span>
|
||||
</a>
|
||||
{#if !$user.username}
|
||||
<a class="button" href="/user" use:highlight_current_page>
|
||||
<i class="icon">dashboard</i>
|
||||
<span class="hide_small">Dashboard</span>
|
||||
</a>
|
||||
{#if $user.is_admin}
|
||||
<a class="button" href="/admin" use:highlight_current_page>
|
||||
<i class="icon">admin_panel_settings</i>
|
||||
<span class="hide_small">Admin Panel</span>
|
||||
</a>
|
||||
{/if}
|
||||
<button class="button" on:click={()=> logout_user("/")}>
|
||||
<i class="icon">logout</i>
|
||||
<span class="hide_small">Log out</span>
|
||||
</button>
|
||||
{:else}
|
||||
<a class="button" href="/login" use:highlight_current_page>
|
||||
<i class="icon">login</i>
|
||||
<span class="hide_small">Login</span>
|
||||
@@ -55,22 +84,10 @@ onMount(async () => {
|
||||
<i class="icon">how_to_reg</i>
|
||||
<span class="hide_small">Register</span>
|
||||
</a>
|
||||
{:else}
|
||||
<a class="button" href="/user" use:highlight_current_page>
|
||||
<i class="icon">dashboard</i>
|
||||
<span class="hide_small">Dashboard</span>
|
||||
</a>
|
||||
<a class="button" href="/d/me" use:highlight_current_page>
|
||||
<i class="icon">folder</i>
|
||||
<span class="hide_small">Filesystem</span>
|
||||
</a>
|
||||
{#if $user.is_admin}
|
||||
<a class="button" href="/admin" use:highlight_current_page>
|
||||
<i class="icon">admin_panel_settings</i>
|
||||
<span class="hide_small">Admin Panel</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<a class="button" href="/speedtest" use:highlight_current_page>
|
||||
<i class="icon">speed</i>
|
||||
<span class="hide_small">Speedtest</span>
|
||||
@@ -80,7 +97,8 @@ onMount(async () => {
|
||||
<span class="hide_small">Themes</span>
|
||||
</a>
|
||||
|
||||
<div class="separator hide_small"></div>
|
||||
<div class="separator"></div>
|
||||
|
||||
<Bookmarks/>
|
||||
</nav>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user