Use thumbnail view when sending attachments
So we can preview videos etc.
This commit is contained in:
parent
2b2c736311
commit
e5bb2d7202
4 changed files with 164 additions and 93 deletions
|
|
@ -20,17 +20,14 @@
|
|||
@dragleave.prevent="dropTarget = false"
|
||||
@dragenter.prevent="dropTarget = true"
|
||||
>
|
||||
<v-img
|
||||
v-if="currentAttachment && currentAttachment.src && currentAttachment.status === 'loaded'"
|
||||
:src="currentAttachment.src"
|
||||
/>
|
||||
<div v-else-if="currentAttachment" class="filename">
|
||||
<div v-if="currentAttachment && currentAttachment.status === 'loading'" class="filename">
|
||||
<div>{{ currentAttachment.file.name }}</div>
|
||||
<div v-if="currentAttachment.status === 'loading'" style="font-size: 0.7em; opacity: 0.7">
|
||||
<div style="font-size: 0.7em; opacity: 0.7">
|
||||
{{ $t("message.preparing_to_upload") }}
|
||||
<v-progress-linear indeterminate class="mb-0"></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
<ThumbnailView v-else :file="currentAttachment" />
|
||||
<v-btn
|
||||
class="info-button clickable"
|
||||
icon="information"
|
||||
|
|
@ -47,7 +44,7 @@
|
|||
<template v-slot:badge><span v-bind="props"> </span></template>
|
||||
<div
|
||||
:class="{ 'file-drop-thumbnail': true, clickable: true, current: index == currentItemIndex }"
|
||||
@click="currentItemIndex = index"
|
||||
@click="() => { currentItemIndex = index }"
|
||||
>
|
||||
<v-img v-if="attachment && attachment.src" :src="attachment.src" />
|
||||
<div
|
||||
|
|
@ -195,15 +192,15 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, reactive } from "vue";
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { Attachment } from "../../models/attachment";
|
||||
import C2PABadge from "../c2pa/C2PABadge.vue";
|
||||
import { createUploadBatch } from "../../models/attachmentManager";
|
||||
import AttachmentInfo from "./AttachmentInfo.vue";
|
||||
import ThumbnailView from "./ThumbnailView.vue";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [messageMixin],
|
||||
components: { C2PABadge, AttachmentInfo },
|
||||
components: { C2PABadge, AttachmentInfo, ThumbnailView },
|
||||
emits: ["pick-file", "close"],
|
||||
props: {
|
||||
defaultRootMessageText: {
|
||||
|
|
|
|||
|
|
@ -1,108 +1,107 @@
|
|||
<template>
|
||||
<div ref="thumbnailRef" style="width: 100%;height: 100%;">
|
||||
<v-responsive
|
||||
v-if="item.event.getContent().msgtype == 'm.video' && item.src"
|
||||
:class="{ 'thumbnail-item': true, preview: previewOnly }"
|
||||
>
|
||||
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
|
||||
<div ref="thumbnailRef" style="width: 100%; height: 100%">
|
||||
<v-responsive v-if="isVideo && source" :class="{ 'thumbnail-item': true, preview: previewOnly }">
|
||||
<video :src="source" :controls="!previewOnly" class="w-100 h-100">
|
||||
{{ $t("fallbacks.video_file") }}
|
||||
</video>
|
||||
</v-responsive>
|
||||
<ImageWithProgress
|
||||
v-else-if="item.event.getContent().msgtype == 'm.image'"
|
||||
v-else-if="isImage"
|
||||
:aspect-ratio="previewOnly ? 16 / 9 : undefined"
|
||||
:class="{ 'thumbnail-item': true, preview: previewOnly }"
|
||||
:src="item.src ? item.src : item.thumbnail"
|
||||
:src="source"
|
||||
:contain="!previewOnly"
|
||||
:cover="previewOnly"
|
||||
:loadingProgress="previewOnly ? item.thumbnailProgress : item.srcProgress"
|
||||
:loadingProgress="loadingProgress"
|
||||
/>
|
||||
<div v-else :class="{ 'thumbnail-item': true, preview: previewOnly, 'file-item': true }">
|
||||
<v-icon :class="fileTypeIconClass">{{ fileTypeIcon }}</v-icon>
|
||||
<div class="file-name">{{ $sanitize(fileName) }}</div>
|
||||
<div class="file-name">{{ $$sanitize(fileName) }}</div>
|
||||
<div class="file-size">{{ fileSize }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import util from "../../plugins/utils";
|
||||
import { defineComponent } from "vue";
|
||||
import type { PropType } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import utils from "../../plugins/utils";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref, useTemplateRef, watch } from "vue";
|
||||
import { EventAttachment } from "../../models/eventAttachment";
|
||||
import ImageWithProgress from "../ImageWithProgress";
|
||||
import { useThumbnail } from "../messages/composition/useThumbnail";
|
||||
import { Attachment } from "../../models/attachment";
|
||||
|
||||
export default defineComponent({
|
||||
components: { ImageWithProgress },
|
||||
props: {
|
||||
/**
|
||||
* Item is an object of { event: MXEvent, src: URL }
|
||||
*/
|
||||
item: {
|
||||
type: Object as PropType<EventAttachment>,
|
||||
default: function () {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
previewOnly: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fileTypeIcon() {
|
||||
if (util.isFileTypeAPK(this.item.event)) {
|
||||
if (this.item.event.isChannelMessage) {
|
||||
return "$vuetify.icons.ic_channel_apk";
|
||||
}
|
||||
return "$vuetify.icons.ic_apk";
|
||||
} else if (util.isFileTypeIPA(this.item.event)) {
|
||||
return "$vuetify.icons.ic_ipa";
|
||||
} else if (util.isFileTypePDF(this.item.event)) {
|
||||
if (this.item.event.isChannelMessage) {
|
||||
return "$vuetify.icons.ic_channel_pdf";
|
||||
}
|
||||
return "$vuetify.icons.ic_pdf";
|
||||
} else if (util.isFileTypeZip(this.item.event)) {
|
||||
return "$vuetify.icons.ic_zip";
|
||||
}
|
||||
return "description";
|
||||
},
|
||||
fileTypeIconClass() {
|
||||
if (util.isFileTypeZip(this.item.event)) {
|
||||
return "zip";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
fileName() {
|
||||
return util.getFileName(this.item.event);
|
||||
},
|
||||
fileSize() {
|
||||
return util.getFileSizeFormatted(this.item.event);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
|
||||
initThumbnailHammerJs(element: any) {
|
||||
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
|
||||
const $$sanitize: any = inject("globalSanitize");
|
||||
|
||||
hammerInstance.on("singletap doubletap", (ev: any) => {
|
||||
if (ev.type === "singletap") {
|
||||
this.$emit("itemclick", { item: this.item });
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.thumbnailRef) {
|
||||
this.initThumbnailHammerJs(this.$refs.thumbnailRef);
|
||||
const thumbnailRef = useTemplateRef("thumbnailRef");
|
||||
|
||||
interface ThumbnailProps {
|
||||
item?: EventAttachment;
|
||||
file?: Attachment;
|
||||
previewOnly?: boolean;
|
||||
}
|
||||
|
||||
type ThumbnailEmits = {
|
||||
(event: "itemclick", value: { item: EventAttachment }): void;
|
||||
};
|
||||
|
||||
const props = defineProps<ThumbnailProps>();
|
||||
const { item, file, previewOnly = false } = props;
|
||||
const emits = defineEmits<ThumbnailEmits>();
|
||||
|
||||
let { isVideo, isImage, fileTypeIcon, fileTypeIconClass, fileName, fileSize } = useThumbnail(file?.file ?? item?.event);
|
||||
const fileURL: Ref<string | undefined> = ref(undefined);
|
||||
|
||||
watch(props, (props: ThumbnailProps) => {
|
||||
const updates = useThumbnail(props.file?.file ?? props.item?.event);
|
||||
isVideo.value = updates.isVideo.value;
|
||||
isImage.value = updates.isImage.value;
|
||||
fileTypeIcon = updates.fileTypeIcon;
|
||||
fileTypeIconClass = updates.fileTypeIconClass;
|
||||
fileName = updates.fileName;
|
||||
fileSize = updates.fileSize;
|
||||
});
|
||||
|
||||
const source = computed(() => {
|
||||
if (item) {
|
||||
if (isVideo.value) {
|
||||
return item.src;
|
||||
}
|
||||
if (!this.previewOnly && this.item) {
|
||||
this.item.loadSrc();
|
||||
return item.src ? item.src : item.thumbnail;
|
||||
} else if (file) {
|
||||
if (!fileURL.value) {
|
||||
fileURL.value = URL.createObjectURL(file.file);
|
||||
}
|
||||
},
|
||||
return fileURL.value;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const loadingProgress = computed(() => {
|
||||
if (item) {
|
||||
return previewOnly ? item.thumbnailProgress : item.srcProgress;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (thumbnailRef.value && (item as EventAttachment)) {
|
||||
const hammerInstance = utils.singleOrDoubleTabRecognizer(thumbnailRef.value);
|
||||
hammerInstance.on("singletap doubletap", (ev: any) => {
|
||||
if (ev.type === "singletap") {
|
||||
emits("itemclick", { item: item as EventAttachment });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!previewOnly && item && (item as EventAttachment)) {
|
||||
(item as EventAttachment).loadSrc();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (fileURL.value) {
|
||||
URL.revokeObjectURL(fileURL.value);
|
||||
fileURL.value = undefined;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
74
src/components/messages/composition/useThumbnail.ts
Normal file
74
src/components/messages/composition/useThumbnail.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { KeanuEvent } from "../../../models/eventAttachment";
|
||||
import utils from "@/plugins/utils";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
export const useThumbnail = (source: KeanuEvent | File | undefined) => {
|
||||
const isVideo = ref(false);
|
||||
const isImage = ref(false);
|
||||
const isAPK = ref(false);
|
||||
const isIPA = ref(false);
|
||||
const isPDF = ref(false);
|
||||
const isZip = ref(false);
|
||||
const isChannelMessage = ref(false);
|
||||
const fileName = ref("");
|
||||
const fileSize = ref("");
|
||||
|
||||
function isEvent(source: KeanuEvent | File | undefined): source is KeanuEvent {
|
||||
return (source as KeanuEvent).getId !== undefined;
|
||||
}
|
||||
|
||||
if (isEvent(source)) {
|
||||
const event = source;
|
||||
isVideo.value = event.getContent().msgtype == "m.video";
|
||||
isImage.value = event.getContent().msgtype == "m.image";
|
||||
isAPK.value = utils.isFileTypeAPK(event);
|
||||
isIPA.value = utils.isFileTypeIPA(event);
|
||||
isPDF.value = utils.isFileTypePDF(event);
|
||||
isZip.value = utils.isFileTypeZip(event);
|
||||
isChannelMessage.value = event.isChannelMessage ?? false;
|
||||
fileName.value = utils.getFileName(event);
|
||||
fileSize.value = utils.getFileSizeFormatted(event);
|
||||
} else if (source) {
|
||||
const file = source as File;
|
||||
isVideo.value = file.type.startsWith("video/");
|
||||
isImage.value = file.type.startsWith("image/");
|
||||
fileName.value = file.name;
|
||||
fileSize.value = prettyBytes(file.size);
|
||||
}
|
||||
|
||||
const fileTypeIcon = computed(() => {
|
||||
if (isAPK.value) {
|
||||
if (isChannelMessage.value) {
|
||||
return "$vuetify.icons.ic_channel_apk";
|
||||
}
|
||||
return "$vuetify.icons.ic_apk";
|
||||
} else if (isIPA.value) {
|
||||
return "$vuetify.icons.ic_ipa";
|
||||
} else if (isPDF.value) {
|
||||
if (isChannelMessage.value) {
|
||||
return "$vuetify.icons.ic_channel_pdf";
|
||||
}
|
||||
return "$vuetify.icons.ic_pdf";
|
||||
} else if (isZip.value) {
|
||||
return "$vuetify.icons.ic_zip";
|
||||
}
|
||||
return "description";
|
||||
});
|
||||
|
||||
const fileTypeIconClass = computed(() => {
|
||||
if (isZip.value) {
|
||||
return "zip";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return {
|
||||
isVideo,
|
||||
isImage,
|
||||
fileTypeIcon,
|
||||
fileTypeIconClass,
|
||||
fileName,
|
||||
fileSize,
|
||||
};
|
||||
};
|
||||
|
|
@ -8,11 +8,13 @@ export type KeanuEventExtension = {
|
|||
replyEvent?: MatrixEvent & KeanuEventExtension;
|
||||
}
|
||||
|
||||
export type KeanuEvent = MatrixEvent & KeanuEventExtension;
|
||||
|
||||
export type EventAttachmentUrlType = "src" | "thumbnail";
|
||||
export type EventAttachmentUrlData = {data: string, type: EventAttachmentUrlType};
|
||||
|
||||
export type EventAttachment = {
|
||||
event: MatrixEvent & KeanuEventExtension;
|
||||
event: KeanuEvent;
|
||||
name: string;
|
||||
src?: string;
|
||||
srcSize: number;
|
||||
|
|
@ -28,7 +30,6 @@ export type EventAttachment = {
|
|||
release: (src: boolean, thumbnail: boolean) => void;
|
||||
};
|
||||
|
||||
export type KeanuEvent = MatrixEvent & KeanuEventExtension;
|
||||
|
||||
export type KeanuRoom = Room & {
|
||||
displayType: "im.keanu.room_type_default" | "im.keanu.room_type_voice" | "im.keanu.room_type_file" | "im.keanu.room_type_channel" | undefined;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue