import * as plugins from '../plugins.js'; import { apiService } from '../services/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; @customElement('tsview-s3-preview') export class TsviewS3Preview extends DeesElement { @property({ type: String }) public accessor bucketName: string = ''; @property({ type: String }) public accessor objectKey: string = ''; @state() private accessor loading: boolean = false; @state() private accessor content: string = ''; @state() private accessor contentType: string = ''; @state() private accessor size: number = 0; @state() private accessor lastModified: string = ''; @state() private accessor error: string = ''; public static styles = [ cssManager.defaultStyles, css` :host { display: block; height: 100%; } .preview-container { display: flex; flex-direction: column; height: 100%; } .preview-header { padding: 12px; border-bottom: 1px solid #333; } .preview-title { font-size: 14px; font-weight: 500; margin-bottom: 8px; word-break: break-all; } .preview-meta { display: flex; flex-wrap: wrap; gap: 16px; font-size: 12px; color: #888; } .meta-item { display: flex; align-items: center; gap: 4px; } .preview-content { flex: 1; overflow: auto; padding: 12px; } .preview-image { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px; } .preview-text { font-family: 'Monaco', 'Menlo', monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; color: #ccc; background: rgba(0, 0, 0, 0.3); padding: 12px; border-radius: 6px; } .preview-actions { padding: 12px; border-top: 1px solid #333; display: flex; gap: 8px; } .action-btn { flex: 1; padding: 8px 16px; background: rgba(99, 102, 241, 0.2); border: 1px solid #6366f1; color: #818cf8; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.15s; } .action-btn:hover { background: rgba(99, 102, 241, 0.3); } .action-btn.danger { background: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #f87171; } .action-btn.danger:hover { background: rgba(239, 68, 68, 0.3); } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; text-align: center; padding: 24px; } .empty-state svg { width: 48px; height: 48px; margin-bottom: 12px; opacity: 0.5; } .loading-state { display: flex; align-items: center; justify-content: center; height: 100%; color: #888; } .error-state { padding: 16px; color: #f87171; text-align: center; } .binary-preview { text-align: center; color: #888; padding: 24px; } `, ]; updated(changedProperties: Map) { if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) { if (this.objectKey) { this.loadObject(); } else { this.content = ''; this.contentType = ''; } } } private async loadObject() { if (!this.objectKey || !this.bucketName) return; this.loading = true; this.error = ''; try { const result = await apiService.getObject(this.bucketName, this.objectKey); this.content = result.content; this.contentType = result.contentType; this.size = result.size; this.lastModified = result.lastModified; } catch (err) { console.error('Error loading object:', err); this.error = 'Failed to load object'; } this.loading = false; } private getFileName(path: string): string { const parts = path.split('/'); return parts[parts.length - 1] || path; } private formatSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`; } private formatDate(dateStr: string): string { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString(); } private isImage(): boolean { return this.contentType.startsWith('image/'); } private isText(): boolean { return ( this.contentType.startsWith('text/') || this.contentType === 'application/json' || this.contentType === 'application/xml' || this.contentType === 'application/javascript' ); } private getTextContent(): string { try { return atob(this.content); } catch { return 'Unable to decode content'; } } private async handleDownload() { try { const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], { type: this.contentType, }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = this.getFileName(this.objectKey); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { console.error('Error downloading:', err); } } private async handleDelete() { if (!confirm(`Delete "${this.getFileName(this.objectKey)}"?`)) return; try { await apiService.deleteObject(this.bucketName, this.objectKey); this.dispatchEvent( new CustomEvent('object-deleted', { detail: { key: this.objectKey }, bubbles: true, composed: true, }) ); } catch (err) { console.error('Error deleting object:', err); } } render() { if (!this.objectKey) { return html`

Select a file to preview

`; } if (this.loading) { return html`
Loading...
`; } if (this.error) { return html`
${this.error}
`; } return html`
${this.getFileName(this.objectKey)}
${this.contentType} ${this.formatSize(this.size)} ${this.formatDate(this.lastModified)}
${this.isImage() ? html`` : this.isText() ? html`
${this.getTextContent()}
` : html`

Binary file preview not available

Download to view

`}
`; } }