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