Slave/upload ui #8

Merged
nobswebdev merged 15 commits from slave/upload-ui into master 2026-10-25 22:43:57 +00:00
4 changed files with 173 additions and 3 deletions
Showing only changes of commit f5be0a0de3 - Show all commits
+4
View File
@@ -11,12 +11,16 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
InlineTabList: typeof import('./components/InlineTabList.vue')['default']
MetadataInspector: typeof import('./components/upload/MetadataInspector.vue')['default']
MetadataOutputPanel: typeof import('./components/upload/MetadataOutputPanel.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShareLinkField: typeof import('./components/upload/ShareLinkField.vue')['default']
UploadConfigureModal: typeof import('./components/upload/UploadConfigureModal.vue')['default']
UploadDropzone: typeof import('./components/upload/UploadDropzone.vue')['default']
UploadQueueList: typeof import('./components/upload/UploadQueueList.vue')['default']
UploadSettingsPanel: typeof import('./components/upload/UploadSettingsPanel.vue')['default']
UploadShareResults: typeof import('./components/upload/UploadShareResults.vue')['default']
}
}
@@ -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>
+14 -3
View File
@@ -31,12 +31,18 @@
<p v-if="selectionError" class="text-sm text-destructive">{{ selectionError }}</p>
</div>
<p v-if="confirmedBatchSummary" class="text-center text-xs text-muted-foreground" aria-live="polite">
<p v-if="confirmedBatchSummary" class="text-center text-xs text-muted-foreground">
{{ confirmedBatchSummary }}
</p>
<UploadQueueList :rows="queueRows" :selected-file="selectedFile" @select="selectedFile = $event" />
<UploadShareResults
v-if="showShareLinks"
:file-name="selectedFile?.name ?? null"
:upload="selectedUpload"
/>
<MetadataInspector :file-name="selectedFile?.name ?? null" :entry="selectedPreview" />
<UploadConfigureModal
@@ -87,6 +93,7 @@ const batchSettings = ref<UploadBatchSettingsForm>({
});
const configureModalOpen = ref(false);
const uploadConfirmed = ref(false);
const showShareLinks = ref(false);
const confirmedBatchSummary = ref<string | null>(null);
onBeforeMount(async () => {
@@ -114,6 +121,8 @@ const queueRows = computed(() =>
const selectedPreview = computed(() => metadataStore.getPreview(selectedFile.value));
const selectedUpload = computed(() => uploadStore.getUpload(selectedFile.value));
const metadataErrorCount = computed(
() => queuedFiles.value.filter(file => metadataStore.getPreview(file).status === 'error').length
);
@@ -125,7 +134,7 @@ const onFilesPicked = async (picked: File[]): Promise<void> => {
return;
}
resetUploadSelection();
resetUploadSession();
if (picked.length === 0) {
return;
@@ -168,7 +177,7 @@ const onFilesPicked = async (picked: File[]): Promise<void> => {
}
};
const resetUploadSelection = (): void => {
const resetUploadSession = (): void => {
selectionError.value = null;
clearAllPreviews();
queuedFiles.value = [];
@@ -177,6 +186,7 @@ const resetUploadSelection = (): void => {
dropzoneRef.value?.clearInput();
configureModalOpen.value = false;
uploadConfirmed.value = false;
showShareLinks.value = false;
confirmedBatchSummary.value = null;
};
@@ -195,6 +205,7 @@ const onBatchUpload = async (uploadDespiteMetadataFailure?: boolean): Promise<vo
console.error(error);
} finally {
batchUploading.value = false;
showShareLinks.value = true;
}
};
</script>