web/download: support downloading and sharing raw files
This commit is contained in:
parent
b1f41cae41
commit
853bc26587
5 changed files with 120 additions and 50 deletions
|
@ -18,7 +18,13 @@
|
||||||
$: itemType = item.type ?? "photo";
|
$: itemType = item.type ?? "photo";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="picker-item" on:click={() => downloadFile(item.url)}>
|
<button
|
||||||
|
class="picker-item"
|
||||||
|
on:click={() =>
|
||||||
|
downloadFile({
|
||||||
|
url: item.url,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div class="picker-type">
|
<div class="picker-type">
|
||||||
{#if itemType === "video"}
|
{#if itemType === "video"}
|
||||||
<IconMovie />
|
<IconMovie />
|
||||||
|
@ -32,11 +38,9 @@
|
||||||
<img
|
<img
|
||||||
class="picker-image"
|
class="picker-image"
|
||||||
src={item.thumb ?? item.url}
|
src={item.thumb ?? item.url}
|
||||||
|
|
||||||
class:loading={!imageLoaded}
|
class:loading={!imageLoaded}
|
||||||
class:video-thumbnail={["video", "gif"].includes(itemType)}
|
class:video-thumbnail={["video", "gif"].includes(itemType)}
|
||||||
on:load={() => imageLoaded = true}
|
on:load={() => (imageLoaded = true)}
|
||||||
|
|
||||||
alt="{$t(`a11y.dialog.picker.item.${itemType}`)} {number}"
|
alt="{$t(`a11y.dialog.picker.item.${itemType}`)} {number}"
|
||||||
/>
|
/>
|
||||||
<Skeleton class="picker-image elevated" hidden={imageLoaded} />
|
<Skeleton class="picker-image elevated" hidden={imageLoaded} />
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
import { t } from "$lib/i18n/translations";
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
import { device } from "$lib/device";
|
import { device } from "$lib/device";
|
||||||
import { copyURL, openURL, shareURL } from "$lib/download";
|
import {
|
||||||
|
copyURL,
|
||||||
|
openURL,
|
||||||
|
shareURL,
|
||||||
|
openFile,
|
||||||
|
shareFile,
|
||||||
|
} from "$lib/download";
|
||||||
|
|
||||||
import DialogContainer from "$components/dialog/DialogContainer.svelte";
|
import DialogContainer from "$components/dialog/DialogContainer.svelte";
|
||||||
|
|
||||||
|
@ -17,9 +23,11 @@
|
||||||
|
|
||||||
import CopyIcon from "$components/misc/CopyIcon.svelte";
|
import CopyIcon from "$components/misc/CopyIcon.svelte";
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let url: string;
|
|
||||||
export let bodyText: string = "";
|
|
||||||
export let dismissable = true;
|
export let dismissable = true;
|
||||||
|
export let bodyText: string = "";
|
||||||
|
|
||||||
|
export let url: string = "";
|
||||||
|
export let file: File | undefined = undefined;
|
||||||
|
|
||||||
let close: () => void;
|
let close: () => void;
|
||||||
|
|
||||||
|
@ -45,13 +53,20 @@
|
||||||
{$t("dialog.saving.title")}
|
{$t("dialog.saving.title")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
{#if device.supports.directDownload}
|
{#if device.supports.directDownload}
|
||||||
<VerticalActionButton
|
<VerticalActionButton
|
||||||
id="save-download"
|
id="save-download"
|
||||||
fill
|
fill
|
||||||
elevated
|
elevated
|
||||||
click={() => openURL(url)}
|
click={() => {
|
||||||
|
if (file) {
|
||||||
|
return openFile(file);
|
||||||
|
} else if (url) {
|
||||||
|
return openURL(url);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconDownload />
|
<IconDownload />
|
||||||
{$t("button.download")}
|
{$t("button.download")}
|
||||||
|
@ -63,13 +78,20 @@
|
||||||
id="save-share"
|
id="save-share"
|
||||||
fill
|
fill
|
||||||
elevated
|
elevated
|
||||||
click={async () => await shareURL(url)}
|
click={async () => {
|
||||||
|
if (file) {
|
||||||
|
return await shareFile(file);
|
||||||
|
} else if (url) {
|
||||||
|
return await shareURL(url);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconShare2 />
|
<IconShare2 />
|
||||||
{$t("button.share")}
|
{$t("button.share")}
|
||||||
</VerticalActionButton>
|
</VerticalActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if !file}
|
||||||
<VerticalActionButton
|
<VerticalActionButton
|
||||||
id="save-copy"
|
id="save-copy"
|
||||||
fill
|
fill
|
||||||
|
@ -83,6 +105,7 @@
|
||||||
<CopyIcon check={copied} />
|
<CopyIcon check={copied} />
|
||||||
{$t("button.copy")}
|
{$t("button.copy")}
|
||||||
</VerticalActionButton>
|
</VerticalActionButton>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if device.is.iOS}
|
{#if device.is.iOS}
|
||||||
|
|
|
@ -30,29 +30,29 @@
|
||||||
type DownloadButtonState = "idle" | "think" | "check" | "done" | "error";
|
type DownloadButtonState = "idle" | "think" | "check" | "done" | "error";
|
||||||
|
|
||||||
const changeDownloadButton = (state: DownloadButtonState) => {
|
const changeDownloadButton = (state: DownloadButtonState) => {
|
||||||
disabled = state !== 'idle';
|
disabled = state !== "idle";
|
||||||
|
|
||||||
buttonText = ({
|
buttonText = {
|
||||||
idle: ">>",
|
idle: ">>",
|
||||||
think: "...",
|
think: "...",
|
||||||
check: "..?",
|
check: "..?",
|
||||||
done: ">>>",
|
done: ">>>",
|
||||||
error: "!!"
|
error: "!!",
|
||||||
})[state];
|
}[state];
|
||||||
|
|
||||||
buttonAltText = $t(
|
buttonAltText = $t(
|
||||||
({
|
{
|
||||||
idle: "a11y.save.download",
|
idle: "a11y.save.download",
|
||||||
think: "a11y.save.download.think",
|
think: "a11y.save.download.think",
|
||||||
check: "a11y.save.download.check",
|
check: "a11y.save.download.check",
|
||||||
done: "a11y.save.download.done",
|
done: "a11y.save.download.done",
|
||||||
error: "a11y.save.download.error",
|
error: "a11y.save.download.error",
|
||||||
})[state]
|
}[state]
|
||||||
);
|
);
|
||||||
|
|
||||||
// states that don't wait for anything, and thus can
|
// states that don't wait for anything, and thus can
|
||||||
// transition back to idle after some period of time.
|
// transition back to idle after some period of time.
|
||||||
const final: DownloadButtonState[] = ['done', 'error'];
|
const final: DownloadButtonState[] = ["done", "error"];
|
||||||
if (final.includes(state)) {
|
if (final.includes(state)) {
|
||||||
setTimeout(() => changeDownloadButton("idle"), 1500);
|
setTimeout(() => changeDownloadButton("idle"), 1500);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,9 @@
|
||||||
if (response.status === "redirect") {
|
if (response.status === "redirect") {
|
||||||
changeDownloadButton("done");
|
changeDownloadButton("done");
|
||||||
|
|
||||||
return downloadFile(response.url);
|
return downloadFile({
|
||||||
|
url: response.url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === "tunnel") {
|
if (response.status === "tunnel") {
|
||||||
|
@ -95,7 +97,9 @@
|
||||||
if (probeResult === 200) {
|
if (probeResult === 200) {
|
||||||
changeDownloadButton("done");
|
changeDownloadButton("done");
|
||||||
|
|
||||||
return downloadFile(response.url);
|
return downloadFile({
|
||||||
|
url: response.url,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
changeDownloadButton("error");
|
changeDownloadButton("error");
|
||||||
|
|
||||||
|
@ -122,7 +126,9 @@
|
||||||
text: $t("button.download.audio"),
|
text: $t("button.download.audio"),
|
||||||
main: false,
|
main: false,
|
||||||
action: () => {
|
action: () => {
|
||||||
downloadFile(pickerAudio);
|
downloadFile({
|
||||||
|
url: pickerAudio,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,23 +8,47 @@ import { t } from "$lib/i18n/translations";
|
||||||
import { createDialog } from "$lib/dialogs";
|
import { createDialog } from "$lib/dialogs";
|
||||||
import type { DialogInfo } from "$lib/types/dialog";
|
import type { DialogInfo } from "$lib/types/dialog";
|
||||||
|
|
||||||
export const openSavingDialog = (url: string, body: string | void) => {
|
const openSavingDialog = ({ url, file, body }: { url?: string, file?: File, body?: string }) => {
|
||||||
const dialogData: DialogInfo = {
|
const dialogData: DialogInfo = {
|
||||||
type: "saving",
|
type: "saving",
|
||||||
id: "saving",
|
id: "saving",
|
||||||
url
|
file,
|
||||||
|
url,
|
||||||
}
|
}
|
||||||
if (body) dialogData.bodyText = body;
|
if (body) dialogData.bodyText = body;
|
||||||
|
|
||||||
createDialog(dialogData)
|
createDialog(dialogData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const openFile = async (file: File) => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shareFile = async (file: File) => {
|
||||||
|
return await navigator?.share({
|
||||||
|
files: [
|
||||||
|
new File([file], file.name, {
|
||||||
|
type: file.type,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const openURL = (url: string) => {
|
export const openURL = (url: string) => {
|
||||||
const open = window.open(url, "_blank");
|
const open = window.open(url, "_blank");
|
||||||
|
|
||||||
/* if new tab got blocked by user agent, show a saving dialog */
|
/* if new tab got blocked by user agent, show a saving dialog */
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return openSavingDialog(url, get(t)("dialog.saving.blocked"));
|
return openSavingDialog({
|
||||||
|
url,
|
||||||
|
body: get(t)("dialog.saving.blocked")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +60,9 @@ export const copyURL = async (url: string) => {
|
||||||
return await navigator?.clipboard?.writeText(url);
|
return await navigator?.clipboard?.writeText(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadFile = (url: string) => {
|
export const downloadFile = ({ url, file }: { url?: string, file?: File }) => {
|
||||||
|
if (!url && !file) throw new Error("attempted to download void");
|
||||||
|
|
||||||
const pref = get(settings).save.savingMethod;
|
const pref = get(settings).save.savingMethod;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -50,18 +76,28 @@ export const downloadFile = (url: string) => {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (pref === "ask" || !navigator.userActivation.isActive) {
|
if (pref === "ask" || !navigator.userActivation.isActive) {
|
||||||
return openSavingDialog(url);
|
return openSavingDialog({ url, file });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (file) {
|
||||||
|
if (pref === "share" && device.supports.share) {
|
||||||
|
return shareFile(file);
|
||||||
|
} else if (pref === "download" && device.supports.directDownload) {
|
||||||
|
return openFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
if (pref === "share" && device.supports.share) {
|
if (pref === "share" && device.supports.share) {
|
||||||
return shareURL(url);
|
return shareURL(url);
|
||||||
} else if (pref === "download" && device.supports.directDownload) {
|
} else if (pref === "download" && device.supports.directDownload) {
|
||||||
return openURL(url);
|
return openURL(url);
|
||||||
} else if (pref === "copy") {
|
} else if (pref === "copy" && !file) {
|
||||||
return copyURL(url);
|
return copyURL(url);
|
||||||
}
|
}
|
||||||
} catch {}
|
}
|
||||||
|
} catch { /* catch & ignore */ }
|
||||||
|
|
||||||
return openSavingDialog(url);
|
return openSavingDialog({ url, file });
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,9 @@ type PickerDialog = Dialog & {
|
||||||
|
|
||||||
type SavingDialog = Dialog & {
|
type SavingDialog = Dialog & {
|
||||||
type: "saving",
|
type: "saving",
|
||||||
url: string,
|
|
||||||
bodyText?: string,
|
bodyText?: string,
|
||||||
|
url?: string,
|
||||||
|
file?: File,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DialogInfo = SmallDialog | PickerDialog | SavingDialog;
|
export type DialogInfo = SmallDialog | PickerDialog | SavingDialog;
|
||||||
|
|
Loading…
Reference in a new issue