Show copyable share links after batch upload completes.

Add share results panel gated by showShareLinks, build URLs from queue entry secrets, and align file name handling with the metadata inspector.
This commit is contained in:
2026-10-25 22:00:16 +01:00
parent f305ce7a39
commit f5be0a0de3
4 changed files with 173 additions and 3 deletions
@@ -0,0 +1,77 @@
<template>
<label class="flex flex-col gap-1.5 text-xs">
<span class="font-medium text-muted-foreground">{{ label }}</span>
<div class="flex gap-2">
<input
type="text"
readonly
class="min-w-0 flex-1 rounded-md border border-input bg-muted/40 px-3 py-2 font-mono text-xs text-foreground"
:value="value"
/>
<button
type="button"
class="shrink-0 rounded-md border border-border px-3 py-2 text-xs font-medium transition-colors hover:bg-muted/60"
@click="onCopyClick"
>
{{ copied ? 'Copied' : 'Copy' }}
</button>
</div>
</label>
</template>
<script setup lang="ts">
import { onUnmounted, ref, watch } from 'vue';
const COPIED_RESET_MS = 1000;
const props = defineProps({
label: {
type: String,
required: true
},
value: {
type: String,
required: true
}
});
const copied = ref(false);
let copiedResetTimeout: ReturnType<typeof setTimeout> | undefined;
// component stay mounted when user switching between images so we need to reset the copied state when the value changes
watch(
() => props.value,
() => resetCopied()
);
onUnmounted(() => clearCopiedResetTimeout());
const clearCopiedResetTimeout = (): void => {
if (copiedResetTimeout !== undefined) {
clearTimeout(copiedResetTimeout);
copiedResetTimeout = undefined;
}
};
const resetCopied = (): void => {
clearCopiedResetTimeout();
copied.value = false;
};
const onCopyClick = async (): Promise<void> => {
try {
await navigator.clipboard.writeText(props.value);
copied.value = true;
clearCopiedResetTimeout();
copiedResetTimeout = setTimeout(() => {
copied.value = false;
}, COPIED_RESET_MS);
} catch (error) {
console.error('[ShareLinkField] copy failed', error);
}
};
</script>
@@ -0,0 +1,78 @@
<template>
<section class="flex min-h-48 flex-col rounded-lg border border-border bg-card">
<header class="border-b border-border px-4 py-3">
<h2 class="text-sm font-medium text-card-foreground">Share links</h2>
<p v-if="fileName" class="mt-0.5 truncate text-xs text-muted-foreground">{{ fileName }}</p>
</header>
<div class="flex flex-1 flex-col gap-3 p-4">
<p v-if="!fileName" class="text-sm text-muted-foreground">Select an image to view share links.</p>
<template v-else-if="shareUrls">
<InlineTabList v-model="activeNetworkTab" :options="shareNetworkTabOptions" />
<div class="flex flex-col gap-3">
<ShareLinkField label="Viewer link" :value="shareUrls.viewer[activeNetworkTab]" />
<ShareLinkField label="Delete link" :value="shareUrls.delete[activeNetworkTab]" />
<ShareLinkField
v-if="shareUrls.mode === 'hotlink'"
label="Direct image"
:value="shareUrls.direct[activeNetworkTab]"
/>
<template v-if="activeNetworkTab === 'clearnet'">
<template v-if="shareUrls.mode === 'hotlink'">
<ShareLinkField label="BBCode" :value="shareUrls.bbcode" />
<ShareLinkField label="Markdown image" :value="shareUrls.markdownImage" />
<ShareLinkField label="HTML image" :value="shareUrls.htmlImage" />
</template>
<template v-else>
<ShareLinkField label="Markdown link" :value="shareUrls.markdownLink" />
<ShareLinkField label="HTML link" :value="shareUrls.htmlLink" />
</template>
</template>
</div>
</template>
<p v-else class="text-sm text-destructive">No share links are available.</p>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref, watch, type PropType } from 'vue';
import type { ShareNetworkTab } from '@/types/share/ShareNetworkTab';
import type { InlineTabOption } from '@/types/ui/InlineTabOption';
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
import { buildUploadedShareUrls } from '@/utils/share/buildUploadedShareUrls';
const shareNetworkTabOptions: InlineTabOption<ShareNetworkTab>[] = [
{ value: 'clearnet', label: 'Clearnet' },
{ value: 'onion', label: 'Onion' }
];
const props = defineProps({
fileName: {
type: String as PropType<string | null>,
default: null
},
upload: {
type: Object as PropType<UploadQueueEntry>,
required: true
}
});
const activeNetworkTab = ref<ShareNetworkTab>('clearnet');
const shareUrls = computed(() =>
props.fileName ? buildUploadedShareUrls(props.upload, props.fileName) : null
);
watch(
() => props.fileName,
() => {
activeNetworkTab.value = 'clearnet';
}
);
</script>