351 lines
10 KiB
Vue
351 lines
10 KiB
Vue
<script lang="ts" setup>
|
|
import byteSize from 'byte-size';
|
|
import { toast } from 'vue-sonner';
|
|
import {
|
|
fetchShareFile,
|
|
getShare,
|
|
listShareFiles,
|
|
verifySharePassword,
|
|
} from '~/lib/api/shares';
|
|
import type { DirectoryEntry } from '~/shared/types';
|
|
import type { Share } from '~/shared/types/shares';
|
|
import { useImageViewer, useTextEditor } from '~/stores/viewers';
|
|
|
|
definePageMeta({
|
|
layout: 'share',
|
|
});
|
|
|
|
const warrenStore = useWarrenStore();
|
|
const route = useRoute();
|
|
|
|
const imageViewer = useImageViewer();
|
|
const textEditor = useTextEditor();
|
|
|
|
const entries = computed(() =>
|
|
warrenStore.current != null && warrenStore.current.dir != null
|
|
? warrenStore.current.dir.entries
|
|
: null
|
|
);
|
|
const parent = computed(() =>
|
|
warrenStore.current != null && warrenStore.current.dir != null
|
|
? warrenStore.current.dir.parent
|
|
: null
|
|
);
|
|
|
|
const share = await getShareFromQuery();
|
|
const password = ref<string>('');
|
|
const loading = ref<boolean>(false);
|
|
const passwordValid = ref<boolean>(
|
|
share == null ? false : !share.data.password
|
|
);
|
|
|
|
if (share != null) {
|
|
warrenStore.setCurrentWarren(share.data.warrenId, '/');
|
|
|
|
if (!share.data.password) {
|
|
await loadFiles();
|
|
}
|
|
}
|
|
|
|
async function getShareFromQuery(): Promise<{
|
|
data: Share;
|
|
file: DirectoryEntry;
|
|
} | null> {
|
|
const shareId = route.query.id;
|
|
|
|
if (shareId == null || typeof shareId !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const result = await getShare(shareId);
|
|
|
|
if (!result.success) {
|
|
return null;
|
|
}
|
|
|
|
return { data: result.share, file: result.file };
|
|
}
|
|
|
|
async function submitPassword() {
|
|
if (share == null || loading.value) {
|
|
return;
|
|
}
|
|
|
|
if (!passwordValid.value) {
|
|
loading.value = true;
|
|
const result = await verifySharePassword(share.data.id, password.value);
|
|
loading.value = false;
|
|
|
|
if (result.success) {
|
|
passwordValid.value = true;
|
|
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
|
|
|
|
if (share.data.expiresAt != null) {
|
|
const dayjs = useDayjs();
|
|
|
|
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
|
|
cookie += `Max-Age=${diff};`;
|
|
}
|
|
|
|
document.cookie = cookie;
|
|
} else {
|
|
toast.error('Share', {
|
|
id: 'SHARE_PASSWORD_TOAST',
|
|
description: 'Invalid password',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (share.file.fileType === 'directory') {
|
|
loadFiles();
|
|
}
|
|
}
|
|
|
|
async function loadFiles() {
|
|
if (loading.value || share == null || warrenStore.current == null) {
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
const result = await listShareFiles(
|
|
share.data.id,
|
|
warrenStore.current.path,
|
|
password.value.length > 0 ? password.value : null
|
|
);
|
|
|
|
if (result.success) {
|
|
warrenStore.setCurrentWarrenEntries(result.files, result.parent);
|
|
}
|
|
|
|
loading.value = false;
|
|
}
|
|
|
|
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
|
event.stopPropagation();
|
|
|
|
if (
|
|
warrenStore.current == null ||
|
|
share == null ||
|
|
(share.data.password && !passwordValid.value)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (warrenStore.handleSelectionClick(entry, event)) {
|
|
return;
|
|
}
|
|
|
|
const entryPath =
|
|
entry !== share.file
|
|
? joinPaths(warrenStore.current.path, entry.name)
|
|
: warrenStore.current.path;
|
|
|
|
if (entry.fileType === 'directory') {
|
|
warrenStore.setCurrentWarrenPath(entryPath);
|
|
await loadFiles();
|
|
return;
|
|
}
|
|
|
|
if (entry.mimeType == null) {
|
|
return;
|
|
}
|
|
|
|
if (entry.mimeType.startsWith('image/')) {
|
|
const result = await fetchShareFile(
|
|
share!.data.id,
|
|
entryPath,
|
|
password.value.length > 0 ? password.value : null
|
|
);
|
|
|
|
if (result.success) {
|
|
const url = URL.createObjectURL(result.data);
|
|
imageViewer.open(url);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (entry.mimeType.startsWith('text/')) {
|
|
const result = await fetchShareFile(
|
|
share!.data.id,
|
|
entryPath,
|
|
password.value.length > 0 ? password.value : null
|
|
);
|
|
|
|
if (result.success) {
|
|
textEditor.open(
|
|
warrenStore.current.warrenId,
|
|
warrenStore.current.path,
|
|
entry,
|
|
result.data
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function onBack() {
|
|
if (warrenStore.backCurrentPath()) {
|
|
await loadFiles();
|
|
}
|
|
}
|
|
|
|
function onDowloadClicked() {
|
|
if (share == null) {
|
|
return;
|
|
}
|
|
|
|
const downloadName =
|
|
share.file.fileType === 'directory'
|
|
? `${share.file.name}.zip`
|
|
: share.file.name;
|
|
const downloadApiUrl = getApiUrl(
|
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=/`
|
|
);
|
|
|
|
downloadFile(downloadName, downloadApiUrl);
|
|
}
|
|
|
|
function onEntryDownload(entry: DirectoryEntry) {
|
|
if (share == null || warrenStore.current == null) {
|
|
return;
|
|
}
|
|
|
|
let downloadName: string;
|
|
let downloadApiUrl: string;
|
|
|
|
const selectionSize = warrenStore.selection.size;
|
|
|
|
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
|
|
downloadName =
|
|
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
|
|
downloadApiUrl = getApiUrl(
|
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
|
|
);
|
|
} else {
|
|
downloadName = 'download.zip';
|
|
const paths = Array.from(warrenStore.selection.values()).map((entry) =>
|
|
joinPaths(warrenStore.current!.path, entry.name)
|
|
);
|
|
|
|
downloadApiUrl = getApiUrl(
|
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=${paths.join(':')}`
|
|
);
|
|
}
|
|
|
|
downloadFile(downloadName, downloadApiUrl);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="share != null"
|
|
class="flex h-full w-full items-center justify-center px-2"
|
|
>
|
|
<div
|
|
:class="[
|
|
'w-full rounded-lg border transition-all',
|
|
passwordValid && share.file.fileType === 'directory'
|
|
? 'h-[min(98vh,600px)] max-w-screen-xl'
|
|
: 'max-w-2xl',
|
|
]"
|
|
>
|
|
<div class="flex flex-row items-center justify-between gap-4 p-6">
|
|
<button
|
|
:disabled="share.data.password && !passwordValid"
|
|
:class="[
|
|
'flex min-w-0 grow flex-row items-center gap-2 text-left',
|
|
(!share.data.password || passwordValid) &&
|
|
'cursor-pointer',
|
|
]"
|
|
@click="(e) => onEntryClicked(share!.file, e)"
|
|
>
|
|
<DirectoryEntryIcon :entry="share.file" />
|
|
|
|
<div class="flex flex-col overflow-hidden">
|
|
<h3
|
|
:title="share.file.name"
|
|
class="truncate leading-none font-semibold"
|
|
>
|
|
{{ share.file.name }}
|
|
</h3>
|
|
<p class="text-muted-foreground text-sm text-nowrap">
|
|
{{ byteSize(share.file.size) }}
|
|
</p>
|
|
<p class="text-muted-foreground text-sm text-nowrap">
|
|
Created
|
|
{{
|
|
$dayjs(share.data.createdAt).format(
|
|
'MMM D, YYYY HH:mm'
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
|
|
<div class="flex flex-row items-end">
|
|
<Button
|
|
:class="
|
|
share.data.password && !passwordValid && 'hidden'
|
|
"
|
|
size="icon"
|
|
variant="outline"
|
|
@click="onDowloadClicked"
|
|
><Icon name="lucide:download"
|
|
/></Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="
|
|
share.file.fileType === 'directory' ||
|
|
(share.data.password && !passwordValid)
|
|
"
|
|
class="flex w-full flex-col px-6 pb-6"
|
|
>
|
|
<DirectoryList
|
|
v-if="entries != null"
|
|
:entries
|
|
:parent
|
|
:disable-entries="loading"
|
|
:entries-draggable="false"
|
|
@entry-click="onEntryClicked"
|
|
@entry-download="onEntryDownload"
|
|
@back="onBack"
|
|
/>
|
|
<div
|
|
v-else-if="share.data.password"
|
|
class="flex h-full flex-col justify-between gap-2"
|
|
>
|
|
<div class="flex flex-col gap-1">
|
|
<Label for="password">Password</Label>
|
|
<Input
|
|
id="password"
|
|
v-model="password"
|
|
type="password"
|
|
name="password"
|
|
autocomplete="off"
|
|
data-1p-ignore
|
|
data-protonpass-ignore
|
|
data-bwignore
|
|
/>
|
|
</div>
|
|
<div class="flex flex-row-reverse items-end">
|
|
<Button
|
|
:disabled="loading || password.length < 1"
|
|
@click="submitPassword"
|
|
>Enter</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="bg-accent/20 rounded-md p-4">
|
|
<p class="text-destructive-foreground">Failed to get share</p>
|
|
</div>
|
|
</template>
|