desktop: components/desktop — main shell: window rendering, app launcher, session picker dialog, animated gradient background
This commit is contained in:
parent
eea8197ece
commit
8a9429c946
85
desktop/src/app/components/desktop/desktop.component.html
Normal file
85
desktop/src/app/components/desktop/desktop.component.html
Normal 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>
|
||||
}
|
||||
135
desktop/src/app/components/desktop/desktop.component.scss
Normal file
135
desktop/src/app/components/desktop/desktop.component.scss
Normal 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); }
|
||||
}
|
||||
186
desktop/src/app/components/desktop/desktop.component.ts
Normal file
186
desktop/src/app/components/desktop/desktop.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user