Files
warren/frontend/pages/share.vue
2025-09-06 01:21:13 +02:00

304 lines
8.7 KiB
Vue

<script lang="ts" setup>
import { fetchShareFile, getShare, listShareFiles } 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() {
loadFiles();
}
async function loadFiles() {
if (loading.value || share == null || warrenStore.current == null) {
return;
}
if (share.file.fileType !== 'directory') {
return;
}
loading.value = true;
const result = await listShareFiles(
share.data.id,
warrenStore.current.path,
password.value.length > 0 ? password.value : null
);
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;
warrenStore.setCurrentWarrenEntries(result.files, result.parent);
}
loading.value = false;
}
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
event.stopPropagation();
if (warrenStore.current == null) {
return;
}
if (warrenStore.handleSelectionClick(entry, event)) {
return;
}
const entryPath = joinPaths(warrenStore.current.path, entry.name);
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="[
'h-[min(98vh,600px)] w-full max-w-screen-xl rounded-lg border transition-all',
passwordValid ? 'max-w-screen-xl' : 'max-w-lg',
]"
>
<div
class="flex flex-row items-center justify-between gap-4 px-6 pt-6"
>
<div class="flex w-full flex-row">
<div class="flex grow flex-col gap-1.5">
<h3 class="leading-none font-semibold">Share</h3>
<p class="text-muted-foreground text-sm">
Created
{{
$dayjs(share.data.createdAt).format(
'MMM D, YYYY HH:mm'
)
}}
</p>
</div>
<div class="flex flex-row items-center justify-end gap-4">
<p>{{ share.file.name }}</p>
<DirectoryEntryIcon
:entry="{ ...share.file, name: '/' }"
/>
</div>
</div>
<div class="flex flex-row items-end">
<Button
:class="
share.file.fileType !== 'file' &&
entries == null &&
'hidden'
"
size="icon"
variant="outline"
@click="onDowloadClicked"
><Icon name="lucide:download"
/></Button>
</div>
</div>
<div class="flex w-full flex-col p-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>