mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-20 13:52:24 +02:00
fix(ui): respect iOS safe areas, screen height and landscape layout
This commit is contained in:
@@ -6929,13 +6929,6 @@ import { invalidateSettings } from './appConfig.js';
|
||||
}
|
||||
});
|
||||
}
|
||||
// Smooth viewport resize when keyboard opens/closes
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
document.documentElement.style.setProperty('--vh', window.visualViewport.height + 'px');
|
||||
});
|
||||
document.documentElement.style.setProperty('--vh', window.visualViewport.height + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// If the browser discarded and restored this tab, reload the current session
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from './utils.js';
|
||||
import { state } from './state.js';
|
||||
import { getSettings } from '../appConfig.js';
|
||||
import { collapseSidebarToRail } from '../modalSnap.js';
|
||||
import { clampWindowLeft, collapseSidebarToRail, leftNavRight, safeInsetPx } from '../modalSnap.js';
|
||||
import { emailApiUrl } from '../emailShared.js';
|
||||
import {
|
||||
bindMenuDismiss,
|
||||
@@ -823,7 +823,7 @@ function _readCssPx(name) {
|
||||
}
|
||||
|
||||
function _emailSplitLeftEdge() {
|
||||
return _readCssPx('--icon-rail-w') + _readCssPx('--sidebar-w');
|
||||
return leftNavRight();
|
||||
}
|
||||
|
||||
function _setEmailDocumentSplit(leftEdge, emailWidth) {
|
||||
@@ -899,7 +899,8 @@ function _clearEmailDocumentSplit() {
|
||||
// rail. Used by the "try collapsing the sidebar first" path so we can decide
|
||||
// whether collapsing recovers enough room before minimizing email.
|
||||
function _emailSplitLeftEdgeIfSidebarCollapsed() {
|
||||
return _readCssPx('--icon-rail-w');
|
||||
const navOnRight = document.getElementById('sidebar')?.classList.contains('right-side');
|
||||
return safeInsetPx('left') + (navOnRight ? 0 : _readCssPx('--icon-rail-w'));
|
||||
}
|
||||
|
||||
function _hasDesktopRoomForEmailAndDocument(modal, opts = {}) {
|
||||
@@ -2415,7 +2416,7 @@ export function openEmailLibrary(opts = {}) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth;
|
||||
const refH = window.innerHeight * 0.85;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - refH) / 2) + 'px';
|
||||
content.style.transform = 'none';
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import spinnerModule from '../spinner.js';
|
||||
import * as Modals from '../modalManager.js';
|
||||
import { clampWindowLeft } from '../modalSnap.js';
|
||||
import { showToast } from '../ui.js?v=20260916largetoolscroll1';
|
||||
import { state } from './state.js';
|
||||
import { _esc, _extractName, _renderEmailSummaryError } from './utils.js';
|
||||
@@ -428,7 +429,7 @@ export async function _openEmailWindow(em, folder) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth, h = content.offsetHeight;
|
||||
const off = (_emailWindowSeq % 6) * 28;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2 + off) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2 + off), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - h) / 3 + off) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export function createPanel({
|
||||
// which left it as a side panel squeezing the chat).
|
||||
if (window.innerWidth <= 768) {
|
||||
pane.style.position = 'fixed';
|
||||
pane.style.inset = '0';
|
||||
pane.style.inset = 'var(--sheet-top) 0 0';
|
||||
pane.style.width = '100%';
|
||||
pane.style.maxWidth = '100%';
|
||||
pane.style.zIndex = '170';
|
||||
|
||||
@@ -369,6 +369,31 @@ document.addEventListener('DOMContentLoaded', markComposerUserEdited, { once: tr
|
||||
/* ---- Mobile viewport fix — keep chat visible when virtual keyboard opens ---- */
|
||||
{
|
||||
if (window.visualViewport) {
|
||||
const root = document.documentElement;
|
||||
// iOS overlays the keyboard without resizing the layout viewport and scrolls
|
||||
// the page to reveal the field, leaving it shifted after the keyboard closes.
|
||||
const _syncKeyboardViewport = function() {
|
||||
const vv = window.visualViewport;
|
||||
if (vv.scale > 1.01) return;
|
||||
const keyboardOpen = window.innerHeight - vv.height > 120;
|
||||
root.classList.toggle('keyboard-open', keyboardOpen);
|
||||
if (keyboardOpen) root.style.setProperty('--keyboard-viewport-height', vv.height + 'px');
|
||||
else root.style.removeProperty('--keyboard-viewport-height');
|
||||
if (window.scrollY) window.scrollTo(0, 0);
|
||||
};
|
||||
// iOS fires visualViewport resize only once, mid-animation, and no focusin when
|
||||
// the tapped field already had focus (e.g. the composer after a new chat)
|
||||
const _syncThroughKeyboardAnimation = () => {
|
||||
_syncKeyboardViewport();
|
||||
for (const delay of [100, 300, 600, 1000]) setTimeout(_syncKeyboardViewport, delay);
|
||||
};
|
||||
_syncKeyboardViewport();
|
||||
window.visualViewport.addEventListener('resize', _syncThroughKeyboardAnimation);
|
||||
window.visualViewport.addEventListener('scroll', _syncKeyboardViewport);
|
||||
document.addEventListener('focusin', _syncThroughKeyboardAnimation);
|
||||
document.addEventListener('focusout', _syncThroughKeyboardAnimation);
|
||||
window.addEventListener('scroll', () => { if (window.scrollY) window.scrollTo(0, 0); }, { passive: true });
|
||||
|
||||
let _lastVVHeight = window.visualViewport.height;
|
||||
window.visualViewport.addEventListener('resize', function() {
|
||||
const vv = window.visualViewport;
|
||||
|
||||
+45
-7
@@ -110,7 +110,7 @@ function _clampRightDockWidth(width) {
|
||||
const min = _minEdgeDockWidth();
|
||||
const navRight = _leftNavRight();
|
||||
const leftDockW = _activeDockWidth('left');
|
||||
const maxByChat = window.innerWidth - navRight - leftDockW - MIN_CHAT_WIDTH;
|
||||
const maxByChat = window.innerWidth - navRight - _rightNavInset() - leftDockW - MIN_CHAT_WIDTH;
|
||||
const max = Math.min(Math.round(window.innerWidth * 0.82), maxByChat);
|
||||
return _clampDockWidthToSpace(width, min, max);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function _clampRightDockWidth(width) {
|
||||
function _clampLeftDockWidth(width, left = _leftNavRight()) {
|
||||
const min = _minEdgeDockWidth();
|
||||
const rightDockW = _activeDockWidth('right');
|
||||
const available = Math.max(0, window.innerWidth - left - rightDockW);
|
||||
const available = Math.max(0, window.innerWidth - left - _rightNavInset() - rightDockW);
|
||||
const max = Math.min(Math.round(available * 0.82), available - MIN_CHAT_WIDTH);
|
||||
return _clampDockWidthToSpace(width, min, max);
|
||||
}
|
||||
@@ -170,13 +170,23 @@ function _shouldAutoCollapseSidebar(dockW) {
|
||||
return remaining < MIN_CHAT_WIDTH;
|
||||
}
|
||||
|
||||
function _navOnRight() {
|
||||
return !!document.getElementById('sidebar')?.classList.contains('right-side');
|
||||
}
|
||||
|
||||
// Body padding carries the device safe-area insets (notch, Dynamic Island).
|
||||
function _bodyPaddingPx(side) {
|
||||
return parseFloat(window.getComputedStyle(document.body)[side === 'left' ? 'paddingLeft' : 'paddingRight']) || 0;
|
||||
}
|
||||
|
||||
// Right edge (px) of whatever left navigation is currently showing — the
|
||||
// expanded sidebar if visible, otherwise the icon rail. Used to anchor the
|
||||
// left dock so it always sits flush to the right of the nav.
|
||||
function _leftNavRight() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rail = document.getElementById('icon-rail');
|
||||
let x = 0;
|
||||
let x = _bodyPaddingPx('left');
|
||||
if (_navOnRight()) return x;
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
const r = sidebar.getBoundingClientRect();
|
||||
if (r.width) x = Math.max(x, r.right);
|
||||
@@ -188,8 +198,36 @@ function _leftNavRight() {
|
||||
return x;
|
||||
}
|
||||
|
||||
// Distance (px) from the viewport's right edge that a right-side nav and the
|
||||
// safe area occupy. The right dock sits flush to the left of it.
|
||||
function _rightNavInset() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rail = document.getElementById('icon-rail');
|
||||
let inset = _bodyPaddingPx('right');
|
||||
if (!_navOnRight()) return inset;
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
const r = sidebar.getBoundingClientRect();
|
||||
if (r.width) inset = Math.max(inset, window.innerWidth - r.left);
|
||||
}
|
||||
if (rail && window.getComputedStyle(rail).display !== 'none') {
|
||||
const r = rail.getBoundingClientRect();
|
||||
if (r.width) inset = Math.max(inset, window.innerWidth - r.left);
|
||||
}
|
||||
return inset;
|
||||
}
|
||||
|
||||
export function leftNavRight() { return _leftNavRight(); }
|
||||
export function rightNavInset() { return _rightNavInset(); }
|
||||
export function safeInsetPx(side) { return _bodyPaddingPx(side); }
|
||||
|
||||
// Keeps a floating window between the nav and the safe areas
|
||||
export function clampWindowLeft(left, width) {
|
||||
const max = window.innerWidth - _rightNavInset() - width;
|
||||
return Math.max(_leftNavRight(), Math.min(left, max));
|
||||
}
|
||||
|
||||
function _clampEmailDocSplitWidth(width, left = _leftNavRight()) {
|
||||
const available = Math.max(0, window.innerWidth - left);
|
||||
const available = Math.max(0, window.innerWidth - left - _rightNavInset());
|
||||
if (!available) return 0;
|
||||
const compact = available < 760;
|
||||
const minEmail = compact ? 260 : 340;
|
||||
@@ -235,7 +273,7 @@ function _applyEmailDocSplitGeometry(left, emailWidth) {
|
||||
if (!docPane || window.innerWidth <= 768) return;
|
||||
docPane.style.setProperty('position', 'fixed', 'important');
|
||||
docPane.style.setProperty('left', `${x}px`, 'important');
|
||||
docPane.style.setProperty('right', 'var(--right-dock-w, 0px)', 'important');
|
||||
docPane.style.setProperty('right', `calc(var(--right-dock-w, 0px) + ${_rightNavInset()}px)`, 'important');
|
||||
docPane.style.setProperty('top', '0px', 'important');
|
||||
docPane.style.setProperty('bottom', '0px', 'important');
|
||||
docPane.style.setProperty('width', 'auto', 'important');
|
||||
@@ -497,7 +535,7 @@ function _applyDockInternal(modal, side, dockClass) {
|
||||
} else {
|
||||
w = _resolveRightDockWidth(modal, content);
|
||||
content.style.left = 'auto';
|
||||
content.style.right = '0';
|
||||
content.style.right = _rightNavInset() + 'px';
|
||||
content.style.width = w + 'px';
|
||||
content.style.maxWidth = w + 'px';
|
||||
document.body.classList.add('right-dock-active');
|
||||
@@ -872,7 +910,7 @@ export function makeEdgeDockController(modal, side = 'right', dockClass) {
|
||||
w = _clampRightDockWidth(window.innerWidth - clientX);
|
||||
content._userDockWidth = w;
|
||||
content.style.left = 'auto';
|
||||
content.style.right = '0';
|
||||
content.style.right = _rightNavInset() + 'px';
|
||||
content.style.width = w + 'px';
|
||||
content.style.maxWidth = w + 'px';
|
||||
document.body.classList.add('right-dock-active');
|
||||
|
||||
@@ -75,12 +75,15 @@ export function initSidebarLayout(Storage, opts) {
|
||||
_setSidebarModeClasses(mode);
|
||||
}
|
||||
|
||||
// Phones in landscape get the desktop layout, but the icon rail is too small to use there
|
||||
const _phoneLandscape = window.matchMedia('(pointer: coarse) and (max-height: 500px)');
|
||||
|
||||
function _syncRailSideCore() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!iconRail) return;
|
||||
const isRight = sidebar.classList.contains('right-side');
|
||||
const sidebarHidden = sidebar.classList.contains('hidden');
|
||||
const railHidden = iconRail.classList.contains('rail-hidden');
|
||||
const railHidden = iconRail.classList.contains('rail-hidden') || _phoneLandscape.matches;
|
||||
const isMobileMini = iconRail.classList.contains('mobile-mini');
|
||||
iconRail.classList.toggle('right-side', isRight);
|
||||
// On mobile mini mode, JS already set inline styles — don't touch
|
||||
@@ -118,6 +121,7 @@ export function initSidebarLayout(Storage, opts) {
|
||||
// Set initial reference and expose globally
|
||||
_syncRailSideFn = _syncRailSideCore;
|
||||
window.syncRailSide = syncRailSide;
|
||||
_phoneLandscape.addEventListener('change', () => syncRailSide());
|
||||
|
||||
// Restore sidebar side preference
|
||||
if (Storage.get(Storage.KEYS.SIDEBAR_SIDE) === 'right') {
|
||||
|
||||
@@ -257,12 +257,19 @@ export function makeWindowResizable(content, options = {}) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||
if (saved && saved.w && saved.h) {
|
||||
const w = Math.max(minW, Math.min(saved.w, window.innerWidth));
|
||||
const bodyStyle = getComputedStyle(document.body);
|
||||
const padLeft = parseFloat(bodyStyle.paddingLeft) || 0;
|
||||
const padRight = parseFloat(bodyStyle.paddingRight) || 0;
|
||||
const w = Math.max(minW, Math.min(saved.w, window.innerWidth - padLeft - padRight));
|
||||
const h = Math.max(minH, Math.min(saved.h, window.innerHeight));
|
||||
content.style.width = w + 'px';
|
||||
content.style.height = h + 'px';
|
||||
content.style.maxWidth = 'none';
|
||||
content.style.maxHeight = 'none';
|
||||
const left = parseFloat(content.style.left);
|
||||
if (Number.isFinite(left) && left + w > window.innerWidth - padRight) {
|
||||
content.style.left = Math.max(padLeft, window.innerWidth - padRight - w) + 'px';
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user