desktop: components/apps/file-explorer — dual-pane file browser with navigation, icons, selection, status bar
This commit is contained in:
parent
283e53d1dc
commit
0048eabb60
@ -0,0 +1,39 @@
|
||||
<div class="file-explorer">
|
||||
<!-- Toolbar -->
|
||||
<div class="fe-toolbar">
|
||||
<button class="fe-btn" (click)="goUp()" title="Go up">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 7l5-5 5 5M7 2v10" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<button class="fe-btn" (click)="goHome()" title="Home">🏠</button>
|
||||
<div class="fe-path">
|
||||
<span class="path-text">{{ currentPath() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File list -->
|
||||
<div class="fe-content">
|
||||
@for (file of files(); track file.name) {
|
||||
<div
|
||||
class="fe-item"
|
||||
[class.selected]="selectedFile() === file.name"
|
||||
(click)="onSelect(file)"
|
||||
(dblclick)="onDoubleClick(file)"
|
||||
>
|
||||
<span class="fe-icon">{{ file.type === 'folder' ? '📁' : '📄' }}</span>
|
||||
<span class="fe-name">{{ file.name }}</span>
|
||||
@if (file.size) {
|
||||
<span class="fe-size">{{ file.size }}</span>
|
||||
}
|
||||
<span class="fe-date">{{ file.modified }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div class="fe-statusbar">
|
||||
<span>{{ files().length }} items</span>
|
||||
@if (selectedFile()) {
|
||||
<span>{{ selectedFile() }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,124 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-explorer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.fe-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #404040;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.fe-btn {
|
||||
width: 30px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.fe-path {
|
||||
flex: 1;
|
||||
height: 26px;
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #383838;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.path-text {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.fe-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.fe-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
color: #ddd;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: rgba(0, 120, 212, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.fe-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fe-size {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-date {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-statusbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 24px;
|
||||
padding: 0 12px;
|
||||
background: #2d2d2d;
|
||||
border-top: 1px solid #404040;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
align-items: center;
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
type: 'folder' | 'file';
|
||||
size?: string;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-explorer',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './file-explorer.component.html',
|
||||
styleUrl: './file-explorer.component.scss',
|
||||
})
|
||||
export class FileExplorerComponent {
|
||||
readonly currentPath = signal('/home/butterfly');
|
||||
readonly selectedFile = signal<string | null>(null);
|
||||
|
||||
readonly files = signal<FileItem[]>([
|
||||
{ name: 'Documents', type: 'folder', modified: '2026-04-07' },
|
||||
{ name: 'Downloads', type: 'folder', modified: '2026-04-07' },
|
||||
{ name: 'Pictures', type: 'folder', modified: '2026-04-06' },
|
||||
{ name: 'Desktop', type: 'folder', modified: '2026-04-07' },
|
||||
{ name: 'readme.txt', type: 'file', size: '1.2 KB', modified: '2026-04-07' },
|
||||
{ name: 'notes.md', type: 'file', size: '3.4 KB', modified: '2026-04-06' },
|
||||
{ name: 'config.json', type: 'file', size: '512 B', modified: '2026-04-05' },
|
||||
]);
|
||||
|
||||
onSelect(file: FileItem): void {
|
||||
this.selectedFile.set(file.name);
|
||||
}
|
||||
|
||||
onDoubleClick(file: FileItem): void {
|
||||
if (file.type === 'folder') {
|
||||
this.currentPath.set(this.currentPath() + '/' + file.name);
|
||||
}
|
||||
}
|
||||
|
||||
goUp(): void {
|
||||
const path = this.currentPath();
|
||||
const idx = path.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
this.currentPath.set(path.substring(0, idx));
|
||||
}
|
||||
}
|
||||
|
||||
goHome(): void {
|
||||
this.currentPath.set('/home/butterfly');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user