diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index f01be1e91..6afa27588 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -107,7 +107,10 @@ 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 { SessionIFrameComponent } from './sessions/session/session-iframe/session-iframe.component';
import { SessionComponent } from './sessions/session/session.component';
+import { TilingWindowManagerComponent } from './sessions/session/tiling-window-manager/tiling-window-manager.component';
import { SessionOverviewComponent } from './sessions/session-overview/session-overview.component';
import { SessionsComponent } from './sessions/sessions.component';
import { ActiveSessionsComponent } from './sessions/user-sessions-wrapper/active-sessions/active-sessions.component';
@@ -172,6 +175,7 @@ import { SettingsComponent } from './settings/settings.component';
EventsComponent,
FileBrowserDialogComponent,
FileExistsDialogComponent,
+ FloatingWindowManagerComponent,
FooterComponent,
FormFieldSkeletonLoaderComponent,
GitSettingsComponent,
@@ -211,6 +215,7 @@ import { SettingsComponent } from './settings/settings.component';
ProjectWrapperComponent,
PureVariantsComponent,
SessionComponent,
+ SessionIFrameComponent,
SessionOverviewComponent,
SessionsComponent,
SettingsComponent,
@@ -220,6 +225,7 @@ import { SettingsComponent } from './settings/settings.component';
T4CSettingsComponent,
T4CSettingsWrapperComponent,
TextLineSkeletonLoaderComponent,
+ TilingWindowManagerComponent,
ToolDeletionDialogComponent,
ToolDetailsComponent,
ToolIntegrationsComponent,
diff --git a/frontend/src/app/schemes.ts b/frontend/src/app/schemes.ts
index 9e71d8461..cf37f6e06 100644
--- a/frontend/src/app/schemes.ts
+++ b/frontend/src/app/schemes.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { SafeResourceUrl } from '@angular/platform-browser';
import { User } from 'src/app/services/user/user.service';
import { ToolVersionWithTool } from 'src/app/settings/core/tools-settings/tool.service';
import { Project } from './projects/service/project.service';
@@ -24,9 +23,6 @@ export interface Session {
owner: User;
t4c_password: string;
download_in_progress: boolean;
- safeResourceURL?: SafeResourceUrl;
- focused: boolean;
- reloadToResize: boolean;
}
export interface ReadonlySession extends Session {
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..a9b0b19b2
--- /dev/null
+++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+ control_camera
+
+ {{ session.version?.tool?.name }} {{ session.version?.name }},
+ {{ session.type }}
+
+ (project {{ session.project!.name }})
+
+
+
+
+
+
+ Focused phonelink
+
+
+ Not focused
+ phonelink_off
+
+
+
+
+ fullscreen
+ fullscreen_exit
+
+
+
+
+
+
+
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..debd6543e
--- /dev/null
+++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Component, OnInit } from '@angular/core';
+import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
+import { debounceTime, fromEvent } from 'rxjs';
+import { SessionViewerService, ViewerSession } from '../session-viewer.service';
+
+@Component({
+ selector: 'app-floating-window-manager',
+ templateUrl: './floating-window-manager.component.html',
+})
+@UntilDestroy()
+export class FloatingWindowManagerComponent implements OnInit {
+ draggingActive = false;
+
+ constructor(public sessionViewerService: SessionViewerService) {}
+
+ ngOnInit(): void {
+ fromEvent(window, 'resize')
+ .pipe(untilDestroyed(this), debounceTime(250))
+ .subscribe(() => this.sessionViewerService.resizeSessions());
+ }
+
+ dragStart(): void {
+ this.draggingActive = true;
+ }
+
+ dragStop(): void {
+ this.draggingActive = false;
+ }
+
+ trackBySessionId(_: number, session: ViewerSession): string {
+ return session.id;
+ }
+}
diff --git a/frontend/src/app/sessions/session/session-iframe/session-iframe.component.css b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.css
new file mode 100644
index 000000000..229fd309c
--- /dev/null
+++ b/frontend/src/app/sessions/session/session-iframe/session-iframe.component.css
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+.iframe-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ z-index: 40;
+}
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
index 00d1f5573..24491c155 100644
--- a/frontend/src/app/sessions/session/session.component.css
+++ b/frontend/src/app/sessions/session/session.component.css
@@ -6,13 +6,3 @@
.height {
height: calc(100vh - 2vh - 65px - 110px);
}
-
-.iframe-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-
- z-index: 40;
-}
diff --git a/frontend/src/app/sessions/session/session.component.html b/frontend/src/app/sessions/session/session.component.html
index 5404bc475..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
@@ -49,6 +53,15 @@
server will not let you connect to the sessions. We're working on making
this possible.
+
+
+
+ Floating window manager
+
+ Tiling window manager
+
+
+
-
+
-
-
- control_camera
-
- {{ session.version?.tool?.name }} {{ session.version?.name }},
- {{ session.type }}
- (project {{ session.project!.name }})
-
-
-
- Focused phonelink
-
-
- Not focused
- phonelink_off
-
-
+
-
+
+
+
-
-
-
- fullscreen
- fullscreen_exit
+
-
-
+
+ fullscreen
+
+
+ fullscreen_exit
+
+
+
+
diff --git a/frontend/src/app/sessions/session/session.component.ts b/frontend/src/app/sessions/session/session.component.ts
index f4f23cd51..c1bd4f95d 100644
--- a/frontend/src/app/sessions/session/session.component.ts
+++ b/frontend/src/app/sessions/session/session.component.ts
@@ -3,17 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Component, HostListener, 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',
@@ -21,38 +18,42 @@ import { UserSessionService } from 'src/app/sessions/service/user-session.servic
styleUrls: ['./session.component.css'],
})
@UntilDestroy()
-export class SessionComponent implements OnInit {
+export class SessionComponent implements OnInit, OnDestroy {
cachedSessions?: CachedSession[] = undefined;
- selectedSessions: Session[] = [];
- private debounceTimer?: number;
- draggingActive = false;
+ 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);
}
- changeSessionSelection(event: MatCheckboxChange, session: CachedSession) {
- session.checked = event.checked;
+ get isTilingWindowManager(): boolean {
+ return this.selectedWindowType === 'tiling';
+ }
+
+ get isFloatingWindowManager(): boolean {
+ return this.selectedWindowType === 'floating';
}
ngOnInit(): void {
this.initializeCachedSessions();
+ }
- this.fullscreenService.isFullscreen$
- .pipe(untilDestroyed(this))
- .subscribe(() => this.resizeSessions());
+ ngOnDestroy(): void {
+ this.sessionViewerService.clearSessions();
}
initializeCachedSessions() {
@@ -71,77 +72,13 @@ 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);
}
});
}
-
- focusSession(session: Session) {
- this.unfocusSession(session);
-
- document.getElementById('session-' + session.id)?.focus();
- session.focused = true;
- }
-
- unfocusSession(focusedSession: Session) {
- this.selectedSessions
- .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.selectedSessions.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);
- }
}
type CachedSession = 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 }})
+
+
+
+
+
+ Focused phonelink
+
+
+ Not focused
+ phonelink_off
+
+
+
+
+
+
+ arrow_back
+
+
+
+ arrow_forward
+
+
+
+
+ fullscreen
+ fullscreen_exit
+
+
+
+
+
+
+
+
+
+
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;
+};