Add new sharing dialog to filesystem

This commit is contained in:
2025-06-05 21:29:07 +02:00
parent 9c8c9bb5f5
commit dd1a038bca
7 changed files with 232 additions and 68 deletions

View File

@@ -101,8 +101,10 @@ const download = (href, file_name) => {
let a = document.createElement("a") let a = document.createElement("a")
a.href = href a.href = href
a.download = file_name a.download = file_name
a.click()
a.remove() // You can't call .click() on an element that is not in the DOM. But
// emitting a click event works
a.dispatchEvent(new MouseEvent("click"))
} }
</script> </script>

View File

@@ -2,7 +2,7 @@
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting"; import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import { fs_path_url, fs_share_path, fs_share_url, fs_timeseries, type FSNode } from "./FilesystemAPI"; import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "./FilesystemAPI";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util.svelte";
import { tick } from "svelte"; import { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
@@ -21,7 +21,7 @@ const visibility_change = visible => {
$: direct_url = $nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : "" $: direct_url = $nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : ""
$: share_url = fs_share_url($nav.path) $: share_url = fs_share_url($nav.path)
$: direct_share_url = $nav.base.path ? window.location.origin+fs_path_url(fs_share_path($nav.path)) : "" $: direct_share_url = fs_share_hotlink_url($nav.path)
let chart let chart
let chart_timespan = 0 let chart_timespan = 0

View File

@@ -339,6 +339,15 @@ export const fs_share_url = (path: FSNode[]): string => {
return share_path return share_path
} }
export const fs_share_hotlink_url = (path: FSNode[]): string => {
let share_path = fs_share_path(path)
if (share_path !== "") {
share_path = window.location.protocol + "//" + window.location.host + fs_path_url(share_path)
}
return share_path
}
export const fs_share_path = (path: FSNode[]): string => { export const fs_share_path = (path: FSNode[]): string => {
let share_url = "" let share_url = ""
let bucket_idx = -1 let bucket_idx = -1
@@ -373,6 +382,7 @@ export const fs_download = (node: FSNode) => {
a.download = node.name + ".zip" a.download = node.name + ".zip"
} }
a.click() // You can't call .click() on an element that is not in the DOM. But
a.remove() // emitting a click event works
a.dispatchEvent(new MouseEvent("click"))
} }

View File

@@ -4,9 +4,10 @@ import Button from "layout/Button.svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import { user } from "lib/UserStore"; import { user } from "lib/UserStore";
import Dialog from "layout/Dialog.svelte";
let button: HTMLButtonElement let button: HTMLButtonElement
let dialog: HTMLDialogElement let dialog: Dialog
export let no_login_label = "Pixeldrain" export let no_login_label = "Pixeldrain"
@@ -17,35 +18,7 @@ export let style = ""
export let embedded = false export let embedded = false
$: target = embedded ? "_blank" : "_self" $: target = embedded ? "_blank" : "_self"
const open = () => { const open = () => dialog.open(button.getBoundingClientRect())
// Show the window so we can get the location
dialog.showModal()
const edge_offset = 5
// Get the egdes of the screen, so the window does not spawn off-screen
const window_rect = dialog.getBoundingClientRect()
const max_left = window.innerWidth - window_rect.width - edge_offset
const max_top = window.innerHeight - window_rect.height - edge_offset
// Get the location of the button
const button_rect = button.getBoundingClientRect()
// Prevent the window from being glued to the edges
const min_left = Math.max(button_rect.left, edge_offset)
const min_top = Math.max(button_rect.bottom, edge_offset)
// Place the window
dialog.style.left = Math.round(Math.min(min_left, max_left)) + "px"
dialog.style.top = Math.round(Math.min(min_top, max_top)) + "px"
}
// Close the dialog when the user clicks the background
const click = (e: MouseEvent) => {
if (e.target === dialog) {
dialog.close()
}
}
</script> </script>
<div class="wrapper"> <div class="wrapper">
@@ -59,9 +32,7 @@ const click = (e: MouseEvent) => {
</button> </button>
</div> </div>
<!-- svelte-ignore a11y-click-events-have-key-events --> <Dialog bind:this={dialog}>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog bind:this={dialog} on:click={click}>
<div class="menu"> <div class="menu">
{#if $user.username !== undefined && $user.username !== ""} {#if $user.username !== undefined && $user.username !== ""}
@@ -114,7 +85,7 @@ const click = (e: MouseEvent) => {
<Button link_href="/register" link_target={target} icon="person" label="Register"/> <Button link_href="/register" link_target={target} icon="person" label="Register"/>
{/if} {/if}
</div> </div>
</dialog> </Dialog>
<style> <style>
.wrapper { .wrapper {
@@ -130,16 +101,6 @@ const click = (e: MouseEvent) => {
.button_username { .button_username {
margin: 0 4px; margin: 0 4px;
} }
dialog {
background-color: var(--card_color);
color: var(--body_text_color);
border-radius: 8px;
border: none;
padding: 4px;
margin: 0;
box-shadow: 2px 2px 10px var(--shadow_color);
}
.menu { .menu {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -0,0 +1,152 @@
<script lang="ts">
import type { FSNavigator } from "./FSNavigator";
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, type FSNode } from "./FilesystemAPI";
import { copy_text } from "util/Util.svelte";
import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte";
export let nav: FSNavigator
let path: FSNode[]
let base: FSNode
let toast = ""
let share_url = ""
let direct_share_url = ""
let is_parent = false
let parent_node: FSNode
let dialog: Dialog
export const open = async (e: MouseEvent, p: FSNode[]) => {
path = p
base = path[path.length-1]
share_url = fs_share_url(path)
if (share_url === "") {
// File is not public, make public
await make_public()
}
await share()
if (e.target instanceof HTMLButtonElement) {
dialog.open(e.target.getBoundingClientRect())
} else {
dialog.open((e.target as HTMLElement).parentElement.getBoundingClientRect())
}
}
const make_public = async () => {
base = await fs_update(base.path, {shared: true})
await nav.reload()
// Insert the new FSNode into the path
path[path.length-1] = base
}
const share = async () => {
// If the base node does not have a public ID then the file is shared
// through a parent node. In that case we ask the user if it was their
// intention to share the parent directory
is_parent = base.id === undefined
if (is_parent) {
// Walk path backwards looking for the last public ID
for (let i = path.length - 1; i >= 0; i--) {
if (path[i].id !== undefined && path[i].id !== "me") {
parent_node = path[i]
break
}
}
}
share_url = fs_share_url(path)
direct_share_url = fs_share_hotlink_url(path)
try {
await navigator.share({
title: base.name,
text: "I would like to share '" + base.name + "' with you",
url: share_url,
})
} catch(_) {
if (copy_text(share_url)) {
toast = "Link copied to clipboard"
setTimeout(() => {toast = ""}, 10000)
} else {
alert("Could not copy text")
}
}
}
</script>
<Dialog bind:this={dialog}>
<div class="dialog_inner">
{#if toast !== "" && !is_parent}
<div class="highlight_green">{toast}</div>
<div class="separator"></div>
{/if}
{#if is_parent}
<div>
By sharing this link you also share the parent directory:
<img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/>
{parent_node.name}
<br/>
<button on:click={async e => {await make_public(); await share()}}>
Click here to only share
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
{base.name}
</button>
</div>
<div class="separator"></div>
{/if}
<div>Sharing link</div>
<div class="link_copy">
<div class="button_container">
<CopyButton text={share_url}>Copy</CopyButton>
</div>
<a href="{share_url}">{share_url}</a>
</div>
<div class="separator"></div>
<div>Direct sharing link (hotlink)</div>
<div class="link_copy">
<div class="button_container">
<CopyButton text={direct_share_url}>Copy</CopyButton>
</div>
<a href="{direct_share_url}">{direct_share_url}</a>
</div>
</div>
</Dialog>
<style>
.dialog_inner {
display: flex;
flex-direction: column;
text-align: center;
}
.link_copy {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
}
.link_copy > .button_container {
flex: 0 0 auto;
}
.link_copy > a {
flex: 1 1 auto;
text-overflow: ellipsis;
white-space: nowrap;
overflow-x: hidden;
}
.node_icon {
width: 1.5em;
height: 1.5em;
vertical-align: middle;
}
.separator {
height: 1px;
border: none;
background-color: var(--separator);
margin: 0.5em;
}
</style>

View File

@@ -6,6 +6,7 @@ import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte"; import EditWindow from "./edit_window/EditWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte"; import FilePreview from "./viewers/FilePreview.svelte";
import { fs_share_url } from "./FilesystemAPI"; import { fs_share_url } from "./FilesystemAPI";
import ShareDialog from "./ShareDialog.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -15,6 +16,7 @@ export let edit_window: EditWindow
export let edit_visible = false export let edit_visible = false
export let file_viewer: HTMLDivElement export let file_viewer: HTMLDivElement
export let file_preview: FilePreview export let file_preview: FilePreview
let share_dialog: ShareDialog
$: share_url = fs_share_url($nav.path) $: share_url = fs_share_url($nav.path)
let link_copied = false let link_copied = false
@@ -28,22 +30,6 @@ export const copy_link = () => {
link_copied = true link_copied = true
setTimeout(() => {link_copied = false}, 60000) setTimeout(() => {link_copied = false}, 60000)
} }
let share = async () => {
if (share_url === "" || navigator.share === undefined) {
edit_window.edit(nav.base, true, "share")
return
}
if (navigator.share) {
await navigator.share({
title: nav.base.name,
text: "I would like to share '" + nav.base.name + "' with you",
url: share_url
})
} else {
alert("Navigator does not support sharing, use copy link button to copy the link instead")
}
}
let fullscreen = false let fullscreen = false
export const toggle_fullscreen = () => { export const toggle_fullscreen = () => {
@@ -107,13 +93,13 @@ let expand = (e: Event) => {
{#if share_url !== ""} {#if share_url !== ""}
<button on:click={copy_link} class:button_highlight={link_copied}> <button on:click={copy_link} class:button_highlight={link_copied}>
<i class="icon">content_copy</i> <i class="icon">content_copy</i>
<span><u>C</u>opy Link</span> <span><u>C</u>opy link</span>
</button> </button>
{/if} {/if}
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)--> <!-- 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 $nav.base.id !== "me" && (navigator.share !== undefined || $nav.permissions.write === true)}
<button on:click={share}> <button on:click={(e) => share_dialog.open(e, nav.path)}>
<i class="icon">share</i> <i class="icon">share</i>
<span>Share</span> <span>Share</span>
</button> </button>
@@ -148,6 +134,8 @@ let expand = (e: Event) => {
</div> </div>
</div> </div>
<ShareDialog nav={nav} bind:this={share_dialog}/>
<style> <style>
.toolbar { .toolbar {
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -0,0 +1,51 @@
<script lang="ts">
let dialog: HTMLDialogElement
export const open = (location: DOMRect) => {
// Show the window so we can get the location
dialog.showModal()
const edge_offset = 5
// Get the egdes of the screen, so the window does not spawn off-screen
const window_rect = dialog.getBoundingClientRect()
const max_left = window.innerWidth - window_rect.width - edge_offset
const max_top = window.innerHeight - window_rect.height - edge_offset
// Prevent the window from being glued to the edges
const min_left = Math.max(location.left, edge_offset)
const min_top = Math.max(location.bottom, edge_offset)
// Place the window
dialog.style.left = Math.round(Math.min(min_left, max_left)) + "px"
dialog.style.top = Math.round(Math.min(min_top, max_top)) + "px"
}
// Attach a click handler so that the dialog is closed when the user clicks
// outside the dialog. The way this check works is that there is usually an
// element inside the dialog with all the content. When the click target is
// the dialog itself then the click was on the dialog background
const click = (e: MouseEvent) => {
if (e.target === dialog) {
dialog.close()
}
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog bind:this={dialog} on:click={click}>
<slot></slot>
</dialog>
<style>
dialog {
background-color: var(--card_color);
color: var(--body_text_color);
border-radius: 8px;
border: none;
padding: 4px;
margin: 0;
box-shadow: 2px 2px 10px -4px #000000;
}
</style>