Merge branch '608-pdf-inline-view-further-improvements' into 'dev'
PDF inline further improvements See merge request keanuapp/keanuapp-weblite!363
This commit is contained in:
commit
c7ed77ff5b
5 changed files with 103 additions and 122 deletions
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gallery-current-item">
|
<div class="gallery-current-item">
|
||||||
<ThumbnailView :item="currentAttachment" />
|
<ThumbnailView :item="currentAttachment" showInlinePDF />
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-thumbnail-container">
|
<div class="gallery-thumbnail-container">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,39 @@
|
||||||
:cover="previewOnly"
|
:cover="previewOnly"
|
||||||
:loadingProgress="loadingProgress"
|
:loadingProgress="loadingProgress"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="{ 'thumbnail-item': true, preview: previewOnly, 'file-item': true }">
|
<div v-else :class="{ 'thumbnail-item': true, preview: previewOnly, 'file-item': true, 'pdf-file': isPDF }">
|
||||||
<v-icon :class="fileTypeIconClass">{{ fileTypeIcon }}</v-icon>
|
<div class="pdf-container" v-if="showInlinePDF && isPDF">
|
||||||
<div class="file-name">{{ $$sanitize(fileName) }}</div>
|
<div
|
||||||
<div class="file-size">{{ fileSize }}</div>
|
v-for="(pageNum, index) in pageNums"
|
||||||
|
:key="pageNum"
|
||||||
|
:ref="(el) => { if ((el != null && el as Element)) pageRefs[index] = el as Element }"
|
||||||
|
>
|
||||||
|
<vue-pdf-embed
|
||||||
|
v-if="pageVisibility[pageNum]"
|
||||||
|
:source="doc"
|
||||||
|
:page="pageNum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<v-icon :class="fileTypeIconClass">{{ fileTypeIcon }}</v-icon>
|
||||||
|
<div class="file-name">{{ $$sanitize(fileName) }}</div>
|
||||||
|
<div class="file-size">{{ fileSize }}</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { singleOrDoubleTapRecognizer } from "../../plugins/touch";
|
import { singleOrDoubleTapRecognizer } from "../../plugins/touch";
|
||||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, useTemplateRef, watch } from "vue";
|
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, useTemplateRef, watch, nextTick } from "vue";
|
||||||
import { EventAttachment } from "../../models/eventAttachment";
|
import { EventAttachment } from "../../models/eventAttachment";
|
||||||
import { useThumbnail } from "../messages/composition/useThumbnail";
|
import { useThumbnail } from "../messages/composition/useThumbnail";
|
||||||
import { Attachment } from "../../models/attachment";
|
import { Attachment } from "../../models/attachment";
|
||||||
import ImageWithProgress from "../ImageWithProgress.vue";
|
import ImageWithProgress from "../ImageWithProgress.vue";
|
||||||
|
import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed';
|
||||||
|
import { Source } from 'vue-pdf-embed/types';
|
||||||
|
|
||||||
function isEventAttachment(source: EventAttachment | Attachment | undefined): source is EventAttachment {
|
function isEventAttachment(source: EventAttachment | Attachment | undefined): source is EventAttachment {
|
||||||
return (source as EventAttachment)?.event !== undefined;
|
return (source as EventAttachment)?.event !== undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +62,7 @@ const thumbnailRef = useTemplateRef("thumbnailRef");
|
||||||
interface ThumbnailProps {
|
interface ThumbnailProps {
|
||||||
item?: EventAttachment | Attachment;
|
item?: EventAttachment | Attachment;
|
||||||
previewOnly?: boolean;
|
previewOnly?: boolean;
|
||||||
|
showInlinePDF?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThumbnailEmits = {
|
type ThumbnailEmits = {
|
||||||
|
|
@ -52,10 +70,10 @@ type ThumbnailEmits = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<ThumbnailProps>();
|
const props = defineProps<ThumbnailProps>();
|
||||||
const { item, previewOnly = false } = props;
|
const { item, previewOnly = false, showInlinePDF = false } = props;
|
||||||
const emits = defineEmits<ThumbnailEmits>();
|
const emits = defineEmits<ThumbnailEmits>();
|
||||||
|
|
||||||
let { isVideo, isImage, fileTypeIcon, fileTypeIconClass, fileName, fileSize } = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
let { isVideo, isImage, fileTypeIcon, fileTypeIconClass, fileName, fileSize, isPDF } = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
||||||
|
|
||||||
const fileURL: Ref<string | undefined> = ref(undefined);
|
const fileURL: Ref<string | undefined> = ref(undefined);
|
||||||
const source: Ref<string | undefined> = ref(undefined);
|
const source: Ref<string | undefined> = ref(undefined);
|
||||||
|
|
@ -67,15 +85,15 @@ const updateSource = () => {
|
||||||
if (eventAttachment.src) {
|
if (eventAttachment.src) {
|
||||||
source.value = eventAttachment.src;
|
source.value = eventAttachment.src;
|
||||||
} else if (previewOnly) {
|
} else if (previewOnly) {
|
||||||
eventAttachment.loadThumbnail().then((url) => {
|
eventAttachment.loadThumbnail().then((url) => {
|
||||||
source.value = url.data;
|
source.value = url.data;
|
||||||
})
|
})
|
||||||
} else if (isImage.value) {
|
} else if (isImage.value) {
|
||||||
eventAttachment.loadSrc().then((url) => {
|
eventAttachment.loadSrc().then((url) => {
|
||||||
source.value = url.data;
|
source.value = url.data;
|
||||||
})
|
})
|
||||||
} else if (isVideo.value) {
|
} else if (isVideo.value) {
|
||||||
eventAttachment.loadSrc().then((url) => {
|
eventAttachment.loadSrc().then((url) => {
|
||||||
source.value = url.data;
|
source.value = url.data;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +135,7 @@ watch(props, (props: ThumbnailProps) => {
|
||||||
const updates = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
const updates = useThumbnail(isEventAttachment(props.item) ? props.item.event : isAttachment(props.item) ? props.item.file : undefined);
|
||||||
isVideo.value = updates.isVideo.value;
|
isVideo.value = updates.isVideo.value;
|
||||||
isImage.value = updates.isImage.value;
|
isImage.value = updates.isImage.value;
|
||||||
|
isPDF.value = updates.isPDF.value;
|
||||||
fileTypeIcon = updates.fileTypeIcon;
|
fileTypeIcon = updates.fileTypeIcon;
|
||||||
fileTypeIconClass = updates.fileTypeIconClass;
|
fileTypeIconClass = updates.fileTypeIconClass;
|
||||||
fileName = updates.fileName;
|
fileName = updates.fileName;
|
||||||
|
|
@ -125,6 +144,51 @@ watch(props, (props: ThumbnailProps) => {
|
||||||
updatePoster();
|
updatePoster();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageRefs: Ref<(Element)[]> = ref([]);
|
||||||
|
const pageVisibility: Ref<{ [n: number]: boolean }> = ref({});
|
||||||
|
let pageIntersectionObserver: IntersectionObserver | null = null;
|
||||||
|
const pdfSource: Ref<Source | null> = ref(null);
|
||||||
|
|
||||||
|
const { doc } = useVuePdfEmbed({
|
||||||
|
source: pdfSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageNums = computed(() =>
|
||||||
|
doc.value ? [...Array(doc.value.numPages + 1).keys()].slice(1) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetPageIntersectionObserver = () => {
|
||||||
|
pageIntersectionObserver?.disconnect()
|
||||||
|
pageIntersectionObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const index = pageRefs.value.findIndex((el) => el === entry.target)
|
||||||
|
if (index !== -1) {
|
||||||
|
const pageNum = pageNums.value[index]
|
||||||
|
pageVisibility.value[pageNum] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
pageRefs.value.forEach((element) => {
|
||||||
|
if (element && pageIntersectionObserver) pageIntersectionObserver.observe(element)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(pageNums, (newPageNums) => {
|
||||||
|
pageVisibility.value = { [newPageNums[0]]: true }
|
||||||
|
pageRefs.value = []
|
||||||
|
nextTick(resetPageIntersectionObserver)
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(source, (newSource) => {
|
||||||
|
if (isPDF.value) {
|
||||||
|
pdfSource.value = newSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const loadingProgress = computed(() => {
|
const loadingProgress = computed(() => {
|
||||||
if (isEventAttachment(item)) {
|
if (isEventAttachment(item)) {
|
||||||
const eventAttachment = item;
|
const eventAttachment = item;
|
||||||
|
|
@ -152,6 +216,7 @@ onBeforeUnmount(() => {
|
||||||
URL.revokeObjectURL(fileURL.value);
|
URL.revokeObjectURL(fileURL.value);
|
||||||
fileURL.value = undefined;
|
fileURL.value = undefined;
|
||||||
}
|
}
|
||||||
|
pageIntersectionObserver?.disconnect()
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -163,6 +228,10 @@ onBeforeUnmount(() => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.pdf-file {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
|
|
@ -177,4 +246,19 @@ onBeforeUnmount(() => {
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-container {
|
||||||
|
padding: 24px 16px;
|
||||||
|
margin-top: 200px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 30px auto 0 auto;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-pdf-embed {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,48 +7,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<ThumbnailView class="clickable" v-on:itemclick="onItemClicked" :item="attachment" />
|
<ThumbnailView class="clickable" :item="attachment" />
|
||||||
<span class="edit-marker" v-if="event?.replacingEventId()">{{ $t("message.edited") }}</span>
|
<span class="edit-marker" v-if="event?.replacingEventId()">{{ $t("message.edited") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-dialog v-model="showPdfDialog" fullscreen v-if="util.isMobileOrTabletBrowser()">
|
|
||||||
<v-card>
|
|
||||||
<v-toolbar :elevation="2" density="compact" class="position-fixed pdf-header" color="black">
|
|
||||||
<v-btn
|
|
||||||
icon="arrow_back"
|
|
||||||
@click="showPdfDialog = false"
|
|
||||||
></v-btn>
|
|
||||||
<v-toolbar-title>{{ attachment?.name }}</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<div class="pdf-container">
|
|
||||||
<div
|
|
||||||
v-for="(pageNum, index) in pageNums"
|
|
||||||
:key="pageNum"
|
|
||||||
:ref="(el) => { if ((el != null && el as Element)) pageRefs[index] = el as Element }"
|
|
||||||
>
|
|
||||||
<vue-pdf-embed
|
|
||||||
v-if="pageVisibility[pageNum]"
|
|
||||||
:source="doc"
|
|
||||||
:page="pageNum"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, Ref, nextTick, watch, onBeforeUnmount } from "vue";
|
import { computed, inject } from "vue";
|
||||||
import MessageIncoming from "./MessageIncoming.vue";
|
import MessageIncoming from "./MessageIncoming.vue";
|
||||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { MessageProps, useMessage } from "./useMessage";
|
import { MessageProps, useMessage } from "./useMessage";
|
||||||
import { KeanuEvent } from "../../../models/eventAttachment";
|
import { KeanuEvent } from "../../../models/eventAttachment";
|
||||||
import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed';
|
|
||||||
import { Source } from 'vue-pdf-embed/types';
|
|
||||||
import util from "@/plugins/utils";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const $matrix: any = inject("globalMatrix");
|
const $matrix: any = inject("globalMatrix");
|
||||||
|
|
@ -68,85 +41,8 @@ const { event, isIncoming, attachment, inReplyToText, inReplyToSender, linkify }
|
||||||
const rootComponent = computed(() => {
|
const rootComponent = computed(() => {
|
||||||
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onItemClicked = () => {
|
|
||||||
if(util.isMobileOrTabletBrowser()) {
|
|
||||||
showPdfDialog.value = true
|
|
||||||
} else {
|
|
||||||
emits("download", event.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showPdfDialog = ref(false)
|
|
||||||
const pageRefs: Ref<(Element)[]> = ref([]);
|
|
||||||
const pageVisibility: Ref<{ [n: number]: boolean }> = ref({});
|
|
||||||
let pageIntersectionObserver: IntersectionObserver | null = null;
|
|
||||||
const pdfSource: Ref<Source | null> = ref(null);
|
|
||||||
|
|
||||||
const { doc } = useVuePdfEmbed({
|
|
||||||
source: pdfSource,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageNums = computed(() =>
|
|
||||||
doc.value ? [...Array(doc.value.numPages + 1).keys()].slice(1) : []
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetPageIntersectionObserver = () => {
|
|
||||||
if(!util.isMobileOrTabletBrowser()) return
|
|
||||||
pageIntersectionObserver?.disconnect()
|
|
||||||
pageIntersectionObserver = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const index = pageRefs.value.findIndex((el) => el === entry.target)
|
|
||||||
if (index !== -1) {
|
|
||||||
const pageNum = pageNums.value[index]
|
|
||||||
pageVisibility.value[pageNum] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
pageRefs.value.forEach((element) => {
|
|
||||||
if (element && pageIntersectionObserver) pageIntersectionObserver.observe(element)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(pageNums, (newPageNums) => {
|
|
||||||
if(!util.isMobileOrTabletBrowser()) return
|
|
||||||
pageVisibility.value = { [newPageNums[0]]: true }
|
|
||||||
pageRefs.value = []
|
|
||||||
nextTick(resetPageIntersectionObserver)
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(showPdfDialog, (pdfViewerShown) => {
|
|
||||||
if(!util.isMobileOrTabletBrowser()) return
|
|
||||||
if(attachment.value && pdfViewerShown && !pdfSource.value) {
|
|
||||||
pdfSource.value = attachment.value.src
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
pageIntersectionObserver?.disconnect()
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "@/assets/css/chat.scss" as *;
|
@use "@/assets/css/chat.scss" as *;
|
||||||
.pdf-header {
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-container {
|
|
||||||
padding: 24px 16px;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin: 30px auto 0 auto;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-pdf-embed {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ const showMessageText = computed((): boolean => {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSinglePDF = computed(() => items.value.length === 1 && util.isFileTypePDF(items.value[0].event))
|
||||||
|
|
||||||
const showMultiview = computed((): boolean => {
|
const showMultiview = computed((): boolean => {
|
||||||
if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) {
|
if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -169,10 +171,8 @@ const showMultiview = computed((): boolean => {
|
||||||
(isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
|
(isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
|
||||||
items.value?.length > 1 ||
|
items.value?.length > 1 ||
|
||||||
(event.value && event.value.isRedacted()) ||
|
(event.value && event.value.isRedacted()) ||
|
||||||
(props.room.displayType == ROOM_TYPE_CHANNEL &&
|
(props.room.displayType == ROOM_TYPE_CHANNEL && isSinglePDF.value) ||
|
||||||
items.value.length == 1 &&
|
messageText.value?.length > 0 || isSinglePDF.value
|
||||||
util.isFileTypePDF(items.value[0].event)) ||
|
|
||||||
messageText.value?.length > 0
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,6 @@ export const useThumbnail = (source: KeanuEvent | File | undefined) => {
|
||||||
fileTypeIconClass,
|
fileTypeIconClass,
|
||||||
fileName,
|
fileName,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
isPDF
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue