desktop: components/desktop — main shell: window rendering, app launcher, session picker dialog, animated gradient background

This commit is contained in:
Butterfly Dev 2026-04-07 03:40:11 +00:00
parent eea8197ece
commit 8a9429c946
3 changed files with 406 additions and 0 deletions

View File

@ -0,0 +1,85 @@
<!-- Desktop background + window area -->
<div class="desktop-area" (click)="onDesktopClick()">
<!-- Render each window -->
@for (win of wm.visibleWindows(); track win.id) {
<app-window
[windowState]="win"
(close)="onWindowClose($event)"
(minimize)="onWindowMinimize($event)"
(maximize)="onWindowMaximize($event)"
(focus)="onWindowFocus($event)"
(geometryChange)="onWindowGeometryChange($event)"
>
<!-- File Explorer -->
@if (shouldShowApp(win, 'file-explorer')) {
<app-file-explorer />
}
<!-- Terminal -->
@if (shouldShowApp(win, 'terminal')) {
<app-terminal />
}
<!-- Text Editor -->
@if (shouldShowApp(win, 'text-editor')) {
<app-text-editor />
}
<!-- Settings -->
@if (shouldShowApp(win, 'settings')) {
<app-settings />
}
<!-- Browser -->
@if (shouldShowApp(win, 'browser')) {
<app-browser />
}
<!-- Remote Display -->
@if (shouldShowApp(win, 'remote-display') && win.sessionId) {
<app-remote-display [sessionId]="win.sessionId" />
}
</app-window>
}
</div>
<!-- Taskbar -->
<app-taskbar
(startClick)="showStartMenu.set(!showStartMenu())"
(launchApp)="onLaunchApp($event)"
/>
<!-- Start Menu -->
@if (showStartMenu()) {
<app-start-menu
(launchApp)="onLaunchApp($event)"
(close)="showStartMenu.set(false)"
/>
}
<!-- Session Picker Dialog -->
@if (showSessionPicker()) {
<div class="dialog-backdrop" (click)="closeSessionPicker()"></div>
<div class="session-picker">
<h3>Connect to Remote Session</h3>
<div class="session-list">
@for (session of sessions(); track session.id) {
<button class="session-item" (click)="openRemoteSession(session.id)">
<span class="session-icon">🖥️</span>
<div class="session-info">
<span class="session-id">{{ session.id.substring(0, 12) }}…</span>
<span class="session-status" [class.active]="session.status === 'active'">
{{ session.status }}
</span>
</div>
</button>
}
</div>
<div class="session-actions">
<button class="btn-primary" [disabled]="newSessionLoading()" (click)="createAndOpenSession()">
@if (newSessionLoading()) { Creating… } @else { + New Session }
</button>
<button class="btn-secondary" (click)="closeSessionPicker()">Cancel</button>
</div>
</div>
}

View File

@ -0,0 +1,135 @@
:host {
display: block;
width: 100%;
height: 100%;
}
// Desktop area
.desktop-area {
position: fixed;
inset: 0;
bottom: 48px;
// Windows 11-style gradient background
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 25%, #0f3460 50%, #1a1a2e 75%, #0d1b2a 100%);
overflow: hidden;
// Subtle animated gradient
background-size: 400% 400%;
animation: gradientShift 20s ease infinite;
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
// Session Picker Dialog
.dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9000;
}
.session-picker {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 12px;
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
z-index: 9100;
padding: 24px;
h3 {
margin: 0 0 16px;
color: #e0e0e0;
font-size: 16px;
}
}
.session-list {
max-height: 240px;
overflow-y: auto;
margin-bottom: 16px;
}
.session-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 10px 12px;
border: none;
background: rgba(255, 255, 255, 0.03);
color: #ccc;
cursor: pointer;
border-radius: 6px;
margin-bottom: 4px;
font-size: 13px;
&:hover {
background: rgba(0, 120, 212, 0.15);
}
}
.session-icon {
font-size: 20px;
}
.session-info {
display: flex;
flex-direction: column;
text-align: left;
}
.session-id {
font-family: 'Consolas', monospace;
font-size: 12px;
}
.session-status {
font-size: 11px;
color: #888;
text-transform: capitalize;
&.active {
color: #4caf50;
}
}
.session-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.btn-primary {
padding: 8px 20px;
border: none;
background: #0078d4;
color: #fff;
font-size: 13px;
border-radius: 6px;
cursor: pointer;
&:hover { background: #1a86d9; }
&:disabled { opacity: 0.5; cursor: not-allowed; }
}
.btn-secondary {
padding: 8px 20px;
border: 1px solid #555;
background: transparent;
color: #ccc;
font-size: 13px;
border-radius: 6px;
cursor: pointer;
&:hover { background: rgba(255, 255, 255, 0.05); }
}

View File

@ -0,0 +1,186 @@
import { Component, signal, computed, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { WindowManagerService, type WindowState } from '../../services/window-manager.service';
import { ApiService, type Session } from '../../services/api.service';
import { WindowComponent } from '../window/window.component';
import { TaskbarComponent } from '../taskbar/taskbar.component';
import { StartMenuComponent } from '../start-menu/start-menu.component';
import { RemoteDisplayComponent } from '../remote-display/remote-display.component';
import { TerminalComponent } from '../apps/terminal/terminal.component';
import { TextEditorComponent } from '../apps/text-editor/text-editor.component';
import { FileExplorerComponent } from '../apps/file-explorer/file-explorer.component';
import { SettingsComponent } from '../apps/settings/settings.component';
import { BrowserComponent } from '../apps/browser/browser.component';
@Component({
selector: 'app-desktop',
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule,
WindowComponent,
TaskbarComponent,
StartMenuComponent,
RemoteDisplayComponent,
TerminalComponent,
TextEditorComponent,
FileExplorerComponent,
SettingsComponent,
BrowserComponent,
],
templateUrl: './desktop.component.html',
styleUrl: './desktop.component.scss',
})
export class DesktopComponent implements OnInit, OnDestroy {
/** Whether the start menu is visible. */
readonly showStartMenu = signal(false);
/** Session list for the start menu / remote desktop launcher. */
readonly sessions = signal<Session[]>([]);
readonly showSessionPicker = signal(false);
readonly newSessionLoading = signal(false);
/** Clock for desktop (separate from taskbar). */
readonly currentTime = computed(() => {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
});
constructor(
public wm: WindowManagerService,
private api: ApiService,
) {}
ngOnInit(): void {
// Refresh clock every second.
// Load available sessions.
this.loadSessions();
// Click outside start menu to close it.
document.addEventListener('mousedown', this.onGlobalClick);
}
ngOnDestroy(): void {
document.removeEventListener('mousedown', this.onGlobalClick);
}
private onGlobalClick = (e: MouseEvent): void => {
// Close start menu if clicking outside it and outside the taskbar start button.
const target = e.target as HTMLElement;
if (
this.showStartMenu() &&
!target.closest('app-start-menu') &&
!target.closest('.start-btn')
) {
this.showStartMenu.set(false);
}
};
// ── App launching ───────────────────────────────────────────────────────
onLaunchApp(appId: string): void {
switch (appId) {
case 'file-explorer':
this.wm.openWindow({ title: 'File Explorer', icon: '📁', appId: 'file-explorer', width: 750, height: 480 });
break;
case 'terminal':
this.wm.openWindow({ title: 'Terminal', icon: '⬛', appId: 'terminal', width: 680, height: 420 });
break;
case 'text-editor':
this.wm.openWindow({ title: 'Text Editor', icon: '📝', appId: 'text-editor', width: 700, height: 500 });
break;
case 'settings':
this.wm.openWindow({ title: 'Settings', icon: '⚙️', appId: 'settings', width: 650, height: 450 });
break;
case 'browser':
this.wm.openWindow({ title: 'Browser', icon: '🌐', appId: 'browser', width: 900, height: 600 });
break;
case 'remote-display':
this.showSessionPicker.set(true);
break;
}
}
// ── Session management ──────────────────────────────────────────────────
loadSessions(): void {
this.api.listSessions().subscribe({
next: (resp) => {
if (resp.ok && resp.data) {
this.sessions.set(resp.data);
}
},
});
}
createAndOpenSession(): void {
this.newSessionLoading.set(true);
this.api.createSession().subscribe({
next: (resp) => {
this.newSessionLoading.set(false);
if (resp.ok && resp.data) {
this.openRemoteSession(resp.data.id);
this.loadSessions();
}
},
error: () => {
this.newSessionLoading.set(false);
},
});
}
openRemoteSession(sessionId: string): void {
this.wm.openWindow({
title: `Remote Desktop — ${sessionId.substring(0, 8)}`,
icon: '🖥️',
appId: 'remote-display',
sessionId,
width: 960,
height: 640,
});
this.showSessionPicker.set(false);
}
closeSessionPicker(): void {
this.showSessionPicker.set(false);
}
// ── Window events ───────────────────────────────────────────────────────
onWindowClose(id: string): void {
this.wm.closeWindow(id);
}
onWindowMinimize(id: string): void {
this.wm.minimizeWindow(id);
}
onWindowMaximize(id: string): void {
this.wm.toggleMaximize(id);
}
onWindowFocus(id: string): void {
this.wm.focusWindow(id);
}
onWindowGeometryChange(event: { id: string; x: number; y: number; width: number; height: number }): void {
this.wm.updateWindowGeometry(event.id, { x: event.x, y: event.y, width: event.width, height: event.height });
}
// ── Desktop click (deselect) ────────────────────────────────────────────
onDesktopClick(): void {
this.wm.activeWindowId.set(null);
this.showStartMenu.set(false);
}
// ── Render the correct app component inside a window ────────────────────
shouldShowApp(windowState: WindowState, appId: string): boolean {
return windowState.appId === appId;
}
}