import { Component, ElementRef, computed, effect, inject, input, signal, viewChild, } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; import type { FileEntry } from '@shared/types'; import { formatDate, formatSize } from '../format'; import { PanelsService, type Side, type SortKey } from '../panels.service'; @Component({ selector: 'app-file-panel', imports: [NgIcon], templateUrl: './file-panel.html', host: { class: 'flex min-w-0 flex-1 flex-col overflow-hidden' }, }) export class FilePanel { readonly side = input.required(); readonly panels = inject(PanelsService); readonly panel = computed(() => this.panels.get(this.side())); readonly isActive = computed(() => this.panels.activeSide() === this.side()); readonly pathDraft = signal(''); private readonly listEl = viewChild.required>('list'); private lastClickedIndex = 0; readonly formatSize = formatSize; readonly formatDate = formatDate; constructor() { // keep the editable path bar in sync with the actual cwd effect(() => this.pathDraft.set(this.panel().cwd())); // focus follows the active side (Tab switches panels) effect(() => { if (this.isActive()) this.listEl().nativeElement.focus(); }); } activate(): void { this.panels.activeSide.set(this.side()); } iconFor(entry: FileEntry): string { return entry.type === 'dir' ? 'mynaFolder' : 'mynaFile'; } isSelected(entry: FileEntry): boolean { return this.panel().selected().has(entry.name); } sortIndicator(key: SortKey): string { if (this.panel().sortKey() !== key) return ''; return this.panel().sortAsc() ? '↑' : '↓'; } commitPath(): void { const value = this.pathDraft().trim(); if (value) void this.panel().load(value); this.listEl().nativeElement.focus(); } resetPath(): void { this.pathDraft.set(this.panel().cwd()); this.listEl().nativeElement.focus(); } rowClick(event: MouseEvent, index: number): void { this.activate(); const panel = this.panel(); if (event.shiftKey) { panel.selectRange(this.lastClickedIndex, index); } else if (event.metaKey || event.ctrlKey) { const entry = panel.sorted()[index]; if (entry) panel.toggleSelect(entry.name); } else { panel.clearSelection(); } panel.cursor.set(index); this.lastClickedIndex = index; } openAt(index: number): void { const entry = this.panel().sorted()[index]; if (entry) this.panel().open(entry); } onKeydown(event: KeyboardEvent): void { const panel = this.panel(); const key = event.key; if (key === 'Tab') { event.preventDefault(); this.panels.activeSide.set(this.panels.otherSide(this.side())); return; } if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === 'a') { event.preventDefault(); panel.selectAll(); return; } switch (key) { case 'ArrowDown': panel.moveCursor(1); break; case 'ArrowUp': panel.moveCursor(-1); break; case 'PageDown': panel.moveCursor(20); break; case 'PageUp': panel.moveCursor(-20); break; case 'Home': panel.cursor.set(0); break; case 'End': panel.cursor.set(Math.max(0, panel.sorted().length - 1)); break; case ' ': { const entry = panel.sorted()[panel.cursor()]; if (entry) panel.toggleSelect(entry.name); panel.moveCursor(1); } break; case 'Enter': this.openAt(panel.cursor()); return void event.preventDefault(); case 'Backspace': panel.up(); break; default: return; // let unhandled keys through } event.preventDefault(); this.scrollCursorIntoView(); } private scrollCursorIntoView(): void { const row = this.listEl().nativeElement.querySelector( `[data-index="${this.panel().cursor()}"]`, ); row?.scrollIntoView({ block: 'nearest' }); } }