Slave/upload ui #8
Vendored
+4
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user