From 0fd41608b9be4fc9e20ebbe353a7333dd17bc93d Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Mon, 17 Oct 2022 16:11:06 +0200 Subject: [PATCH] Finished window system --- .../.idea/dataSources.local.xml | 2 +- .../95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml | 4 +- .../.idea.WebDesktop 2.0/.idea/workspace.xml | 84 +++--- Frontend/src/app/app-routing.module.ts | 7 +- Frontend/src/app/app.module.ts | 12 +- .../window-wrapper.component.html | 25 ++ .../window-wrapper.component.scss | 67 +++++ .../window-wrapper.component.ts | 267 ++++++++++++++++++ Frontend/src/app/services/backend.service.ts | 2 + .../app/sites/desktop/desktop.component.html | 20 ++ .../app/sites/desktop/desktop.component.scss | 88 ++++++ .../app/sites/desktop/desktop.component.ts | 142 ++++++++++ .../taskbar-icon/taskbar-icon.component.html | 18 ++ .../taskbar-icon/taskbar-icon.component.scss | 129 +++++++++ .../taskbar-icon/taskbar-icon.component.ts | 91 ++++++ .../src/app/sites/login/login.component.ts | 1 + .../app/sites/register/register.component.ts | 1 + Frontend/src/assets/icons/defender.png | Bin 0 -> 9195 bytes Frontend/src/styles.scss | 4 + 19 files changed, 922 insertions(+), 42 deletions(-) create mode 100644 Frontend/src/app/components/window-wrapper/window-wrapper.component.html create mode 100644 Frontend/src/app/components/window-wrapper/window-wrapper.component.scss create mode 100644 Frontend/src/app/components/window-wrapper/window-wrapper.component.ts create mode 100644 Frontend/src/app/sites/desktop/desktop.component.html create mode 100644 Frontend/src/app/sites/desktop/desktop.component.scss create mode 100644 Frontend/src/app/sites/desktop/desktop.component.ts create mode 100644 Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.html create mode 100644 Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.scss create mode 100644 Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.ts create mode 100644 Frontend/src/assets/icons/defender.png diff --git a/.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml b/.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml index d625f38..646bf29 100644 --- a/.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml +++ b/.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + #@ diff --git a/.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml b/.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml index 6431f9d..20ecec0 100644 --- a/.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml +++ b/.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml @@ -5,9 +5,9 @@ exact 10.3.34 - + + 1 - \ No newline at end of file diff --git a/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml b/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml index c3c5ee6..c2e80f0 100644 --- a/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml +++ b/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml @@ -6,29 +6,24 @@ - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - + + + + - { + "keyToString": { + "ASKED_ADD_EXTERNAL_FILES": "true", + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "WebServerToolWindowFactoryState": "false", + "list.type.of.created.stylesheet": "SCSS", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings", + "ts.external.directory.path": "D:\\Programmierstuff\\Projekte\\WebDesktop 2.0\\Frontend\\node_modules\\typescript\\lib", + "vue.rearranger.settings.migration": "true" }, - "keyToStringList": { - "DatabaseDriversLRU": [ - "mariadb" + "keyToStringList": { + "DatabaseDriversLRU": [ + "mariadb" ] } -}]]> +} @@ -178,5 +190,7 @@ \ No newline at end of file diff --git a/Frontend/src/app/app-routing.module.ts b/Frontend/src/app/app-routing.module.ts index 8ca5f12..fca7ef6 100644 --- a/Frontend/src/app/app-routing.module.ts +++ b/Frontend/src/app/app-routing.module.ts @@ -2,10 +2,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import {LoginComponent} from "./sites/login/login.component"; import {RegisterComponent} from "./sites/register/register.component"; +import {DesktopComponent} from "./sites/desktop/desktop.component"; const routes: Routes = [ - {path: "login", component: LoginComponent}, - {path: "register", component: RegisterComponent} + {path: '', component: DesktopComponent}, + {path: 'login', component: LoginComponent}, + {path: 'register', component: RegisterComponent}, + {path: '*', redirectTo: ''} ]; @NgModule({ diff --git a/Frontend/src/app/app.module.ts b/Frontend/src/app/app.module.ts index 81fee20..25d2482 100644 --- a/Frontend/src/app/app.module.ts +++ b/Frontend/src/app/app.module.ts @@ -12,12 +12,19 @@ import {MatButtonModule} from "@angular/material/button"; import {MatDividerModule} from "@angular/material/divider"; import { RegisterComponent } from './sites/register/register.component'; import {ReactiveFormsModule} from "@angular/forms"; +import { DesktopComponent } from './sites/desktop/desktop.component'; +import { TaskbarIcon } from './sites/desktop/taskbar-icon/taskbar-icon.component'; +import { WindowWrapper } from './components/window-wrapper/window-wrapper.component'; +import {MatIconModule} from "@angular/material/icon"; @NgModule({ declarations: [ AppComponent, LoginComponent, - RegisterComponent + RegisterComponent, + DesktopComponent, + TaskbarIcon, + WindowWrapper ], imports: [ BrowserModule, @@ -28,7 +35,8 @@ import {ReactiveFormsModule} from "@angular/forms"; MatInputModule, MatButtonModule, MatDividerModule, - ReactiveFormsModule + ReactiveFormsModule, + MatIconModule ], providers: [], bootstrap: [AppComponent] diff --git a/Frontend/src/app/components/window-wrapper/window-wrapper.component.html b/Frontend/src/app/components/window-wrapper/window-wrapper.component.html new file mode 100644 index 0000000..8ef6ba6 --- /dev/null +++ b/Frontend/src/app/components/window-wrapper/window-wrapper.component.html @@ -0,0 +1,25 @@ +
+
+ logo + {{title}} + +
+
+ minimize +
+
+ {{resizeIcon}} +
+
+ close +
+
+
+ +
diff --git a/Frontend/src/app/components/window-wrapper/window-wrapper.component.scss b/Frontend/src/app/components/window-wrapper/window-wrapper.component.scss new file mode 100644 index 0000000..d1beb31 --- /dev/null +++ b/Frontend/src/app/components/window-wrapper/window-wrapper.component.scss @@ -0,0 +1,67 @@ +@use "src/colors"; + +.window-wrapper { + position: absolute; + width: 800px; + height: 600px; + border: 2px solid colors.$medium; + border-radius: 10px; + background-color: colors.$dark; + overflow: hidden; + box-sizing: border-box; + user-select: none; + + display: flex; + flex-direction: column; + + /*transition: width 300ms, height 300ms, left 300ms, top 300ms;*/ + + header { + width: 100%; + height: 30px; + background-color: colors.$medium; + display: flex; + gap: 5px; + align-items: center; + + img[alt="logo"] { + width: 20px; + height: 20px; + margin-left: 5px; + } + + .buttons { + margin-left: auto; + margin-right: 5px; + display: flex; + gap: 2px; + + div { + width: 26px; + height: 26px; + border-radius: 13px; + transition: background-color 150ms; + line-height: 26px; + text-align: center; + + &:hover { + background-color: rgba(colors.$text, 0.2); + } + + mat-icon { + position: absolute; + transform: translate(-50%, 5%); + + &.minimize { + transform: translate(-50%, -7px); + } + } + } + } + } + + iframe { + border: none; + flex-grow: 1; + } +} diff --git a/Frontend/src/app/components/window-wrapper/window-wrapper.component.ts b/Frontend/src/app/components/window-wrapper/window-wrapper.component.ts new file mode 100644 index 0000000..92ff49c --- /dev/null +++ b/Frontend/src/app/components/window-wrapper/window-wrapper.component.ts @@ -0,0 +1,267 @@ +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {DesktopComponent, ProgramArgs} from "../../sites/desktop/desktop.component"; +import {TaskbarIcon} from "../../sites/desktop/taskbar-icon/taskbar-icon.component"; + +@Component({ + selector: 'app-window-wrapper', + templateUrl: './window-wrapper.component.html', + styleUrls: ['./window-wrapper.component.scss'] +}) +export class WindowWrapper { + @ViewChild('wrapper') wrapper: ElementRef; + @ViewChild('content') content: ElementRef; + public program: ProgramArgs; + public uuid: number; + public taskbar: TaskbarIcon; + public dragHandler: DragHandler; + public resizeHandler: ResizeHandler; + public title: string; + public focused: boolean; + public contentLoaded: boolean = false; + + public resizeIcon: string = "fullscreen"; + public maximized: boolean = false; + private lastPos: { left: string, top: string, width: string, height: string }; + + constructor(private object: ElementRef) { + this.uuid = DesktopComponent.instance.generateWindowUUID(); + } + + public initialize(taskbar: TaskbarIcon) { + this.title = this.program.name; + this.dragHandler = new DragHandler(this.wrapper.nativeElement, this); + this.resizeHandler = new ResizeHandler(this.wrapper.nativeElement, this); + this.taskbar = taskbar; + this.content.nativeElement.src = this.program.handlerUrl; + this.focus(); + } + + public close() { + this.object.nativeElement.parentElement.removeChild(this.object.nativeElement); + this.taskbar.onClose(this); + } + + public toggleMinimized() { + const minimized = this.object.nativeElement.style.display == 'none'; + + if (minimized) { + this.object.nativeElement.style.display = 'block'; + this.taskbar.setIndicator('wide'); + } else { + this.object.nativeElement.style.display = 'none'; + this.taskbar.setIndicator('dot'); + } + } + + public toggleMaximized() { + const wrapper = this.object.nativeElement.children.item(0) as HTMLElement; + + if (this.lastPos == undefined) { + this.lastPos = { + width: wrapper.style.width, + height: wrapper.style.height, + left: wrapper.style.left, + top: wrapper.style.top + }; + + wrapper.style.width = '100%'; + wrapper.style.height = '100%'; + wrapper.style.left = '0'; + wrapper.style.top = '0'; + + this.resizeIcon = "fullscreen_exit"; + this.maximized = true; + } else { + wrapper.style.width = this.lastPos.width; + wrapper.style.height = this.lastPos.height; + wrapper.style.left = this.lastPos.left; + wrapper.style.top = this.lastPos.top; + delete this.lastPos; + + this.resizeIcon = "fullscreen"; + this.maximized = false; + } + } + + public focus() { + if (this.focused) return; + DesktopComponent.instance.unfocusAll(this); + + this.object.nativeElement.style.display = 'block'; + this.taskbar.setIndicator('wide'); + + this.wrapper.nativeElement.style.zIndex = '7'; + + DesktopComponent.focusedWindow = this; + this.focused = true; + } + + public unfocus() { + this.taskbar.setIndicator('dot'); + + this.wrapper.nativeElement.style.zIndex = '5'; + + this.focused = false; + } + + public applyContentListeners(content: HTMLIFrameElement) { + content.contentDocument.addEventListener('mousemove', this.onMove.bind(this)); + content.contentDocument.addEventListener('mousedown', this.focus.bind(this)); + content.contentDocument.addEventListener('mouseup', this.resizeHandler?.windowResizeStop.bind(this.resizeHandler)); + + this.contentLoaded = true; + } + + public onMove(event: MouseEvent) { + this.dragHandler?.windowDrag(event); + this.resizeHandler?.windowResize(event); + } + +} + +class DragHandler { + private offsetX: number; + private offsetY: number; + public dragging: boolean; + private origTransitions: string; + + public constructor(private object: HTMLElement, private wrapper: WindowWrapper) { + } + + public windowDrag(event: MouseEvent): void { + if (!this.dragging) return; + if (!this.wrapper.contentLoaded) return; + const x = event.clientX - this.offsetX; + const y = event.clientY - this.offsetY; + this.object.style.left = x + 'px'; + this.object.style.top = y + 'px'; + } + + public windowDragStart(event: MouseEvent): void { + if (this.wrapper.maximized) return; + if (this.wrapper.resizeHandler?.resizing) return; + + if (this.origTransitions == undefined) + this.origTransitions = this.object.style.transition; + this.object.style.transition = 'none'; + + this.offsetX = event.clientX - this.object.offsetLeft; + this.offsetY = event.clientY - this.object.offsetTop; + this.dragging = true; + } + + public windowDragStop(): void { + if (!this.dragging) return; + this.object.style.transition = this.origTransitions; + delete this.origTransitions; + this.dragging = false; + } + + public get isDragging(): boolean { + return this.dragging; + } +} + +class ResizeHandler { + public minSize: { width: number, height: number } = {width: 800, height: 600}; + private readonly resizingArea: number = 10; + public resizing: boolean = false; + private lastResizeManager: string; + + public constructor(private object: HTMLElement, private wrapper: WindowWrapper) { + } + + public windowResizeStart(event: MouseEvent): void { + if (this.wrapper.maximized) return; + if (!this.wrapper.contentLoaded) return; + if (this.wrapper.dragHandler?.dragging) return; + if (!this.isHoverBorder(event)) return; + + this.object.classList.add("unselectable"); + this.lastResizeManager = this.isHoverBorder(event); + + this.resizing = true; + } + + public windowResizeStop(): void { + if (!this.resizing) return; + this.resizing = false; + delete this.lastResizeManager; + this.object.classList.remove("unselectable"); + } + + public windowResize(event: MouseEvent) { + if (!this.wrapper.focused) return; + if (this.wrapper.maximized) return; + + if (!this.resizing) { + const c = this.isHoverBorder(event); + + if (c) { + this.object.style.cursor = c + "-resize"; + this.lastResizeManager = c; + } else + this.object.style.cursor = "auto"; + } + + if (this.resizing) this.handleResizing(event); + } + + private handleResizing(event: MouseEvent): void { + let newDimensions: {x: number, y: number, width: number, height: number} = { + x: undefined, + y: undefined, + width: undefined, + height: undefined + }; + + if (this.lastResizeManager.includes("n")) { //TOP + newDimensions.y = this.object.offsetTop + event.movementY; + newDimensions.height = this.object.offsetHeight - event.movementY; + } else if (this.lastResizeManager.includes("s")) { //BOTTOM + //this.object.style.height = (this.resizeOrigin.height + event.movementY) + "px"; + newDimensions.height = this.object.offsetHeight + event.movementY; + } + if (this.lastResizeManager.includes("w")) { //LEFT + /*this.object.style.left = (this.resizeOrigin.x + event.movementX) + "px"; + this.object.style.width = (this.resizeOrigin.width - event.movementX) + "px";*/ + newDimensions.x = this.object.offsetLeft + event.movementX; + newDimensions.width = this.object.offsetWidth - event.movementX; + } else if (this.lastResizeManager.includes("e")) { //RIGHT + //this.object.style.width = (this.resizeOrigin.width + event.movementX) + "px"; + newDimensions.width = this.object.offsetWidth + event.movementX; + } + + if (newDimensions.width < this.minSize.width) { + newDimensions.width = this.minSize.width; + this.windowResizeStop(); + } + if (newDimensions.height < this.minSize.height) { + newDimensions.height = this.minSize.height; + this.windowResizeStop(); + } + + if (newDimensions.x) this.object.style.left = newDimensions.x + 'px'; + if (newDimensions.y) this.object.style.top = newDimensions.y + 'px'; + if (newDimensions.width) this.object.style.width = newDimensions.width + 'px'; + if (newDimensions.height) this.object.style.height = newDimensions.height + 'px'; + } + + private isHoverBorder(event: MouseEvent): string { + const delta = this.resizingArea; // the thickness of the hovered border area + + const rect = this.object.getBoundingClientRect(); + const x = event.clientX - rect.left, // the relative mouse position to the element + y = event.clientY - rect.top, // ... + w = rect.right - rect.left, // width of the element + h = rect.bottom - rect.top; // height of the element + + let c = ""; // which cursor to use + if (y < delta) c += "n"; // north + else if (y > h - delta) c += "s"; // south + if (x < delta) c += "w"; // west + else if (x > w - delta) c += "e"; // east + + return c; + } +} diff --git a/Frontend/src/app/services/backend.service.ts b/Frontend/src/app/services/backend.service.ts index 837af99..f6bb396 100644 --- a/Frontend/src/app/services/backend.service.ts +++ b/Frontend/src/app/services/backend.service.ts @@ -73,6 +73,8 @@ export class BackendService { return this.sendRequest(type, endpoint, body, options); } } + if (error.status == 0) + return {content: undefined, success: false, code: error.status, message: "Server nicht erreichbar!"}; return {content: undefined, success: false, code: error.status, message: error.error}; } diff --git a/Frontend/src/app/sites/desktop/desktop.component.html b/Frontend/src/app/sites/desktop/desktop.component.html new file mode 100644 index 0000000..9632727 --- /dev/null +++ b/Frontend/src/app/sites/desktop/desktop.component.html @@ -0,0 +1,20 @@ +
+
+ +
+ +
+
+ home-button +
+
+ defender + +
DEU
+
+ {{time}} + {{date}} +
+
+
+
diff --git a/Frontend/src/app/sites/desktop/desktop.component.scss b/Frontend/src/app/sites/desktop/desktop.component.scss new file mode 100644 index 0000000..57879a9 --- /dev/null +++ b/Frontend/src/app/sites/desktop/desktop.component.scss @@ -0,0 +1,88 @@ +@use "src/styles"; +@use "src/colors"; + +.desktop { + width: 100vw; + height: 100vh; + + background-image: url(styles.$background); + + display: flex; + flex-direction: column; + + .windows { + width: 100%; + flex-grow: 1; + position: relative; + } + + .taskbar { + width: 100%; + height: 50px; + background-color: rgba(colors.$medium, 0.6); + backdrop-filter: blur(10px); + box-sizing: border-box; + z-index: 100; + + display: flex; + padding: 5px; + + .icons { + margin-right: auto; + display: flex; + gap: 10px; + + & > * { + padding: 5px; + border-radius: 5px; + transition: background-color 200ms; + + &:hover, &.focus { + background-color: rgba(colors.$light, 0.3); + } + } + } + + .right { + display: flex; + gap: 10px; + margin-left: auto; + + img { + width: 20px; + height: 20px; + align-self: center; + + &:hover { + cursor: pointer; + } + } + + .lang { + align-self: center; + font-size: 11px; + user-select: none; + } + + .datetime { + display: flex; + flex-direction: column; + justify-content: space-evenly; + margin-right: 10px; + border-radius: 5px; + padding-inline: 5px; + transition: background-color 200ms; + + span { + text-align: right; + user-select: none; + font-size: 12px; + } + + &:hover, &.focus { + background-color: rgba(colors.$light, 0.3); + } + } + } + } +} diff --git a/Frontend/src/app/sites/desktop/desktop.component.ts b/Frontend/src/app/sites/desktop/desktop.component.ts new file mode 100644 index 0000000..8bc3661 --- /dev/null +++ b/Frontend/src/app/sites/desktop/desktop.component.ts @@ -0,0 +1,142 @@ +import {ChangeDetectorRef, Component, OnInit, ViewChild, ViewContainerRef} from '@angular/core'; +import {TaskbarIcon} from "./taskbar-icon/taskbar-icon.component"; +import {WindowWrapper} from "../../components/window-wrapper/window-wrapper.component"; + +export interface IconType { + uuid: number; + icon: string; + + program: ProgramArgs; +} + +export interface ProgramArgs { + identifier: string; + name: string; + handlerUrl: string; + + permission?: string[]; + args?: string[]; + openFiles?: string[]; +} + +export const programs: {[programUUID: string]: ProgramArgs} = { + ['defender']: { + identifier: 'defender', + name: 'Windows Defender', + handlerUrl: 'http://localhost:4200/', + } +} + +@Component({ + selector: 'app-desktop', + templateUrl: './desktop.component.html', + styleUrls: ['./desktop.component.scss'] +}) +export class DesktopComponent implements OnInit { + @ViewChild('windows', {read: ViewContainerRef}) windowsRef: ViewContainerRef; + @ViewChild('taskbarIcons', {read: ViewContainerRef}) taskbarIconsRef: ViewContainerRef; + public static instance: DesktopComponent; + public static focusedWindow: WindowWrapper; + + time: string; + date: string; + + private taskbarIcons: {icon: TaskbarIcon, removeOnClose: boolean}[] = []; + + constructor(public cdr: ChangeDetectorRef) { } + + ngOnInit(): void { + DesktopComponent.instance = this; + setInterval(() => { + const dt = new Date(); + this.time = dt.toLocaleTimeString(); + this.date = dt.toLocaleDateString(); + }, 200); + + document.addEventListener('mousemove', this.mouseMove); + + setTimeout(() => { + this.addTaskbarIcon(programs['defender']); + }); + } + + public openProgram(programUUID: string) { + const program = programs[programUUID]; + const exists = this.getTaskbarIcon(programUUID) != undefined; + + if (!exists) + this.addTaskbarIcon(program, true); + + this.getTaskbarIcon(programUUID).icon.openProgram(); + } + + public addTaskbarIcon(program: ProgramArgs, removeOnClose: boolean = false, index?: number) { + const type: IconType = { + uuid: 1, + icon: program.handlerUrl + 'favicon.ico', + + program: program + }; + + const icon = this.taskbarIconsRef.createComponent(TaskbarIcon, {index}); + icon.instance.initialize(type); + this.taskbarIcons.push({icon: icon.instance, removeOnClose}); + } + + public removeTaskbarIcon(programUUID: string) { + const icon = this.getTaskbarIcon(programUUID)?.icon; + if (icon == undefined) return; + if (icon.windows.length > 0) return; + icon.object.nativeElement.parentElement.removeChild(icon.object.nativeElement); + this.taskbarIcons.slice(this.taskbarIcons.indexOf(this.getTaskbarIcon(programUUID)), 1); + } + + public getTaskbarIcon(programUUID: string): {icon: TaskbarIcon, removeOnClose: boolean} { + for (let icon of this.taskbarIcons) { + if (icon.icon.type.program.identifier == programUUID) + return icon; + } + return undefined; + } + + public onAllWindowsClosed(programUUID: string) { + const icon = this.getTaskbarIcon(programUUID); + if (icon?.removeOnClose) + this.removeTaskbarIcon(programUUID); + } + + public unfocusAll(caller?: WindowWrapper) { + for (let icon of this.taskbarIcons) { + for (let window of icon.icon.windows) { + if (window == caller || window == undefined) continue; + window.unfocus(); + } + } + } + + public getWindow(uuid: number): WindowWrapper { + for (let icon of this.taskbarIcons) { + for (let window of icon.icon.windows) { + if (window.uuid == uuid) return window; + } + } + + return undefined; + } + + public generateWindowUUID(): number { + let uuid = 0; + while (this.getWindow(uuid) != undefined) { + uuid++; + } + return uuid; + } + + public mouseMove(event: MouseEvent) { + DesktopComponent.focusedWindow?.onMove(event); + } + + public static get windowContainer(): ViewContainerRef { + return this.instance.windowsRef; + } +} diff --git a/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.html b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.html new file mode 100644 index 0000000..6f36e5c --- /dev/null +++ b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.html @@ -0,0 +1,18 @@ +
+
+ {{type.uuid}} + {{type.program.name}} +
+
+ +
+
+ {{type.uuid}} + {{instance.title}} + +
+
+
diff --git a/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.scss b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.scss new file mode 100644 index 0000000..3ea4561 --- /dev/null +++ b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.scss @@ -0,0 +1,129 @@ +@use "src/colors"; + +.icon-wrapper { + position: relative; + height: 40px; + width: 40px; + aspect-ratio: 1 / 1; + + .inner-wrapper { + width: 100%; + height: 100%; + padding: 5px; + box-sizing: border-box; + border-radius: 5px; + transition: background-color 200ms; + + img { + height: 30px; + width: auto; + aspect-ratio: 1 / 1; + transition: all 200ms; + + &.click { + height: 85%; + margin: 7.5%; + } + } + + &:hover { + background-color: rgba(colors.$light, 0.3); + + .icon-tooltip { + opacity: 1; + visibility: visible; + } + } + + .icon-tooltip { + position: absolute; + bottom: 52px; + display: block; + background-color: rgba(colors.$dark, 0.4); + color: var(--colors-text); + border-radius: 2px; + padding: 5px; + transition: opacity 100ms; + visibility: hidden; + opacity: 0; + width: max-content; + transform: translateX(-30%); + } + + + .icon-indicator { + --dot: 5px; + --wide: 15px; + + position: absolute; + width: 0; + height: 3px; + border-radius: 1.5px; + background-color: rgba(colors.$text, 0.4); + transition: width 300ms ease-out; + opacity: 0; + + top: 90%; + left: 50%; + transform: translate(-50%, 0); + } + } + + .instances { + display: flex; + flex-direction: column; + position: absolute; + bottom: 52px; + background-color: rgba(colors.$dark, 0.4); + color: colors.$text; + border-radius: 2px; + padding: 5px; + transition: opacity 100ms; + visibility: hidden; + opacity: 0; + width: max-content; + height: max-content; + transform: translateX(-30%); + gap: 5px; + max-width: 200px; + + div { + width: 100%; + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 5px; + overflow: hidden; + + &:hover { + background-color: rgba(colors.$light, 0.3); + } + + img { + height: 24px; + width: 24px; + flex-shrink: 0; + } + + span { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + cursor: default; + } + + button { + height: 24px; + width: 24px; + display: grid; + place-items: center; + flex-shrink: 0; + + mat-icon { + font-size: 20px; + transform: translateY(-8px); + } + } + } + } +} diff --git a/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.ts b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.ts new file mode 100644 index 0000000..77d6a35 --- /dev/null +++ b/Frontend/src/app/sites/desktop/taskbar-icon/taskbar-icon.component.ts @@ -0,0 +1,91 @@ +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {DesktopComponent, IconType} from "../desktop.component"; +import {WindowWrapper} from "../../../components/window-wrapper/window-wrapper.component"; + +@Component({ + selector: 'app-taskbar-icon', + templateUrl: './taskbar-icon.component.html', + styleUrls: ['./taskbar-icon.component.scss'] +}) +export class TaskbarIcon { + @ViewChild('indicator') indicator: ElementRef; + @ViewChild('instances') instances: ElementRef; + + public type: IconType; + public windows: WindowWrapper[] = []; + public instancesOpen: boolean = false; + + constructor(public object: ElementRef) { } + + public initialize(type: IconType) { + this.type = type; + } + + public openProgram() { + const window = DesktopComponent.windowContainer.createComponent(WindowWrapper); + window.instance.program = this.type.program; + DesktopComponent.instance.cdr.detectChanges(); + window.instance.initialize(this); + + this.windows.push(window.instance); + this.setIndicator('wide'); + } + + public onTaskbarClick(event: MouseEvent) { + if (this.instancesOpen) return; + + if (this.windows.length == 0 || event.shiftKey) { + this.openProgram(); + return; + } + + if (this.windows.length == 1) { + this.windows[0].toggleMinimized(); + return; + } + + this.instances.nativeElement.style.visibility = 'visible'; + this.instances.nativeElement.style.opacity = '1'; + this.instancesOpen = true; + } + + public updateInstances() { + if (this.windows.length <= 1) { + this.instances.nativeElement.style.visibility = 'hidden'; + this.instances.nativeElement.style.opacity = '0'; + this.instancesOpen = false; + } + } + + public setIndicator(mode: 'wide' | 'dot' | 'hidden') { + switch (mode) { + case "hidden": + this.indicator.nativeElement.style.opacity = '0'; + this.indicator.nativeElement.style.width = '0'; + break; + + case "dot": + this.indicator.nativeElement.style.opacity = '1'; + this.indicator.nativeElement.style.width = 'var(--dot)'; + break; + + case "wide": + this.indicator.nativeElement.style.opacity = '1'; + this.indicator.nativeElement.style.width = 'var(--wide)'; + break; + } + } + + public onClose(window: WindowWrapper) { + this.windows.splice(this.windows.indexOf(window), 1); + + if (this.windows.length > 0) + this.setIndicator('dot'); + else + this.setIndicator('hidden'); + + if (this.windows.length <= 0) + DesktopComponent.instance.onAllWindowsClosed(this.type.program.identifier); + } + +} diff --git a/Frontend/src/app/sites/login/login.component.ts b/Frontend/src/app/sites/login/login.component.ts index 5c19247..869fe7a 100644 --- a/Frontend/src/app/sites/login/login.component.ts +++ b/Frontend/src/app/sites/login/login.component.ts @@ -15,6 +15,7 @@ export class LoginComponent { constructor(private users: UserApi, private router: Router) { } public async login(form: HTMLFormElement, username: string, password: string) { + if (this.disableLogin) return; if (!form.reportValidity()) return; const login: UserLogin = {usernameOrEmail: username, password: password}; diff --git a/Frontend/src/app/sites/register/register.component.ts b/Frontend/src/app/sites/register/register.component.ts index eaada3e..e00343a 100644 --- a/Frontend/src/app/sites/register/register.component.ts +++ b/Frontend/src/app/sites/register/register.component.ts @@ -15,6 +15,7 @@ export class RegisterComponent { constructor(private users: UserApi, private router: Router) { } public async register(form: HTMLFormElement, register: UserEditor, pwRepeat: string) { + if (this.disableRegister) return; if (!form.reportValidity()) return; if (register.password !== pwRepeat) { diff --git a/Frontend/src/assets/icons/defender.png b/Frontend/src/assets/icons/defender.png new file mode 100644 index 0000000000000000000000000000000000000000..90bfe5fde07d585c7274a62cec7914a728abfb92 GIT binary patch literal 9195 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 literal 0 HcmV?d00001 diff --git a/Frontend/src/styles.scss b/Frontend/src/styles.scss index 8bd0938..fcac00f 100644 --- a/Frontend/src/styles.scss +++ b/Frontend/src/styles.scss @@ -10,6 +10,10 @@ $background: "/assets/background.png"; color: colors.$text; } +.unselectable { + user-select: none; +} + .glass { background: rgba(colors.$light, 0.15); backdrop-filter: blur(10px);