Browse Source
Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/182pull/186/head
20 changed files with 388 additions and 17 deletions
@ -0,0 +1,44 @@ |
|||
package app.timesafari.safearea; |
|||
|
|||
import android.os.Build; |
|||
import android.view.WindowInsets; |
|||
import com.getcapacitor.JSObject; |
|||
import com.getcapacitor.Plugin; |
|||
import com.getcapacitor.PluginCall; |
|||
import com.getcapacitor.PluginMethod; |
|||
import com.getcapacitor.annotation.CapacitorPlugin; |
|||
|
|||
@CapacitorPlugin(name = "SafeArea") |
|||
public class SafeAreaPlugin extends Plugin { |
|||
|
|||
@PluginMethod |
|||
public void getSafeAreaInsets(PluginCall call) { |
|||
JSObject result = new JSObject(); |
|||
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { |
|||
WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets(); |
|||
if (insets != null) { |
|||
int top = insets.getInsets(WindowInsets.Type.statusBars()).top; |
|||
int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom; |
|||
int left = insets.getInsets(WindowInsets.Type.systemBars()).left; |
|||
int right = insets.getInsets(WindowInsets.Type.systemBars()).right; |
|||
|
|||
result.put("top", top); |
|||
result.put("bottom", bottom); |
|||
result.put("left", left); |
|||
result.put("right", right); |
|||
|
|||
call.resolve(result); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// Fallback values
|
|||
result.put("top", 0); |
|||
result.put("bottom", 0); |
|||
result.put("left", 0); |
|||
result.put("right", 0); |
|||
|
|||
call.resolve(result); |
|||
} |
|||
} |
@ -0,0 +1,226 @@ |
|||
/** |
|||
* Safe Area Inset Injection for Android WebView |
|||
* |
|||
* This script injects safe area inset values into CSS environment variables |
|||
* when running in Android WebView, since Android doesn't natively support |
|||
* CSS env(safe-area-inset-*) variables like iOS does. |
|||
*/ |
|||
|
|||
// Check if we're running in Android WebView with Capacitor
|
|||
const isAndroidWebView = () => { |
|||
// Check if we're on iOS - if so, skip this script entirely
|
|||
const isIOS = |
|||
/iPad|iPhone|iPod/.test(navigator.userAgent) || |
|||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); |
|||
|
|||
if (isIOS) { |
|||
return false; |
|||
} |
|||
|
|||
// Check if we're on Android
|
|||
const isAndroid = /Android/.test(navigator.userAgent); |
|||
|
|||
// Check if we have Capacitor (required for Android WebView)
|
|||
const hasCapacitor = window.Capacitor !== undefined; |
|||
|
|||
// Only run on Android with Capacitor
|
|||
return isAndroid && hasCapacitor; |
|||
}; |
|||
|
|||
// Wait for Capacitor to be available
|
|||
const waitForCapacitor = () => { |
|||
return new Promise((resolve) => { |
|||
if (window.Capacitor) { |
|||
resolve(window.Capacitor); |
|||
return; |
|||
} |
|||
|
|||
// Wait for Capacitor to be available
|
|||
const checkCapacitor = () => { |
|||
if (window.Capacitor) { |
|||
resolve(window.Capacitor); |
|||
} else { |
|||
setTimeout(checkCapacitor, 100); |
|||
} |
|||
}; |
|||
|
|||
checkCapacitor(); |
|||
}); |
|||
}; |
|||
|
|||
// Inject safe area inset values into CSS custom properties
|
|||
const injectSafeAreaInsets = async () => { |
|||
try { |
|||
// Wait for Capacitor to be available
|
|||
const Capacitor = await waitForCapacitor(); |
|||
|
|||
// Try to get safe area insets using StatusBar plugin (which is already available)
|
|||
|
|||
let top = 0, |
|||
bottom = 0, |
|||
left = 0, |
|||
right = 0; |
|||
|
|||
try { |
|||
// Use StatusBar plugin to get status bar height
|
|||
if (Capacitor.Plugins.StatusBar) { |
|||
const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo(); |
|||
// Status bar height is typically the top safe area inset
|
|||
top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0; |
|||
} |
|||
} catch (error) { |
|||
// Status bar info not available, will use fallback
|
|||
} |
|||
|
|||
// Detect navigation bar and gesture bar heights
|
|||
const detectNavigationBar = () => { |
|||
const screenHeight = window.screen.height; |
|||
const screenWidth = window.screen.width; |
|||
const windowHeight = window.innerHeight; |
|||
const devicePixelRatio = window.devicePixelRatio || 1; |
|||
|
|||
// Calculate navigation bar height
|
|||
let navBarHeight = 0; |
|||
|
|||
// Method 1: Direct comparison (most reliable)
|
|||
if (windowHeight < screenHeight) { |
|||
navBarHeight = screenHeight - windowHeight; |
|||
} |
|||
|
|||
// Method 2: Check for gesture navigation indicators
|
|||
if (navBarHeight === 0) { |
|||
// Look for common gesture navigation patterns
|
|||
const isTallDevice = screenHeight > 2000; |
|||
const isModernDevice = screenHeight > 1800; |
|||
const hasHighDensity = devicePixelRatio >= 2.5; |
|||
|
|||
if (isTallDevice && hasHighDensity) { |
|||
// Modern gesture-based device
|
|||
navBarHeight = 12; // Typical gesture bar height
|
|||
} else if (isModernDevice) { |
|||
// Modern device with traditional navigation
|
|||
navBarHeight = 48; // Traditional navigation bar height
|
|||
} |
|||
} |
|||
|
|||
// Method 3: Check visual viewport (more accurate for WebView)
|
|||
if (navBarHeight === 0) { |
|||
if (window.visualViewport) { |
|||
const visualHeight = window.visualViewport.height; |
|||
|
|||
if (visualHeight < windowHeight) { |
|||
navBarHeight = windowHeight - visualHeight; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Method 4: Device-specific estimation based on screen dimensions
|
|||
if (navBarHeight === 0) { |
|||
// Common Android navigation bar heights in pixels
|
|||
const commonNavBarHeights = { |
|||
"1080x2400": 48, // Common 1080p devices
|
|||
"1440x3200": 64, // QHD devices
|
|||
"720x1600": 32, // HD devices
|
|||
}; |
|||
|
|||
const resolution = `${screenWidth}x${screenHeight}`; |
|||
const estimatedHeight = commonNavBarHeights[resolution]; |
|||
|
|||
if (estimatedHeight) { |
|||
navBarHeight = estimatedHeight; |
|||
} else { |
|||
// Fallback: estimate based on screen height
|
|||
navBarHeight = screenHeight > 2000 ? 48 : 32; |
|||
} |
|||
} |
|||
|
|||
return navBarHeight; |
|||
}; |
|||
|
|||
// Get navigation bar height
|
|||
bottom = detectNavigationBar(); |
|||
|
|||
// If we still don't have a top value, estimate it
|
|||
if (top === 0) { |
|||
const screenHeight = window.screen.height; |
|||
// Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some
|
|||
top = screenHeight > 1920 ? 64 : 48; |
|||
} |
|||
|
|||
// Left/right safe areas are rare on Android
|
|||
left = 0; |
|||
right = 0; |
|||
|
|||
// Create CSS custom properties
|
|||
const style = document.createElement("style"); |
|||
style.textContent = ` |
|||
:root { |
|||
--safe-area-inset-top: ${top}px; |
|||
--safe-area-inset-bottom: ${bottom}px; |
|||
--safe-area-inset-left: ${left}px; |
|||
--safe-area-inset-right: ${right}px; |
|||
} |
|||
`;
|
|||
|
|||
// Inject the style into the document head
|
|||
document.head.appendChild(style); |
|||
|
|||
// Also set CSS environment variables if supported
|
|||
if (CSS.supports("env(safe-area-inset-top)")) { |
|||
document.documentElement.style.setProperty( |
|||
"--env-safe-area-inset-top", |
|||
`${top}px`, |
|||
); |
|||
document.documentElement.style.setProperty( |
|||
"--env-safe-area-inset-bottom", |
|||
`${bottom}px`, |
|||
); |
|||
document.documentElement.style.setProperty( |
|||
"--env-safe-area-inset-left", |
|||
`${left}px`, |
|||
); |
|||
document.documentElement.style.setProperty( |
|||
"--env-safe-area-inset-right", |
|||
`${right}px`, |
|||
); |
|||
} |
|||
} catch (error) { |
|||
// Error injecting safe area insets, will use fallback values
|
|||
} |
|||
}; |
|||
|
|||
// Initialize when DOM is ready
|
|||
const initializeSafeArea = () => { |
|||
// Check if we should run this script at all
|
|||
if (!isAndroidWebView()) { |
|||
return; |
|||
} |
|||
|
|||
// Add a small delay to ensure WebView is fully initialized
|
|||
setTimeout(() => { |
|||
injectSafeAreaInsets(); |
|||
}, 100); |
|||
}; |
|||
|
|||
if (document.readyState === "loading") { |
|||
document.addEventListener("DOMContentLoaded", initializeSafeArea); |
|||
} else { |
|||
initializeSafeArea(); |
|||
} |
|||
|
|||
// Re-inject on orientation change (only on Android)
|
|||
window.addEventListener("orientationchange", () => { |
|||
if (isAndroidWebView()) { |
|||
setTimeout(() => injectSafeAreaInsets(), 100); |
|||
} |
|||
}); |
|||
|
|||
// Re-inject on resize (only on Android)
|
|||
window.addEventListener("resize", () => { |
|||
if (isAndroidWebView()) { |
|||
setTimeout(() => injectSafeAreaInsets(), 100); |
|||
} |
|||
}); |
|||
|
|||
// Export for use in other modules
|
|||
export { injectSafeAreaInsets, isAndroidWebView }; |
Loading…
Reference in new issue