Skip to content

Commit 50e9bfe

Browse files
author
Sandeep
committed
orientation fixes
1 parent 1de45c7 commit 50e9bfe

File tree

4 files changed

+195
-15
lines changed

4 files changed

+195
-15
lines changed

src/app/components/chat/chat.component.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ import { ScreenShareService } from '../../services/screen-share.service';
229229
</div>
230230
</div>
231231
232-
<div class="chat-footer">
232+
<div class="chat-footer hide-on-landscape">
233233
Powered By <a href="https://www.publichome.page" target="_blank">www.publichome.page</a> All rights reserved. <a href="#">Terms & Policy</a>
234234
</div>
235235
</div>
@@ -246,6 +246,22 @@ import { ScreenShareService } from '../../services/screen-share.service';
246246
padding: 20px;
247247
max-width: 100%;
248248
box-sizing: border-box;
249+
transition: padding 0.3s;
250+
}
251+
252+
/* Aggressive mobile landscape optimizations */
253+
@media (max-width: 932px) and (orientation: landscape) {
254+
.app-shell {
255+
padding: 0 !important;
256+
}
257+
.hide-on-landscape {
258+
display: none !important;
259+
}
260+
.app-shell .chat-header,
261+
.app-shell .tab-bar,
262+
.app-shell .chat-footer {
263+
display: none !important;
264+
}
249265
}
250266
251267
/* ==============================
@@ -1074,9 +1090,33 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
10741090
if (e.matches && this.ss.status() === 'connected') {
10751091
this.activeTab.set('screen');
10761092
}
1093+
// When transitioning to desktop/landscape mode with screen share active,
1094+
// maximize the screen share panel automatically.
1095+
if (!e.matches && this.ss.status() === 'connected') {
1096+
this.desktopLayout.set('screen-full');
1097+
}
10771098
this.isMobile.set(e.matches);
10781099
};
10791100

1101+
@HostListener('window:orientationchange')
1102+
onOrientationChange(): void {
1103+
// Give the browser time to settle dimensions after rotation
1104+
setTimeout(() => {
1105+
if (this.ss.status() !== 'connected') return;
1106+
const isLandscape = window.matchMedia('(orientation: landscape)').matches;
1107+
if (isLandscape) {
1108+
if (this.isMobile()) {
1109+
// Still in mobile mode (narrow landscape) — ensure screen tab is active
1110+
// so the screen share panel is visible even though the tab bar is hidden
1111+
this.activeTab.set('screen');
1112+
} else {
1113+
// Desktop mode — maximize screen share panel
1114+
this.desktopLayout.set('screen-full');
1115+
}
1116+
}
1117+
}, 150);
1118+
}
1119+
10801120
ngAfterViewChecked(): void {
10811121
if (this.shouldScroll) {
10821122
this.scrollToBottom();

src/app/components/screen-share/screen-share.component.ts

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ElementRef, ViewChild, OnInit, AfterViewInit, computed, effect, signal } from '@angular/core';
1+
import { Component, ElementRef, ViewChild, OnInit, AfterViewInit, computed, effect, signal, HostListener } from '@angular/core';
22
import { FormsModule } from '@angular/forms';
33
import { CommonModule } from '@angular/common';
44
import { ScreenShareService } from '../../services/screen-share.service';
@@ -143,6 +143,13 @@ import { ScreenShareService } from '../../services/screen-share.service';
143143
</svg>
144144
<span class="btn-text">Fullscreen</span>
145145
</button>
146+
<button class="tool-btn hide-text-mobile" (click)="toggleRotate()" title="Rotate View" [class.active]="isRotated()">
147+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
148+
<polyline points="23 4 23 10 17 10"/>
149+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
150+
</svg>
151+
<span class="btn-text">Rotate</span>
152+
</button>
146153
<button class="tool-btn hide-text-mobile" (click)="ss.toggleScaleViewport()" title="Scale to Fit" [class.active]="ss.scaleViewport()">
147154
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
148155
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
@@ -151,13 +158,6 @@ import { ScreenShareService } from '../../services/screen-share.service';
151158
</svg>
152159
<span class="btn-text">Scale to Fit</span>
153160
</button>
154-
<button class="tool-btn hide-text-mobile" (click)="ss.sendCtrlAltDel()" title="Send Ctrl+Alt+Del">
155-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
156-
<rect x="2" y="4" width="20" height="16" rx="2"/>
157-
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h8M6 16h.01M18 16h.01"/>
158-
</svg>
159-
<span class="btn-text">Ctrl+Alt+Del</span>
160-
</button>
161161
<div class="toolbar-spacer"></div>
162162
<button class="tool-btn tool-btn-danger" (click)="disconnect()">
163163
Disconnect
@@ -658,6 +658,11 @@ export class ScreenShareComponent implements AfterViewInit {
658658

659659
readonly configOpen = signal(false);
660660
readonly showAll = signal(false);
661+
readonly isRotated = signal(false);
662+
663+
// True when fullscreen auto-rotated on mobile (so we undo it on exit).
664+
private autoRotatedForFullscreen = false;
665+
private readonly mobileQuery = window.matchMedia('(max-width: 767px)');
661666

662667
readonly isConnected = computed(() => this.ss.status() === 'connected');
663668
readonly isConnecting = computed(() => this.ss.status() === 'connecting');
@@ -732,20 +737,126 @@ export class ScreenShareComponent implements AfterViewInit {
732737
}
733738

734739
disconnect(): void {
740+
if (this.isRotated()) {
741+
this.isRotated.set(false);
742+
this.clearRotationStyles();
743+
}
735744
this.ss.disconnect();
736745
}
737746

738747
dismissError(): void {
739748
this.errorDismissed.set(true);
740749
}
741750

751+
@HostListener('window:resize')
752+
@HostListener('window:orientationchange')
753+
handleResize(): void {
754+
if (this.isConnected()) {
755+
setTimeout(() => {
756+
if (this.isRotated()) {
757+
this.applyRotationStyles();
758+
} else {
759+
this.ss.recalculateScaling();
760+
}
761+
}, 100);
762+
}
763+
}
764+
765+
@HostListener('document:fullscreenchange')
766+
onFullscreenChange(): void {
767+
if (document.fullscreenElement) {
768+
// ── Entering fullscreen ──────────────────────────────────────────────
769+
if (this.mobileQuery.matches && !this.isRotated()) {
770+
// Mobile only: auto-rotate if not already rotated.
771+
this.autoRotatedForFullscreen = true;
772+
this.isRotated.set(true);
773+
setTimeout(() => this.applyRotationStyles(), 150);
774+
} else {
775+
// Already rotated (any device) or desktop — just re-fit dimensions.
776+
setTimeout(() => {
777+
if (this.isRotated()) this.applyRotationStyles();
778+
else this.ss.recalculateScaling();
779+
}, 150);
780+
}
781+
} else {
782+
// ── Exiting fullscreen ───────────────────────────────────────────────
783+
if (this.autoRotatedForFullscreen) {
784+
// Undo the auto-rotation that fullscreen applied.
785+
this.autoRotatedForFullscreen = false;
786+
this.isRotated.set(false);
787+
this.clearRotationStyles();
788+
this.ss.recalculateScaling();
789+
} else {
790+
setTimeout(() => {
791+
if (this.isRotated()) this.applyRotationStyles();
792+
else this.ss.recalculateScaling();
793+
}, 150);
794+
}
795+
}
796+
}
797+
798+
toggleRotate(): void {
799+
this.isRotated.update(v => !v);
800+
setTimeout(() => {
801+
if (this.isRotated()) {
802+
this.applyRotationStyles();
803+
} else {
804+
this.clearRotationStyles();
805+
this.ss.recalculateScaling();
806+
}
807+
}, 50);
808+
}
809+
810+
private applyRotationStyles(): void {
811+
const container = this.vncContainer?.nativeElement;
812+
if (!container) return;
813+
const parent = container.parentElement!;
814+
815+
// When rotated, the element's layout box extends outside the parent bounds
816+
// (negative left offset). Allow it — the *visual* result (post-transform)
817+
// still fits inside. The ancestor main-layout provides the outer clip.
818+
parent.style.overflow = 'visible';
819+
820+
// In fullscreen, window.inner* gives the settled viewport size immediately;
821+
// parent.clientWidth/Height may not have updated yet when this runs.
822+
const inFullscreen = !!document.fullscreenElement;
823+
const pw = inFullscreen ? window.innerWidth : parent.clientWidth;
824+
const ph = inFullscreen ? window.innerHeight : parent.clientHeight;
825+
826+
container.style.position = 'absolute';
827+
container.style.width = ph + 'px';
828+
container.style.height = pw + 'px';
829+
container.style.top = ((ph - pw) / 2) + 'px';
830+
container.style.left = ((pw - ph) / 2) + 'px';
831+
container.style.transformOrigin = 'center center';
832+
container.style.transform = 'rotate(90deg)';
833+
this.ss.recalculateScaling();
834+
}
835+
836+
private clearRotationStyles(): void {
837+
const container = this.vncContainer?.nativeElement;
838+
if (!container) return;
839+
// Restore the parent's overflow before clearing container styles.
840+
if (container.parentElement) {
841+
container.parentElement.style.overflow = '';
842+
}
843+
container.style.position = '';
844+
container.style.width = '';
845+
container.style.height = '';
846+
container.style.top = '';
847+
container.style.left = '';
848+
container.style.transform = '';
849+
}
850+
742851
toggleFullscreen(): void {
743852
const container = this.vncContainer?.nativeElement;
744853
if (!container) return;
745854
if (document.fullscreenElement) {
746855
document.exitFullscreen();
747856
} else {
748-
container.requestFullscreen();
857+
// Fullscreen the *parent* (viewer-area) so the vncContainer's rotation
858+
// styles remain relative to a stable parent element throughout.
859+
(container.parentElement ?? container).requestFullscreen();
749860
}
750861
}
751862

src/app/components/settings/settings.component.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Component, EventEmitter, Output, signal } from '@angular/core';
1+
import { Component, EventEmitter, Output, effect, signal } from '@angular/core';
22
import { FormsModule } from '@angular/forms';
33
import { CommonModule } from '@angular/common';
4-
import { ConnectionConfig } from '../../services/openclaw.service';
4+
import { ConnectionConfig, OpenClawService } from '../../services/openclaw.service';
55

66
@Component({
77
selector: 'app-settings',
@@ -285,8 +285,21 @@ export class SettingsComponent {
285285
authToken = '';
286286
authPassword = '';
287287

288-
constructor() {
288+
constructor(private openClaw: OpenClawService) {
289289
this.loadFromStorage();
290+
291+
// Auto-close when connected; re-open on disconnect/error (only after a
292+
// successful connection so the panel doesn't pop on every fresh page load).
293+
let wasConnected = false;
294+
effect(() => {
295+
const s = this.openClaw.connectionStatus();
296+
if (s === 'connected') {
297+
wasConnected = true;
298+
this.isOpen.set(false);
299+
} else if (wasConnected && (s === 'disconnected' || s === 'error')) {
300+
this.isOpen.set(true);
301+
}
302+
});
290303
}
291304

292305
toggle(): void {

src/app/services/screen-share.service.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,24 @@ export class ScreenShareService {
8080
}
8181
}
8282

83-
sendCtrlAltDel(): void {
84-
this.rfb?.sendCtrlAltDel();
83+
84+
85+
/**
86+
* Forces the noVNC viewer to recalculate its scaling and viewport.
87+
* Useful when the device orientation changes or the container is resized.
88+
*/
89+
recalculateScaling(): void {
90+
if (this.rfb && this.scaleViewport()) {
91+
// Re-applying the scaleViewport property triggers noVNC's internal
92+
// scaling logic which recalculates based on the current container size.
93+
this.rfb.scaleViewport = false;
94+
// Small tick to ensure the DOM layout has settled
95+
setTimeout(() => {
96+
if (this.rfb) {
97+
this.rfb.scaleViewport = true;
98+
}
99+
}, 10);
100+
}
85101
}
86102

87103
async preloadNoVNC(): Promise<void> {

0 commit comments

Comments
 (0)