documents preview - image
This commit is contained in:
@@ -235,17 +235,14 @@
|
||||
<a class="bubble-download" [href]="entry.downloadUrl" [download]="entry.fileName">Download</a>
|
||||
}
|
||||
|
||||
@if (hasPdfPreview(entry)) {
|
||||
@if (hasDocumentPreviewImage(entry)) {
|
||||
<div class="bubble-preview">
|
||||
<div class="bubble-preview-label">Preview</div>
|
||||
@defer (on viewport) {
|
||||
<app-office-pdf-preview
|
||||
[src]="pdfPreviewUrl(entry)!"
|
||||
[fileName]="entry.fileName ?? 'document.pdf'"
|
||||
></app-office-pdf-preview>
|
||||
} @placeholder {
|
||||
<div class="bubble-preview-placeholder">Loading preview…</div>
|
||||
}
|
||||
<img
|
||||
class="bubble-preview-image"
|
||||
[src]="documentPreviewImageUrl(entry)"
|
||||
[alt]="entry.fileName || 'Document preview'"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -538,17 +538,14 @@
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.bubble-preview-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
.bubble-preview-image {
|
||||
display: block;
|
||||
width: min(240px, 100%);
|
||||
min-height: 320px;
|
||||
padding: 1rem;
|
||||
border: 1px dashed var(--input-border);
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 1rem;
|
||||
color: var(--page-text-soft);
|
||||
background: color-mix(in srgb, var(--surface-background) 85%, white);
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bubble-image,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Component, computed, effect, ElementRef, inject, NgZone, OnDestroy, sig
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { OfficePdfPreviewComponent } from './office-pdf-preview.component';
|
||||
|
||||
import { PeerCallModalComponent } from './peer-call-modal.component';
|
||||
import { ChatSessionService } from './chat-session.service';
|
||||
@@ -17,7 +16,6 @@ import type { CallMode, ChatEntry, ConnectionState, PeerSummary } from './models
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
JsonFileViewerComponent,
|
||||
OfficePdfPreviewComponent,
|
||||
PeerCallModalComponent,
|
||||
],
|
||||
templateUrl: './chat-page.component.html',
|
||||
@@ -646,31 +644,20 @@ export class ChatPageComponent implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
hasPdfPreview(entry: ChatEntry): boolean {
|
||||
hasDocumentPreviewImage(entry: ChatEntry): boolean {
|
||||
return (
|
||||
entry.kind === 'file' &&
|
||||
(
|
||||
(
|
||||
entry.previewMimeType === 'application/pdf' &&
|
||||
!!entry.previewDownloadUrl
|
||||
) ||
|
||||
(
|
||||
!!entry.downloadUrl &&
|
||||
(
|
||||
entry.fileMimeType === 'application/pdf' ||
|
||||
entry.fileName?.toLowerCase().endsWith('.pdf') === true
|
||||
)
|
||||
)
|
||||
)
|
||||
!!entry.previewDownloadUrl &&
|
||||
(entry.previewMimeType?.startsWith('image/') ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
pdfPreviewUrl(entry: ChatEntry): string | null {
|
||||
if (!this.hasPdfPreview(entry)) {
|
||||
documentPreviewImageUrl(entry: ChatEntry): string | null {
|
||||
if (!this.hasDocumentPreviewImage(entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.previewDownloadUrl ?? entry.downloadUrl ?? null;
|
||||
return entry.previewDownloadUrl ?? null;
|
||||
}
|
||||
|
||||
isPeerTyping(peerId: string): boolean {
|
||||
|
||||
@@ -98,9 +98,9 @@ type PersistedChatEntryContent = {
|
||||
previewMimeType?: string;
|
||||
};
|
||||
|
||||
type OfficePreviewResponse = {
|
||||
type DocumentPreviewImageResponse = {
|
||||
mimeType: string;
|
||||
pdfBase64: string;
|
||||
imageBase64: string;
|
||||
};
|
||||
|
||||
type RuntimeEnv = {
|
||||
@@ -1586,13 +1586,13 @@ export class ChatSessionService {
|
||||
let previewMimeType: string | undefined;
|
||||
let previewDownloadUrl: string | undefined;
|
||||
|
||||
if (transfer.kind === 'file' && this.isOfficeDocumentFile(transfer.name, transfer.mimeType)) {
|
||||
const officePreview = await this.generateOfficeDocumentPreview(transfer.name, blob);
|
||||
if (transfer.kind === 'file' && this.isPreviewableDocumentFile(transfer.name, transfer.mimeType)) {
|
||||
const imagePreview = await this.generateDocumentPreviewImage(transfer.name, blob);
|
||||
|
||||
if (officePreview) {
|
||||
previewBlob = officePreview.blob;
|
||||
previewMimeType = officePreview.mimeType;
|
||||
previewDownloadUrl = URL.createObjectURL(officePreview.blob);
|
||||
if (imagePreview) {
|
||||
previewBlob = imagePreview.blob;
|
||||
previewMimeType = imagePreview.mimeType;
|
||||
previewDownloadUrl = URL.createObjectURL(imagePreview.blob);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2287,7 +2287,7 @@ export class ChatSessionService {
|
||||
Uint8Array.from(entry.previewIv).buffer,
|
||||
);
|
||||
const previewBlob = new Blob([decryptedPreview], {
|
||||
type: content.previewMimeType || 'application/pdf',
|
||||
type: content.previewMimeType || 'image/png',
|
||||
});
|
||||
previewDownloadUrl = URL.createObjectURL(previewBlob);
|
||||
}
|
||||
@@ -2344,7 +2344,7 @@ export class ChatSessionService {
|
||||
: undefined;
|
||||
const previewBlob = entry.previewBlob
|
||||
? new Blob([await entry.previewBlob.arrayBuffer()], {
|
||||
type: entry.previewMimeType || 'application/pdf',
|
||||
type: entry.previewMimeType || 'image/png',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -2931,7 +2931,7 @@ export class ChatSessionService {
|
||||
return new Blob([bytes], { type: mimeType });
|
||||
}
|
||||
|
||||
private async generateOfficeDocumentPreview(
|
||||
private async generateDocumentPreviewImage(
|
||||
fileName: string,
|
||||
fileBlob: Blob,
|
||||
): Promise<{ blob: Blob; mimeType: string } | null> {
|
||||
@@ -2943,8 +2943,8 @@ export class ChatSessionService {
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.http.post<OfficePreviewResponse>(
|
||||
`${this.serverUrl()}/api/files/office-preview`,
|
||||
this.http.post<DocumentPreviewImageResponse>(
|
||||
`${this.serverUrl()}/api/files/document-preview-image`,
|
||||
{
|
||||
fileName,
|
||||
mimeType: fileBlob.type || 'application/octet-stream',
|
||||
@@ -2957,15 +2957,19 @@ export class ChatSessionService {
|
||||
);
|
||||
|
||||
return {
|
||||
blob: this.base64ToBlob(response.pdfBase64, response.mimeType),
|
||||
blob: this.base64ToBlob(response.imageBase64, response.mimeType),
|
||||
mimeType: response.mimeType,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Could not generate office document preview.', error);
|
||||
console.warn('Could not generate document preview image.', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private isPreviewableDocumentFile(fileName?: string, mimeType?: string): boolean {
|
||||
return this.isOfficeDocumentFile(fileName, mimeType) || this.isPdfFile(fileName, mimeType);
|
||||
}
|
||||
|
||||
private isOfficeDocumentFile(fileName?: string, mimeType?: string): boolean {
|
||||
const normalizedName = fileName?.trim().toLowerCase() ?? '';
|
||||
const normalizedMimeType = mimeType?.trim().toLowerCase() ?? '';
|
||||
@@ -2981,6 +2985,13 @@ export class ChatSessionService {
|
||||
return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedName);
|
||||
}
|
||||
|
||||
private isPdfFile(fileName?: string, mimeType?: string): boolean {
|
||||
const normalizedName = fileName?.trim().toLowerCase() ?? '';
|
||||
const normalizedMimeType = mimeType?.trim().toLowerCase() ?? '';
|
||||
|
||||
return normalizedMimeType === 'application/pdf' || normalizedName.endsWith('.pdf');
|
||||
}
|
||||
|
||||
private fileExtensionForMimeType(mimeType: string): string {
|
||||
const normalizedMimeType = mimeType.split(';', 1)[0]?.trim().toLowerCase() || 'application/octet-stream';
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: min(240px, 100%);
|
||||
}
|
||||
|
||||
.office-pdf-preview-shell {
|
||||
width: min(240px, 100%);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-office-pdf-preview',
|
||||
imports: [NgxExtendedPdfViewerModule],
|
||||
template: `
|
||||
<div class="office-pdf-preview-shell">
|
||||
<ngx-extended-pdf-viewer
|
||||
[src]="src"
|
||||
[filenameForDownload]="fileName"
|
||||
[height]="'320px'"
|
||||
[page]="1"
|
||||
[zoom]="'page-width'"
|
||||
[showToolbar]="false"
|
||||
[showSidebarButton]="false"
|
||||
[showFindButton]="false"
|
||||
[showPagingButtons]="false"
|
||||
[showPageNumber]="false"
|
||||
[showPageLabel]="false"
|
||||
[showZoomButtons]="false"
|
||||
[showZoomDropdown]="false"
|
||||
[showPresentationModeButton]="false"
|
||||
[showOpenFileButton]="false"
|
||||
[showPrintButton]="false"
|
||||
[showDownloadButton]="false"
|
||||
[showSecondaryToolbarButton]="false"
|
||||
[showRotateButton]="false"
|
||||
[showRotateCwButton]="false"
|
||||
[showRotateCcwButton]="false"
|
||||
[showScrollingButtons]="false"
|
||||
[showSpreadButton]="false"
|
||||
[showPropertiesButton]="false"
|
||||
[showHandToolButton]="false"
|
||||
[showBorders]="false"
|
||||
[textLayer]="false"
|
||||
></ngx-extended-pdf-viewer>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './office-pdf-preview.component.scss',
|
||||
})
|
||||
export class OfficePdfPreviewComponent {
|
||||
@Input({ required: true }) src!: string;
|
||||
@Input() fileName = 'document.pdf';
|
||||
}
|
||||
Reference in New Issue
Block a user