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 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
@@ -36,6 +31,9 @@
+
+
+
- {
+ "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"
]
}
-}]]>
+}
@@ -152,8 +150,22 @@
-
+
+
+
+
+
+
+
+
+ 1662545562560
+
+
+
+ 1662545562560
+
+
@@ -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 @@
+
+
+
+ {{title}}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+

+
+
+

+
+
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.program.name}}
+
+
+
+
+
+

+
{{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 0000000..90bfe5f
Binary files /dev/null and b/Frontend/src/assets/icons/defender.png differ
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);