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' ;
22import { FormsModule } from '@angular/forms' ;
33import { CommonModule } from '@angular/common' ;
44import { 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
0 commit comments