import * as plugins from '../plugins.js'; import { apiService } from '../services/index.js'; import { formatSize, getFileName } from '../utilities/index.js'; import { themeStyles } from '../styles/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 saving: boolean = false; @state() private accessor content: string = ''; @state() private accessor originalTextContent: string = ''; @state() private accessor hasChanges: boolean = false; @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, themeStyles, 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-content.code-editor { padding: 0; overflow: hidden; } .preview-content.code-editor dees-input-code { height: 100%; } .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(255, 255, 255, 0.1); border: 1px solid #404040; color: #e0e0e0; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.15s; } .action-btn:hover { background: rgba(255, 255, 255, 0.15); } .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); } .action-btn.primary { background: rgba(59, 130, 246, 0.3); border-color: #3b82f6; color: #60a5fa; } .action-btn.primary:hover { background: rgba(59, 130, 246, 0.4); } .action-btn.primary:disabled { opacity: 0.5; cursor: not-allowed; } .action-btn.secondary { background: rgba(255, 255, 255, 0.05); border-color: #555; color: #aaa; } .action-btn.secondary:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .unsaved-indicator { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 4px; font-size: 12px; color: #fbbf24; } .unsaved-dot { width: 6px; height: 6px; border-radius: 50%; background: #fbbf24; } .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 = ''; this.error = ''; this.originalTextContent = ''; this.hasChanges = false; } } } private async loadObject() { if (!this.objectKey || !this.bucketName) return; this.loading = true; this.error = ''; this.hasChanges = false; 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; // For text files, decode and store original content if (this.isText()) { this.originalTextContent = this.getTextContent(); } } catch (err) { console.error('Error loading object:', err); this.error = 'Failed to load object'; } this.loading = false; } 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 { // Properly decode base64 to UTF-8 text const binaryString = atob(this.content); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return new TextDecoder('utf-8').decode(bytes); } 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 = 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 "${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); } } private getLanguage(): string { const ext = this.objectKey.split('.').pop()?.toLowerCase() || ''; const languageMap: Record = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript', json: 'json', html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss', less: 'less', md: 'markdown', markdown: 'markdown', xml: 'xml', yaml: 'yaml', yml: 'yaml', py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp', php: 'php', sh: 'shell', bash: 'shell', zsh: 'shell', sql: 'sql', graphql: 'graphql', gql: 'graphql', dockerfile: 'dockerfile', txt: 'plaintext', }; return languageMap[ext] || 'plaintext'; } private handleContentChange(event: CustomEvent) { const newValue = event.detail as string; this.hasChanges = newValue !== this.originalTextContent; } private handleDiscard() { const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any; if (codeEditor) { codeEditor.value = this.originalTextContent; } this.hasChanges = false; } private async handleSave() { if (!this.hasChanges || this.saving) return; this.saving = true; try { // Get current content from the editor const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any; const currentContent = codeEditor?.value ?? ''; // Encode the text content to base64 const encoder = new TextEncoder(); const bytes = encoder.encode(currentContent); const base64Content = btoa(String.fromCharCode(...bytes)); const success = await apiService.putObject( this.bucketName, this.objectKey, base64Content, this.contentType ); if (success) { this.originalTextContent = currentContent; this.hasChanges = false; // Update the stored content as well this.content = base64Content; } } catch (err) { console.error('Error saving object:', err); } this.saving = false; } 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`
${getFileName(this.objectKey)}
${this.contentType} ${formatSize(this.size)} ${this.formatDate(this.lastModified)} ${this.hasChanges ? html` Unsaved changes ` : ''}
${this.isImage() ? html`` : this.isText() ? html` this.handleContentChange(e)} > ` : html`

Binary file preview not available

Download to view

`}
${this.hasChanges ? html` ` : html` `}
`; } }