added new logout button and fixed pdf viewing

This commit is contained in:
2025-08-14 10:41:31 +02:00
parent f91a87054d
commit 33c0f1c76c
4 changed files with 291 additions and 129 deletions
@@ -1,102 +1,89 @@
// tt-file-gallery.js
Vue.component('tt-file-gallery', {
Vue.component('tt-fullscreen-viewer', {
props: {
files: { type: Array, default: () => [] },
editMode: { type: Boolean, default: false },
deleteMode: { type: Boolean, default: false },
selectable: { type: Boolean, default: false },
},
data() {
return {
fullscreenItem: null,
currentImageIndex: 0,
editingFile: null,
selectedFiles: [],
zoom: 1,
pan: { x: 0, y: 0 },
isPanning: false,
panStart: { x: 0, y: 0 },
lastPinchDist: 0,
}
item: {type: Object, required: true},
url: {type: String, default: null},
items: {type: Array, default: () => []},
initialIndex: {type: Number, default: -1},
},
data: () => ({
currentItem: null,
currentImageIndex: -1,
zoom: 1,
pan: {x: 0, y: 0},
isPanning: false,
panStart: {x: 0, y: 0},
lastPinchDist: 0,
// NEW: State to track if the content is loading
isLoading: true,
}),
computed: {
imageFiles() {
return this.files.filter(this.isImage);
contentSrc() {
if (this.url) {
return this.url;
}
if (this.currentItem?.fileId) {
return `/File/show?id=${this.currentItem.fileId}`;
}
return null;
},
isViewingImage() {
return this.fullscreenItem && this.isImage(this.fullscreenItem);
return this.currentItem && this.isImage(this.currentItem);
},
imageTransformStyle() {
const { x, y } = this.pan;
return {
transform: `translate(${x}px, ${y}px) scale(${this.zoom})`,
transform: `translate(${this.pan.x}px, ${this.pan.y}px) scale(${this.zoom})`,
cursor: this.isPanning ? 'grabbing' : 'grab',
transition: this.isPanning ? 'none' : 'transform 0.2s',
};
},
fullscreenDownloadUrl() {
if (!this.fullscreenItem) return '#';
return `/File/download?id=${this.fullscreenItem.fileId}`;
downloadUrl() {
return this.currentItem?.fileId ? `/File/download?id=${this.currentItem.fileId}` : '#';
}
},
methods: {
isImage: file => file.mimetype && file.mimetype.startsWith('image/'),
isPdf: file => file.mimetype === 'application/pdf',
getFileIcon(file) {
const extension = file.fileName?.split('.').pop().toLowerCase();
switch (extension) {
case 'doc': case 'docx': return 'fas fa-file-word text-primary';
case 'xls': case 'xlsx': return 'fas fa-file-excel text-success';
case 'zip': case 'rar': return 'fas fa-file-archive text-warning';
default: return 'fas fa-file text-secondary';
}
isImage: file => file?.mimetype?.startsWith('image/'),
isPdf: file => file?.mimetype === 'application/pdf',
onContentLoad() {
this.isLoading = false;
},
toggleSelection(fileId) {
if (!this.selectable) return;
const index = this.selectedFiles.indexOf(fileId);
if (index > -1) {
this.selectedFiles.splice(index, 1);
} else {
this.selectedFiles.push(fileId);
}
this.$emit('selection-changed', this.selectedFiles);
closeViewer() {
this.$emit('close');
},
openViewer(file) {
if(this.editingFile) return;
this.fullscreenItem = file;
if (this.isImage(file)) {
this.currentImageIndex = this.imageFiles.findIndex(img => img.id === file.id);
}
this.resetZoomAndPan();
this.$nextTick(() => { this.$refs.viewer?.focus(); });
},
closeViewer() { this.fullscreenItem = null; },
navigateImage(direction) {
const newIndex = this.currentImageIndex + direction;
if (newIndex >= 0 && newIndex < this.imageFiles.length) {
if (newIndex >= 0 && newIndex < this.items.length) {
// NEW: Reset loading state before loading the new image
this.isLoading = true;
this.currentImageIndex = newIndex;
this.fullscreenItem = this.imageFiles[newIndex];
this.currentItem = this.items[newIndex];
this.resetZoomAndPan();
}
},
handleKeyDown(event) {
event.stopPropagation();
if (!this.fullscreenItem) return;
switch (event.key) {
case 'Escape': this.closeViewer(); break;
case 'ArrowLeft': this.isViewingImage && this.navigateImage(-1); break;
case 'ArrowRight': this.isViewingImage && this.navigateImage(1); break;
handleKeyDown(e) {
e.stopPropagation();
if (!this.currentItem) return;
switch (e.key) {
case 'Escape':
this.closeViewer();
break;
case 'ArrowLeft':
this.isViewingImage && this.navigateImage(-1);
break;
case 'ArrowRight':
this.isViewingImage && this.navigateImage(1);
break;
}
},
resetZoomAndPan() {
this.zoom = 1; this.pan = { x: 0, y: 0 }; this.isPanning = false;
this.zoom = 1;
this.pan = {x: 0, y: 0};
this.isPanning = false;
},
handleWheel(e) {
if (!this.isViewingImage) return;
e.preventDefault();
const scaleFactor = 0.2;
const newZoom = this.zoom - (e.deltaY > 0 ? scaleFactor : -scaleFactor);
this.zoom = Math.max(1, Math.min(newZoom, 5));
const scaleFactor = e.deltaY > 0 ? -0.2 : 0.2;
this.zoom = Math.max(1, Math.min(this.zoom + scaleFactor, 5));
},
onPanStart(e) {
if (this.zoom <= 1) return;
@@ -110,16 +97,134 @@ Vue.component('tt-file-gallery', {
this.pan.x = e.clientX - this.panStart.x;
this.pan.y = e.clientY - this.panStart.y;
},
onPanEnd() { this.isPanning = false; },
onTouchStart(e) { /* ... touch logic ... */ },
onTouchMove(e) { /* ... touch logic ... */ },
onTouchEnd(e) { /* ... touch logic ... */ },
onPanEnd() {
this.isPanning = false;
},
onTouchStart(e) {},
onTouchMove(e) {},
onTouchEnd(e) {},
},
created() {
this.currentItem = this.item;
this.currentImageIndex = this.initialIndex;
},
mounted() {
document.body.style.overflow = 'hidden';
this.$nextTick(() => this.$refs.viewer?.focus());
},
beforeDestroy() {
document.body.style.overflow = '';
},
//language=Vue
template: `
<div class="tt-fullscreen-overlay" @click.self="closeViewer" @keydown="handleKeyDown" tabindex="-1" ref="viewer">
<div class="tt-fullscreen-toolbar">
<a v-if="isViewingImage && currentItem.fileId" :href="downloadUrl" download class="tt-fullscreen-btn"
title="Download">
<i class="fas fa-download"></i>
</a>
<button class="tt-fullscreen-btn" @click="closeViewer" title="Close"><i class="fas fa-times"></i></button>
</div>
<div class="tt-fullscreen-content" @click.self="closeViewer">
<div v-if="isLoading" class="tt-fullscreen-loader">
<svg version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve"
style="width: 80px; height: 80px;">
<path fill="#fff" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
</path>
</svg>
</div>
<template v-if="isViewingImage">
<div class="tt-fullscreen-image-wrapper" @wheel="handleWheel" @mousedown="onPanStart" @mousemove="onPanMove"
@mouseup="onPanEnd" @mouseleave="onPanEnd">
<img v-show="!isLoading" :src="contentSrc" class="tt-fullscreen-image" :style="imageTransformStyle"
@load="onContentLoad" @error="onContentLoad" @click.stop/>
</div>
</template>
<iframe v-else-if="isPdf(currentItem)" v-show="!isLoading" :src="contentSrc" class="tt-fullscreen-pdf"
@load="onContentLoad" @error="onContentLoad" @click.stop></iframe>
</div>
<template v-if="isViewingImage && items.length > 1">
<button class="tt-fullscreen-nav-btn left" @click.stop="navigateImage(-1)" v-if="currentImageIndex > 0">
<i class="fas fa-chevron-left"></i>
</button>
<button class="tt-fullscreen-nav-btn right" @click.stop="navigateImage(1)"
v-if="currentImageIndex < items.length - 1">
<i class="fas fa-chevron-right"></i>
</button>
</template>
</div>`,
});
// You can add this style to your main CSS file or a <style> tag in the component file
/*
*/
Vue.component('tt-file-gallery', {
props: {
files: {type: Array, default: () => []},
editMode: {type: Boolean, default: false},
deleteMode: {type: Boolean, default: false},
selectable: {type: Boolean, default: false},
},
data: () => ({
fullscreenItem: null,
editingFile: null,
selectedFiles: [],
}),
computed: {
imageFiles() {
return this.files.filter(this.isImage);
},
},
methods: {
isImage: file => file.mimetype?.startsWith('image/'),
isPdf: file => file.mimetype === 'application/pdf',
getFileIcon(file) {
const extension = file.fileName?.split('.').pop().toLowerCase();
switch (extension) {
case 'doc':
case 'docx':
return 'fas fa-file-word text-primary';
case 'xls':
case 'xlsx':
return 'fas fa-file-excel text-success';
case 'zip':
case 'rar':
return 'fas fa-file-archive text-warning';
default:
return 'fas fa-file text-secondary';
}
},
toggleSelection(fileId) {
if (!this.selectable) return;
const index = this.selectedFiles.indexOf(fileId);
if (index > -1) this.selectedFiles.splice(index, 1);
else this.selectedFiles.push(fileId);
this.$emit('selection-changed', this.selectedFiles);
},
openViewer(file) {
if (this.editingFile) return;
this.fullscreenItem = file;
},
closeViewer() {
this.fullscreenItem = null;
},
startEdit(file, event) {
event?.stopPropagation();
this.editingFile = { ...file }; // create a copy for editing
this.editingFile = {...file};
},
cancelEdit(event) {
cancelEdit() {
this.editingFile = null;
},
saveEdit(event) {
@@ -134,77 +239,46 @@ Vue.component('tt-file-gallery', {
}
},
},
watch: {
fullscreenItem(newItem) {
document.body.style.overflow = newItem ? 'hidden' : '';
}
},
template: `
<div class="card">
<div class="card-header"><i class="fas fa-images mr-2"></i><h5>Hochgeladene Dokumente</h5></div>
<div v-if="!files.length" class="card-body text-center text-muted">Keine Dokumente vorhanden.</div>
<div v-else class="card-body">
<div class="tt-file-gallery-grid">
<div v-for="file in files" :key="file.id"
class="tt-file-gallery-item"
<div v-for="file in files" :key="file.id" class="tt-file-gallery-item"
:class="[{ 'selected': selectable && selectedFiles.includes(file.id) }, file.class]"
@click="openViewer(file)">
<div v-if="selectable" class="selection-indicator" @click.stop="toggleSelection(file.id)">
<i :class="selectedFiles.includes(file.id) ? 'fas fa-check-circle text-primary' : 'far fa-circle text-muted'"></i>
</div>
<div class="tt-file-gallery-thumbnail-wrapper">
<template v-if="isImage(file)">
<img :src="'/File/show?id=' + file.fileId + '&size=small'" class="tt-file-gallery-thumbnail" :alt="file.fileName">
</template>
<template v-else-if="isPdf(file)">
<div class="tt-file-gallery-icon-container"><i class="fas fa-file-pdf fa-3x text-danger"></i></div>
</template>
<template v-else>
<a :href="'/File/download?id=' + file.fileId" target="_blank" @click.stop class="tt-file-gallery-icon-container">
<i :class="getFileIcon(file)" class="fa-3x"></i>
</a>
</template>
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)"><i class="fas fa-search-plus"></i></div>
<img v-if="isImage(file)" :src="'/File/show?id=' + file.fileId + '&size=small'"
class="tt-file-gallery-thumbnail" :alt="file.fileName">
<div v-else-if="isPdf(file)" class="tt-file-gallery-icon-container"><i
class="fas fa-file-pdf fa-3x text-danger"></i></div>
<a v-else :href="'/File/download?id=' + file.fileId" target="_blank" @click.stop
class="tt-file-gallery-icon-container">
<i :class="getFileIcon(file)" class="fa-3x"></i>
</a>
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)"><i class="fas fa-search-plus"></i>
</div>
</div>
<div class="tt-file-gallery-filename" :title="file.fileName">
<span>{{ file.fileName }}</span>
<i v-if="editMode" class="fas fa-edit text-primary ml-1 action-icon" @click="startEdit(file, $event)"></i>
<i v-if="deleteMode" class="fas fa-trash text-danger ml-1 action-icon" @click="deleteFile(file, $event)"></i>
<i v-if="editMode" class="fas fa-edit text-primary ml-1 action-icon"
@click="startEdit(file, $event)"></i>
<i v-if="deleteMode" class="fas fa-trash text-danger ml-1 action-icon"
@click="deleteFile(file, $event)"></i>
</div>
</div>
</div>
</div>
<tt-modal v-if="editingFile" :show="true" title="Dokument bearbeiten" @update:show="cancelEdit" @submit="saveEdit" :delete="false" :disable-min-height="true">
<tt-modal v-if="editingFile" :show="true" title="Dokument bearbeiten" @update:show="cancelEdit"
@submit="saveEdit" :delete="false" :disable-min-height="true">
<slot name="file-edit" :file="editingFile"></slot>
</tt-modal>
<div v-if="fullscreenItem" class="tt-fullscreen-overlay" @click.self="closeViewer" @keydown="handleKeyDown" tabindex="-1" ref="viewer">
<div class="tt-fullscreen-toolbar">
<a v-if="isViewingImage" :href="fullscreenDownloadUrl" download class="tt-fullscreen-btn" title="Download">
<i class="fas fa-download"></i>
</a>
<button class="tt-fullscreen-btn" @click="closeViewer" title="Close"><i class="fas fa-times"></i></button>
</div>
<div class="tt-fullscreen-content" @click.self="closeViewer">
<template v-if="isViewingImage">
<div class="tt-fullscreen-image-wrapper" @wheel="handleWheel" @mousedown="onPanStart" @mousemove="onPanMove" @mouseup="onPanEnd" @mouseleave="onPanEnd">
<img :src="'/File/show?id=' + fullscreenItem.fileId" class="tt-fullscreen-image" :style="imageTransformStyle" @click.stop />
</div>
</template>
<template v-else-if="isPdf(fullscreenItem)">
<iframe :src="'/File/show?id=' + fullscreenItem.fileId" class="tt-fullscreen-pdf" @click.stop></iframe>
</template>
</div>
<template v-if="isViewingImage">
<button class="tt-fullscreen-nav-btn left" @click.stop="navigateImage(-1)" v-if="currentImageIndex > 0"><i class="fas fa-chevron-left"></i></button>
<button class="tt-fullscreen-nav-btn right" @click.stop="navigateImage(1)" v-if="currentImageIndex < imageFiles.length - 1"><i class="fas fa-chevron-right"></i></button>
</template>
</div>
</div>
`
<tt-fullscreen-viewer v-if="fullscreenItem" :item="fullscreenItem" :items="imageFiles"
:initial-index="imageFiles.findIndex(f => f.id === fullscreenItem.id)"
@close="closeViewer"/>
</div>`
});