From 8a9429c94646b77679709ef146fbe394e0511ae3 Mon Sep 17 00:00:00 2001 From: Butterfly Dev Date: Tue, 7 Apr 2026 03:40:11 +0000 Subject: [PATCH] =?UTF-8?q?desktop:=20components/desktop=20=E2=80=94=20mai?= =?UTF-8?q?n=20shell:=20window=20rendering,=20app=20launcher,=20session=20?= =?UTF-8?q?picker=20dialog,=20animated=20gradient=20background?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/desktop/desktop.component.html | 85 ++++++++ .../components/desktop/desktop.component.scss | 135 +++++++++++++ .../components/desktop/desktop.component.ts | 186 ++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 desktop/src/app/components/desktop/desktop.component.html create mode 100644 desktop/src/app/components/desktop/desktop.component.scss create mode 100644 desktop/src/app/components/desktop/desktop.component.ts diff --git a/desktop/src/app/components/desktop/desktop.component.html b/desktop/src/app/components/desktop/desktop.component.html new file mode 100644 index 0000000..ac7ca97 --- /dev/null +++ b/desktop/src/app/components/desktop/desktop.component.html @@ -0,0 +1,85 @@ + +
+ + @for (win of wm.visibleWindows(); track win.id) { + + + @if (shouldShowApp(win, 'file-explorer')) { + + } + + + @if (shouldShowApp(win, 'terminal')) { + + } + + + @if (shouldShowApp(win, 'text-editor')) { + + } + + + @if (shouldShowApp(win, 'settings')) { + + } + + + @if (shouldShowApp(win, 'browser')) { + + } + + + @if (shouldShowApp(win, 'remote-display') && win.sessionId) { + + } + + } +
+ + + + + +@if (showStartMenu()) { + +} + + +@if (showSessionPicker()) { +
+
+

Connect to Remote Session

+
+ @for (session of sessions(); track session.id) { + + } +
+
+ + +
+
+} diff --git a/desktop/src/app/components/desktop/desktop.component.scss b/desktop/src/app/components/desktop/desktop.component.scss new file mode 100644 index 0000000..d432dc8 --- /dev/null +++ b/desktop/src/app/components/desktop/desktop.component.scss @@ -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); } +} diff --git a/desktop/src/app/components/desktop/desktop.component.ts b/desktop/src/app/components/desktop/desktop.component.ts new file mode 100644 index 0000000..8258d17 --- /dev/null +++ b/desktop/src/app/components/desktop/desktop.component.ts @@ -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([]); + 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; + } +}