feat: add inline PDF viewer for task attachments (#2541)

This commit is contained in:
kolaente 2026-04-04 21:25:54 +02:00 committed by GitHub
parent 33d607714d
commit f5752b97e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 7 deletions

View File

@ -37,6 +37,7 @@ import {
faEyeSlash,
faFile,
faFileImage,
faFilePdf,
faFillDrip,
faFilter,
faForward,
@ -111,6 +112,7 @@ library.add(faSquareCheck)
library.add(faTable)
library.add(faFile)
library.add(faFileImage)
library.add(faFilePdf)
library.add(faCheckSquare)
library.add(faStrikethrough)
library.add(faCode)

View File

@ -95,7 +95,7 @@
<Icon icon="trash-alt" />
</BaseButton>
<BaseButton
v-if="editEnabled && canPreview(a)"
v-if="editEnabled && canPreviewImage(a)"
v-tooltip="task.coverImageAttachmentId === a.id
? $t('task.attachment.unsetAsCover')
: $t('task.attachment.setAsCover')"
@ -168,6 +168,19 @@
alt=""
>
</Modal>
<!-- Attachment PDF modal -->
<Modal
:enabled="attachmentPdfBlobUrl !== null"
:wide="true"
@close="attachmentPdfBlobUrl = null"
>
<iframe
v-if="attachmentPdfBlobUrl"
:src="attachmentPdfBlobUrl"
class="pdf-preview-iframe"
/>
</Modal>
</div>
</template>
@ -180,7 +193,7 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
import {canPreview} from '@/models/attachment'
import {canPreviewImage, canPreviewPdf} from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
@ -365,10 +378,13 @@ async function deleteAttachment() {
}
const attachmentImageBlobUrl = ref<string | null>(null)
const attachmentPdfBlobUrl = ref<string | null>(null)
async function viewOrDownload(attachment: IAttachment) {
if (canPreview(attachment)) {
if (canPreviewImage(attachment)) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else if (canPreviewPdf(attachment)) {
attachmentPdfBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else {
downloadAttachment(attachment)
}
@ -576,6 +592,15 @@ defineExpose({
block-size: 100%;
}
.pdf-preview-iframe {
inline-size: 100%;
max-inline-size: calc(100% - 4rem);
block-size: calc(100vh - var(--modal-content-spacing-tablet));
border: none;
margin: 0 auto;
display: block;
}
.is-task-cover {
background: var(--primary);
color: var(--white);

View File

@ -6,6 +6,17 @@
alt="Attachment preview"
>
<!-- PDF icon -->
<div
v-else-if="isPdf"
class="icon-wrapper"
>
<Icon
size="6x"
icon="file-pdf"
/>
</div>
<!-- Fallback -->
<div
v-else
@ -19,10 +30,10 @@
</template>
<script setup lang="ts">
import {ref, shallowReactive, watchEffect} from 'vue'
import {computed, ref, shallowReactive, watchEffect} from 'vue'
import AttachmentService, {PREVIEW_SIZE} from '@/services/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import {canPreview} from '@/models/attachment'
import {canPreviewImage, canPreviewPdf} from '@/models/attachment'
const props = defineProps<{
modelValue?: IAttachment
@ -30,9 +41,10 @@ const props = defineProps<{
const attachmentService = shallowReactive(new AttachmentService())
const blobUrl = ref<string | undefined>(undefined)
const isPdf = computed(() => props.modelValue && canPreviewPdf(props.modelValue))
watchEffect(async () => {
if (props.modelValue && canPreview(props.modelValue)) {
if (props.modelValue && canPreviewImage(props.modelValue)) {
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue, PREVIEW_SIZE.MD)
}
})

View File

@ -6,9 +6,18 @@ import type { IFile } from '@/modelTypes/IFile'
import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpeg', '.jpg', '.png', '.bmp', '.gif']
export const SUPPORTED_PDF_SUFFIX = ['.pdf']
export function canPreviewImage(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
export function canPreviewPdf(attachment: IAttachment): boolean {
return SUPPORTED_PDF_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
export function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
return canPreviewImage(attachment) || canPreviewPdf(attachment)
}
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {