From 56540c6431e0ac3dec2bc1f506cda9de64bea642 Mon Sep 17 00:00:00 2001 From: dominik003 Date: Tue, 31 Oct 2023 14:14:15 +0100 Subject: [PATCH 1/2] refactor: Create floating window mgr component --- frontend/src/app/app.module.ts | 2 + .../floating-window-manager.component.css} | 0 .../floating-window-manager.component.html | 59 +++++++++++++ .../floating-window-manager.component.ts | 83 +++++++++++++++++++ .../sessions/session/session.component.html | 65 +++------------ .../app/sessions/session/session.component.ts | 76 +++-------------- 6 files changed, 166 insertions(+), 119 deletions(-) rename frontend/src/app/sessions/session/{session.component.css => floating-window-manager/floating-window-manager.component.css} (100%) create mode 100644 frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html create mode 100644 frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f01be1e91..99d8fcf77 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -107,6 +107,7 @@ import { ProjectOverviewComponent } from './projects/project-overview/project-ov import { ProjectWrapperComponent } from './projects/project-wrapper/project-wrapper.component'; import { WhitespaceUrlInterceptor } from './services/encoder/encoder.interceptor'; import { DeleteSessionDialogComponent } from './sessions/delete-session-dialog/delete-session-dialog.component'; +import { FloatingWindowManagerComponent } from './sessions/session/floating-window-manager/floating-window-manager.component'; import { SessionComponent } from './sessions/session/session.component'; import { SessionOverviewComponent } from './sessions/session-overview/session-overview.component'; import { SessionsComponent } from './sessions/sessions.component'; @@ -172,6 +173,7 @@ import { SettingsComponent } from './settings/settings.component'; EventsComponent, FileBrowserDialogComponent, FileExistsDialogComponent, + FloatingWindowManagerComponent, FooterComponent, FormFieldSkeletonLoaderComponent, GitSettingsComponent, diff --git a/frontend/src/app/sessions/session/session.component.css b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css similarity index 100% rename from frontend/src/app/sessions/session/session.component.css rename to frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html new file mode 100644 index 000000000..f82e079b1 --- /dev/null +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html @@ -0,0 +1,59 @@ + + +
+
+
+
+ control_camera + + {{ session.version?.tool?.name }} {{ session.version?.name }}, + {{ session.type }} + (project {{ session.project!.name }}) + +
+
+ Focusedphonelink +
+
+ Not focused + phonelink_off +
+
+ +
+
+
+ +
+
+
diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts new file mode 100644 index 000000000..dfdc2cb37 --- /dev/null +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Session } from 'src/app/schemes'; +import { FullscreenService } from '../../service/fullscreen.service'; + +@Component({ + selector: 'app-floating-window-manager', + templateUrl: './floating-window-manager.component.html', + styleUrls: ['./floating-window-manager.component.css'], +}) +@UntilDestroy() +export class FloatingWindowManagerComponent implements OnInit { + @Input() sessions: Session[] = []; + + draggingActive = false; + + private debounceTimer?: number; + + constructor(public fullscreenService: FullscreenService) {} + + ngOnInit(): void { + this.fullscreenService.isFullscreen$ + .pipe(untilDestroyed(this)) + .subscribe(() => this.resizeSessions()); + } + + focusSession(session: Session) { + this.unfocusSession(session); + + document.getElementById('session-' + session.id)?.focus(); + session.focused = true; + } + + unfocusSession(focusedSession: Session) { + this.sessions + .filter((session) => session !== focusedSession) + .map((session) => (session.focused = false)); + } + + dragStart() { + this.draggingActive = true; + } + + dragStop() { + this.draggingActive = false; + } + + @HostListener('window:resize', ['$event']) + onResize() { + window.clearTimeout(this.debounceTimer); + + this.debounceTimer = window.setTimeout(() => { + this.resizeSessions(); + }, 250); + } + + resizeSessions() { + Array.from(document.getElementsByTagName('iframe')).forEach((iframe) => { + const session = this.sessions.find( + (session) => 'session-' + session.id === iframe.id, + ); + + if (session?.reloadToResize) { + this.reloadIFrame(iframe); + } + }); + } + + reloadIFrame(iframe: HTMLIFrameElement) { + const src = iframe.src; + + iframe.removeAttribute('src'); + + setTimeout(() => { + iframe.src = src; + }, 100); + } +} diff --git a/frontend/src/app/sessions/session/session.component.html b/frontend/src/app/sessions/session/session.component.html index 5404bc475..1485192ff 100644 --- a/frontend/src/app/sessions/session/session.component.html +++ b/frontend/src/app/sessions/session/session.component.html @@ -49,6 +49,13 @@ server will not let you connect to the sessions. We're working on making this possible. +
+ + Floating window + Tailing window + +
+
-
-
-
-
- control_camera - - {{ session.version?.tool?.name }} {{ session.version?.name }}, - {{ session.type }} - (project {{ session.project!.name }}) - -
-
- Focusedphonelink -
-
- Not focused - phonelink_off -
-
- -
-
-
- -
-
-
+
+
+ + - + diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts index dfdc2cb37..debd6543e 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts @@ -3,81 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { Session } from 'src/app/schemes'; -import { FullscreenService } from '../../service/fullscreen.service'; +import { debounceTime, fromEvent } from 'rxjs'; +import { SessionViewerService, ViewerSession } from '../session-viewer.service'; @Component({ selector: 'app-floating-window-manager', templateUrl: './floating-window-manager.component.html', - styleUrls: ['./floating-window-manager.component.css'], }) @UntilDestroy() export class FloatingWindowManagerComponent implements OnInit { - @Input() sessions: Session[] = []; - draggingActive = false; - private debounceTimer?: number; - - constructor(public fullscreenService: FullscreenService) {} + constructor(public sessionViewerService: SessionViewerService) {} ngOnInit(): void { - this.fullscreenService.isFullscreen$ - .pipe(untilDestroyed(this)) - .subscribe(() => this.resizeSessions()); - } - - focusSession(session: Session) { - this.unfocusSession(session); - - document.getElementById('session-' + session.id)?.focus(); - session.focused = true; - } - - unfocusSession(focusedSession: Session) { - this.sessions - .filter((session) => session !== focusedSession) - .map((session) => (session.focused = false)); + fromEvent(window, 'resize') + .pipe(untilDestroyed(this), debounceTime(250)) + .subscribe(() => this.sessionViewerService.resizeSessions()); } - dragStart() { + dragStart(): void { this.draggingActive = true; } - dragStop() { + dragStop(): void { this.draggingActive = false; } - @HostListener('window:resize', ['$event']) - onResize() { - window.clearTimeout(this.debounceTimer); - - this.debounceTimer = window.setTimeout(() => { - this.resizeSessions(); - }, 250); - } - - resizeSessions() { - Array.from(document.getElementsByTagName('iframe')).forEach((iframe) => { - const session = this.sessions.find( - (session) => 'session-' + session.id === iframe.id, - ); - - if (session?.reloadToResize) { - this.reloadIFrame(iframe); - } - }); - } - - reloadIFrame(iframe: HTMLIFrameElement) { - const src = iframe.src; - - iframe.removeAttribute('src'); - - setTimeout(() => { - iframe.src = src; - }, 100); + trackBySessionId(_: number, session: ViewerSession): string { + return session.id; } } diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.css similarity index 81% rename from frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css rename to frontend/src/app/sessions/session/session-iframe/session-iframe.component.css index 00d1f5573..229fd309c 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css +++ b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.css @@ -3,10 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -.height { - height: calc(100vh - 2vh - 65px - 110px); -} - .iframe-overlay { position: absolute; top: 0; diff --git a/frontend/src/app/sessions/session/session-iframe/session-iframe.component.html b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.html new file mode 100644 index 000000000..c304f291f --- /dev/null +++ b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.html @@ -0,0 +1,25 @@ + + +
+
+ +
diff --git a/frontend/src/app/sessions/session/session-iframe/session-iframe.component.ts b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.ts new file mode 100644 index 000000000..577cbec71 --- /dev/null +++ b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, Input } from '@angular/core'; +import { ViewerSession } from '../session-viewer.service'; + +@Component({ + selector: 'app-session-iframe', + templateUrl: './session-iframe.component.html', + styleUrls: ['./session-iframe.component.css'], +}) +export class SessionIFrameComponent { + @Input({ required: true }) session!: ViewerSession; +} diff --git a/frontend/src/app/sessions/session/session-viewer.service.ts b/frontend/src/app/sessions/session/session-viewer.service.ts new file mode 100644 index 000000000..8adc3df87 --- /dev/null +++ b/frontend/src/app/sessions/session/session-viewer.service.ts @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { BehaviorSubject, map } from 'rxjs'; +import { LocalStorageService } from 'src/app/general/auth/local-storage/local-storage.service'; +import { Session } from 'src/app/schemes'; +import { GuacamoleService } from 'src/app/services/guacamole/guacamole.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionViewerService { + constructor( + private guacamoleService: GuacamoleService, + private localStorageService: LocalStorageService, + private domSanitizer: DomSanitizer, + ) {} + + private _sessions = new BehaviorSubject( + undefined, + ); + + public readonly sessions$ = this._sessions.pipe( + map((sessions) => { + const fullscreenSession = sessions?.find((session) => session.fullscreen); + return fullscreenSession ? [fullscreenSession] : sessions; + }), + ); + + pushJupyterSession(session: Session): void { + if (session.jupyter_uri) { + const viewerSession = session as ViewerSession; + + viewerSession.focused = false; + viewerSession.safeResourceURL = + this.domSanitizer.bypassSecurityTrustResourceUrl(session.jupyter_uri); + viewerSession.reloadToResize = false; + + this.insertViewerSession(viewerSession); + } + } + + pushGuacamoleSession(session: Session): void { + const viewerSession = session as ViewerSession; + + this.guacamoleService.getGucamoleToken(session.id).subscribe({ + next: (guacamoleAuthInfo) => { + this.localStorageService.setValue('GUAC_AUTH', guacamoleAuthInfo.token); + + viewerSession.focused = false; + viewerSession.safeResourceURL = + this.domSanitizer.bypassSecurityTrustResourceUrl( + guacamoleAuthInfo.url, + ); + viewerSession.reloadToResize = true; + + this.insertViewerSession(viewerSession); + }, + }); + } + + focusSession(session: Session): void { + document.getElementById('session-' + session.id)?.focus(); + + const updatedSessions = this._sessions.value?.map((curSession) => ({ + ...curSession, + focused: session.id === curSession.id, + })); + + this._sessions.next(updatedSessions); + } + + resizeSessions(): void { + document.querySelectorAll('iframe').forEach((iframe) => { + const session = this._sessions.value?.find( + (session) => `session-${session.id}` === iframe.id, + ); + + if (session?.reloadToResize) { + this.reloadIFrame(iframe); + } + }); + } + + resizeSession(session: ViewerSession): void { + const sessionIFrame = document.querySelector( + `iframe#session-${session.id}`, + ); + + if (sessionIFrame && session.reloadToResize) { + this.reloadIFrame(sessionIFrame); + } + } + + toggleFullscreen(session: ViewerSession): void { + const updatedSessions = this._sessions.value?.map((curSession) => { + if (session.id === curSession.id) { + return { ...curSession, fullscreen: !curSession.fullscreen }; + } + return curSession; + }); + this._sessions.next(updatedSessions); + this.resizeSession(session); + } + + clearSessions(): void { + this._sessions.next(undefined); + } + + private reloadIFrame(iframe: HTMLIFrameElement) { + const src = iframe.src; + + iframe.removeAttribute('src'); + + setTimeout(() => { + iframe.src = src; + }, 100); + } + + private insertViewerSession(viewerSession: ViewerSession): void { + const currentSessions = this._sessions.value; + + if (currentSessions === undefined) { + this._sessions.next([viewerSession]); + } else { + this._sessions.next([...currentSessions, viewerSession]); + } + } +} + +export type ViewerSession = Session & { + safeResourceURL?: SafeResourceUrl; + focused: boolean; + reloadToResize: boolean; + fullscreen: boolean; +}; diff --git a/frontend/src/app/sessions/session/session.component.css b/frontend/src/app/sessions/session/session.component.css new file mode 100644 index 000000000..24491c155 --- /dev/null +++ b/frontend/src/app/sessions/session/session.component.css @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.height { + height: calc(100vh - 2vh - 65px - 110px); +} diff --git a/frontend/src/app/sessions/session/session.component.html b/frontend/src/app/sessions/session/session.component.html index 1485192ff..56d1a4ea5 100644 --- a/frontend/src/app/sessions/session/session.component.html +++ b/frontend/src/app/sessions/session/session.component.html @@ -3,11 +3,15 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -
- Please select the sessions that you'd like to open in the session - viewer: +
+ + Please select the sessions that you'd like to open in the session viewer: +
No sessions found. Please create a session in the 'Sessions' tab first.
@@ -33,7 +37,7 @@ matTooltip="Please wait until the session is running. You can check the status in the 'Sessions' tab." [matTooltipDisabled]="sessionService.beautifyState(session.state).success" [value]="session.id" - (change)="changeSessionSelection($event, session)" + [(ngModel)]="session.checked" *ngFor="let session of cachedSessions" >{{ session.version?.tool?.name }} {{ session.version?.name }}, {{ session.type @@ -51,8 +55,10 @@
- Floating window - Tailing window + + Floating window manager + + Tiling window manager
@@ -69,22 +75,36 @@
- - -
- -
+ + fullscreen + + + fullscreen_exit + + + + diff --git a/frontend/src/app/sessions/session/session.component.ts b/frontend/src/app/sessions/session/session.component.ts index d2350426a..c1bd4f95d 100644 --- a/frontend/src/app/sessions/session/session.component.ts +++ b/frontend/src/app/sessions/session/session.component.ts @@ -3,58 +3,59 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, OnInit } from '@angular/core'; -import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox'; -import { DomSanitizer } from '@angular/platform-browser'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { filter, take } from 'rxjs'; -import { LocalStorageService } from 'src/app/general/auth/local-storage/local-storage.service'; import { Session } from 'src/app/schemes'; -import { GuacamoleService } from 'src/app/services/guacamole/guacamole.service'; import { FullscreenService } from 'src/app/sessions/service/fullscreen.service'; import { SessionService } from 'src/app/sessions/service/session.service'; import { UserSessionService } from 'src/app/sessions/service/user-session.service'; +import { SessionViewerService } from './session-viewer.service'; @Component({ selector: 'app-session', templateUrl: './session.component.html', + styleUrls: ['./session.component.css'], }) -export class SessionComponent implements OnInit { +@UntilDestroy() +export class SessionComponent implements OnInit, OnDestroy { cachedSessions?: CachedSession[] = undefined; - selectedSessions: Session[] = []; selectedWindowType: string = 'floating'; constructor( public userSessionService: UserSessionService, public sessionService: SessionService, + public sessionViewerService: SessionViewerService, public fullscreenService: FullscreenService, - private guacamoleService: GuacamoleService, - private localStorageService: LocalStorageService, - private domSanitizer: DomSanitizer, ) { this.userSessionService.loadSessions(); + + this.fullscreenService.isFullscreen$ + .pipe(untilDestroyed(this)) + .subscribe(() => this.sessionViewerService.resizeSessions()); } get checkedSessions(): undefined | CachedSession[] { return this.cachedSessions?.filter((session) => session.checked); } - get isTailingWindow(): boolean { - return this.selectedWindowType === 'tailing'; + get isTilingWindowManager(): boolean { + return this.selectedWindowType === 'tiling'; } - get isFloatingWindow() { + get isFloatingWindowManager(): boolean { return this.selectedWindowType === 'floating'; } - changeSessionSelection(event: MatCheckboxChange, session: CachedSession) { - session.checked = event.checked; - } - ngOnInit(): void { this.initializeCachedSessions(); } + ngOnDestroy(): void { + this.sessionViewerService.clearSessions(); + } + initializeCachedSessions() { this.userSessionService.sessions$ .pipe( @@ -71,20 +72,10 @@ export class SessionComponent implements OnInit { selectSessions() { this.checkedSessions?.forEach((session) => { - session.focused = false; if (session.jupyter_uri) { - session.safeResourceURL = - this.domSanitizer.bypassSecurityTrustResourceUrl(session.jupyter_uri); - session.reloadToResize = false; - this.selectedSessions.push(session); + this.sessionViewerService.pushJupyterSession(session); } else { - this.guacamoleService.getGucamoleToken(session?.id).subscribe((res) => { - this.localStorageService.setValue('GUAC_AUTH', res.token); - session.safeResourceURL = - this.domSanitizer.bypassSecurityTrustResourceUrl(res.url); - session.reloadToResize = true; - this.selectedSessions.push(session); - }); + this.sessionViewerService.pushGuacamoleSession(session); } }); } diff --git a/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html new file mode 100644 index 000000000..d516f4918 --- /dev/null +++ b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html @@ -0,0 +1,78 @@ + + +
+ +
+
+ + {{ session.version?.tool?.name }} {{ session.version?.name }}, + {{ session.type }} + + (project {{ session.project!.name }}) + + + +
+
+ Focusedphonelink +
+
+ Not focused + phonelink_off +
+
+ +
+
+ + + +
+ + +
+
+ + +
+ +
+
+
diff --git a/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts new file mode 100644 index 000000000..a061060b8 --- /dev/null +++ b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, HostListener, OnInit } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { filter } from 'rxjs'; +import { SessionViewerService, ViewerSession } from '../session-viewer.service'; + +@Component({ + selector: 'app-tiling-window-manager', + templateUrl: './tiling-window-manager.component.html', +}) +@UntilDestroy() +export class TilingWindowManagerComponent implements OnInit { + private _tilingSessions: TilingSession[] = []; + + get sessions(): TilingSession[] { + return this._tilingSessions.sort((a, b) => a.index - b.index); + } + + public resizeState: ResizeState = {}; + + private minimumSessionWidth = 0; + + constructor(public sessionViewerService: SessionViewerService) {} + + ngOnInit(): void { + this.sessionViewerService.sessions$ + .pipe(untilDestroyed(this), filter(Boolean)) + .subscribe((viewerSessions) => { + const sessionMap = new Map(viewerSessions.map((s) => [s.id, s])); + + const hasSessionDifference = + this._tilingSessions.length !== viewerSessions.length || + viewerSessions.some( + (session) => + !this._tilingSessions.some((ts) => ts.id === session.id), + ); + + if (hasSessionDifference) { + this._tilingSessions = viewerSessions.map((session, index) => ({ + ...session, + index: index, + width: -1, + fullscreen: false, + })); + this.resetWidths(); + } else { + this._tilingSessions = this._tilingSessions.map((tilingSession) => { + const sessionUpdate = sessionMap.get(tilingSession.id); + return sessionUpdate + ? { ...tilingSession, ...sessionUpdate } + : tilingSession; + }); + } + }); + } + + onMouseDown(event: MouseEvent, index: number): void { + const leftSession = this.getSessionByIndex(index); + const rightSession = this.getSessionByIndex(index + 1); + + if (leftSession && rightSession) { + this.resizeState = { + index: index, + startX: event.clientX, + leftSession: leftSession, + rightSession: rightSession, + startWidthLeft: leftSession.width, + startWidthRight: rightSession.width, + }; + } + } + + @HostListener('window:mousemove', ['$event']) + onMouseMove(event: MouseEvent): void { + if (this.isValidResizeState(this.resizeState)) { + const delta = event.clientX - this.resizeState.startX; + const [newWidthLeft, newWidthRight] = this.calculateNewWidths( + this.resizeState, + delta, + ); + + this.resizeState.leftSession.width = newWidthLeft; + this.resizeState.rightSession.width = newWidthRight; + } + } + + @HostListener('window:mouseup') + onMouseUp(): void { + if (this.isValidResizeState(this.resizeState)) { + this.resizeState = {}; + this.sessionViewerService.resizeSessions(); + } + } + + @HostListener('window:resize') + onResize() { + this.resetWidths(); + } + + onLeftArrowClick(session: TilingSession) { + const leftIndexSession = this.getSessionByIndex(session.index - 1); + + if (leftIndexSession) { + this.swapSessions(session, leftIndexSession); + } + } + + onRightArrowClick(session: TilingSession) { + const rightIndexSession = this.getSessionByIndex(session.index + 1); + + if (rightIndexSession) { + this.swapSessions(session, rightIndexSession); + } + } + + trackBySessionIndexAndId(_: number, session: TilingSession): string { + return `${session.index}-${session.id}`; + } + + isValidResizeState(state: ResizeState): state is ValidResizeState { + return ( + state.index !== undefined && + state.startX !== undefined && + state.leftSession !== undefined && + state.rightSession !== undefined && + state.startWidthLeft !== undefined && + state.startWidthRight !== undefined + ); + } + + private calculateNewWidths( + validResizeState: ValidResizeState, + delta: number, + ): [number, number] { + let newWidthLeft = validResizeState.startWidthLeft + delta; + let newWidthRight = validResizeState.startWidthRight - delta; + + [newWidthLeft, newWidthRight] = this.adjustWidthsWithMinimum( + validResizeState, + newWidthLeft, + newWidthRight, + ); + return [newWidthLeft, newWidthRight]; + } + + private adjustWidthsWithMinimum( + validResizeState: ValidResizeState, + newWidthLeft: number, + newWidthRight: number, + ): [number, number] { + if (newWidthLeft < this.minimumSessionWidth) { + newWidthLeft = this.minimumSessionWidth; + newWidthRight = + validResizeState.startWidthLeft + + validResizeState.startWidthRight - + newWidthLeft; + } else if (newWidthRight < this.minimumSessionWidth) { + newWidthRight = this.minimumSessionWidth; + newWidthLeft = + validResizeState.startWidthLeft + + validResizeState.startWidthRight - + newWidthRight; + } + return [newWidthLeft, newWidthRight]; + } + + private resetWidths() { + this.minimumSessionWidth = window.innerWidth * 0.15; + this._tilingSessions.forEach( + (session) => + (session.width = window.innerWidth / this._tilingSessions.length), + ); + this.sessionViewerService.resizeSessions(); + } + + private getSessionByIndex(index: number): TilingSession | undefined { + return this._tilingSessions.find((session) => session.index === index); + } + + private swapSessions( + firstSession: TilingSession, + secondSession: TilingSession, + ) { + const firstSessionIndex = firstSession.index; + const firstSessionWidth = firstSession.width; + + firstSession.index = secondSession.index; + firstSession.width = secondSession.width; + + secondSession.index = firstSessionIndex; + secondSession.width = firstSessionWidth; + } +} + +type ResizeState = { + index?: number; + startX?: number; + leftSession?: TilingSession; + rightSession?: TilingSession; + startWidthLeft?: number; + startWidthRight?: number; +}; + +type ValidResizeState = Required; + +type TilingSession = ViewerSession & { + index: number; + width: number; + fullscreen: boolean; +};