fix: enhance Modern UI mobile styles, layout, and UX issues (#7793)

* fix: fix Modern UI mobile styles * fix: update mobile styles for better responsiveness and button spacing * fix: enhance mobile file action buttons and dropdown for better usability * fix: Modern UI mobile UX - touch input, tab nav, layout, fullscreen * fix: restore masthead overflow:visible so user dropdown isn't clipped * fix: enhance toolbar menu usability for mobile view, by adding a more buttons My Users and My Files section * fix: fix mobile action toolbar wrapping in modern UI * fix: Modern UI mobile UX - layout, tab nav, panel titles, dark mode * fix: update mobile UI to use "More" button for actions and settings * fix: enhance mobile UI styling for device and panel titles, improve dropdown button appearance * fix: correct mouse event handling for rotated canvas in desktop view * fix: improve mobile UI keyboard shortcut handling and enhance action panel * fix: add mobile soft-keyboard toggle button and input handling * fix: implement trackpad mode for modern ui mobile remote desktop interaction * fix: add right-click mode toggle for modern ui mobile desktop actions * fix: hide desktop More button on non-mobile browsers * fix: pin trackpad cursor hot-spot to transform-origin on screen rotation * fix: mobile remote desktop improvements for Modern UI * fix: enhance mobile UI responsiveness and dropdown behavior for trackpad and right-click modes --------- Co-authored-by: Sammy Ndabo <sammy.ndabo@evoludata.com>

Bruno Kambere committed May 27, 2026 at 23:00 UTC 3860c1d3d4e6e207da4bb33d2af36dab6afd037f
2 files changed +1958 -114
public/styles/style-bootstrap.css
+1090 -38
@@ -1411,6 +1411,11 @@ NoMeshesPanel img {
1411 background-color: #DDD;
1412 }
1413
1414 +.mobileFileActionDropdown,
1415 +.mobileActionDropdown {
1416 + display: none !important;
1417 +}
1418 +
1419 .pwState {
1420 display: table-cell;
1421 height: 16px;
@@ -3526,18 +3531,993 @@ body:not(.fullscreen) .notifiyBox {
3531 display: none !important;
3532 }
3533
3529 -/* hide .sidebar when on mobile */
3534 +body.is-mobile #page_leftbar { display: none !important; }
3535 +
3536 +/* Device list: separator between items */
3537 +/* List view (xxdevice2) rows — add padding and a subtle divider line */
3538 +body.is-mobile #xdevices tr[name="xxdevice2"] > td:first-child {
3539 + padding-top: 8px !important;
3540 + padding-bottom: 8px !important;
3541 + border-bottom: 1px solid rgba(0,0,0,0.08) !important;
3542 +}
3543 +/* Group-header divs that are direct children of xdevices */
3544 +body.is-mobile #xdevices > div { margin-top: 10px !important; }
3545 +
3546 +/* p5toolbarBottom: same transparent fix as p13toolbarBottom */
3547 +#p5toolbarBottom .style6 { background: transparent !important; }
3548 +
3549 +/* All nav title bars (no back button): same style as device sub-panels
3550 + but with left padding instead of the blue back button.
3551 + Covers: My Devices(1) | My Events(3) | My Users(4) | My Files(5) |
3552 + My Server(6) | Stats(40) | Trace(41) | Plugins(42) |
3553 + User Groups(50) | Recordings(52) | My Reports(60) */
3554 +body.is-mobile #p1title,
3555 +body.is-mobile #p3title, body.is-mobile #p4title,
3556 +body.is-mobile #p5title, body.is-mobile #p6title,
3557 +body.is-mobile #p40title, body.is-mobile #p41title,
3558 +body.is-mobile #p42title, body.is-mobile #p50title,
3559 +body.is-mobile #p52title, body.is-mobile #p60title {
3560 + padding: 0 12px 0 15px !important;
3561 + background: #fff;
3562 + min-height: 50px;
3563 + position: relative !important;
3564 + z-index: 1 !important;
3565 + box-shadow: 0 1px 0 0 rgba(0,0,0,0.15);
3566 + align-items: center !important;
3567 + display: flex !important;
3568 +}
3569 +body.night.is-mobile #p1title,
3570 +body.night.is-mobile #p3title, body.night.is-mobile #p4title,
3571 +body.night.is-mobile #p5title, body.night.is-mobile #p6title,
3572 +body.night.is-mobile #p40title, body.night.is-mobile #p41title,
3573 +body.night.is-mobile #p42title, body.night.is-mobile #p50title,
3574 +body.night.is-mobile #p52title, body.night.is-mobile #p60title {
3575 + box-shadow: 0 1px 0 0 #444 !important;
3576 +}
3577 +
3578 +/* My Server: server icon small, right-aligned */
3579 +body.is-mobile #MainMeshImage {
3580 + width: 32px !important;
3581 + height: 32px !important;
3582 + object-fit: contain;
3583 + border-radius: 6px;
3584 + flex-shrink: 0;
3585 +}
3586 +
3587 +/* "More" dropdowns: mobileActionDropdown (My Users/Files) +
3588 + mobileFileActionDropdown (Device Files tab) — shared styling
3589 + Pill shape, item dividers, proper light/dark colours. */
3590 +.mobileActionDropdown .dropdown-menu,
3591 +.mobileFileActionDropdown .dropdown-menu {
3592 + border-radius: 12px !important;
3593 + overflow: hidden !important;
3594 + box-shadow: 0 4px 24px rgba(0,0,0,0.28) !important;
3595 + padding: 0 !important;
3596 + min-width: 200px !important;
3597 +}
3598 +.mobileActionDropdown .dropdown-item,
3599 +.mobileFileActionDropdown .dropdown-item {
3600 + padding: 10px 16px !important;
3601 + font-size: 15px !important;
3602 + border-bottom: 1px solid rgba(0,0,0,0.08) !important;
3603 +}
3604 +.mobileActionDropdown .dropdown-item:last-child,
3605 +.mobileFileActionDropdown .dropdown-item:last-child { border-bottom: none !important; }
3606 +/* Dark mode */
3607 +.night .mobileActionDropdown .dropdown-menu,
3608 +.night .mobileFileActionDropdown .dropdown-menu {
3609 + background: #1c1c1e !important;
3610 + border-color: #3a3a3c !important;
3611 +}
3612 +.night .mobileActionDropdown .dropdown-item,
3613 +.night .mobileFileActionDropdown .dropdown-item {
3614 + color: #e5e5e7 !important;
3615 + border-bottom-color: rgba(255,255,255,0.1) !important;
3616 + background: transparent !important;
3617 +}
3618 +.night .mobileActionDropdown .dropdown-item:hover,
3619 +.night .mobileActionDropdown .dropdown-item:focus,
3620 +.night .mobileFileActionDropdown .dropdown-item:hover,
3621 +.night .mobileFileActionDropdown .dropdown-item:focus {
3622 + background: rgba(255,255,255,0.08) !important;
3623 + color: #fff !important;
3624 +}
3625 +
3626 +/* Title bar text: consistent 1rem across ALL panel titles
3627 + One rule covers both device sub-panels (p10–p19) and main-nav panels
3628 + (p3–p6) so font size, colour and line-height are identical everywhere. */
3629 +body.is-mobile #p3title .fs-4, body.is-mobile #p4title .fs-4,
3630 +body.is-mobile #p5title .fs-4, body.is-mobile #p6title .fs-4,
3631 +body.is-mobile #p10title .fs-4, body.is-mobile #p11title .fs-4,
3632 +body.is-mobile #p12title .fs-4, body.is-mobile #p13title .fs-4,
3633 +body.is-mobile #p14title .fs-4, body.is-mobile #p15title .fs-4,
3634 +body.is-mobile #p16title .fs-4, body.is-mobile #p17title .fs-4,
3635 +body.is-mobile #p19title .fs-4 {
3636 + font-size: 1rem !important;
3637 + color: #212529 !important;
3638 + line-height: 1.25 !important;
3639 + font-weight: 600 !important;
3640 +}
3641 +
3642 +/* All main-nav panels: flex-column so content fills height
3643 + Covers My Events(3), My Users(4), My Files(5), My Server(6),
3644 + Server Stats(40)/Trace(41)/Plugins(42),
3645 + User Groups(50)/Recordings(52), My Reports(60) */
3646 +body.is-mobile #p1,
3647 +body.is-mobile #p3, body.is-mobile #p4,
3648 +body.is-mobile #p5, body.is-mobile #p6,
3649 +body.is-mobile #p20, body.is-mobile #p21,
3650 +body.is-mobile #p30, body.is-mobile #p31,
3651 +body.is-mobile #p40, body.is-mobile #p41,
3652 +body.is-mobile #p42, body.is-mobile #p50,
3653 +body.is-mobile #p51, body.is-mobile #p52,
3654 +body.is-mobile #p60 {
3655 + display: flex;
3656 + flex-direction: column;
3657 + height: 100% !important;
3658 + overflow: hidden !important;
3659 +}
3660 +body.is-mobile #p1title, body.is-mobile #p3title,
3661 +body.is-mobile #p4title, body.is-mobile #p5title,
3662 +body.is-mobile #p40title, body.is-mobile #p41title,
3663 +body.is-mobile #p42title, body.is-mobile #p50title,
3664 +body.is-mobile #p52title, body.is-mobile #p60title { flex-shrink: 0 !important; }
3665 +/* Events list fills remaining space */
3666 +body.is-mobile #p3events {
3667 + flex: 1 1 0 !important;
3668 + min-height: 0 !important;
3669 + overflow-y: auto !important;
3670 + height: auto !important;
3671 + max-height: none !important;
3672 +}
3673 +/* Users list fills remaining space */
3674 +body.is-mobile #p3users {
3675 + flex: 1 1 0 !important;
3676 + min-height: 0 !important;
3677 + overflow-y: auto !important;
3678 + height: auto !important;
3679 + max-height: none !important;
3680 +}
3681 +/* Files table fills remaining space */
3682 +body.is-mobile #p5filetable {
3683 + flex: 1 1 0 !important;
3684 + min-height: 0 !important;
3685 + height: auto !important;
3686 + max-height: none !important;
3687 +}
3688 +/* My Server: p6info fills remaining space. Also handles xdevices for p1. */
3689 +body.is-mobile #p6info,
3690 +body.is-mobile #xdevices {
3691 + flex: 1 1 0 !important;
3692 + min-height: 0 !important;
3693 + overflow-y: auto !important;
3694 + height: auto !important;
3695 + max-height: none !important;
3696 +}
3697 +/* Content areas that fill remaining flex height */
3698 +body.is-mobile #p50groups,
3699 +body.is-mobile #p52recordings,
3700 +body.is-mobile #p41events,
3701 +body.is-mobile #p20main,
3702 +body.is-mobile #p21main,
3703 +body.is-mobile #p30info,
3704 +body.is-mobile #p31events,
3705 +body.is-mobile #p51info {
3706 + flex: 1 1 0 !important;
3707 + min-height: 0 !important;
3708 + overflow-y: auto !important;
3709 + height: auto !important;
3710 + max-height: none !important;
3711 +}
3712 +body.is-mobile #serverMainStats {
3713 + flex: 1 1 0 !important;
3714 + min-height: 0 !important;
3715 + height: auto !important;
3716 + max-height: none !important;
3717 +}
3718 +
3719 +/* User profile image: <picture> wraps a <img width="200" height="200">.
3720 + Must target the inner img since the HTML attribute beats CSS on the picture. */
3721 +body.is-mobile #MainUserImage img,
3722 +body.is-mobile #MainUserImageEx {
3723 + width: 64px !important;
3724 + height: 64px !important;
3725 + border-radius: 50%;
3726 + object-fit: cover;
3727 +}
3728 +/* Group/mesh image in p20 (group detail): same issue — target inner img */
3729 +body.is-mobile #p20main #MainMeshImage img,
3730 +body.is-mobile #p20main #MainMeshImage {
3731 + width: 40px !important;
3732 + height: 40px !important;
3733 + border-radius: 6px;
3734 + object-fit: contain;
3735 +}
3736 +
3737 +/* Title text consistency: also cover the user/group detail panels */
3738 +body.is-mobile #p40title .fs-4, body.is-mobile #p41title .fs-4,
3739 +body.is-mobile #p42title .fs-4, body.is-mobile #p50title .fs-4,
3740 +body.is-mobile #p51title .fs-4, body.is-mobile #p52title .fs-4,
3741 +body.is-mobile #p60title .fs-4, body.is-mobile #p30title .fs-4,
3742 +body.is-mobile #p31title .fs-4 {
3743 + font-size: 1rem !important;
3744 + color: #212529 !important;
3745 + line-height: 1.25 !important;
3746 + font-weight: 600 !important;
3747 +}
3748 +
3749 +body.is-mobile #MainSubMenuSpan,
3750 +body.is-mobile #MeshSubMenuSpan,
3751 +body.is-mobile #EventsSubMenuSpan,
3752 +body.is-mobile #UserSubMenuSpan,
3753 +body.is-mobile #UsersSubMenuSpan,
3754 +body.is-mobile #ServerSubMenuSpan {
3755 + position: fixed !important;
3756 + bottom: 0 !important;
3757 + left: 0 !important;
3758 + right: 0 !important;
3759 + top: auto !important;
3760 + height: clamp(44px, 13vw, 54px) !important;
3761 + z-index: 500 !important;
3762 + overflow: hidden !important;
3763 + background: #113962 !important;
3764 + border-top: 1px solid #000 !important;
3765 + display: flex;
3766 + align-items: stretch;
3767 +}
3768 +
3769 +body.is-mobile #MainSubMenu,
3770 +body.is-mobile #MeshSubMenu,
3771 +body.is-mobile #EventsSubMenu,
3772 +body.is-mobile #UserSubMenu,
3773 +body.is-mobile #UsersSubMenu,
3774 +body.is-mobile #ServerSubMenu {
3775 + /* Become a flex item that fills the span, and a flex container for the tr row */
3776 + display: flex !important;
3777 + flex: 1 1 auto !important;
3778 + width: 100% !important;
3779 + height: clamp(44px, 13vw, 54px) !important;
3780 + background: transparent !important;
3781 +}
3782 +
3783 +/* tbody is an auto-inserted anonymous box — make it layout-transparent */
3784 +body.is-mobile #MainSubMenu tbody,
3785 +body.is-mobile #MeshSubMenu tbody,
3786 +body.is-mobile #EventsSubMenu tbody,
3787 +body.is-mobile #UserSubMenu tbody,
3788 +body.is-mobile #UsersSubMenu tbody,
3789 +body.is-mobile #ServerSubMenu tbody { display: contents !important; }
3790 +
3791 +/* tr becomes the flex item filling the table, and a flex container for the tds */
3792 +body.is-mobile #MainSubMenu tr,
3793 +body.is-mobile #MeshSubMenu tr,
3794 +body.is-mobile #EventsSubMenu tr,
3795 +body.is-mobile #UserSubMenu tr,
3796 +body.is-mobile #UsersSubMenu tr,
3797 +body.is-mobile #ServerSubMenu tr {
3798 + display: flex !important;
3799 + flex: 1 1 auto !important;
3800 + width: 100% !important;
3801 + align-items: stretch !important;
3802 +}
3803 +
3804 +body.is-mobile .topbar_td {
3805 + /* display:flex WITHOUT !important — makes the td a flex container so
3806 + align-items/justify-content center text, while still letting
3807 + QV()'s inline display:none override it for permission-hidden tabs. */
3808 + display: flex;
3809 + flex: 1 1 0 !important;
3810 + min-width: 0 !important;
3811 + width: auto !important;
3812 + align-items: center !important;
3813 + justify-content: center !important;
3814 + /* Adaptive height and font: scales with viewport width.
3815 + clamp(min, preferred, max) — preferred grows with screen size via vw units. */
3816 + height: clamp(44px, 13vw, 54px) !important;
3817 + padding: 0 clamp(4px, 1.5vw, 10px) !important;
3818 + color: rgba(255,255,255,0.75) !important;
3819 + font-size: clamp(12px, 3.5vw, 15px) !important;
3820 + border-right: 1px solid rgba(255,255,255,0.15);
3821 + white-space: nowrap;
3822 + overflow: hidden;
3823 + text-overflow: ellipsis;
3824 +}
3825 +
3826 +/* Trailing spacer td — hide it on mobile so it doesn't eat space from the right */
3827 +body.is-mobile .topbar_td_end {
3828 + display: none !important;
3829 +}
3830 +
3831 +body.is-mobile .topbar_td.style3x {
3832 + color: rgba(255,255,255,0.7) !important;
3833 + background: transparent !important;
3834 +}
3835 +
3836 +body.is-mobile .topbar_td.style3x:hover,
3837 +body.is-mobile .topbar_td.style3x:active {
3838 + background: rgba(255,255,255,0.12) !important;
3839 + color: #fff !important;
3840 +}
3841 +
3842 +body.is-mobile .topbar_td.style3sel {
3843 + color: #fff !important;
3844 + background: rgba(255,255,255,0.18) !important;
3845 + border-bottom: 3px solid #fff !important;
3846 +}
3847 +
3848 +/* Classic Mobile tab shuffle: the active device sub-tab is hidden from the bottom bar.
3849 + Hiding it frees up space so all other tabs remain visible without horizontal scroll.
3850 + Using display:none !important to beat QV()'s inline style. */
3851 +body.is-mobile .mob-active-tab { display: none !important; }
3852 +
3853 +/* Collapse #topbar to 0 height on mobile */
3854 +body.is-mobile #topbar {
3855 + height: 0 !important;
3856 + overflow: visible !important;
3857 + min-height: 0 !important;
3858 + padding: 0 !important;
3859 +}
3860 +
3861 +/* Hide the main navigation row — accessible via the hamburger menu on mobile */
3862 +body.is-mobile #MainMenuSpan { display: none !important; }
3863 +
3864 +/* Footer sits at bottom:0 z-index:1000 and covers the bottom tab bar */
3865 +body.is-mobile #footer { display: none !important; }
3866 +
3867 +/* Container: full width, no side borders/margins. */
3868 +body.is-mobile #container {
3869 + width: 100% !important;
3870 + min-width: 0 !important;
3871 + margin: 0 !important;
3872 + border-left: none !important;
3873 + border-right: none !important;
3874 + padding-left: 0 !important;
3875 + grid-template-columns: 0 1fr !important;
3876 + grid-template-rows: 66px 0 auto 0 !important;
3877 +}
3878 +
3879 +/* Content area: pad bottom so the fixed 44px tab bar doesn't overlap content */
3880 +/* column_l: zero horizontal padding so structural bars (toolbars, title bars,
3881 + canvas, terminal) stretch edge-to-edge like a native app. Bottom padding
3882 + reserves space for the fixed 44px tab bar. */
3883 +body.is-mobile #column_l {
3884 + /* Match padding-bottom to the tab bar height so nothing is hidden behind it */
3885 + padding: 0 0 clamp(44px, 13vw, 54px) 0 !important;
3886 +}
3887 +
3888 +/* Content areas: 10px breathing room so text/cards/tables don't press against
3889 + screen edges. Structural bars, canvas, terminal stay full-width (not listed). */
3890 +body.is-mobile #p10info,
3891 +body.is-mobile #p2info,
3892 +body.is-mobile #p6info,
3893 +body.is-mobile #p20main,
3894 +body.is-mobile #p21main,
3895 +body.is-mobile #p30info,
3896 +body.is-mobile #p31events,
3897 +body.is-mobile #p51info,
3898 +body.is-mobile #xdevices,
3899 +body.is-mobile #xdevicesmap,
3900 +body.is-mobile #p3events,
3901 +body.is-mobile #p3users,
3902 +body.is-mobile #p50groups,
3903 +body.is-mobile #serverMainStats,
3904 +body.is-mobile #p5filetable,
3905 +body.is-mobile #p13filetable,
3906 +body.is-mobile #p16events,
3907 +body.is-mobile #p17info,
3908 +body.is-mobile #p15agentConsole,
3909 +body.is-mobile #p15agentConsoleText {
3910 + padding-left: 10px !important;
3911 + padding-right: 10px !important;
3912 +}
3913 +
3914 +/* Details tab: section header icons are 64px — too large on a phone.
3915 + The JS renders each section as <img src=details/xxx64.png width=64> in a
3916 + <td style=width:64px> table cell. Shrink to 32px on mobile. */
3917 +body.is-mobile #p17info img[src*="details/"] {
3918 + width: 32px !important;
3919 + height: 32px !important;
3920 +}
3921 +body.is-mobile #p17info td[style*="width:64px"] {
3922 + width: 36px !important;
3923 +}
3924 +
3925 +/* Panel title bar: full-width, masthead-integrated
3926 + #column_l has padding: 0 15px which makes everything 30px narrower than the
3927 + screen. Negative margins break the title bars out to true full width.
3928 + Back button: flush to left screen edge, masthead blue (#036), rounded on
3929 + the right only — visually "attached" to the masthead above. */
3930 +
3931 +body.is-mobile #p10title,
3932 +body.is-mobile #p11title,
3933 +body.is-mobile #p12title,
3934 +body.is-mobile #p13title,
3935 +body.is-mobile #p14title,
3936 +body.is-mobile #p15title,
3937 +body.is-mobile #p16title,
3938 +body.is-mobile #p17title,
3939 +body.is-mobile #p19title,
3940 +body.is-mobile #p20title,
3941 +body.is-mobile #p21title,
3942 +body.is-mobile #p30title,
3943 +body.is-mobile #p31title,
3944 +body.is-mobile #p51title {
3945 + padding: 0 12px 0 0 !important;
3946 + background: #fff;
3947 + min-height: 50px;
3948 + position: relative !important;
3949 + z-index: 1 !important;
3950 + box-shadow: 0 1px 0 0 rgba(0,0,0,0.15);
3951 + align-items: stretch !important;
3952 + gap: 0;
3953 +}
3954 +
3955 +/* p20title is nested inside p20main which has padding-left/right: 10px.
3956 + Negative margins cancel that padding so the title bar spans full width. */
3957 +/* p20title and p6title live inside their scrollable parent (p20main / p6info)
3958 + which has padding: 0 10px. Negative margins cancel that so they span full width. */
3959 +body.is-mobile #p20title,
3960 +body.is-mobile #p6title {
3961 + margin-left: -10px !important;
3962 + margin-right: -10px !important;
3963 +}
3964 +
3965 +/* Server console (p15) without the back button: add left indent to match
3966 + other title bars that DO have the back button providing the indent.
3967 + The JS adds class 'no-back-btn' to p15title when view is 115. */
3968 +body.is-mobile #p15title.no-back-btn {
3969 + padding-left: 15px !important;
3970 +}
3971 +
3972 +/* Option bars (areaHead) below the title bar: slightly tinted so they're
3973 + visually distinct from the pure-white title bar. Overrides --sub-menu-bg
3974 + (the white gradient) for a consistent look on all device panels. */
3975 +body.is-mobile { --sub-menu-bg: #f0f2f5; }
3976 +
3977 +/* Night mode: keep the white title bar (it reads well as a card against the
3978 + dark column_l background). Only adjust the separator line colour. */
3979 +body.night.is-mobile #p10title, body.night.is-mobile #p11title,
3980 +body.night.is-mobile #p12title, body.night.is-mobile #p13title,
3981 +body.night.is-mobile #p14title, body.night.is-mobile #p15title,
3982 +body.night.is-mobile #p16title, body.night.is-mobile #p17title,
3983 +body.night.is-mobile #p19title,
3984 +body.night.is-mobile #p20title, body.night.is-mobile #p21title,
3985 +body.night.is-mobile #p30title, body.night.is-mobile #p31title,
3986 +body.night.is-mobile #p51title {
3987 + box-shadow: 0 1px 0 0 #444 !important;
3988 +}
3989 +
3990 +/* Back button: flush to screen left edge, masthead colour, pill-shaped right side */
3991 +/* Back buttons: NO display:flex !important — QV() hides them with
3992 + element.style.display='none' (inline style). CSS !important would
3993 + override that, keeping the button visible even when it should hide
3994 + (e.g. server console reuses p15 but shouldn't show p15BackButton). */
3995 +body.is-mobile #p10BackButton,
3996 +body.is-mobile #p11BackButton,
3997 +body.is-mobile #p12BackButton,
3998 +body.is-mobile #p13BackButton,
3999 +body.is-mobile #p14BackButton,
4000 +body.is-mobile #p15BackButton,
4001 +body.is-mobile #p16BackButton,
4002 +body.is-mobile #p17BackButton,
4003 +body.is-mobile #p19BackButton,
4004 +body.is-mobile #p20BackButton,
4005 +body.is-mobile #p21BackButton,
4006 +body.is-mobile #p30BackButton,
4007 +body.is-mobile #p31BackButton,
4008 +body.is-mobile #p51BackButton {
4009 + background: #036;
4010 + border-radius: 0 0 12px 0;
4011 + padding: 0 14px !important;
4012 + margin: 0 12px 0 0;
4013 + display: flex; /* no !important — allows QV() to override with display:none */
4014 + align-items: center;
4015 + align-self: stretch;
4016 + flex-shrink: 0;
4017 +}
4018 +
4019 +body.is-mobile #p10BackButton .fa-square-caret-left,
4020 +body.is-mobile #p11BackButton .fa-square-caret-left,
4021 +body.is-mobile #p12BackButton .fa-square-caret-left,
4022 +body.is-mobile #p13BackButton .fa-square-caret-left,
4023 +body.is-mobile #p14BackButton .fa-square-caret-left,
4024 +body.is-mobile #p15BackButton .fa-square-caret-left,
4025 +body.is-mobile #p16BackButton .fa-square-caret-left,
4026 +body.is-mobile #p17BackButton .fa-square-caret-left,
4027 +body.is-mobile #p19BackButton .fa-square-caret-left,
4028 +body.is-mobile #p20BackButton .fa-square-caret-left,
4029 +body.is-mobile #p21BackButton .fa-square-caret-left,
4030 +body.is-mobile #p30BackButton .fa-square-caret-left,
4031 +body.is-mobile #p31BackButton .fa-square-caret-left,
4032 +body.is-mobile #p51BackButton .fa-square-caret-left {
4033 + color: #fff !important;
4034 + font-size: 1.2rem !important;
4035 +}
4036 +
4037 +/* Title text: 1rem, dark, vertically centred, truncates with … when too long.
4038 + flex:1 + min-width:0 allows the text to shrink in the flex row. */
4039 +body.is-mobile #p10title .fs-4,
4040 +body.is-mobile #p11title .fs-4,
4041 +body.is-mobile #p12title .fs-4,
4042 +body.is-mobile #p13title .fs-4,
4043 +body.is-mobile #p14title .fs-4,
4044 +body.is-mobile #p15title .fs-4,
4045 +body.is-mobile #p16title .fs-4,
4046 +body.is-mobile #p17title .fs-4,
4047 +body.is-mobile #p19title .fs-4,
4048 +body.is-mobile #p20title .fs-4,
4049 +body.is-mobile #p21title .fs-4,
4050 +body.is-mobile #p30title .fs-4,
4051 +body.is-mobile #p31title .fs-4,
4052 +body.is-mobile #p51title .fs-4 {
4053 + font-size: 1rem !important;
4054 + color: #212529 !important;
4055 + line-height: 1.25;
4056 + display: flex;
4057 + align-items: center;
4058 + flex: 1 1 0 !important;
4059 + min-width: 0 !important;
4060 + overflow: hidden !important;
4061 + white-space: nowrap !important;
4062 + text-overflow: ellipsis !important;
4063 +}
4064 +
4065 +/* Right-side icon containers in title bars: always a single horizontal row, never shrink. */
4066 +body.is-mobile #devListToolbarViewIcons,
4067 +body.is-mobile #devListToolbarViewIcons2,
4068 +body.is-mobile #devListToolbarViewIcons3 {
4069 + display: flex !important;
4070 + flex-direction: row !important;
4071 + align-items: center !important;
4072 + gap: 6px !important;
4073 + flex-shrink: 0 !important;
4074 + white-space: nowrap !important;
4075 +}
4076 +
4077 +/* Hide group/mesh name: JS appends <span class="deviceGroupName"> - GroupName </span> */
4078 +body.is-mobile .deviceGroupName { display: none !important; }
4079 +
4080 +/* adjustPanels() computes panel heights from 100vh without knowing about the
4081 + 44px fixed tab bar on mobile. Override each panel to add 44px to the offset
4082 + so content stops above the tab bar. !important beats the JS inline style. */
4083 +body.is-mobile #xdevices,
4084 +body.is-mobile #xdevicesmap { max-height: calc(100vh - 90px) !important; }
4085 +/* adjustPanels() sizes panels using calc(100vh - N). These overrides keep the
4086 + same N but use 100dvh so the browser URL bar is accounted for. */
4087 +body.is-mobile #xdevices,
4088 +body.is-mobile #xdevicesmap { max-height: calc(100dvh - 241px) !important; }
4089 +body.is-mobile #p2info { height: calc(100dvh - 215px) !important;
4090 + max-height: calc(100dvh - 215px) !important; }
4091 +body.is-mobile #p3events { height: calc(100dvh - 245px) !important; }
4092 +body.is-mobile #p3users,
4093 +body.is-mobile #p50groups { max-height: calc(100dvh - 245px) !important; }
4094 +body.is-mobile #serverMainStats { height: calc(100dvh - 245px) !important;
4095 + max-height: calc(100dvh - 245px) !important; }
4096 +body.is-mobile #p5filetable { height: calc(100dvh - 294px) !important; }
4097 +
4098 +/* All device panels: flex column that fills column_l's content height.
4099 + IMPORTANT: display and flex-direction must NOT have !important here.
4100 + QV() hides panels with element.style.display='none' (inline style).
4101 + CSS !important beats inline styles, so display:flex !important would
4102 + make panels impossible to hide and break tab navigation. Without
4103 + !important, inline style.display='none' correctly overrides the CSS. */
4104 +body.is-mobile #p10,
4105 +body.is-mobile #p11,
4106 +body.is-mobile #p12,
4107 +body.is-mobile #p13,
4108 +body.is-mobile #p15,
4109 +body.is-mobile #p16,
4110 +body.is-mobile #p17 {
4111 + display: flex;
4112 + flex-direction: column;
4113 + height: 100% !important;
4114 + overflow: hidden !important;
4115 +}
4116 +
4117 +/* Title bars: never collapse */
4118 +body.is-mobile #p10title, body.is-mobile #p11title, body.is-mobile #p12title,
4119 +body.is-mobile #p13title, body.is-mobile #p15title,
4120 +body.is-mobile #p16title, body.is-mobile #p17title { flex-shrink: 0 !important; }
4121 +
4122 +/* p10 General: p10info fills remaining space and scrolls internally */
4123 +body.is-mobile #p10info {
4124 + flex: 1 1 0 !important;
4125 + min-height: 0 !important;
4126 + overflow-y: auto !important;
4127 + height: auto !important;
4128 + max-height: none !important;
4129 +}
4130 +
4131 +/* p11 Desktop: same flex-column pattern as Console.
4132 + deskarea0 fills p11's remaining height as a flex column:
4133 + deskarea1 (connect bar) -> flex-shrink:0, natural height
4134 + deskarea3x (canvas) -> flex:1, fills all remaining space
4135 + deskarea4 (controls bar) -> flex-shrink:0, always visible at bottom
4136 + No !important on display/flex-direction so QV() can still hide deskarea0
4137 + if ever needed, but !important is fine for the structural properties. */
4138 +body.is-mobile #deskarea0 {
4139 + flex: 1 1 0 !important;
4140 + min-height: 0 !important;
4141 + display: flex;
4142 + flex-direction: column;
4143 + overflow: hidden !important;
4144 +}
4145 +body.is-mobile #deskarea1 { flex-shrink: 0 !important; }
4146 +body.is-mobile #deskarea4 { flex-shrink: 0 !important; }
4147 +body.is-mobile #deskarea3x,
4148 +body.is-mobile .room4submenu #deskarea3x {
4149 + flex: 1 1 0 !important;
4150 + min-height: 0 !important;
4151 + height: auto !important;
4152 + max-height: none !important;
4153 + overflow: hidden !important;
4154 +}
4155 +
4156 +/* p12 Terminal: termTable (div) fills remaining space; inner <table> is
4157 + absolutely positioned to span 100% so CSS table sizing distributes rows:
4158 + areaHead row -> min-height; termarea3x row -> fills remainder; areaFoot -> min-height */
4159 +body.is-mobile #termTable {
4160 + flex: 1 1 0 !important;
4161 + min-height: 0 !important;
4162 + position: relative !important;
4163 + overflow: hidden !important;
4164 +}
4165 +body.is-mobile #termTable > table {
4166 + position: absolute !important;
4167 + inset: 0 !important;
4168 + height: 100% !important;
4169 + width: 100% !important;
4170 +}
4171 +body.is-mobile #termarea3x {
4172 + height: 100% !important;
4173 + max-height: none !important;
4174 + overflow: hidden !important;
4175 +}
4176 +body.is-mobile #termarea3xdiv { height: 100% !important; }
4177 +
4178 +/* p15 Console: override CSS table display to flex-column so row heights
4179 + distribute correctly. display:flex on <table>/<tbody> is well-supported
4180 + and gives us proper flex semantics (no table-height-distribution quirks).
4181 + Row order: areaHead(shrink) | areaProgress(shrink) | output(flex:1) | input(shrink) */
4182 +body.is-mobile #consoleTable {
4183 + display: flex !important;
4184 + flex-direction: column !important;
4185 + flex: 1 1 0 !important;
4186 + min-height: 0 !important;
4187 + overflow: hidden !important;
4188 + width: 100% !important;
4189 +}
4190 +body.is-mobile #consoleTable > tbody {
4191 + display: flex !important;
4192 + flex-direction: column !important;
4193 + flex: 1 1 0 !important;
4194 + min-height: 0 !important;
4195 +}
4196 +/* All rows: block, don't grow (single-td rows so display:block is fine) */
4197 +body.is-mobile #consoleTable > tbody > tr {
4198 + display: block !important;
4199 + flex-shrink: 0 !important;
4200 + width: 100% !important;
4201 +}
4202 +/* td cells: when tr is display:block, td loses automatic table-cell width.
4203 + Make all td elements full-width block elements. */
4204 +body.is-mobile #consoleTable > tbody > tr > td {
4205 + display: block !important;
4206 + width: 100% !important;
4207 + box-sizing: border-box !important;
4208 +}
4209 +/* Output row (3rd): fills all remaining space */
4210 +body.is-mobile #consoleTable > tbody > tr:nth-child(3) {
4211 + flex: 1 1 0 !important;
4212 + min-height: 0 !important;
4213 + display: flex !important;
4214 +}
4215 +/* p15agentConsole td: fills the flex output row, scrolls its content */
4216 +body.is-mobile #p15agentConsole {
4217 + flex: 1 1 0 !important;
4218 + min-height: 0 !important;
4219 + height: auto !important;
4220 + max-height: none !important;
4221 + overflow-y: auto !important;
4222 + display: block !important;
4223 +}
4224 +body.is-mobile #p15agentConsoleText {
4225 + height: auto !important;
4226 + max-height: none !important;
4227 + /* Base CSS hardcodes width:930px - overrides to full viewport width */
4228 + width: 100% !important;
4229 + max-width: 100% !important;
4230 + /* pre defaults to white-space:pre which never wraps, causing horizontal scroll.
4231 + pre-wrap preserves spacing/newlines but wraps at the container edge. */
4232 + white-space: pre-wrap !important;
4233 + word-break: break-all !important;
4234 + overflow-x: hidden !important;
4235 + box-sizing: border-box !important;
4236 +}
4237 +
4238 +/* p16 Events: events list fills remaining space after title + filter bar */
4239 +body.is-mobile #p16events {
4240 + flex: 1 1 0 !important;
4241 + min-height: 0 !important;
4242 + overflow-y: auto !important;
4243 + height: auto !important;
4244 + max-height: none !important;
4245 +}
4246 +
4247 +/* p17 Details: details content fills remaining space */
4248 +body.is-mobile #p17info {
4249 + flex: 1 1 0 !important;
4250 + min-height: 0 !important;
4251 + overflow-y: auto !important;
4252 + height: auto !important;
4253 + max-height: none !important;
4254 +}
4255 +
4256 +/* Prevent window-level scroll on mobile. */
4257 +html.is-mobile,
4258 +body.is-mobile {
4259 + overflow: hidden !important;
4260 + overscroll-behavior: none;
4261 +}
4262 +
4263 +/* Center the remote desktop canvas inside its container on mobile. */
4264 +body.is-mobile #DeskParent {
4265 + display: flex !important;
4266 + align-items: center !important;
4267 + justify-content: center !important;
4268 +}
4269 +body.is-mobile #Desk {
4270 + position: relative !important;
4271 + top: auto !important;
4272 + bottom: auto !important;
4273 + left: auto !important;
4274 + right: auto !important;
4275 +}
4276 +
4277 +/* Canvas area: 66px masthead + ~35px deskarea1 + ~35px deskarea4 + 44px tab-bar padding = 180px.
4278 + This makes total document height exactly 100vh with no page scroll needed. */
4279 +body.is-mobile #deskarea3x {
4280 + max-height: calc(100vh - 180px) !important;
4281 + height: calc(100vh - 180px) !important;
4282 +}
4283 +
4284 +body.is-mobile .room4submenu #deskarea3x {
4285 + max-height: calc(100vh - 180px) !important;
4286 + height: calc(100vh - 180px) !important;
4287 +}
4288 +
4289 +body.is-mobile.fulldesk #deskarea3x {
4290 + max-height: 100% !important;
4291 + height: 100% !important;
4292 +}
4293 +
4294 +/* Fullscreen mode: hide both toolbars so the canvas fills the entire viewport.
4295 + All controls move into the floating button panel. */
4296 +body.is-mobile.fulldesk #p11title,
4297 +body.is-mobile.fulldesk #deskarea1,
4298 +body.is-mobile.fulldesk #deskarea4 { display: none !important; }
4299 +
4300 +/* Floating button: hidden normally, only shown in fullscreen on mobile */
4301 +body.is-mobile.fulldesk #deskFsBtn { display: flex !important; }
4302 +
4303 +/* Top desktop toolbar: compact single row, horizontal scroll for overflow */
4304 +/* deskarea1 (connect/disconnect bar): compact single row */
4305 +body.is-mobile #deskarea1 {
4306 + flex-wrap: nowrap !important;
4307 + overflow-x: auto !important;
4308 + -webkit-overflow-scrolling: touch;
4309 + padding-left: 8px !important;
4310 + padding-right: 8px !important;
4311 +}
4312 +
4313 +/* Hide secondary/non-mobile-friendly items from the top desktop toolbar.
4314 + .deskareaicon (rotate L/R, view mode toggle) are kept — useful on mobile.
4315 + #idx_deskFullBtn2 (exit fullscreen ✖) shown only when actually in fullscreen,
4316 + vertically centred (float:right conflicts with flex parent). */
4317 +body.is-mobile #autoconnectbutton1,
4318 +body.is-mobile #connectbutton1rspan,
4319 +body.is-mobile #connectbutton1hspan,
4320 +body.is-mobile #desktopCustomUiButtons,
4321 +body.is-mobile #desktopCustomUpperRight,
4322 +body.is-mobile #deskFocusBtn { display: none !important; }
4323 +
4324 +body.is-mobile #idx_deskFullBtn2 { display: none !important; }
4325 +body.is-mobile.fulldesk #idx_deskFullBtn2 {
4326 + display: flex !important;
4327 + align-items: center !important;
4328 + float: none !important;
4329 + align-self: center !important;
4330 +}
4331 +
4332 +/* Bottom desktop toolbar: compact single row */
4333 +body.is-mobile #deskarea4 {
4334 + flex-wrap: nowrap !important;
4335 + overflow-x: auto !important;
4336 + -webkit-overflow-scrolling: touch;
4337 + font-size: 12px;
4338 +}
4339 +
4340 +/* Keyboard shortcut selector: constrain width on mobile so it doesn't
4341 + crowd the other buttons in the scrollable deskarea4 bar. */
4342 +/* Keyboard shortcut selector: compact Bootstrap look on mobile */
4343 +body.is-mobile #deskkeys {
4344 + max-width: 110px !important;
4345 + font-size: 11px !important;
4346 + /* ensure Bootstrap form-select styling is applied */
4347 + appearance: auto;
4348 +}
4349 +
4350 +/* Type and keyboard toggle moved into More panel - free toolbar space */
4351 +body.is-mobile #DeskType { display: none !important; }
4352 +body.is-mobile #DeskSoftKbdBtn { display: none !important; }
4353 +
4354 +/* Move Actions + Settings into the More panel on mobile */
4355 +body.is-mobile #deskActionsBtn,
4356 +body.is-mobile #deskActionsSettings { display: none !important; }
4357 +
4358 +/* Replace the desktop icon spray with the More button on mobile */
4359 +body.is-mobile #deskarea4Icons { display: none !important; }
4360 +body.is-mobile #deskMobileActions { display: flex !important; flex-shrink: 0 !important; }
4361 +body.is-mobile #deskMobileActions .btn { white-space: nowrap !important; }
4362 +/* Clipboard dialog button moved into the More panel on mobile */
4363 +body.is-mobile #DeskClip { display: none !important; }
4364 +
4365 +
4366 +
4367 +/* fulldesk = remote-desktop fullscreen mode (body class added by deskToggleFull()).
4368 + NOT 'fullscreen' - that class is always on body in the Modern UI (it is the
4369 + normal grid layout mode) and would apply these rules to every mobile page view,
4370 + making column_l overflow-y:auto and creating a double scrollbar on all views. */
4371 +body.is-mobile.fulldesk #container {
4372 + width: 100% !important;
4373 + min-width: 0 !important;
4374 + padding-left: 0 !important;
4375 + display: flex !important;
4376 + flex-direction: column !important;
4377 +}
4378 +
4379 +body.is-mobile.fulldesk #column_l {
4380 + flex: 1 1 0 !important;
4381 + min-height: 0 !important;
4382 + overflow-y: auto !important;
4383 +}
4384 +
4385 +/* Mobile landscape: reclaim vertical space
4386 + 'is-landscape' is toggled by JS on resize so deskAdjust() is also called,
4387 + ensuring the canvas recalculates dimensions immediately on rotation. */
4388 +
4389 +/* Hide panel title bars in landscape — saves ~50px; back nav still works
4390 + via the browser gesture or rotating back to portrait. */
4391 +body.is-mobile.is-landscape #p11title,
4392 +body.is-mobile.is-landscape #p12title,
4393 +body.is-mobile.is-landscape #p13title { display: none !important; }
4394 +
4395 +/* Compact the connect/disconnect toolbar to save a few more pixels */
4396 +body.is-mobile.is-landscape #deskarea1 {
4397 + min-height: 0 !important;
4398 + padding-top: 2px !important;
4399 + padding-bottom: 2px !important;
4400 +}
4401 +
4402 +/* Canvas scale-fill in landscape fullscreen.
4403 + deskAdjust() and onScreenSizeChange set portrait-sized pixel values via
4404 + inline styles. !important in CSS always beats inline styles, so the canvas
4405 + is permanently 100%x100% of DeskParent whenever both classes are present —
4406 + regardless of how many times deskAdjust() fires. */
4407 +body.is-mobile.is-landscape.fulldesk #Desk {
4408 + width: 100% !important;
4409 + height: 100% !important;
4410 +}
4411 +body.is-mobile.is-landscape.fulldesk #DeskParent {
4412 + overflow: hidden !important;
4413 +}
4414 +
4415 +/* Native 1:1 zoom: DeskParent becomes content-sized (native resolution) inside
4416 + deskarea3x. JS in dtouchmove manually sets deskarea3x.scrollLeft/Top to pan.
4417 + overflow:hidden on deskarea3x is kept (base CSS) — JS can still scroll it. */
4418 +body.is-mobile.native-zoom #DeskParent {
4419 + position: relative !important;
4420 + display: block !important;
4421 + width: var(--native-desk-w, 100%) !important;
4422 + height: var(--native-desk-h, 100%) !important;
4423 +}
4424 +body.is-mobile.native-zoom #Desk {
4425 + width: 100% !important;
4426 + height: 100% !important;
4427 +}
4428 +
4429 +/* Mobile terminal */
4430 +
4431 +/* deskAdjust() sets termarea3x height via inline style based on desktop layout.
4432 + On mobile the offsets are different: 66px masthead + ~50px p12title +
4433 + ~44px areaHead + ~44px areaFoot + 44px bottom-tab-bar = 248px.
4434 + !important beats the inline style set by JS. */
4435 +body.is-mobile #termarea3x {
4436 + height: calc(100vh - 252px) !important;
4437 +}
4438 +body.is-mobile #termarea3xdiv {
4439 + height: calc(100vh - 252px) !important;
4440 +}
4441 +
4442 +/* Terminal toolbar and footer: scroll horizontally instead of wrapping rows */
4443 +body.is-mobile #termTable .areaHead,
4444 +body.is-mobile #termTable .areaFoot {
4445 + flex-wrap: nowrap !important;
4446 + overflow-x: auto !important;
4447 + -webkit-overflow-scrolling: touch;
4448 + align-items: center !important;
4449 +}
4450 +
4451 +/* Files panel toolbar: same no-wrap treatment as Terminal (both rows) */
4452 +body.is-mobile #p13toolbar .areaHead,
4453 +body.is-mobile #p13toolbar .areaHead2 {
4454 + flex-wrap: nowrap !important;
4455 + overflow-x: auto !important;
4456 + -webkit-overflow-scrolling: touch;
4457 + align-items: center !important;
4458 +}
4459 +
4460 +/* Files "More" dropdown: scrollable list so all items are reachable */
4461 +body.is-mobile .mobileFileActionDropdown .dropdown-menu {
4462 + max-height: 50vh !important;
4463 + overflow-y: auto !important;
4464 + overflow-x: hidden !important;
4465 +}
4466 +
4467 +/* On mobile, hide the Actions button from Terminal/Files toolbars — it wraps
4468 + on small screens and is accessible via device actions elsewhere. */
4469 +body.is-mobile #termActionsBtn,
4470 +body.is-mobile #filesActionsBtn { display: none !important; }
4471 +
4472 +/* Tab key button: hidden everywhere except mobile */
4473 +body.is-mobile #tabbutton { display: inline-flex !important; }
4474 +
4475 +/* Mobile files panel */
4476 +
4477 +/* p13filetable: flex:1 fills all remaining space in the p13 flex column,
4478 + replacing the fragile fixed-height calc. adjustPanels() inline style is
4479 + cancelled by height:auto !important. */
4480 +body.is-mobile #p13filetable {
4481 + flex: 1 1 0 !important;
4482 + min-height: 0 !important;
4483 + height: auto !important;
4484 + max-height: none !important;
4485 +}
4486 +
4487 +/* p13toolbarBottom: the status bar below the file list (shows "3 files: 0 bytes").
4488 + style6 uses a white gradient background that appears as a visible bar in dark mode
4489 + on both desktop and mobile. Transparent inherits the panel background,
4490 + matching both light and dark themes. */
4491 +#p13toolbarBottom .style6 {
4492 + background: transparent !important;
4493 +}
4494 +
4495 +/* These apply on any narrow viewport (desktop narrow window, tablet, phone). */
4496 @media (max-width: 768px) {
3531 - #page_leftbar {
3532 - display: none !important;
4497 + #container,
4498 + .fullscreen #container,
4499 + #column_l,
4500 + .fullscreen #column_l {
4501 + width: 100% !important;
4502 + min-width: 0 !important;
4503 + max-width: 100% !important;
4504 + box-sizing: border-box;
4505 + /* overflow: hidden (not overflow-x: hidden) — setting only one axis
4506 + forces the other to 'auto' per CSS spec, creating an unwanted scrollbar */
4507 + overflow: hidden;
4508 + }
4509 +
4510 + /* masthead needs overflow:visible so its dropdown menu can extend below it */
4511 + #masthead {
4512 + width: 100% !important;
4513 + min-width: 0 !important;
4514 + max-width: 100% !important;
4515 + box-sizing: border-box;
4516 + overflow: visible;
4517 }
4518
4519 .fullscreen #container {
3536 - width: 100vw !important;
3537 - min-width: 100vw !important;
3538 - padding-left: 0 !important;
3539 - display: flex !important;
3540 - flex-direction: column !important;
4520 + grid-template-columns: 70px minmax(0, 1fr);
4521 }
4522
4523 #masthead .title {
@@ -3562,18 +4542,108 @@ body:not(.fullscreen) .notifiyBox {
4542 gap: 0;
4543 }
4544
3565 - .masthead-titles-container {
3566 - flex-direction: column;
3567 - align-items: flex-start;
3568 - gap: 0;
3569 - }
3570 -
4545 #masthead {
4546 background-size: 18rem 2.5rem !important;
4547 background-position: center left;
4548 background-repeat: no-repeat !important;
4549 padding-right: 20px !important;
4550 }
4551 +
4552 + #p10info {
4553 + overflow-x: hidden !important;
4554 + }
4555 +
4556 + #p10info > table {
4557 + table-layout: fixed;
4558 + }
4559 +
4560 + #p10info > table > tbody > tr > td:first-child {
4561 + width: calc(100% - 140px) !important;
4562 + }
4563 +
4564 + #p10info > table > tbody > tr > td:nth-child(2),
4565 + #p10info > table > tbody > tr > td:nth-child(3) {
4566 + width: 0 !important;
4567 + }
4568 +
4569 + #p10info > table > tbody > tr > td:nth-child(4) {
4570 + width: 140px !important;
4571 + }
4572 +
4573 + #p10info #MainComputerImage {
4574 + width: 120px;
4575 + height: 120px;
4576 + }
4577 +
4578 + #p13toolbar,
4579 + #p13toolbar tbody,
4580 + #p13toolbar tr,
4581 + #p13toolbar .areaHead,
4582 + #p13toolbar .areaHead2 {
4583 + display: block;
4584 + width: 100%;
4585 + max-width: 100%;
4586 + box-sizing: border-box;
4587 + }
4588 +
4589 + #p13toolbar .areaHead,
4590 + #p13toolbar .areaHead2,
4591 + #p13toolbar .areaHead > .d-flex,
4592 + #p13toolbar .areaHead2 > .d-flex,
4593 + #p4toolbarActions,
4594 + #p4toolbarActions > .d-flex,
4595 + #p5filehead,
4596 + #p5filehead > .d-flex {
4597 + display: flex !important;
4598 + flex-wrap: wrap !important;
4599 + min-width: 0;
4600 + row-gap: 4px;
4601 + }
4602 +
4603 + #p13toolbar .areaHead {
4604 + column-gap: 4px;
4605 + }
4606 +
4607 + #p13toolbar .areaHead > .d-flex.ms-auto {
4608 + margin-left: 0 !important;
4609 + }
4610 +
4611 + #p13toolbar .areaHead input,
4612 + #p13toolbar .areaHead button,
4613 + #p13toolbar .areaHead2 input,
4614 + #p13toolbar .areaHead2 button,
4615 + #p4toolbarActions input,
4616 + #p4toolbarActions button,
4617 + #p5filehead input,
4618 + #p5filehead button {
4619 + margin-bottom: 4px;
4620 + }
4621 +
4622 + #p5toolbar,
4623 + #p5toolbar tbody,
4624 + #p5toolbar tr,
4625 + #p5filehead,
4626 + #p5filesubhead {
4627 + display: block;
4628 + width: 100%;
4629 + max-width: 100%;
4630 + box-sizing: border-box;
4631 + }
4632 +
4633 + #UserSearchInput {
4634 + max-width: 150px;
4635 + }
4636 +
4637 + .desktopFileAction,
4638 + .desktopAction {
4639 + display: none !important;
4640 + }
4641 +
4642 + .mobileFileActionDropdown,
4643 + .mobileActionDropdown {
4644 + display: inline-flex !important;
4645 + }
4646 +
4647 #MainMeshImage /*, #p2AccountImageFrame */ {
4648 /* text-align: center !important; */
4649 float: none !important;
@@ -3585,6 +4655,7 @@ body:not(.fullscreen) .notifiyBox {
4655 float: none !important;
4656 text-align: left;
4657 }
4658 +
4659 #userDropdown {
4660 display: flex !important;
4661 }
@@ -3601,17 +4672,6 @@ body:not(.fullscreen) .notifiyBox {
4672 display: block;
4673 }
4674
3604 - .userDropdownSubmenu {
3605 - right: 100%;
3606 - left: auto;
3607 - top: 13rem !important;
3608 - margin-left: 0;
3609 - margin-right: 4px;
3610 - }
3611 - .userDropdownUISettings {
3612 - position: relative;
3613 - }
3614 -
4675 .userDropdownSubmenu {
4676 position: static !important;
4677 right: auto !important;
@@ -3626,6 +4686,10 @@ body:not(.fullscreen) .notifiyBox {
4686 border-radius: 0 !important;
4687 }
4688
4689 + .userDropdownUISettings {
4690 + position: relative;
4691 + }
4692 +
4693 .userDropdownMenuItem:hover {
4694 border-radius: unset !important;
4695 }
@@ -3638,25 +4702,13 @@ body:not(.fullscreen) .notifiyBox {
4702 margin-right: 40px;
4703 }
4704
3641 - .notifiyBox:after {
3642 - margin-right: 40px;
3643 - }
3644 -
4705 .notifiyBox:before {
4706 content: none;
4707 }
4708
4709 body {
3650 - min-width: 100vw !important;
4710 + min-width: 0 !important;
4711 }
3652 -
3653 -
3654 - /* #container {
3655 - width: 100vw !important;
3656 - padding-left: 0 !important;
3657 - display: flex !important;
3658 - flex-direction: column !important;
3659 - } */
4712 }
4713 /* css fixes for select2 */
4714 /* .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered {
views/default3.handlebars
+868 -76
@@ -4,7 +4,7 @@
4 <head>
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7 - <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
7 + <meta name="viewport" content="user-scalable=yes,initial-scale=1.0,minimum-scale=1.0" />
8 <meta name="apple-mobile-web-app-capable" content="yes" />
9 <meta name="format-detection" content="telephone=no" />
10 <meta name="robots" content="noindex,nofollow">
@@ -67,6 +67,128 @@
67
68 <body id="body" oncontextmenu="handleContextMenu(event)" style="display:none;min-width:495px"
69 onload="if (typeof(startup) !== 'undefined') startup();">
70 + <script>
71 + if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
72 + document.body.classList.add('is-mobile');
73 + document.documentElement.classList.add('is-mobile');
74 + }
75 + // Auto-fullscreen on landscape: when in the desktop view and connected,
76 + // rotating to landscape triggers deskToggleFull() so the existing fulldesk
77 + // CSS maximises the canvas without any new layout code.
78 + // A flag tracks whether WE triggered fullscreen so we can auto-exit on
79 + // rotating back to portrait (but not if the user exited fullscreen manually).
80 + var _mobileAutoFullscreen = false;
81 + var _mobileSavedAspectRatio = 0;
82 + var _mobileZoomMode = 'fit'; // 'native' = 1:1 scrollable | 'fit' = fill viewport
83 + var _nativePinchStartDist = 0, _nativePinchStartW = 0, _nativePinchStartH = 0;
84 + var _nativePinchMidX = 0, _nativePinchMidY = 0;
85 + var _prevHadFulldesk = false;
86 +
87 + // Switch between native 1:1 (scrollable, full precision) and fit-to-viewport.
88 + // In native mode we suspend desktop.m.onScreenSizeChange (= deskAdjust) so
89 + // it can't override the explicit pixel dimensions we set on the canvas.
90 + function _mobileSetZoom(mode) {
91 + _mobileZoomMode = mode;
92 + var desk = document.getElementById('Desk');
93 + var dp = document.getElementById('DeskParent');
94 + if (!desk || !dp) return;
95 + var dm = (typeof desktop !== 'undefined') && desktop && desktop.m;
96 + if (mode === 'native') {
97 + // CSS custom properties lock canvas at native resolution.
98 + // Panning is handled by dtouchmove JS (scrollLeft/Top on deskarea3x).
99 + // Touch listeners stay active so trackpad mode keeps working.
100 + var w = (dm && dm.ScreenWidth) || desk.width || 640;
101 + var h = (dm && dm.ScreenHeight) || desk.height || 480;
102 + document.documentElement.style.setProperty('--native-desk-w', w + 'px');
103 + document.documentElement.style.setProperty('--native-desk-h', h + 'px');
104 + document.body.classList.add('native-zoom');
105 + if (dm && !dm._savedOnScreenSizeChange) {
106 + dm._savedOnScreenSizeChange = dm.onScreenSizeChange;
107 + dm.onScreenSizeChange = function(obj, sw, sh) {
108 + document.documentElement.style.setProperty('--native-desk-w', sw + 'px');
109 + document.documentElement.style.setProperty('--native-desk-h', sh + 'px');
110 + };
111 + }
112 + } else {
113 + document.body.classList.remove('native-zoom');
114 + if (dm && dm._savedOnScreenSizeChange) {
115 + dm.onScreenSizeChange = dm._savedOnScreenSizeChange;
116 + delete dm._savedOnScreenSizeChange;
117 + }
118 + desk.style.width = '100%';
119 + desk.style.height = '100%';
120 + if (typeof deskAdjust === 'function') deskAdjust();
121 + }
122 + }
123 +
124 + // Watch body class for fulldesk being added/removed and apply the right zoom
125 + if (document.body.classList.contains('is-mobile')) {
126 + new MutationObserver(function(muts) {
127 + muts.forEach(function(m) {
128 + if (m.attributeName !== 'class') return;
129 + var inFull = document.body.classList.contains('fulldesk');
130 + if (inFull && !_prevHadFulldesk) {
131 + // Just entered fullscreen: portrait->native, landscape->fit
132 + setTimeout(function() {
133 + var landscape = window.matchMedia('(orientation: landscape)').matches;
134 + _mobileSetZoom(landscape ? 'fit' : 'native');
135 + }, 200);
136 + } else if (!inFull && _prevHadFulldesk) {
137 + _mobileSetZoom('fit'); // always reset to fit on exit
138 + }
139 + _prevHadFulldesk = inFull;
140 + });
141 + }).observe(document.body, { attributes: true });
142 + }
143 + function _doOrientationUpdate() {
144 + if (!document.body.classList.contains('is-mobile')) return;
145 + var landscape = window.matchMedia('(orientation: landscape)').matches;
146 + document.body.classList.toggle('is-landscape', landscape);
147 + // Check DOM state — JS variables like xxcurrentView / fullscreen are
148 + // in a later function scope and not accessible from this early script.
149 + var p11 = document.getElementById('p11');
150 + var inDesktop = p11 && p11.style.display !== 'none';
151 + var alreadyFullscreen = document.body.classList.contains('fulldesk');
152 + if (landscape && inDesktop && !alreadyFullscreen) {
153 + // Auto-enter fullscreen in landscape using the global function
154 + if (typeof deskToggleFull === 'function') {
155 + _mobileAutoFullscreen = true;
156 + _mobileSavedAspectRatio = (typeof deskAspectRatio !== 'undefined') ? deskAspectRatio : 0;
157 + deskToggleFull();
158 + }
159 + // CSS rule body.is-mobile.is-landscape.fulldesk #Desk { width/height: 100% !important }
160 + // handles the canvas fill automatically — no JS override needed here.
161 + } else if (!landscape && alreadyFullscreen && _mobileAutoFullscreen) {
162 + _mobileAutoFullscreen = false;
163 + if (typeof deskToggleFull === 'function') deskToggleFull();
164 + // Let deskAdjust() recalculate naturally for portrait (no forced CSS override)
165 + setTimeout(function() { if (typeof deskAdjust === 'function') deskAdjust(); }, 400);
166 + } else {
167 + // Not entering/exiting fullscreen but still call deskAdjust so the
168 + // canvas recalculates for the new viewport dimensions
169 + if (typeof deskAdjust === 'function') {
170 + requestAnimationFrame(function() { deskAdjust(); });
171 + }
172 + }
173 + }
174 + function _updateOrientation() {
175 + setTimeout(_doOrientationUpdate, 300);
176 + }
177 + window.addEventListener('orientationchange', _updateOrientation);
178 + window.addEventListener('resize', _updateOrientation);
179 +
180 + // Use Popper strategy:'fixed' so dropdown menus escape overflow:hidden parents and canvas layers.
181 + window.addEventListener('load', function() {
182 + if (typeof bootstrap === 'undefined') return;
183 + document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function(el) {
184 + var ex = bootstrap.Dropdown.getInstance(el);
185 + if (ex) ex.dispose();
186 + new bootstrap.Dropdown(el, {
187 + popperConfig: function(cfg) { cfg.strategy = 'fixed'; return cfg; }
188 + });
189 + });
190 + });
191 + </script>
192 <!-- right click menu -->
193 <div id="contextMenu" class="contextMenu noselect" style="display:none">
194 <div id="cxinfo" class="cmtext" onclick="cmaction(1,event)"><b>Information</b></div>
@@ -389,9 +511,7 @@
511 </div>
512 <div id=p1 style="display:none">
513 <div id="p1title" class="d-flex justify-content-between align-items-center">
392 - <div>
393 - <h1>My Devices</h1>
394 - </div>
514 + <div class="fs-4 fw-bold">My Devices</div>
515 <div style="display:none" id="devListToolbarViewIcons">
516 <div id=devViewPageState style="float:left;line-height:32px;height:32px;font-size:16px;display:none"></div>
517 <div tabindex=0 id=devViewPageButton1 class=viewSelector onclick=onDeviceViewPageChange(1)
@@ -658,8 +778,8 @@
778 </div>
779 </div>
780 <div id=p3 style="display:none">
661 - <div id="p3title">
662 - <h1>My Events</h1>
781 + <div id="p3title" class="d-flex align-items-center">
782 + <div class="fs-4 fw-bold">My Events</div>
783 </div>
784 <table class="pTable">
785 <tr>
@@ -693,21 +813,30 @@
813 <div id=p3events></div>
814 </div>
815 <div id=p4 style="display:none">
696 - <div id="p4title">
697 - <h1>My Users</h1>
816 + <div id="p4title" class="d-flex align-items-center">
817 + <div class="fs-4 fw-bold">My Users</div>
818 </div>
819 <table class="pTable">
820 <tr>
701 - <td class="style14 d-flex align-items-center flex-wrap p-1">
821 + <td id="p4toolbarActions" class="style14 d-flex align-items-center flex-wrap p-1">
822 <div class="d-flex align-items-center">
823 <input type=button id=UsersSelectAllButton class="btn btn-secondary btn-sm me-1" onclick="p3usersSelectallButtonFunction()" value="Select All" />
824 <input type=button id=UsersGroupActionButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Group Action" onclick=p3usersGroupActionFunction() />
825 <input id=UserNewAccountButton class="btn btn-success btn-sm me-1" type=button onclick=showCreateNewAccountDialog() value="New Account..." />
826 + <div class="btn-group me-1 mobileActionDropdown">
827 + <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
828 + <div class="dropdown-menu">
829 + <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Broadcast</button>
830 + <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Download user information</button>
831 + <button type="button" id=p4MobileUserBatchCreate class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Batch create accounts</button>
832 + <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Settings</button>
833 + </div>
834 + </div>
835 <div class="d-inline-flex align-items-center">
836 <input id=UserSearchInput type=search class="form-control-sm me-1" placeholder=Filter onchange=onUserSearchInputChanged() onkeyup=onUserSearchInputChanged() autocomplete=off onfocus=onUserSearchFocus(1) onblur=onUserSearchFocus(0) />
837 </div>
838 </div>
710 - <div class="d-flex align-items-center ms-auto">
839 + <div class="d-flex align-items-center ms-auto desktopAction">
840 <input type=button class="btn btn-primary btn-sm me-2"onclick=showUserBroadcastDialog() style=margin-right:6px value="Broadcast" />
841 <a href=# onclick=p4downloadUserInfo()><i role=button class="fa-solid fa-download me-2" title="Download user information"></i></a>
842 <a href=# onclick=p4batchAccountCreate()><i role=button id=p4UserBatchCreate style="display:none" title="Batch create many user accounts" class="fa-solid fa-upload me-1"></i></a>
@@ -719,8 +848,8 @@
848 <div id="p3users"></div>
849 </div>
850 <div id=p5 style="display:none">
722 - <div id="p5title">
723 - <h1>My Files</h1>
851 + <div id="p5title" class="d-flex align-items-center">
852 + <div class="fs-4 fw-bold">My Files</div>
853 </div>
854 <table id="p5toolbar" class="table" cellpadding="0" cellspacing="0">
855 <tr>
@@ -732,12 +861,24 @@
861 <input type=button id=p5RenameFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Rename" onclick="p5renamefile();" />
862 <input type=button id=p5DeleteFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Delete" onclick="p5deletefile();" />
863 <input type=button id=p5ViewFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Edit" onclick="p5viewfile()" />
735 - <input type=button id=p5NewFolderButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="New Folder" onclick="p5createfolder();" />
736 - <input type=button id=p5UploadButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Upload" onclick="p5uploadFile()" />
737 - <input type=button id=p5DownloadButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Download" onclick="p5downloadButton()" />
738 - <input type=button id=p5CutButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Cut" onclick="p5copyFile(1)" />
739 - <input type=button id=p5CopyButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Copy" onclick="p5copyFile(0)" />
740 - <input type=button id=p5PasteButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Paste" onclick="p5pasteFile()" />
864 + <input type=button id=p5NewFolderButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="New Folder" onclick="p5createfolder();" />
865 + <input type=button id=p5UploadButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Upload" onclick="p5uploadFile()" />
866 + <input type=button id=p5DownloadButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Download" onclick="p5downloadButton()" />
867 + <input type=button id=p5CutButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Cut" onclick="p5copyFile(1)" />
868 + <input type=button id=p5CopyButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Copy" onclick="p5copyFile(0)" />
869 + <input type=button id=p5PasteButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Paste" onclick="p5pasteFile()" />
870 + <div class="btn-group me-1 mobileActionDropdown">
871 + <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
872 + <div class="dropdown-menu">
873 + <button type="button" disabled="disabled" id=p5MobileNewFolderButton class="dropdown-item" onclick="p5createfolder()">New Folder</button>
874 + <button type="button" disabled="disabled" id=p5MobileUploadButton class="dropdown-item" onclick="p5uploadFile()">Upload</button>
875 + <button type="button" disabled="disabled" id=p5MobileDownloadButton class="dropdown-item" onclick="p5downloadButton()">Download</button>
876 + <button type="button" disabled="disabled" id=p5MobileCutButton class="dropdown-item" onclick="p5copyFile(1)">Cut</button>
877 + <button type="button" disabled="disabled" id=p5MobileCopyButton class="dropdown-item" onclick="p5copyFile(0)">Copy</button>
878 + <button type="button" disabled="disabled" id=p5MobilePasteButton class="dropdown-item" onclick="p5pasteFile()">Paste</button>
879 + </div>
880 + </div>
881 + </div>
882 </td>
883 </tr>
884 <tr>
@@ -779,9 +920,9 @@
920 </div>
921 <div id=p6 style="display:none">
922 <div id="p6info" style="overflow-y:auto">
782 - <div id="p6title">
783 - <img id=MainMeshImage src="serverpic.ashx">
784 - <h1>My Server</h1>
923 + <div id="p6title" class="d-flex align-items-center">
924 + <div class="fs-4 fw-bold">My Server</div>
925 + <img id=MainMeshImage src="serverpic.ashx" class="ms-auto">
926 </div>
927 <div id="p2ServerActions">
928 <p><strong>Server actions</strong></p>
@@ -845,7 +986,7 @@
986 <div id="p10BackButton" class="pe-2">
987 <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
988 </div>
848 - <div class='fs-4 fw-bold'>General - <span id=p10deviceName></span></div>
989 + <div class='fs-4 fw-bold'><i class="fa-solid fa-circle-info fa-sm me-2 text-secondary"></i>General - <span id=p10deviceName></span></div>
990 </div>
991 <div id=p10info style="overflow-y:auto" class="pt-3">
992 <table style="width:100%" cellpadding="0" cellspacing="0">
@@ -888,11 +1029,25 @@
1029 </div>
1030 </div>
1031 <div id=p11 class="noselect" style="display:none">
1032 + <!-- softKeyboard: lives at p11 level (outside deskarea4) so it stays
1033 + focusable even when the toolbar is hidden in fullscreen/landscape.
1034 + position:fixed keeps it off-screen while remaining a valid focus target. -->
1035 + <input id="softKeyboard" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
1036 + type="text" inputmode="text"
1037 + style="position:fixed;opacity:0;width:1px;height:1px;left:-9999px;top:-9999px"
1038 + oninput="onSoftKeyboardInput(this)"
1039 + onblur="onSoftKeyboardFocusChange(false)"
1040 + onfocus="onSoftKeyboardFocusChange(true)" />
1041 + <!-- Trackpad cursor -->
1042 + <svg id="trackpadCursor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 20" width="18" height="30"
1043 + style="display:none;position:fixed;pointer-events:none;z-index:9999;transform-origin:0 0;filter:drop-shadow(1px 1px 2px rgba(0,0,0,0.8))">
1044 + <path d="M0,0 L0,16 L4,12 L6.5,18 L8.5,17 L6,11 L11,11 Z" fill="white" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
1045 + </svg>
1046 <div id="p11title" class="d-flex align-items-center">
1047 <div id="p11BackButton" class="pe-2">
1048 <i class="fa-solid fa-2xl fa-square-caret-left" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1049 </div>
895 - <div class='fs-4 fw-bold'>Desktop - <span id=p11deviceName></span></div>
1050 + <div class='fs-4 fw-bold'><i class="fa-solid fa-display fa-sm me-2 text-secondary"></i>Desktop - <span id=p11deviceName></span></div>
1051 <div id="p11KeyboardLights" style="font-size:x-small;color:black" class="ms-auto pe-2">
1052 <div id="p11numlock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">NUM</div>
1053 <div id="p11capslock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">CAPS</div>
@@ -973,7 +1128,8 @@
1128 <div id=DeskParent>
1129 <canvas id=Desk width=640 height=480 oncontextmenu="return false"
1130 onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event)
976 - onmousewheel=dmousewheel(event)></canvas>
1131 + onmousewheel=dmousewheel(event)
1132 + ontouchstart=dtouchstart(event) ontouchmove=dtouchmove(event) ontouchend=dtouchend(event)></canvas>
1133 </div>
1134 <div id=DeskTools>
1135 <div id=deskToolsAreaTop>
@@ -1011,13 +1167,25 @@
1167 <div id=deskarea4 class="areaFoot d-flex flex-wrap">
1168 <div class="d-flex align-items-center">
1169 <select id="deskkeys" class="form-select-sm me-1" cmenu=deskKeyShortcutContextMenu></select>
1014 - <input id="DeskWD" type=button class="btn btn-primary btn-sm me-1" value="Send" onkeypress="return false" onkeydown="return false" onclick="deskSendKeys()" />
1170 + <button id="DeskWD" class="btn btn-primary btn-sm me-1" onkeypress="return false" onkeydown="return false" onclick="deskSendKeys()">Send</button>
1171 <input id="DeskESC" style="display:none" type="button" class="btn btn-secondary btn-sm me-1" value="ESC" onkeypress="return false" onkeydown="return false" onclick="sendDeskEsc()" />
1172 <input id="DeskClip" type="button" class="btn btn-secondary btn-sm me-1" value="Clipboard" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()" />
1173 <input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Type" onkeypress="return false" onkeydown="return false" onclick="showDeskType()" />
1174 + <!-- Mobile-only: toggle button for the soft keyboard -->
1175 + <button id="DeskSoftKbdBtn" class="btn btn-secondary btn-sm me-1" type="button"
1176 + onclick="toggleMobileKeyboard()" title="Show/hide on-screen keyboard"
1177 + style="display:none">
1178 + <i class="fa-solid fa-keyboard"></i>
1179 + </button>
1180 <div id="DeskControlSpan" title="Toggle mouse and keyboard input" class="form-check"><input id="DeskControl" type="checkbox" class="form-check-input" onkeypress="return false" onkeydown="return false" onclick="toggleKvmControl()"><label class="form-check-label" for="DeskControl">Input</label></div>&nbsp;
1181 </div>
1020 - <div class="d-flex ms-auto align-items-center">
1182 + <!-- Mobile-only "More" button — opens a fixed-position action panel -->
1183 + <div id="deskMobileActions" class="ms-auto align-items-center" style="display:none">
1184 + <button class="btn btn-secondary btn-sm" type="button" onclick="deskToggleMobileActions()" title="More actions">
1185 + More&nbsp;<i class="fa-solid fa-chevron-up fa-xs"></i>
1186 + </button>
1187 + </div>
1188 + <div id="deskarea4Icons" class="d-flex ms-auto align-items-center">
1189 <span id=DeskMonitorSelectionSpan></span>
1190 <span id="DeskLatency" style="width:70px" class="text-center" title="Desktop Session Latency"></span>
1191 <span id="DeskTimer" style="width:70px" class="text-center" title="Session time"></span>
@@ -1064,7 +1232,30 @@
1232 <span id=DeskRunButton cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Run a script on this computer" role="button">
1233 <i class="fa-solid fa-fw fa-play" onclick=runDeviceCmd()></i>
1234 </span>
1067 - </div>
1235 + </div><!-- /deskarea4Icons -->
1236 + </div>
1237 + <!-- Mobile action panel: fixed above the tab bar, shown via deskToggleMobileActions() -->
1238 + <div id="deskMobileActionsBackdrop" onclick="deskCloseMobileActions()" style="display:none;position:fixed;inset:0;z-index:1998"></div>
1239 + <div id="deskMobileActionsPanel" style="display:none;position:fixed;right:8px;bottom:82px;z-index:1999;min-width:220px;border-radius:12px;overflow-y:auto;overflow-x:hidden;max-height:calc(100dvh - 100px);box-shadow:0 4px 24px rgba(0,0,0,0.28)">
1240 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="toggleMobileKeyboard();deskCloseMobileActions()"><i class="fa-solid fa-keyboard fa-fw me-2 text-primary"></i>On-screen Keyboard</a>
1241 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskType();deskCloseMobileActions()"><i class="fa-solid fa-font fa-fw me-2 text-primary"></i>Type Text</a>
1242 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceActionFunction();deskCloseMobileActions()"><i class="fa-solid fa-bolt fa-fw me-2 text-primary"></i>Actions</a>
1243 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDesktopSettings();deskCloseMobileActions()"><i class="fa-solid fa-gear fa-fw me-2 text-primary"></i>Settings</a>
1244 + <a id="deskTrackpadItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleTrackpad();deskCloseMobileActions()"><i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>Enable Trackpad Mode</a>
1245 + <a id="deskRightClickItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleRightClickMode();deskCloseMobileActions()"><i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>Enable Right-click Mode</a>
1246 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskCustomizeKeys();deskCloseMobileActions()"><i class="fa-solid fa-sliders fa-fw me-2 text-primary"></i>Customize Shortcuts</a>
1247 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskRefreshFunction();deskCloseMobileActions()"><i class="fa-solid fa-rotate fa-fw me-2 text-primary"></i>Refresh Desktop</a>
1248 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskClip();deskCloseMobileActions()"><i class="fa-solid fa-paste fa-fw me-2 text-primary"></i>Clipboard</a>
1249 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskClipboardOutFunction();deskCloseMobileActions()"><i class="fa-solid fa-clipboard fa-fw me-2 text-primary"></i>Upload Clipboard</a>
1250 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceUrlFunction();deskCloseMobileActions()"><i class="fa-solid fa-globe fa-fw me-2 text-primary"></i>Open URL on Remote</a>
1251 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceLockFunction();deskCloseMobileActions()"><i class="fa-solid fa-lock fa-fw me-2 text-primary"></i>Lock Remote</a>
1252 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceToastFunction();deskCloseMobileActions()"><i class="fa-solid fa-bell fa-fw me-2 text-primary"></i>Send Notification</a>
1253 + <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceChat(event);deskCloseMobileActions()"><i class="fa-solid fa-message fa-fw me-2 text-primary"></i>Chat</a>
1254 + <a class="d-block px-3 py-2 text-decoration-none" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="runDeviceCmd();deskCloseMobileActions()"><i class="fa-solid fa-play fa-fw me-2 text-primary"></i>Run Script</a>
1255 + </div>
1256 + <!-- Fullscreen floating button - replaces all toolbars when in fulldesk mode -->
1257 + <div id="deskFsBtn" onclick="deskToggleMobileActions()" style="display:none;position:fixed;bottom:54px;right:12px;z-index:2000;width:48px;height:48px;border-radius:50%;background:rgba(0,0,0,0.55);color:#fff;align-items:center;justify-content:center;cursor:pointer;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border:1.5px solid rgba(255,255,255,0.22);font-size:20px">
1258 + <i class="fa-solid fa-bars"></i>
1259 </div>
1260 </div>
1261 </div>
@@ -1073,7 +1264,7 @@
1264 <div id="p12BackButton" class="pe-2">
1265 <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1266 </div>
1076 - <div class='fs-4 fw-bold'>Terminal - <span id=p12deviceName></span></div>
1267 + <div class='fs-4 fw-bold'><i class="fa-solid fa-terminal fa-sm me-2 text-secondary"></i>Terminal - <span id=p12deviceName></span></div>
1268 <div id="devListToolbarViewIcons2" class="ms-auto">
1269 <i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick=deskToggleFull(event) title="Full Screen. Hold shift to browser full screen."></i>
1270 </div>
@@ -1166,6 +1357,9 @@
1357 <input type=button onkeypress="return false" onkeydown="return false"
1358 class="btn btn-primary me-1 btn-sm" id="escbutton"
1359 value="ESC" onclick="termSendKey(27,'escbutton')" />
1360 + <input type=button onkeypress="return false" onkeydown="return false"
1361 + class="btn btn-primary me-1 btn-sm" id="tabbutton"
1362 + value="Tab" onclick="termSendKey(9,'tabbutton')" style="display:none" />
1363 <input type=button onkeypress="return false" onkeydown="return false"
1364 class="btn btn-primary me-1 btn-sm" id="bsbutton"
1365 value="Backspace" onclick="termSendKey(8,'bsbutton')" style="display:none" />
@@ -1211,7 +1405,7 @@
1405 <div id="p13BackButton" class="pe-2">
1406 <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1407 </div>
1214 - <div class='fs-4 fw-bold'>Files - <span id=p13deviceName></span></div>
1408 + <div class='fs-4 fw-bold'><i class="fa-solid fa-folder-open fa-sm me-2 text-secondary"></i>Files - <span id=p13deviceName></span></div>
1409 </div>
1410 <table id="p13toolbar" cellpadding="0" cellspacing="0">
1411 <tr>
@@ -1258,19 +1452,37 @@
1452 <input type=button disabled="disabled" id=p13RenameFileButton class="btn btn-primary me-1 btn-sm" value="Rename" onclick="p13renamefile()" />
1453 <input type=button disabled="disabled" id=p13DeleteFileButton class="btn btn-primary me-1 btn-sm" value="Delete" onclick="p13deletefile()" />
1454 <input type=button disabled="disabled" id=p13ViewFileButton class="btn btn-primary me-1 btn-sm" value="Edit" onclick="p13viewfile()" />
1261 - <input type=button disabled="disabled" id=p13NewFolderButton class="btn btn-primary me-1 btn-sm" value="New Folder" onclick="p13createfolder()" />
1262 - <input type=button disabled="disabled" id=p13NewFileButton class="btn btn-primary me-1 btn-sm" value="New File" onclick="p13createfile()" />
1263 - <input type=button disabled="disabled" id=p13UploadButton class="btn btn-primary me-1 btn-sm" value="Upload" onclick="p13uploadFile()" />
1264 - <input type=button disabled="disabled" id=p13DownloadButton class="btn btn-primary me-1 btn-sm" value="Download" onclick="p13downloadButton()" />
1265 - <input type=button disabled="disabled" id=p13CutButton class="btn btn-primary me-1 btn-sm" value="Cut" onclick="p13copyFile(1)" />
1266 - <input type=button disabled="disabled" id=p13CopyButton class="btn btn-primary me-1 btn-sm" value="Copy" onclick="p13copyFile(0)" />
1267 - <input type=button disabled="disabled" id=p13PasteButton class="btn btn-primary me-1 btn-sm" value="Paste" onclick="p13pasteFile()" />
1268 - <input type=button disabled="disabled" id=p13ZipButton class="btn btn-primary me-1 btn-sm" value="Zip" onclick="p13zipFiles()" />
1269 - <input type=button disabled="disabled" id=p13UnzipButton class="btn btn-primary me-1 btn-sm" value="Unzip" onclick="p13unzipFile()" />
1270 - <input type=button disabled="disabled" id=p13RefreshButton class="btn btn-primary me-1 btn-sm" value="Refresh" onclick="p13folderup(9999)" />
1271 - <input type=button disabled="disabled" id=p13FindButton class="btn btn-primary me-1 btn-sm" value="Find" onclick="p13findfile()" />
1272 - <input type=button disabled="disabled" id=p13GoToFolderButton class="btn btn-primary me-1 btn-sm" value="GoTo" onclick="p13gotofolder()" />
1273 - <input type=button disabled="disabled" id=p13OpenButton class="btn btn-primary me-1 btn-sm" value="Open" onclick="p13openfilefolder()" />
1455 + <input type=button disabled="disabled" id=p13NewFolderButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="New Folder" onclick="p13createfolder()" />
1456 + <input type=button disabled="disabled" id=p13NewFileButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="New File" onclick="p13createfile()" />
1457 + <input type=button disabled="disabled" id=p13UploadButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Upload" onclick="p13uploadFile()" />
1458 + <input type=button disabled="disabled" id=p13DownloadButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Download" onclick="p13downloadButton()" />
1459 + <input type=button disabled="disabled" id=p13CutButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Cut" onclick="p13copyFile(1)" />
1460 + <input type=button disabled="disabled" id=p13CopyButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Copy" onclick="p13copyFile(0)" />
1461 + <input type=button disabled="disabled" id=p13PasteButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Paste" onclick="p13pasteFile()" />
1462 + <input type=button disabled="disabled" id=p13ZipButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Zip" onclick="p13zipFiles()" />
1463 + <input type=button disabled="disabled" id=p13UnzipButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Unzip" onclick="p13unzipFile()" />
1464 + <input type=button disabled="disabled" id=p13RefreshButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Refresh" onclick="p13folderup(9999)" />
1465 + <input type=button disabled="disabled" id=p13FindButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Find" onclick="p13findfile()" />
1466 + <input type=button disabled="disabled" id=p13GoToFolderButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="GoTo" onclick="p13gotofolder()" />
1467 + <input type=button disabled="disabled" id=p13OpenButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Open" onclick="p13openfilefolder()" />
1468 + <div class="btn-group me-1 mobileFileActionDropdown">
1469 + <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More&nbsp;<i class="fa-solid fa-chevron-down fa-xs"></i></button>
1470 + <div class="dropdown-menu">
1471 + <button type="button" disabled="disabled" id=p13MobileNewFolderButton class="dropdown-item" onclick="p13createfolder()">New Folder</button>
1472 + <button type="button" disabled="disabled" id=p13MobileNewFileButton class="dropdown-item" onclick="p13createfile()">New File</button>
1473 + <button type="button" disabled="disabled" id=p13MobileUploadButton class="dropdown-item" onclick="p13uploadFile()">Upload</button>
1474 + <button type="button" disabled="disabled" id=p13MobileDownloadButton class="dropdown-item" onclick="p13downloadButton()">Download</button>
1475 + <button type="button" disabled="disabled" id=p13MobileCutButton class="dropdown-item" onclick="p13copyFile(1)">Cut</button>
1476 + <button type="button" disabled="disabled" id=p13MobileCopyButton class="dropdown-item" onclick="p13copyFile(0)">Copy</button>
1477 + <button type="button" disabled="disabled" id=p13MobilePasteButton class="dropdown-item" onclick="p13pasteFile()">Paste</button>
1478 + <button type="button" disabled="disabled" id=p13MobileZipButton class="dropdown-item" onclick="p13zipFiles()">Zip</button>
1479 + <button type="button" disabled="disabled" id=p13MobileUnzipButton class="dropdown-item" onclick="p13unzipFile()">Unzip</button>
1480 + <button type="button" disabled="disabled" id=p13MobileRefreshButton class="dropdown-item" onclick="p13folderup(9999)">Refresh</button>
1481 + <button type="button" disabled="disabled" id=p13MobileFindButton class="dropdown-item" onclick="p13findfile()">Find</button>
1482 + <button type="button" disabled="disabled" id=p13MobileGoToFolderButton class="dropdown-item" onclick="p13gotofolder()">GoTo</button>
1483 + <button type="button" disabled="disabled" id=p13MobileOpenButton class="dropdown-item" onclick="p13openfilefolder()">Open</button>
1484 + </div>
1485 + </div>
1486 </div>
1487 </td>
1488 </tr>
@@ -1382,7 +1594,7 @@
1594 <div id="p16BackButton" class="pe-2">
1595 <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1596 </div>
1385 - <div class='fs-4 fw-bold'>Events - <span id=p16deviceName></span></div>
1597 + <div class='fs-4 fw-bold'><i class="fa-solid fa-calendar fa-sm me-2 text-secondary"></i>Events - <span id=p16deviceName></span></div>
1598 </div>
1599 <table class="pTable">
1600 <tr>
@@ -1419,7 +1631,7 @@
1631 <div id="p17BackButton" class="pe-2">
1632 <i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1633 </div>
1422 - <div class='fs-4 fw-bold'>Details - <span id=p17deviceName></span></div>
1634 + <div class='fs-4 fw-bold'><i class="fa-solid fa-list fa-sm me-2 text-secondary"></i>Details - <span id=p17deviceName></span></div>
1635 <div id="devListToolbarViewIcons3" class="ms-auto">
1636 <i class="fa-solid fa-rotate-right fa-xl fa-border" role="button" onclick=refreshDetails(event) title="Refresh details information."></i>
1637 <i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick=deskToggleCpuGraph(event) title="Show device CPU and memory usage."></i>
@@ -1613,8 +1825,8 @@
1825 <div id=p31events style=""></div>
1826 </div>
1827 <div id=p40 style="display:none">
1616 - <div id="p40title">
1617 - <h1>My Server Stats</h1>
1828 + <div id="p40title" class="d-flex align-items-center">
1829 + <div class="fs-4 fw-bold">My Server Stats</div>
1830 </div>
1831 <div class="areaHead d-flex align-items-center flex-wrap p-1">
1832 <div class="d-flex align-items-center">
@@ -1643,8 +1855,8 @@
1855 <canvas id=serverMainStats style=""></canvas>
1856 </div>
1857 <div id=p41 style="display:none">
1646 - <div id="p41title">
1647 - <h1>My Server Tracing</h1>
1858 + <div id="p41title" class="d-flex align-items-center">
1859 + <div class="fs-4 fw-bold">My Server Tracing</div>
1860 </div>
1861 <div class="areaHead d-flex align-items-center flex-wrap p-1">
1862 <div class="d-flex align-items-center">
@@ -1666,7 +1878,9 @@
1878 <div id=p41events style=""></div>
1879 </div>
1880 <div id=p42 style="display:none">
1669 - <h1>My Server Plugins</h1>
1881 + <div id="p42title" class="d-flex align-items-center">
1882 + <div class="fs-4 fw-bold">My Server Plugins</div>
1883 + </div>
1884 <div class="areaHead">
1885 <div class="toright2">
1886 </div>
@@ -1710,8 +1924,8 @@
1924 style="width:100%;height:calc(100vh - 245px);max-height:calc(100vh - 245px)"></iframe>
1925 </div>
1926 <div id=p50 style="display:none">
1713 - <div id="p50title">
1714 - <h1>My User Groups</h1>
1927 + <div id="p50title" class="d-flex align-items-center">
1928 + <div class="fs-4 fw-bold">My User Groups</div>
1929 </div>
1930 <table class="pTable">
1931 <tr>
@@ -1756,8 +1970,8 @@
1970 </div>
1971 </div>
1972 <div id=p52 style="display:none">
1759 - <div id="p52title">
1760 - <h1>My User Recordings</h1>
1973 + <div id="p52title" class="d-flex align-items-center">
1974 + <div class="fs-4 fw-bold">My User Recordings</div>
1975 </div>
1976 <table class="pTable">
1977 <tr>
@@ -1774,8 +1988,8 @@
1988 <div id=p52recordings style="overflow-y:auto"></div>
1989 </div>
1990 <div id=p60 style="display:none">
1777 - <div id="p60title">
1778 - <h1>My Reports</h1>
1991 + <div id="p60title" class="d-flex align-items-center">
1992 + <div class="fs-4 fw-bold">My Reports</div>
1993 </div>
1994 <table class="pTable">
1995 <tr>
@@ -2373,6 +2587,7 @@
2587
2588 // If SSPI or LDAP authentication not used, allow batch account creation.
2589 QV('p4UserBatchCreate', (features & 0x00080000) == 0);
2590 + QV('p4MobileUserBatchCreate', (features & 0x00080000) == 0);
2591
2592 // Set the file editor
2593 d4EditWrapVal = Number(getstore('editorWrap', 0));
@@ -5016,6 +5231,7 @@
5231 if (document.querySelector('.modal.show')) {
5232 return;
5233 }
5234 + mobileKbdGotKeypress = true; // tells onSoftKeyboardInput not to double-send
5235 setSessionActivity();
5236 if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
5237 // Check what keys we are allows to send
@@ -5109,6 +5325,8 @@
5325 }
5326
5327 function ondockeyup(e) {
5328 + mobileKbdGotKeypress = false;
5329 + var sk = Q('softKeyboard'); if (sk && document.activeElement === sk) { sk.value = ''; }
5330 setSessionActivity();
5331 if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
5332 // Check what keys we are allows to send
@@ -8582,7 +8800,7 @@
8800 if (nname.length == 0) { nname = '<i>' + "None" + '</i>'; }
8801 if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Click here to edit the server-side device name" + '" onclick=showEditNodeValueDialog(0) onkeyup="if (event.key == \'Enter\') showEditNodeValueDialog(0)" role="button">' + nname + ' <i class="fa-solid fa-pencil fa-2xs"/></i></span>'; }
8802 nnameEx = nname;
8585 - if (mesh) { nname += '<span style=color:#AAA;font-size:small> - ' + EscapeHtml(mesh.name) + '</span>'; }
8803 + if (mesh) { nname += '<span class="deviceGroupName" style=color:#AAA;font-size:small> - ' + EscapeHtml(mesh.name) + '</span>'; }
8804 QH('p10deviceName', nname);
8805 QH('p11deviceName', nname);
8806 QH('p12deviceName', nname);
@@ -8851,9 +9069,9 @@
9069
9070 x += '</table><br />';
9071 // Show action button, only show if we have permissions 4, 8, 64
8854 - if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
8855 - x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
8856 - x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
9072 + if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
9073 + x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
9074 + x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
9075 if ((node.mtype == 2) && (connectivity & 1) && ((meshrights & 131072) != 0)) { x += '<input type=button class="btn btn-primary btn-sm me-2" cmenu=deskPreConfigScriptContextMenu value="' + "Run" + '" title="' + "Run commands on this device" + '" onclick=runDeviceCmd("' + encodeURIComponentEx(node._id) + '") />'; }
9076 if (node.mtype != 4) {
9077 if ((meshrights & 8) && (connectivity & 1) && ((meshrights & 16384) != 0) || ((node.pmt == 1) && ((features2 & 2) != 0))) { x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Message" + '" title="' + "Display a text message on the remote device" + '" onclick=deviceMessageFunction() />'; }
@@ -9506,6 +9724,85 @@
9724 desktop.m.SendRefresh();
9725 }
9726
9727 + // Adapted from Classic Mobile's deskChangeMouseButton()
9728 + function deskMobileToggleZoom() {
9729 + _mobileSetZoom(_mobileZoomMode === 'native' ? 'fit' : 'native');
9730 + }
9731 +
9732 + function deskToggleRightClickMode() {
9733 + if (xxdialogMode || desktop == null) return;
9734 + desktop.m.SwapMouse = !desktop.m.SwapMouse;
9735 + }
9736 +
9737 + function deskToggleMobileActions() {
9738 + var panel = Q('deskMobileActionsPanel'), backdrop = Q('deskMobileActionsBackdrop');
9739 + if (!panel) return;
9740 + var show = (panel.style.display === 'none');
9741 + if (show) {
9742 + if (document.body.classList.contains('fulldesk')) {
9743 + // Fullscreen mode: rebuild panel with all controls
9744 + _buildFullscreenPanel(panel);
9745 + } else {
9746 + // Normal mode: update dynamic labels to reflect current state
9747 + var rc = Q('deskRightClickItem');
9748 + if (rc) {
9749 + var active = desktop && desktop.m && desktop.m.SwapMouse;
9750 + rc.innerHTML = '<i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>'
9751 + + (active ? 'Disable Right-click Mode' : 'Enable Right-click Mode');
9752 + }
9753 + var tp = Q('deskTrackpadItem');
9754 + if (tp) {
9755 + tp.innerHTML = '<i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>'
9756 + + (trackpadMode ? 'Disable Trackpad Mode' : 'Enable Trackpad Mode');
9757 + }
9758 + }
9759 + }
9760 + QV('deskMobileActionsPanel', show);
9761 + QV('deskMobileActionsBackdrop', show);
9762 + // Toggle floating button icon: when closed -> ✕ when open
9763 + var fsBtn = Q('deskFsBtn');
9764 + if (fsBtn) fsBtn.innerHTML = show ? '<i class="fa-solid fa-xmark"></i>' : '<i class="fa-solid fa-bars"></i>';
9765 + }
9766 +
9767 + function _buildFullscreenPanel(panel) {
9768 + var inputOn = Q('DeskControl') && Q('DeskControl').checked;
9769 + var swap = desktop && desktop.m && desktop.m.SwapMouse;
9770 + var trackpad = (typeof trackpadMode !== 'undefined') && trackpadMode;
9771 + var s = 'font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)';
9772 + var cls = 'd-block px-3 py-2 text-decoration-none border-bottom';
9773 + var end = 'd-block px-3 py-2 text-decoration-none'; // last item, no border
9774 + function row(c, icon, label, action, extra) {
9775 + return '<a class="' + c + '" style="' + s + '" onclick="' + action + '">'
9776 + + '<i class="fa-solid ' + icon + ' fa-fw me-2 ' + (extra || 'text-primary') + '"></i>'
9777 + + label + '</a>';
9778 + }
9779 + panel.innerHTML =
9780 + row(cls, 'fa-compress', 'Exit Fullscreen', 'deskToggleFull();deskCloseMobileActions()', 'text-danger') +
9781 + row(cls, (_mobileZoomMode==='native'?'fa-compress-arrows-alt':'fa-expand-arrows-alt'),
9782 + (_mobileZoomMode==='native'?'Scale to Fit':'Native Size (Scrollable)'), 'deskMobileToggleZoom();deskCloseMobileActions()') +
9783 + row(cls, 'fa-keyboard', 'Input <i class="fa-solid ' + (inputOn?'fa-toggle-on text-success':'fa-toggle-off text-secondary') + ' ms-1"></i>', "Q('DeskControl').click();deskCloseMobileActions()") +
9784 + row(cls, 'fa-keyboard', 'On-screen Keyboard', 'toggleMobileKeyboard();deskCloseMobileActions()') +
9785 + row(cls, 'fa-computer-mouse', (trackpad?'Disable':'Enable')+' Trackpad', 'deskToggleTrackpad();deskCloseMobileActions()') +
9786 + row(cls, 'fa-hand-pointer', (swap?'Disable':'Enable')+' Right-click', 'deskToggleRightClickMode();deskCloseMobileActions()') +
9787 + row(cls, 'fa-font', 'Type Text', 'showDeskType();deskCloseMobileActions()') +
9788 + row(cls, 'fa-terminal', 'Ctrl + Alt + Del', 'if(desktop&&desktop.m)desktop.m.sendcad();deskCloseMobileActions()') +
9789 + row(cls, 'fa-keyboard', 'Send ESC', 'sendDeskEsc();deskCloseMobileActions()') +
9790 + row(cls, 'fa-sliders', 'Keyboard Shortcuts', 'deskCustomizeKeys();deskCloseMobileActions()') +
9791 + row(cls, 'fa-paste', 'Clipboard', 'showDeskClip();deskCloseMobileActions()') +
9792 + row(cls, 'fa-clipboard', 'Upload Clipboard', 'deskClipboardOutFunction();deskCloseMobileActions()') +
9793 + row(cls, 'fa-rotate-right', 'Rotate Right', 'drotate(1);deskCloseMobileActions()') +
9794 + row(cls, 'fa-rotate-left', 'Rotate Left', 'drotate(-1);deskCloseMobileActions()') +
9795 + row(cls, 'fa-rotate', 'Refresh Desktop', 'deskRefreshFunction();deskCloseMobileActions()') +
9796 + row(cls, 'fa-gear', 'Desktop Settings', 'showDesktopSettings();deskCloseMobileActions()') +
9797 + row(end, 'fa-plug-circle-xmark', 'Disconnect', 'connectDesktop(null,0);deskCloseMobileActions()', 'text-danger');
9798 + }
9799 + function deskCloseMobileActions() {
9800 + QV('deskMobileActionsPanel', false);
9801 + QV('deskMobileActionsBackdrop', false);
9802 + var fsBtn = Q('deskFsBtn');
9803 + if (fsBtn) fsBtn.innerHTML = '<i class="fa-solid fa-bars"></i>';
9804 + }
9805 +
9806 function deviceToastFunction() {
9807 if (xxdialogMode) return;
9808 var x = '<select id=d2deviceop style=width:100%;margin-bottom:4px class=form-select><option value=2>' + "Toast Notification" + '</option><option value=1>' + "Message Box" + '</option><option value=3>' + "Alert Box" + '</option></select>';
@@ -10356,7 +10653,7 @@
10653 delete multiDesktop[currentNode._id];
10654 } else {
10655 // Device is not already connected, just setup a blank canvas
10359 - if (desktop == null) { QH('DeskParent', '<canvas id=Desk oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event)></canvas>'); }
10656 + if (desktop == null) { QH('DeskParent', '<canvas id=Desk oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event) ontouchstart=dtouchstart(event) ontouchmove=dtouchmove(event) ontouchend=dtouchend(event)></canvas>'); }
10657 desktopNode = currentNode;
10658 }
10659 // Setup the mouse wheel
@@ -10367,6 +10664,7 @@
10664 if (desktop) { desktop.m.onDisplayinfo = deskDisplayInfo; deskDisplayInfo(desktop.m, desktop.m.displays, desktop.m.selectedDisplay); }
10665 updateDesktopButtons();
10666 deskAdjust();
10667 +
10668 updateMetadata(desktop, 'deskmetadata');
10669 }
10670
@@ -10423,6 +10721,7 @@
10721 QE('connectbutton1h', hwonline);
10722 QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus));
10723 QE('DeskClip', deskState == 3);
10724 + QE('DeskSoftKbdBtn', deskState == 3);
10725 QV('DeskClip', (inputAllowed) && (currentNode.agent) && ((features2 & 0x1800) != 0x1800) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && ((desktop == null) || (desktop.contype != 2)) && ((desktopsettings.autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null))); // Clipboard not supported on macOS
10726 QE('DeskESC', (deskState == 3) && (desktop.contype != 4));
10727 QV('DeskESC', browserfullscreen && inputAllowed);
@@ -10621,6 +10920,70 @@
10920 if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
10921 //desktop.m.onDisplayinfo = deskDisplayInfo;
10922 desktop.m.onScreenSizeChange = deskAdjust;
10923 + if (document.body.classList.contains('is-mobile')) {
10924 + desktop.m.GetPositionOfControl = function(canvas) {
10925 + var r = canvas.getBoundingClientRect();
10926 + return [r.left + window.scrollX, r.top + window.scrollY];
10927 + };
10928 + var _origGrab = desktop.m.GrabMouseInput;
10929 + var _origUngrab = desktop.m.UnGrabMouseInput;
10930 + desktop.m.GrabMouseInput = function() {
10931 + _origGrab.call(desktop.m);
10932 + var c = desktop.m.CanvasId;
10933 + c.ontouchstart = null;
10934 + c.ontouchmove = null;
10935 + c.ontouchend = null;
10936 + c.addEventListener('touchstart', dtouchstart, { passive: false });
10937 + c.addEventListener('touchmove', dtouchmove, { passive: false });
10938 + c.addEventListener('touchend', dtouchend, { passive: false });
10939 + };
10940 + desktop.m.UnGrabMouseInput = function() {
10941 + var c = desktop.m.CanvasId;
10942 + c.removeEventListener('touchstart', dtouchstart);
10943 + c.removeEventListener('touchmove', dtouchmove);
10944 + c.removeEventListener('touchend', dtouchend);
10945 + _origUngrab.call(desktop.m);
10946 + };
10947 + // Override SendMouseMsg to fix the bounds check for rotated canvas.
10948 + // After setRotation(1), canvas.width=1080 & canvas.height=1920 (swapped).
10949 + // The original check is X<=canvas.width (1080), but our rotation-corrected
10950 + // X (= remote.x) can be up to 1920 — clicks in the lower half of the
10951 + // rotated view are silently dropped. We replace the check with the correct
10952 + // unrotated remote dimensions.
10953 + (function() {
10954 + var dm = desktop.m, _origSMM = dm.SendMouseMsg;
10955 + dm.SendMouseMsg = function(Action, event) {
10956 + var rot = this.rotation;
10957 + if (!rot) return _origSMM.call(this, Action, event);
10958 + if (Action == null || !this.Canvas) return;
10959 + var cw = this.Canvas.canvas.width, ch = this.Canvas.canvas.height;
10960 + var ScaleW = cw / this.CanvasId.clientWidth;
10961 + var ScaleH = ch / this.CanvasId.clientHeight;
10962 + var off = this.GetPositionOfControl(this.Canvas.canvas);
10963 + var X = (event.pageX - off[0]) * ScaleW;
10964 + var Y = (event.pageY - off[1]) * ScaleH;
10965 + if (event.addx) X += event.addx;
10966 + if (event.addy) Y += event.addy;
10967 + // Correct max: for rot=1,3 remote.x<=ch and remote.y<=cw (unrotated dims)
10968 + var maxX = (rot===1||rot===3) ? ch : cw;
10969 + var maxY = (rot===1||rot===3) ? cw : ch;
10970 + if (X<0||X>maxX||Y<0||Y>maxY) return;
10971 + var Button = 0;
10972 + if (Action===this.KeyAction.UP||Action===this.KeyAction.DOWN) {
10973 + if (event.which) { Button=(event.which===1)?this.MouseButton.LEFT:((event.which===2)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
10974 + else if (typeof event.button==='number') { Button=(event.button===0)?this.MouseButton.LEFT:((event.button===1)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
10975 + }
10976 + if (this.SwapMouse) { if(Button===this.MouseButton.LEFT){Button=this.MouseButton.RIGHT;}else if(Button===this.MouseButton.RIGHT){Button=this.MouseButton.LEFT;} }
10977 + var msg = String.fromCharCode(0x00,this.InputType.MOUSE,0x00,0x0A,0x00,
10978 + (Action===this.KeyAction.DBLCLICK?0x88:Action===this.KeyAction.DOWN?Button:(Button*2)&0xFF),
10979 + ((X/256)&0xFF),(X&0xFF),((Y/256)&0xFF),(Y&0xFF));
10980 + if (this.State===3) {
10981 + if (Action===this.KeyAction.NONE) { if(this.Alternate===0||this.ipad){this.send(msg);this.Alternate=1;}else{this.Alternate=0;} }
10982 + else { this.send(msg); }
10983 + }
10984 + };
10985 + })();
10986 + }
10987 desktop.Start(desktopNode._id);
10988 desktop.latency.callback = function (ms) { /* console.log('latency', ms); */ updateSessionTime(); };
10989 desktop.contype = 1;
@@ -10820,6 +11183,13 @@
11183 QV('DeskInputUnLockedButton', false);
11184 deskFocusBtn.value = "All Focus";
11185 if (fullscreen == true) { deskToggleFull(); }
11186 + _mobileAutoFullscreen = false; // reset so reconnecting in landscape re-triggers
11187 + // Hide trackpad cursor and reset trackpad state on disconnect
11188 + if (typeof trackpadMode !== 'undefined') {
11189 + trackpadMode = false;
11190 + var _tpc = document.getElementById('trackpadCursor');
11191 + if (_tpc) _tpc.style.display = 'none';
11192 + }
11193 webRtcDesktopReset();
11194 deskPreferedStickyDisplay = 0;
11195 break;
@@ -10838,6 +11208,27 @@
11208 deskLastClipboardSent = null;
11209 }
11210 if (updateSessionTimer == null) { updateSessionTimer = setInterval(updateSessionTime, 1000); }
11211 + // On mobile, wake the agent to start streaming.
11212 + // SendRefresh alone is sometimes ignored before the first mouse event.
11213 + // Synthetic mousedown+mouseup guarantees the agent starts sending frames.
11214 + if (document.body.classList.contains('is-mobile')) {
11215 + // Re-trigger landscape fullscreen if phone is already in landscape
11216 + // when reconnecting (no orientationchange event fires in this case)
11217 + setTimeout(function() { if (typeof _doOrientationUpdate === 'function') _doOrientationUpdate(); }, 600);
11218 + setTimeout(function() {
11219 + if (!desktop || desktop.State !== 3) return;
11220 + desktop.m.SendRefresh();
11221 + var dm = desktop.m;
11222 + if (dm && dm.CanvasId) {
11223 + var r = dm.CanvasId.getBoundingClientRect();
11224 + if (r.width > 0) {
11225 + var ev = { pageX: r.left + window.scrollX + r.width / 2, pageY: r.top + window.scrollY + r.height / 2, button: 0, buttons: 0, which: 1 };
11226 + dm.mousedown(ev);
11227 + dm.mouseup(ev);
11228 + }
11229 + }
11230 + }, 500);
11231 + }
11232 break;
11233 default:
11234 //console.log('Unknown onDesktopStateChange state', state);
@@ -11052,8 +11443,8 @@
11443 QS('termTable')['max-height'] = '100%';
11444 }
11445
11055 - // If shift is pressed, enter browser full screen.
11056 - if (e.shiftKey == true) {
11446 + // On mobile or when shift is pressed, enter browser full screen.
11447 + if (e.shiftKey == true || document.body.classList.contains('is-mobile')) {
11448 enterBrowserFullscreen(Q('body'));
11449 browserfullscreen = true;
11450 }
@@ -11224,16 +11615,28 @@
11615 // Customize keyboard shortcuts
11616 function deskCustomizeKeys() {
11617 if (xxdialogMode) return;
11227 - var x = '<div id=d2shortcuts style="width:100%;height:180px;padding:4px;overflow-y:auto;border:1px solid gray"></div><div style=width:100%;padding:5px>';
11228 - x += '<label><input id=d1kshift type=checkbox class="form-check-input me-2" /> ' + "Shift" + '</label><label> <input id=d1kalt type=checkbox class="form-check-input me-2" /> ' + "Alt" + '</label><label> <input id=d1kctrl type=checkbox class="form-check-input me-2" /> ' + "Ctrl" + '</label> <input id=d1kwin type=checkbox class="form-check-input me-2" /> ' + "Win" + '</label>';
11229 - x += ' <select id=d2keySelect>';
11618 + // Shortcut list
11619 + var x = '<div id="d2shortcuts" class="list-group mb-3" style="max-height:200px;overflow-y:auto"></div>';
11620 + // Modifier checkboxes
11621 + x += '<div class="d-flex flex-wrap gap-3 mb-2">';
11622 + x += '<div class="form-check"><input id="d1kshift" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kshift">Shift</label></div>';
11623 + x += '<div class="form-check"><input id="d1kalt" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kalt">Alt</label></div>';
11624 + x += '<div class="form-check"><input id="d1kctrl" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kctrl">Ctrl</label></div>';
11625 + x += '<div class="form-check"><input id="d1kwin" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kwin">Win</label></div>';
11626 + x += '</div>';
11627 + // Key select + Add button
11628 + x += '<div class="d-flex gap-2 mb-3">';
11629 + x += '<select id="d2keySelect" class="form-select form-select-sm">';
11630 for (var i in keyStrings) { x += '<option value=' + i + '>' + keyStrings[i] + '</option>'; }
11631 for (var i = 1; i <= 12; i++) { x += '<option value=' + (i + 111) + '>F' + i + '</option>'; }
11632 for (var i = 0; i < 10; i++) { x += '<option value=' + (i + 48) + '>' + i + '</option>'; }
11633 for (var i = 0; i < 26; i++) { x += '<option value=' + (i + 65) + '>' + String.fromCharCode(i + 65) + '</option>'; }
11234 - x += '</select> <input type=button value=' + "Add" + ' onclick=addDeskCustomizeKey() /></div>';
11235 - x += '<div style=width:100%;padding:2px;text-align:center><input type=button value="' + "Restore Default Keyboard Shortcuts" + '" onclick=restoreDeskCustomizeKey() /></div>';
11236 - setModalContent('xxAddAgent', "Keyboard Shortcuts Customization", x);
11634 + x += '</select>';
11635 + x += '<button type="button" class="btn btn-primary btn-sm flex-shrink-0" onclick="addDeskCustomizeKey()">Add</button>';
11636 + x += '</div>';
11637 + // Restore defaults
11638 + x += '<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="restoreDeskCustomizeKey()">Restore Default Shortcuts</button>';
11639 + setModalContent('xxAddAgent', "Keyboard Shortcuts", x);
11640 showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deskCustomizeKeysEx());
11641 deskUpdateShortcutList();
11642 }
@@ -11244,14 +11647,22 @@
11647 }
11648
11649 function deskUpdateShortcutList() {
11247 - var x = '';
11650 + var x = '', last = deskKeyboardShortcuts.length - 1;
11651 for (var i in deskKeyboardShortcuts) {
11249 - var kt = keyShortcutTotext(deskKeyboardShortcuts[i]), orderButtons = '';
11250 - if (i != (deskKeyboardShortcuts.length - 1)) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c2.png" onclick=deskCustomizeKeyDown(' + deskKeyboardShortcuts[i] + ')>'; }
11251 - if (i != 0) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c3.png" onclick=deskCustomizeKeyUp(' + deskKeyboardShortcuts[i] + ')>'; }
11252 - x += '<div style="width:100%;background-color:#AAA;border-radius:4px;margin-bottom:4px;padding:4px;text-align:left;box-sizing:border-box" value=' + deskKeyboardShortcuts[i] + '>' + kt + '<img width=10 height=10 style=float:right;cursor:pointer;padding:2px;margin-left:8px src="images/trash.png" onclick=removeDeskCustomizeKey(' + deskKeyboardShortcuts[i] + ')>' + orderButtons + '</div>';
11652 + var k = deskKeyboardShortcuts[i];
11653 + x += '<div class="list-group-item d-flex align-items-center justify-content-between py-2 px-3 gap-2">';
11654 + x += '<span class="flex-grow-1">' + keyShortcutTotext(k) + '</span>';
11655 + x += '<div class="d-flex gap-1 flex-shrink-0">';
11656 + if (parseInt(i) > 0) {
11657 + x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyUp(' + k + ')" title="Move up"><i class="fa-solid fa-arrow-up fa-xs"></i></button>';
11658 + }
11659 + if (parseInt(i) < last) {
11660 + x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyDown(' + k + ')" title="Move down"><i class="fa-solid fa-arrow-down fa-xs"></i></button>';
11661 + }
11662 + x += '<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeDeskCustomizeKey(' + k + ')" title="Remove"><i class="fa-solid fa-trash fa-xs"></i></button>';
11663 + x += '</div></div>';
11664 }
11254 - if (x == '') { x = '<i>' + "No keyboard shortcuts defined" + '</i>'; }
11665 + if (x == '') { x = '<div class="list-group-item text-muted fst-italic">No keyboard shortcuts defined</div>'; }
11666 QH('d2shortcuts', x);
11667 }
11668
@@ -11291,7 +11702,16 @@
11702 if (Q('d1kalt').checked) { k |= 0x020000; }
11703 if (Q('d1kctrl').checked) { k |= 0x080000; }
11704 if (Q('d1kwin').checked) { k |= 0x100000; }
11294 - if ((k > 0) && (deskKeyboardShortcuts.indexOf(k) == -1)) { deskKeyboardShortcuts.push(k); deskUpdateShortcutList(); }
11705 + if ((k > 0) && (deskKeyboardShortcuts.indexOf(k) == -1)) {
11706 + deskKeyboardShortcuts.push(k);
11707 + deskUpdateShortcutList();
11708 + // Reset form so the next shortcut starts fresh
11709 + Q('d1kshift').checked = false;
11710 + Q('d1kalt').checked = false;
11711 + Q('d1kctrl').checked = false;
11712 + Q('d1kwin').checked = false;
11713 + Q('d2keySelect').selectedIndex = 0;
11714 + }
11715 }
11716
11717 // Customize keyboard strings
@@ -11821,6 +12241,335 @@
12241 function dmousemove(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { Q('Desk').style.cursor = ''; if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousemove(e); desktop.m.sendKeepAlive(); } else { desktop.m.mousemove(e); } } else if (!xxdialogMode && desktop != null && !Q('DeskControl').checked) { Q('Desk').style.cursor = 'not-allowed'; } }
12242 function dmousewheel(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousewheel(e); desktop.m.sendKeepAlive(); } else { if (desktop.m.mousewheel) { desktop.m.mousewheel(e); } } haltEvent(e); return true; } return false; }
12243 function drotate(x) { if (!xxdialogMode && desktop != null) { desktop.m.setRotation(desktop.m.rotation + x); deskAdjust(); deskAdjust(); } }
12244 +
12245 + // Mobile touch input for remote desktop.
12246 + // 1-finger tap = left click on remote
12247 + // Direct mode — 1-finger tap=click, long-press=right-click, drag=cursor move
12248 + // Trackpad mode — 1-finger drag moves visible cursor; tap clicks at cursor position
12249 + // Both modes — 2-finger scroll = mouse wheel on remote
12250 + var dtouchStartX = 0, dtouchStartY = 0, dtouchLastX = 0, dtouchLastY = 0;
12251 + var dtouchMoved = false;
12252 + var dtouchLastTapTime = 0, dtouchLastTapX = 0, dtouchLastTapY = 0; // double-tap tracking
12253 + var dtouchLongPressTimer = null;
12254 + var dtouchTwoFingerLastY = 0;
12255 + // Trackpad state (mirrors Classic Mobile)
12256 + var trackpadMode = false;
12257 + var trackpadLastX = null, trackpadLastY = null;
12258 + var trackpadCurX = 0, trackpadCurY = 0;
12259 + var trackpadDown = false;
12260 + var trackpadTapTimer = null;
12261 + var trackpadTapMoved = false;
12262 + var trackpadEdgeTimer = null;
12263 + var trackpadEdgeDX = 0, trackpadEdgeDY = 0;
12264 +
12265 + function dtouchMakeEvent(clientX, clientY, button) {
12266 + // When the canvas is rotated, crotX/crotY (agent-desktop-0.0.2.js) convert
12267 + // canvas-pixel space -> remote-desktop space. We apply that correction here by
12268 + // computing adjusted clientX/Y that will produce the correct remote coords after
12269 + // SendMouseMsg's (pageX - offset) * scale calculation.
12270 + var adjX = clientX, adjY = clientY;
12271 + var dm = dtouchGetModule();
12272 + if (dm && dm.rotation && dm.CanvasId && dm.Canvas && dm.crotX && dm.crotY) {
12273 + var r = dm.CanvasId.getBoundingClientRect();
12274 + if (r.width > 0 && r.height > 0) {
12275 + var cw = dm.Canvas.canvas.width, ch = dm.Canvas.canvas.height;
12276 + var px = (clientX - r.left) * cw / r.width;
12277 + var py = (clientY - r.top) * ch / r.height;
12278 + var rx = dm.crotX(px, py), ry = dm.crotY(px, py);
12279 + adjX = r.left + rx * r.width / cw;
12280 + adjY = r.top + ry * r.height / ch;
12281 + }
12282 + }
12283 + return { pageX: adjX + window.scrollX, pageY: adjY + window.scrollY, button: button || 0, buttons: 0, which: (button === 2) ? 3 : 1 };
12284 + }
12285 +
12286 + function dtouchGetModule() {
12287 + return (webRtcDesktop && webRtcDesktop.softdesktop) ? webRtcDesktop.softdesktop.m : (desktop ? desktop.m : null);
12288 + }
12289 +
12290 + // Trackpad mode (ported from Classic Mobile)
12291 + function deskToggleTrackpad() {
12292 + trackpadMode = !trackpadMode;
12293 + if (trackpadMode) {
12294 + var dp = Q('DeskParent'), r = dp.getBoundingClientRect();
12295 + trackpadMoveCursor(r.left + r.width / 2, r.top + r.height / 2);
12296 + } else {
12297 + trackpadStopEdgeScroll();
12298 + Q('trackpadCursor').style.display = 'none';
12299 + trackpadCurX = 0; trackpadCurY = 0;
12300 + }
12301 + }
12302 +
12303 + function trackpadMoveCursor(clientX, clientY) {
12304 + var deskRect = Q('Desk').getBoundingClientRect();
12305 + // Clamp cursor within canvas bounds
12306 + var cx = Math.max(0, Math.min(deskRect.width, clientX - deskRect.left));
12307 + var cy = Math.max(0, Math.min(deskRect.height, clientY - deskRect.top));
12308 + var cursor = Q('trackpadCursor');
12309 + // position:fixed -> coordinates are viewport-relative (never clipped by overflow:hidden)
12310 + cursor.style.left = (deskRect.left + cx) + 'px';
12311 + cursor.style.top = (deskRect.top + cy) + 'px';
12312 + // Rotate cursor to match canvas rotation so it always points "up" in the current view
12313 + var dm = dtouchGetModule();
12314 + cursor.style.transform = 'rotate(' + ((dm ? (dm.rotation || 0) : 0) * 90) + 'deg)';
12315 + cursor.style.display = 'block';
12316 + trackpadCurX = cx; trackpadCurY = cy;
12317 + }
12318 +
12319 + function trackpadMakeEvent(button) {
12320 + // Build event at cursor position (not finger position); dtouchMakeEvent handles rotation
12321 + var rect = Q('Desk').getBoundingClientRect();
12322 + return dtouchMakeEvent(rect.left + trackpadCurX, rect.top + trackpadCurY, button);
12323 + }
12324 +
12325 + function trackpadSendMove() {
12326 + var dm = dtouchGetModule();
12327 + if (!xxdialogMode && dm) dm.mousemove(trackpadMakeEvent(0));
12328 + }
12329 +
12330 + function trackpadSendClick(button) {
12331 + var dm = dtouchGetModule();
12332 + if (!xxdialogMode && dm) { dm.mousedown(trackpadMakeEvent(button)); dm.mouseup(trackpadMakeEvent(button)); }
12333 + }
12334 +
12335 + function trackpadStopEdgeScroll() {
12336 + if (trackpadEdgeTimer) { clearInterval(trackpadEdgeTimer); trackpadEdgeTimer = null; }
12337 + trackpadEdgeDX = 0; trackpadEdgeDY = 0;
12338 + }
12339 +
12340 + function trackpadStartEdgeScroll() {
12341 + if (trackpadEdgeTimer) return;
12342 + trackpadEdgeTimer = setInterval(function() {
12343 + if (!trackpadMode || (trackpadEdgeDX === 0 && trackpadEdgeDY === 0)) { trackpadStopEdgeScroll(); return; }
12344 + var dp = Q('DeskParent'); dp.scrollLeft += trackpadEdgeDX; dp.scrollTop += trackpadEdgeDY;
12345 + }, 16);
12346 + }
12347 +
12348 + function trackpadCheckEdge() {
12349 + var deskRect = Q('Desk').getBoundingClientRect();
12350 + // Use cursor position relative to canvas for edge detection
12351 + var cx = deskRect.left + trackpadCurX, cy = deskRect.top + trackpadCurY, zone = 60;
12352 + trackpadEdgeDX = cx < deskRect.left + zone ? -Math.round((zone-(cx-deskRect.left))/4) :
12353 + cx > deskRect.right - zone ? Math.round((zone-(deskRect.right-cx))/4) : 0;
12354 + trackpadEdgeDY = cy < deskRect.top + zone ? -Math.round((zone-(cy-deskRect.top))/4) :
12355 + cy > deskRect.bottom - zone ? Math.round((zone-(deskRect.bottom-cy))/4) : 0;
12356 + if (trackpadEdgeDX || trackpadEdgeDY) trackpadStartEdgeScroll(); else trackpadStopEdgeScroll();
12357 + }
12358 +
12359 + // Mobile soft-keyboard: focusing #softKeyboard shows the OS keyboard.
12360 + // Key events bubble to document handlers (backspace, enter, etc.).
12361 + // Mobile keyboards fire 'input' instead of keypress -> SendKeyUnicode.
12362 + var mobileKbdGotKeypress = false;
12363 +
12364 + function onSoftKeyboardInput(el) {
12365 + if (!desktop || !desktop.m || !Q('DeskControl').checked) { el.value = ''; return; }
12366 + if (mobileKbdGotKeypress) { el.value = ''; return; } // desktop keyboard — already handled by keypress
12367 + var str = el.value; el.value = '';
12368 + if (str && desktop.m.SendKeyUnicode) {
12369 + for (var i = 0; i < str.length; i++) {
12370 + var c = str.charCodeAt(i);
12371 + desktop.m.SendKeyUnicode(desktop.m.KeyAction.DOWN, c);
12372 + desktop.m.SendKeyUnicode(desktop.m.KeyAction.UP, c);
12373 + }
12374 + }
12375 + }
12376 +
12377 + function onSoftKeyboardFocusChange(focused) {
12378 + // Update the toggle button icon: solid keyboard = open, regular = closed
12379 + var btn = Q('DeskSoftKbdBtn');
12380 + if (btn) {
12381 + var icon = btn.querySelector('i');
12382 + if (icon) {
12383 + icon.className = focused ? 'fa-solid fa-keyboard text-warning' : 'fa-solid fa-keyboard';
12384 + }
12385 + }
12386 + }
12387 +
12388 + function toggleMobileKeyboard() {
12389 + var kb = Q('softKeyboard');
12390 + if (!kb) return;
12391 + if (document.activeElement === kb) { kb.blur(); } else { kb.focus(); }
12392 + }
12393 +
12394 + function dtouchSendClick(clientX, clientY, button) {
12395 + var ev = dtouchMakeEvent(clientX, clientY, button);
12396 + var dm = dtouchGetModule();
12397 + if (dm) { dm.mousedown(ev); dm.mouseup(ev); }
12398 + }
12399 +
12400 + function dtouchstart(e) {
12401 + setSessionActivity();
12402 + deskCloseMobileActions();
12403 + dtouchMoved = false;
12404 + if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12405 + if (xxdialogMode || desktop == null) return;
12406 + e.preventDefault();
12407 + // Trackpad mode
12408 + if (trackpadMode) {
12409 + trackpadStopEdgeScroll();
12410 + if (e.touches.length !== 1) return;
12411 + var t = e.touches[0];
12412 + trackpadLastX = t.clientX; trackpadLastY = t.clientY;
12413 + trackpadTapMoved = false;
12414 + trackpadTapTimer = setTimeout(function() { trackpadTapTimer = null; }, 250);
12415 + trackpadDown = false;
12416 + return;
12417 + }
12418 + // Direct mode — 2 fingers
12419 + if (e.touches.length === 2) {
12420 + var fdx = e.touches[0].clientX - e.touches[1].clientX;
12421 + var fdy = e.touches[0].clientY - e.touches[1].clientY;
12422 + dtouchTwoFingerLastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12423 + if (_mobileZoomMode === 'native') {
12424 + // Record pinch start for zoom gesture
12425 + _nativePinchStartDist = Math.sqrt(fdx*fdx + fdy*fdy);
12426 + var sc = Q('deskarea3x');
12427 + _nativePinchStartW = sc ? sc.scrollWidth : 640;
12428 + _nativePinchStartH = sc ? sc.scrollHeight : 480;
12429 + _nativePinchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
12430 + _nativePinchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12431 + }
12432 + return;
12433 + }
12434 + if (e.touches.length !== 1) return;
12435 + var t = e.touches[0];
12436 + dtouchStartX = dtouchLastX = t.clientX;
12437 + dtouchStartY = dtouchLastY = t.clientY;
12438 + dtouchLongPressTimer = setTimeout(function() {
12439 + dtouchLongPressTimer = null;
12440 + if (dtouchMoved || xxdialogMode || desktop == null || !Q('DeskControl').checked) return;
12441 + dtouchSendClick(dtouchLastX, dtouchLastY, 2);
12442 + dtouchMoved = true;
12443 + }, 500);
12444 + }
12445 +
12446 + function dtouchmove(e) {
12447 + setSessionActivity();
12448 + if (xxdialogMode || desktop == null) return;
12449 + e.preventDefault();
12450 + // Trackpad mode
12451 + if (trackpadMode) {
12452 + if (e.touches.length !== 1) return;
12453 + var t = e.touches[0];
12454 + if (trackpadLastX === null) { trackpadLastX = t.clientX; trackpadLastY = t.clientY; return; }
12455 + var dx = t.clientX - trackpadLastX, dy = t.clientY - trackpadLastY;
12456 + trackpadLastX = t.clientX; trackpadLastY = t.clientY;
12457 + if (Math.abs(dx) > 1 || Math.abs(dy) > 1) trackpadTapMoved = true;
12458 + var rect = Q('Desk').getBoundingClientRect();
12459 + trackpadMoveCursor(rect.left + trackpadCurX + dx * 1.5, rect.top + trackpadCurY + dy * 1.5);
12460 + trackpadSendMove();
12461 + trackpadCheckEdge();
12462 + return;
12463 + }
12464 + // Native zoom mode — 1 finger: pan by adjusting scrollLeft/Top
12465 + if (_mobileZoomMode === 'native' && e.touches.length === 1) {
12466 + var t = e.touches[0];
12467 + var dx = t.clientX - dtouchLastX, dy = t.clientY - dtouchLastY;
12468 + dtouchLastX = t.clientX; dtouchLastY = t.clientY;
12469 + if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
12470 + dtouchMoved = true;
12471 + var sc = Q('deskarea3x');
12472 + if (sc) { sc.scrollLeft -= dx; sc.scrollTop -= dy; }
12473 + }
12474 + return;
12475 + }
12476 + // Native zoom mode — 2 fingers: pinch to resize canvas (zoom in/out)
12477 + if (_mobileZoomMode === 'native' && e.touches.length === 2 && _nativePinchStartDist > 0) {
12478 + var fdx = e.touches[0].clientX - e.touches[1].clientX;
12479 + var fdy = e.touches[0].clientY - e.touches[1].clientY;
12480 + var newDist = Math.sqrt(fdx*fdx + fdy*fdy);
12481 + var ratio = newDist / _nativePinchStartDist;
12482 + var newW = Math.round(_nativePinchStartW * ratio);
12483 + var newH = Math.round(_nativePinchStartH * ratio);
12484 + // Clamp between 25% and 400% of the native remote resolution
12485 + var dm = dtouchGetModule();
12486 + var minW = Math.round((dm ? dm.ScreenWidth : 640) * 0.25);
12487 + var maxW = Math.round((dm ? dm.ScreenWidth : 640) * 4);
12488 + var minH = Math.round((dm ? dm.ScreenHeight : 480) * 0.25);
12489 + var maxH = Math.round((dm ? dm.ScreenHeight : 480) * 4);
12490 + newW = Math.max(minW, Math.min(maxW, newW));
12491 + newH = Math.max(minH, Math.min(maxH, newH));
12492 + document.documentElement.style.setProperty('--native-desk-w', newW + 'px');
12493 + document.documentElement.style.setProperty('--native-desk-h', newH + 'px');
12494 + // Keep pinch midpoint fixed on screen by adjusting scroll
12495 + var sc = Q('deskarea3x');
12496 + if (sc) {
12497 + var scRect = sc.getBoundingClientRect();
12498 + var midViewX = _nativePinchMidX - scRect.left;
12499 + var midViewY = _nativePinchMidY - scRect.top;
12500 + sc.scrollLeft = (sc.scrollLeft + midViewX) * ratio - midViewX;
12501 + sc.scrollTop = (sc.scrollTop + midViewY) * ratio - midViewY;
12502 + // Update start values for incremental updates each frame
12503 + _nativePinchStartDist = newDist;
12504 + _nativePinchStartW = newW;
12505 + _nativePinchStartH = newH;
12506 + }
12507 + dtouchMoved = true;
12508 + return;
12509 + }
12510 + // Direct mode
12511 + if (e.touches.length === 2) {
12512 + var midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
12513 + var midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12514 + var dy = dtouchTwoFingerLastY - midY;
12515 + dtouchTwoFingerLastY = midY;
12516 + if (Math.abs(dy) > 1 && Q('DeskControl').checked) {
12517 + var dm = dtouchGetModule();
12518 + if (dm && dm.SendMouseMsg && dm.KeyAction) {
12519 + var wev = dtouchMakeEvent(midX, midY, 0);
12520 + wev.wheelDelta = -dy * 4;
12521 + dm.SendMouseMsg(dm.KeyAction.SCROLL, wev);
12522 + }
12523 + }
12524 + return;
12525 + }
12526 + if (e.touches.length !== 1) return;
12527 + var t = e.touches[0];
12528 + dtouchLastX = t.clientX; dtouchLastY = t.clientY;
12529 + if (Math.abs(t.clientX - dtouchStartX) > 8 || Math.abs(t.clientY - dtouchStartY) > 8) {
12530 + dtouchMoved = true;
12531 + if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12532 + }
12533 + }
12534 +
12535 + function dtouchend(e) {
12536 + setSessionActivity();
12537 + if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12538 + if (xxdialogMode || desktop == null) return;
12539 + e.preventDefault();
12540 + // Trackpad mode
12541 + if (trackpadMode) {
12542 + trackpadStopEdgeScroll();
12543 + trackpadLastX = null; trackpadLastY = null; trackpadDown = false;
12544 + if (trackpadTapTimer !== null && !trackpadTapMoved) {
12545 + clearTimeout(trackpadTapTimer); trackpadTapTimer = null;
12546 + if (Q('DeskControl').checked) trackpadSendClick(0);
12547 + }
12548 + return;
12549 + }
12550 + // Direct mode
12551 + if (!Q('DeskControl').checked) return;
12552 + if (e.changedTouches.length !== 1) return;
12553 + var t = e.changedTouches[0];
12554 + // 30px threshold (not dtouchMoved's 8px) — finger pressure causes micro-movement
12555 + // that would block taps; only real drags exceed 30px.
12556 + var totalDx = Math.abs(t.clientX - dtouchStartX);
12557 + var totalDy = Math.abs(t.clientY - dtouchStartY);
12558 + if (totalDx > 30 || totalDy > 30) return; // real drag — no click
12559 + var now = Date.now();
12560 + var dx = Math.abs(t.clientX - dtouchLastTapX), dy = Math.abs(t.clientY - dtouchLastTapY);
12561 + if ((now - dtouchLastTapTime) < 400 && dx < 30 && dy < 30) {
12562 + // Use first tap's position: Windows requires both clicks within 4 remote px;
12563 + // a 2px finger shift × ~5x scale = 10 remote px, breaking detection.
12564 + dtouchSendClick(dtouchLastTapX, dtouchLastTapY, 0);
12565 + dtouchLastTapTime = 0; // reset so a 3rd tap doesn't re-trigger
12566 + } else {
12567 + dtouchSendClick(t.clientX, t.clientY, 0);
12568 + dtouchLastTapTime = now;
12569 + dtouchLastTapX = t.clientX;
12570 + dtouchLastTapY = t.clientY;
12571 + }
12572 + }
12573 function stopProcess(id, name) {
12574 setModalContent('xxAddAgent', "Process Control", format("Stop process #{0} \"{1}\"?", id, name));
12575 showModal('xxAddAgentModal', 'idx_dlgOkButton', () => stopProcessEx(3, (id + '|' + name)));
@@ -12170,6 +12919,9 @@
12919 xterm.loadAddon(xtermimage);
12920 xterm.open(Q('termarea3xdiv')); // termarea3x
12921 xterm.onData(function (data) { if (terminal != null) { if (terminal.urlname == 'sshterminalrelay.ashx') { terminal.socket.send('~' + data); } else { terminal.sendText(data); } } })
12922 + if (document.body.classList.contains('is-mobile')) {
12923 + Q('termarea3xdiv').addEventListener('touchend', function() { xterm.focus(); });
12924 + }
12925 if (xtermfit) { xtermfit.fit(); }
12926 xterm.onTitleChange(function (title) { QH('termtitle', ' - ' + EscapeHtml(title)); });
12927 xterm.onResize(function (size) {
@@ -12848,6 +13600,21 @@
13600 QV('p13ZipButton', filesNode.mtype != 3);
13601 QV('p13UnzipButton', filesNode.mtype != 3);
13602 QV('p13PasteButton', filesNode.mtype != 3);
13603 + [
13604 + ['p13NewFolderButton', 'p13MobileNewFolderButton'],
13605 + ['p13NewFileButton', 'p13MobileNewFileButton'],
13606 + ['p13UploadButton', 'p13MobileUploadButton'],
13607 + ['p13DownloadButton', 'p13MobileDownloadButton'],
13608 + ['p13CutButton', 'p13MobileCutButton'],
13609 + ['p13CopyButton', 'p13MobileCopyButton'],
13610 + ['p13PasteButton', 'p13MobilePasteButton'],
13611 + ['p13ZipButton', 'p13MobileZipButton'],
13612 + ['p13UnzipButton', 'p13MobileUnzipButton'],
13613 + ['p13RefreshButton', 'p13MobileRefreshButton'],
13614 + ['p13FindButton', 'p13MobileFindButton'],
13615 + ['p13GoToFolderButton', 'p13MobileGoToFolderButton'],
13616 + ['p13OpenButton', 'p13MobileOpenButton']
13617 + ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
13618 }
13619
13620 function p13getFileSelCount(includeDirs) { var cc = 0; var checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
@@ -14337,13 +15104,15 @@
15104 QH('p15statetext', '');
15105 QH('p15coreName', '');
15106 QV('p15outputselecttd', false);
15107 + QV('p15BackButton', false); // no "back" navigation from server console
15108 + QC('p15title').add('no-back-btn'); // add left-padding since no back button
15109
15110 if (samenode == false) {
15111 QH('p15agentConsoleText', consoleServerText);
15112 Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
15113 }
15114 } else {
14346 - // Setup the console
15115 + // Setup device console
15116 var samenode = (consoleNode == currentNode);
15117 consoleNode = currentNode;
15118
@@ -17042,6 +17811,14 @@
17811 QE('p5CutButton', (sfc > 0) && (cc == sfc));
17812 QE('p5CopyButton', (sfc > 0) && (cc == sfc));
17813 QE('p5PasteButton', (p5clipboard != null) && (p5clipboard.length > 0) && (filetreelocation.length > 0));
17814 + [
17815 + ['p5NewFolderButton', 'p5MobileNewFolderButton'],
17816 + ['p5UploadButton', 'p5MobileUploadButton'],
17817 + ['p5DownloadButton', 'p5MobileDownloadButton'],
17818 + ['p5CutButton', 'p5MobileCutButton'],
17819 + ['p5CopyButton', 'p5MobileCopyButton'],
17820 + ['p5PasteButton', 'p5MobilePasteButton']
17821 + ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
17822 }
17823
17824 function getFileSelCount(includeDirs) { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
@@ -21245,6 +22022,21 @@
22022 QV('UsersSubMenuSpan', x == 4 || x == 50 || x == 52);
22023 QV('EventsSubMenuSpan', (x == 3) || (x == 60));
22024 var panels = { 3: 'EventsLive', 4: 'UsersGeneral', 10: 'MainDev', 11: 'MainDevDesktop', 12: 'MainDevTerminal', 13: 'MainDevFiles', 14: 'MainDevAmt', 15: 'MainDevConsole', 16: 'MainDevEvents', 17: 'MainDevInfo', 18: 'MainDevApps', 19: 'MainDevPlugins', 20: 'MeshGeneral', 21: 'MeshSummary', 30: 'UserGeneral', 31: 'UserEvents', 6: 'ServerGeneral', 40: 'ServerStats', 41: 'ServerTrace', 42: 'ServerPlugins', 50: 'UsersGroups', 52: 'UsersRecordings', 60: 'EventsReport', 115: 'ServerConsole' };
22025 +
22026 + // Classic Mobile tab shuffle: on mobile, hide the active device sub-tab from
22027 + // the bottom bar so all remaining tabs fit without scrolling.
22028 + if (document.body.classList.contains('is-mobile')) {
22029 + var _devTabIds = ['MainDev','MainDevDesktop','MainDevTerminal','MainDevFiles','MainDevAmt','MainDevConsole','MainDevEvents','MainDevInfo','MainDevPlugins'];
22030 + for (var _dti = 0; _dti < _devTabIds.length; _dti++) {
22031 + var _dtEl = document.getElementById(_devTabIds[_dti]);
22032 + if (_dtEl) _dtEl.classList.remove('mob-active-tab');
22033 + }
22034 + if (x >= 10 && x < 20 && panels[x]) {
22035 + var _dtActive = document.getElementById(panels[x]);
22036 + if (_dtActive) _dtActive.classList.add('mob-active-tab');
22037 + }
22038 + }
22039 +
22040 for (var i in panels) {
22041 QC(panels[i]).remove('style3x');
22042 QC(panels[i]).remove('style3sel');