forked from jsnbuchanan/crowd-funder-for-time-pwa
feat: implement safe area insets for Android and add development tooling
- Add @capacitor/status-bar dependency for safe area detection
- Implement SafeAreaPlugin for Android with proper inset calculation
- Create safeAreaInset.js utility for CSS custom property injection
- Update Android manifest and build configuration for plugin
- Integrate safe area handling across Vue components and views
- Update iOS Podfile and Android gradle configurations
- Add commitlint and husky for commit message validation
Technical changes:
- SafeAreaPlugin uses WindowInsets API for Android R+ devices
- Fallback detection for navigation bar and gesture bar heights
- CSS custom properties: --safe-area-inset-{top,bottom,left,right}
- Platform-specific detection (Android WebView only)
- StatusBar plugin integration for top inset calculation
This commit is contained in:
373
BUILDING.md
373
BUILDING.md
@@ -1159,6 +1159,146 @@ If you need to build manually or want to understand the individual steps:
|
|||||||
|
|
||||||
Prerequisites: Android Studio with Java SDK installed
|
Prerequisites: Android Studio with Java SDK installed
|
||||||
|
|
||||||
|
### Android Safe Area Inset Implementation
|
||||||
|
|
||||||
|
**Date**: 2025-08-22
|
||||||
|
**Status**: ✅ **ACTIVE** - Android WebView safe area support
|
||||||
|
|
||||||
|
TimeSafari now includes comprehensive safe area inset support for Android WebView, addressing the limitation that Android doesn't natively support CSS `env(safe-area-inset-*)` variables like iOS.
|
||||||
|
|
||||||
|
#### Implementation Overview
|
||||||
|
|
||||||
|
The Android safe area implementation consists of two complementary systems:
|
||||||
|
|
||||||
|
1. **Native Android Plugin** (`SafeAreaPlugin.java`) - Provides native safe area measurements
|
||||||
|
2. **JavaScript Injection** (`safeAreaInset.js`) - Injects CSS custom properties for WebView compatibility
|
||||||
|
|
||||||
|
#### Native Android Plugin
|
||||||
|
|
||||||
|
**File**: `android/app/src/main/java/app/timesafari/safearea/SafeAreaPlugin.java`
|
||||||
|
|
||||||
|
The native plugin provides accurate safe area measurements using Android's WindowInsets API:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@CapacitorPlugin(name = "SafeArea")
|
||||||
|
public class SafeAreaPlugin extends Plugin {
|
||||||
|
@PluginMethod
|
||||||
|
public void getSafeAreaInsets(PluginCall call) {
|
||||||
|
// Uses WindowInsets API for Android 11+ (API 30+)
|
||||||
|
// Provides fallback values for older versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Uses Android 11+ WindowInsets API for accurate measurements
|
||||||
|
- Provides status bar, navigation bar, and system bar insets
|
||||||
|
- Fallback values for older Android versions
|
||||||
|
- Capacitor plugin integration
|
||||||
|
|
||||||
|
#### JavaScript Safe Area Injection
|
||||||
|
|
||||||
|
**File**: `src/utils/safeAreaInset.js`
|
||||||
|
|
||||||
|
The JavaScript implementation provides WebView-compatible safe area support:
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- **Platform Detection**: Only runs on Android WebView with Capacitor
|
||||||
|
- **Multiple Detection Methods**: Uses screen dimensions, visual viewport, and device characteristics
|
||||||
|
- **CSS Custom Properties**: Injects `--safe-area-inset-*` variables
|
||||||
|
- **Fallback Support**: Provides reasonable defaults when native API unavailable
|
||||||
|
- **Dynamic Updates**: Re-injects on orientation changes and resize events
|
||||||
|
|
||||||
|
**Detection Methods**:
|
||||||
|
1. **Direct Comparison**: `screenHeight - windowHeight`
|
||||||
|
2. **Gesture Navigation**: Detects modern gesture-based navigation
|
||||||
|
3. **Visual Viewport**: Uses `window.visualViewport` for WebView accuracy
|
||||||
|
4. **Device-Specific**: Common resolution-based estimations
|
||||||
|
|
||||||
|
#### CSS Integration
|
||||||
|
|
||||||
|
The implementation provides both CSS environment variables and custom properties:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* CSS Environment Variables (iOS-style) */
|
||||||
|
env(safe-area-inset-top)
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
|
||||||
|
/* CSS Custom Properties (Android WebView) */
|
||||||
|
var(--safe-area-inset-top, 0px)
|
||||||
|
var(--safe-area-inset-bottom, 0px)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```css
|
||||||
|
/* Cross-platform safe area support */
|
||||||
|
padding-top: max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px));
|
||||||
|
padding-bottom: max(env(safe-area-inset-bottom), var(--safe-area-inset-bottom, 0px));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dependencies Added
|
||||||
|
|
||||||
|
**New Capacitor Plugin**:
|
||||||
|
- `@capacitor/status-bar`: Provides status bar information for safe area calculations
|
||||||
|
|
||||||
|
**Development Dependencies**:
|
||||||
|
- `@commitlint/cli`: Commit message linting
|
||||||
|
- `@commitlint/config-conventional`: Conventional commit standards
|
||||||
|
- `husky`: Git hooks for pre-commit validation
|
||||||
|
- `lint-staged`: Staged file linting
|
||||||
|
|
||||||
|
#### Build Integration
|
||||||
|
|
||||||
|
The safe area implementation is automatically loaded in Capacitor builds:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/main.capacitor.ts
|
||||||
|
import "./utils/safeAreaInset";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build Commands**:
|
||||||
|
```bash
|
||||||
|
# Standard Android builds (now include safe area support)
|
||||||
|
npm run build:android:dev
|
||||||
|
npm run build:android:prod
|
||||||
|
|
||||||
|
# Asset validation (includes safe area assets)
|
||||||
|
npm run assets:validate:android
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing Safe Area Implementation
|
||||||
|
|
||||||
|
**Manual Testing**:
|
||||||
|
1. Build and run on Android device/emulator
|
||||||
|
2. Check safe area insets in different orientations
|
||||||
|
3. Verify CSS custom properties are injected
|
||||||
|
4. Test on devices with different navigation types
|
||||||
|
|
||||||
|
**Debugging**:
|
||||||
|
```javascript
|
||||||
|
// Check if safe area script is running
|
||||||
|
console.log('Safe area script loaded:', window.Capacitor !== undefined);
|
||||||
|
|
||||||
|
// Check injected CSS properties
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-top');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Troubleshooting
|
||||||
|
|
||||||
|
**Common Issues**:
|
||||||
|
- **No safe area detected**: Ensure running on Android with Capacitor
|
||||||
|
- **Incorrect measurements**: Check device orientation and navigation type
|
||||||
|
- **CSS not applied**: Verify CSS custom properties are injected
|
||||||
|
|
||||||
|
**Debug Commands**:
|
||||||
|
```bash
|
||||||
|
# Check Android build includes safe area plugin
|
||||||
|
npm run build:android:dev
|
||||||
|
|
||||||
|
# Validate assets include safe area support
|
||||||
|
npm run assets:validate:android
|
||||||
|
```
|
||||||
|
|
||||||
#### Android Build Commands
|
#### Android Build Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1431,6 +1571,145 @@ npm run lint-fix # Fix linting issues
|
|||||||
|
|
||||||
Use the commands above to check and fix code quality issues.
|
Use the commands above to check and fix code quality issues.
|
||||||
|
|
||||||
|
### Git Hooks and Commit Standards
|
||||||
|
|
||||||
|
**Date**: 2025-08-22
|
||||||
|
**Status**: ✅ **ACTIVE** - Automated code quality enforcement
|
||||||
|
|
||||||
|
TimeSafari now includes comprehensive git hooks and commit standards to ensure code quality and consistent development practices.
|
||||||
|
|
||||||
|
#### Husky Git Hooks
|
||||||
|
|
||||||
|
**Configuration**: `package.json` husky section
|
||||||
|
|
||||||
|
The project uses Husky to manage git hooks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged",
|
||||||
|
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Hooks**:
|
||||||
|
- **pre-commit**: Runs lint-staged to check staged files
|
||||||
|
- **commit-msg**: Validates commit message format using commitlint
|
||||||
|
|
||||||
|
#### Lint-Staged Configuration
|
||||||
|
|
||||||
|
**Configuration**: `package.json` lint-staged section
|
||||||
|
|
||||||
|
Automatically runs linting on staged files before commits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Runs ESLint on JavaScript, TypeScript, Vue, CSS, Markdown, JSON, YAML files
|
||||||
|
- Automatically fixes auto-fixable issues
|
||||||
|
- Continues commit process even if some files can't be fixed
|
||||||
|
|
||||||
|
#### Commit Message Standards
|
||||||
|
|
||||||
|
**Configuration**: `package.json` commitlint section
|
||||||
|
|
||||||
|
Uses conventional commit standards for consistent commit messages:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"commitlint": {
|
||||||
|
"extends": ["@commitlint/config-conventional"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit Message Format**:
|
||||||
|
```
|
||||||
|
type(scope): description
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported Types**:
|
||||||
|
- `feat`: New features
|
||||||
|
- `fix`: Bug fixes
|
||||||
|
- `docs`: Documentation changes
|
||||||
|
- `style`: Code style changes (formatting, etc.)
|
||||||
|
- `refactor`: Code refactoring
|
||||||
|
- `test`: Adding or updating tests
|
||||||
|
- `chore`: Maintenance tasks
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```bash
|
||||||
|
feat(android): add safe area inset support for Android WebView
|
||||||
|
fix(ios): resolve navigation bar overlap issue
|
||||||
|
docs(building): update Android build documentation
|
||||||
|
style(vue): fix component formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development Workflow
|
||||||
|
|
||||||
|
**Pre-commit Process**:
|
||||||
|
1. Stage files: `git add .`
|
||||||
|
2. Pre-commit hook runs automatically
|
||||||
|
3. Lint-staged checks and fixes staged files
|
||||||
|
4. Commit proceeds if all checks pass
|
||||||
|
|
||||||
|
**Commit Message Validation**:
|
||||||
|
1. Write commit message: `git commit -m "type(scope): description"`
|
||||||
|
2. Commit-msg hook validates format
|
||||||
|
3. Commit proceeds if format is valid
|
||||||
|
|
||||||
|
#### Bypassing Hooks (Emergency Only)
|
||||||
|
|
||||||
|
**Warning**: Only use in emergency situations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skip pre-commit hooks
|
||||||
|
git commit --no-verify -m "emergency: critical fix"
|
||||||
|
|
||||||
|
# Skip all hooks
|
||||||
|
git commit --no-verify --no-verify-signatures -m "emergency: critical fix"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Troubleshooting Git Hooks
|
||||||
|
|
||||||
|
**Common Issues**:
|
||||||
|
- **Hooks not running**: Ensure Husky is properly installed and configured
|
||||||
|
- **Lint errors blocking commit**: Fix linting issues or use `--no-verify` (emergency only)
|
||||||
|
- **Commit message rejected**: Follow conventional commit format
|
||||||
|
|
||||||
|
**Debug Commands**:
|
||||||
|
```bash
|
||||||
|
# Check if Husky is installed
|
||||||
|
npm list husky
|
||||||
|
|
||||||
|
# Test lint-staged manually
|
||||||
|
npx lint-staged
|
||||||
|
|
||||||
|
# Test commitlint manually
|
||||||
|
echo "feat: test commit" | npx commitlint
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
|
||||||
|
**Development Dependencies Added**:
|
||||||
|
- `husky`: Git hooks management
|
||||||
|
- `lint-staged`: Staged file linting
|
||||||
|
- `@commitlint/cli`: Commit message validation
|
||||||
|
- `@commitlint/config-conventional`: Conventional commit standards
|
||||||
|
|
||||||
## Code Build Architecture
|
## Code Build Architecture
|
||||||
|
|
||||||
### Web Build Process
|
### Web Build Process
|
||||||
@@ -1668,6 +1947,100 @@ npm run build:android:assets
|
|||||||
- [Web Build Scripts](docs/web-build-scripts.md)
|
- [Web Build Scripts](docs/web-build-scripts.md)
|
||||||
- [Build Troubleshooting](docs/build-troubleshooting.md)
|
- [Build Troubleshooting](docs/build-troubleshooting.md)
|
||||||
|
|
||||||
|
## Recent Updates
|
||||||
|
|
||||||
|
### CSS Safe Area Improvements (2025-08-22)
|
||||||
|
|
||||||
|
**Status**: ✅ **COMPLETED** - Cross-platform safe area support
|
||||||
|
|
||||||
|
TimeSafari's Vue components have been updated to provide consistent safe area support across all platforms, including Android WebView.
|
||||||
|
|
||||||
|
#### Updated Components
|
||||||
|
|
||||||
|
**Core Components**:
|
||||||
|
- `src/App.vue` - Main application layout
|
||||||
|
- `src/components/QuickNav.vue` - Bottom navigation
|
||||||
|
- `src/components/TopMessage.vue` - Top message display
|
||||||
|
|
||||||
|
**View Components**:
|
||||||
|
- `src/views/ContactQRScanFullView.vue` - QR scanner full view
|
||||||
|
- `src/views/ContactQRScanShowView.vue` - QR scanner show view
|
||||||
|
- `src/views/DeepLinkErrorView.vue` - Deep link error handling
|
||||||
|
- `src/views/DeepLinkRedirectView.vue` - Deep link redirect
|
||||||
|
|
||||||
|
#### CSS Pattern Implementation
|
||||||
|
|
||||||
|
All components now use the cross-platform safe area pattern:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Before (iOS-only) */
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
|
||||||
|
/* After (Cross-platform) */
|
||||||
|
padding-top: max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- **iOS**: Uses native `env()` variables
|
||||||
|
- **Android**: Uses injected CSS custom properties
|
||||||
|
- **Web**: Falls back to 0px values
|
||||||
|
- **Future-proof**: Supports new platforms automatically
|
||||||
|
|
||||||
|
#### Implementation Details
|
||||||
|
|
||||||
|
**CSS Custom Properties**:
|
||||||
|
- `--safe-area-inset-top`: Top safe area inset
|
||||||
|
- `--safe-area-inset-bottom`: Bottom safe area inset
|
||||||
|
- `--safe-area-inset-left`: Left safe area inset
|
||||||
|
- `--safe-area-inset-right`: Right safe area inset
|
||||||
|
|
||||||
|
**Fallback Strategy**:
|
||||||
|
```css
|
||||||
|
/* Primary: CSS environment variables (iOS) */
|
||||||
|
/* Secondary: CSS custom properties (Android WebView) */
|
||||||
|
/* Tertiary: Fallback value (Web, older browsers) */
|
||||||
|
max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing Cross-Platform Support
|
||||||
|
|
||||||
|
**iOS Testing**:
|
||||||
|
```bash
|
||||||
|
npm run build:ios:dev
|
||||||
|
# Test on iOS simulator and device
|
||||||
|
# Verify env() variables work correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android Testing**:
|
||||||
|
```bash
|
||||||
|
npm run build:android:dev
|
||||||
|
# Test on Android emulator and device
|
||||||
|
# Verify CSS custom properties are injected
|
||||||
|
```
|
||||||
|
|
||||||
|
**Web Testing**:
|
||||||
|
```bash
|
||||||
|
npm run build:web:dev
|
||||||
|
# Test in browser
|
||||||
|
# Verify fallback values work correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration Guide
|
||||||
|
|
||||||
|
**For New Components**:
|
||||||
|
```css
|
||||||
|
/* Use this pattern for all safe area references */
|
||||||
|
padding-top: max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px));
|
||||||
|
padding-bottom: max(env(safe-area-inset-bottom), var(--safe-area-inset-bottom, 0px));
|
||||||
|
padding-left: max(env(safe-area-inset-left), var(--safe-area-inset-left, 0px));
|
||||||
|
padding-right: max(env(safe-area-inset-right), var(--safe-area-inset-right, 0px));
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Existing Components**:
|
||||||
|
1. Replace `env(safe-area-inset-*)` with the max() pattern
|
||||||
|
2. Test on all target platforms
|
||||||
|
3. Verify safe area behavior is consistent
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Appendix: Build System Organization
|
## Appendix: Build System Organization
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ dependencies {
|
|||||||
implementation project(':capacitor-camera')
|
implementation project(':capacitor-camera')
|
||||||
implementation project(':capacitor-filesystem')
|
implementation project(':capacitor-filesystem')
|
||||||
implementation project(':capacitor-share')
|
implementation project(':capacitor-share')
|
||||||
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capawesome-capacitor-file-picker')
|
implementation project(':capawesome-capacitor-file-picker')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
"pkg": "@capacitor/share",
|
"pkg": "@capacitor/share",
|
||||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/status-bar",
|
||||||
|
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capawesome/capacitor-file-picker",
|
"pkg": "@capawesome/capacitor-file-picker",
|
||||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package app.timesafari;
|
package app.timesafari;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
import app.timesafari.safearea.SafeAreaPlugin;
|
||||||
//import com.getcapacitor.community.sqlite.SQLite;
|
//import com.getcapacitor.community.sqlite.SQLite;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@@ -9,7 +18,39 @@ public class MainActivity extends BridgeActivity {
|
|||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Enable edge-to-edge display for modern Android
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// Android 11+ (API 30+)
|
||||||
|
getWindow().setDecorFitsSystemWindows(false);
|
||||||
|
|
||||||
|
// Set up system UI visibility for edge-to-edge
|
||||||
|
WindowInsetsController controller = getWindow().getInsetsController();
|
||||||
|
if (controller != null) {
|
||||||
|
controller.setSystemBarsAppearance(
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
|
||||||
|
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||||
|
);
|
||||||
|
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy Android (API 21-29)
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
|
||||||
|
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register SafeArea plugin
|
||||||
|
registerPlugin(SafeAreaPlugin.class);
|
||||||
|
|
||||||
// Initialize SQLite
|
// Initialize SQLite
|
||||||
//registerPlugin(SQLite.class);
|
//registerPlugin(SQLite.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,14 @@
|
|||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="android:background">@drawable/splash</item>
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
<item name="android:windowTranslucentStatus">false</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
<item name="android:windowLightNavigationBar">true</item>
|
||||||
|
<item name="android:enforceStatusBarContrast">false</item>
|
||||||
|
<item name="android:enforceNavigationBarContrast">false</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -20,5 +20,8 @@ project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacit
|
|||||||
include ':capacitor-share'
|
include ':capacitor-share'
|
||||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||||
|
|
||||||
|
include ':capacitor-status-bar'
|
||||||
|
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||||
|
|
||||||
include ':capawesome-capacitor-file-picker'
|
include ':capawesome-capacitor-file-picker'
|
||||||
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||||
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ PODS:
|
|||||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||||
- CapacitorShare (6.0.3):
|
- CapacitorShare (6.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
- CapacitorStatusBar (6.0.2):
|
||||||
|
- Capacitor
|
||||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- GoogleDataTransport (9.4.1):
|
- GoogleDataTransport (9.4.1):
|
||||||
@@ -93,6 +95,7 @@ DEPENDENCIES:
|
|||||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||||
|
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -129,6 +132,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
||||||
CapacitorShare:
|
CapacitorShare:
|
||||||
:path: "../../node_modules/@capacitor/share"
|
:path: "../../node_modules/@capacitor/share"
|
||||||
|
CapacitorStatusBar:
|
||||||
|
:path: "../../node_modules/@capacitor/status-bar"
|
||||||
CapawesomeCapacitorFilePicker:
|
CapawesomeCapacitorFilePicker:
|
||||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
||||||
|
|
||||||
@@ -141,6 +146,7 @@ SPEC CHECKSUMS:
|
|||||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||||
|
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||||
@@ -157,6 +163,6 @@ SPEC CHECKSUMS:
|
|||||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||||
|
|
||||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
PODFILE CHECKSUM: 5b7f312bcb943bcf14b44515fdb02f36d11cd50f
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -136,7 +136,9 @@
|
|||||||
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": ["@commitlint/config-conventional"]
|
"extends": [
|
||||||
|
"@commitlint/config-conventional"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
@@ -150,6 +152,7 @@
|
|||||||
"@capacitor/filesystem": "^6.0.0",
|
"@capacitor/filesystem": "^6.0.0",
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
|
"@capacitor/status-bar": "^6.0.2",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
@@ -227,6 +230,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
"@commitlint/cli": "^18.6.1",
|
||||||
|
"@commitlint/config-conventional": "^18.6.2",
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.54.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
@@ -254,13 +259,11 @@
|
|||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"jest": "^30.0.4",
|
"jest": "^30.0.4",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
"husky": "^9.0.11",
|
|
||||||
"lint-staged": "^15.2.2",
|
|
||||||
"@commitlint/cli": "^18.6.1",
|
|
||||||
"@commitlint/config-conventional": "^18.6.2",
|
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
|||||||
35
src/App.vue
35
src/App.vue
@@ -4,7 +4,7 @@
|
|||||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||||
<NotificationGroup group="alert">
|
<NotificationGroup group="alert">
|
||||||
<div
|
<div
|
||||||
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
||||||
>
|
>
|
||||||
<Notification
|
<Notification
|
||||||
v-slot="{ notifications, close }"
|
v-slot="{ notifications, close }"
|
||||||
@@ -175,7 +175,9 @@
|
|||||||
"-permission", "-mute", "-off"
|
"-permission", "-mute", "-off"
|
||||||
-->
|
-->
|
||||||
<NotificationGroup group="modal">
|
<NotificationGroup group="modal">
|
||||||
<div class="fixed z-[100] top-[env(safe-area-inset-top)] inset-x-0 w-full">
|
<div
|
||||||
|
class="fixed z-[100] top-[max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))] inset-x-0 w-full"
|
||||||
|
>
|
||||||
<Notification
|
<Notification
|
||||||
v-slot="{ notifications, close }"
|
v-slot="{ notifications, close }"
|
||||||
enter="transform ease-out duration-300 transition"
|
enter="transform ease-out duration-300 transition"
|
||||||
@@ -506,13 +508,32 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
#Content {
|
#Content {
|
||||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
padding-left: max(
|
||||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
1.5rem,
|
||||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
env(safe-area-inset-left),
|
||||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
var(--safe-area-inset-left, 0px)
|
||||||
|
);
|
||||||
|
padding-right: max(
|
||||||
|
1.5rem,
|
||||||
|
env(safe-area-inset-right),
|
||||||
|
var(--safe-area-inset-right, 0px)
|
||||||
|
);
|
||||||
|
padding-top: max(
|
||||||
|
1.5rem,
|
||||||
|
env(safe-area-inset-top),
|
||||||
|
var(--safe-area-inset-top, 0px)
|
||||||
|
);
|
||||||
|
padding-bottom: max(
|
||||||
|
1.5rem,
|
||||||
|
env(safe-area-inset-bottom),
|
||||||
|
var(--safe-area-inset-bottom, 0px)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#QuickNav ~ #Content {
|
#QuickNav ~ #Content {
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
padding-bottom: calc(
|
||||||
|
max(env(safe-area-inset-bottom), var(--safe-area-inset-bottom, 0px)) +
|
||||||
|
6.333rem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!-- QUICK NAV -->
|
<!-- QUICK NAV -->
|
||||||
<nav
|
<nav
|
||||||
id="QuickNav"
|
id="QuickNav"
|
||||||
class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[env(safe-area-inset-bottom)]"
|
class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px))]"
|
||||||
>
|
>
|
||||||
<ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
|
<ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
|
||||||
<!-- Home Feed -->
|
<!-- Home Feed -->
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]">
|
<div
|
||||||
|
class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))]"
|
||||||
|
>
|
||||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { handleApiError } from "./services/api";
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logger, safeStringify } from "./utils/logger";
|
import { logger, safeStringify } from "./utils/logger";
|
||||||
|
import "./utils/safeAreaInset";
|
||||||
|
|
||||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||||
|
|||||||
226
src/utils/safeAreaInset.js
Normal file
226
src/utils/safeAreaInset.js
Normal file
@@ -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 };
|
||||||
@@ -220,21 +220,21 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
* Computed property for QR code container CSS classes
|
* Computed property for QR code container CSS classes
|
||||||
*/
|
*/
|
||||||
get qrContainerClasses(): string {
|
get qrContainerClasses(): string {
|
||||||
return "block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4";
|
return "block w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto mt-4";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for camera frame CSS classes
|
* Computed property for camera frame CSS classes
|
||||||
*/
|
*/
|
||||||
get cameraFrameClasses(): string {
|
get cameraFrameClasses(): string {
|
||||||
return "relative w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
|
return "relative w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for main content container CSS classes
|
* Computed property for main content container CSS classes
|
||||||
*/
|
*/
|
||||||
get mainContentClasses(): string {
|
get mainContentClasses(): string {
|
||||||
return "p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
|
return "p-6 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -259,11 +259,11 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get qrCodeContainerClasses(): string {
|
get qrCodeContainerClasses(): string {
|
||||||
return "block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4";
|
return "block w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto my-4";
|
||||||
}
|
}
|
||||||
|
|
||||||
get scannerContainerClasses(): string {
|
get scannerContainerClasses(): string {
|
||||||
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
|
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
get statusMessageClasses(): string {
|
get statusMessageClasses(): string {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.safe-area-spacer {
|
.safe-area-spacer {
|
||||||
height: env(safe-area-inset-top);
|
height: max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||||
<div
|
<div
|
||||||
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
|
class="p-6 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto"
|
||||||
>
|
>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user