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