98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { isCapacitorNative } from '../utils/tauri';
|
|
|
|
const KEYBOARD_HEIGHT_THRESHOLD_PX = 120;
|
|
const IMMERSIVE_CLASS = 'immersive-landscape-keyboard';
|
|
|
|
const getIsLandscape = (): boolean => {
|
|
if (typeof window === 'undefined') return false;
|
|
if (window.matchMedia?.('(orientation: landscape)').matches) return true;
|
|
return window.innerWidth > window.innerHeight;
|
|
};
|
|
|
|
const getKeyboardOpen = (): boolean => {
|
|
if (typeof window === 'undefined') return false;
|
|
|
|
const viewport = window.visualViewport;
|
|
if (viewport) {
|
|
const obscured = window.innerHeight - viewport.height - viewport.offsetTop;
|
|
if (obscured > KEYBOARD_HEIGHT_THRESHOLD_PX) return true;
|
|
}
|
|
|
|
const active = document.activeElement;
|
|
if (!(active instanceof HTMLElement)) return false;
|
|
if (active.isContentEditable) return true;
|
|
const tag = active.tagName;
|
|
return tag === 'INPUT' || tag === 'TEXTAREA';
|
|
};
|
|
|
|
const setNativeStatusBarHidden = async (hidden: boolean) => {
|
|
if (!isCapacitorNative()) return;
|
|
try {
|
|
const { StatusBar } = await import('@capacitor/status-bar');
|
|
if (hidden) {
|
|
await StatusBar.hide();
|
|
} else {
|
|
await StatusBar.show();
|
|
}
|
|
} catch {
|
|
// Plugin may be missing in some builds; CSS safe-area collapse still applies.
|
|
}
|
|
};
|
|
|
|
export type MobileKeyboardLayout = {
|
|
isLandscape: boolean;
|
|
keyboardOpen: boolean;
|
|
/** Landscape + keyboard: compact immersive room chrome. */
|
|
immersive: boolean;
|
|
};
|
|
|
|
/**
|
|
* Landscape + soft-keyboard layout signals for compact room chrome.
|
|
* Uses visualViewport when available (Capacitor/Android WebView).
|
|
*/
|
|
export const useMobileKeyboardLayout = (): MobileKeyboardLayout => {
|
|
const [isLandscape, setIsLandscape] = useState(getIsLandscape);
|
|
const [keyboardOpen, setKeyboardOpen] = useState(getKeyboardOpen);
|
|
|
|
const immersive = isLandscape && keyboardOpen;
|
|
|
|
useEffect(() => {
|
|
const sync = () => {
|
|
setIsLandscape(getIsLandscape());
|
|
setKeyboardOpen(getKeyboardOpen());
|
|
};
|
|
|
|
sync();
|
|
|
|
const orientationQuery = window.matchMedia?.('(orientation: landscape)');
|
|
orientationQuery?.addEventListener?.('change', sync);
|
|
window.addEventListener('resize', sync);
|
|
window.visualViewport?.addEventListener('resize', sync);
|
|
window.visualViewport?.addEventListener('scroll', sync);
|
|
window.addEventListener('focusin', sync);
|
|
window.addEventListener('focusout', sync);
|
|
|
|
return () => {
|
|
orientationQuery?.removeEventListener?.('change', sync);
|
|
window.removeEventListener('resize', sync);
|
|
window.visualViewport?.removeEventListener('resize', sync);
|
|
window.visualViewport?.removeEventListener('scroll', sync);
|
|
window.removeEventListener('focusin', sync);
|
|
window.removeEventListener('focusout', sync);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.classList.toggle(IMMERSIVE_CLASS, immersive);
|
|
void setNativeStatusBarHidden(immersive);
|
|
|
|
return () => {
|
|
document.documentElement.classList.remove(IMMERSIVE_CLASS);
|
|
void setNativeStatusBarHidden(false);
|
|
};
|
|
}, [immersive]);
|
|
|
|
return { isLandscape, keyboardOpen, immersive };
|
|
};
|