forked from trent_larson/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
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
@@ -1431,6 +1571,145 @@ npm run lint-fix # Fix linting 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
|
||||
|
||||
### Web Build Process
|
||||
@@ -1668,6 +1947,100 @@ npm run build:android:assets
|
||||
- [Web Build Scripts](docs/web-build-scripts.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
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies {
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:label="@string/title_activity_main"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package app.timesafari;
|
||||
|
||||
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 app.timesafari.safearea.SafeAreaPlugin;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@@ -9,7 +18,39 @@ public class MainActivity extends BridgeActivity {
|
||||
public void onCreate(Bundle 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
|
||||
//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">
|
||||
<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>
|
||||
</resources>
|
||||
@@ -20,5 +20,8 @@ project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacit
|
||||
include ':capacitor-share'
|
||||
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'
|
||||
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 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||
end
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ PODS:
|
||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||
- CapacitorShare (6.0.3):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (6.0.2):
|
||||
- Capacitor
|
||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||
- Capacitor
|
||||
- GoogleDataTransport (9.4.1):
|
||||
@@ -93,6 +95,7 @@ DEPENDENCIES:
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -129,6 +132,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/@capacitor/status-bar"
|
||||
CapawesomeCapacitorFilePicker:
|
||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
||||
|
||||
@@ -141,6 +146,7 @@ SPEC CHECKSUMS:
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||
@@ -157,6 +163,6 @@ SPEC CHECKSUMS:
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||
PODFILE CHECKSUM: 5b7f312bcb943bcf14b44515fdb02f36d11cd50f
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -135,8 +135,10 @@
|
||||
"lint-staged": {
|
||||
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
@@ -150,6 +152,7 @@
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capacitor/status-bar": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
@@ -227,6 +230,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@commitlint/cli": "^18.6.1",
|
||||
"@commitlint/config-conventional": "^18.6.2",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@types/dom-webcodecs": "^0.1.7",
|
||||
"@types/jest": "^30.0.0",
|
||||
@@ -254,13 +259,11 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^30.0.4",
|
||||
"lint-staged": "^15.2.2",
|
||||
"markdownlint": "^0.37.4",
|
||||
"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",
|
||||
"path-browserify": "^1.0.1",
|
||||
"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 -->
|
||||
<NotificationGroup group="alert">
|
||||
<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
|
||||
v-slot="{ notifications, close }"
|
||||
@@ -175,7 +175,9 @@
|
||||
"-permission", "-mute", "-off"
|
||||
-->
|
||||
<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
|
||||
v-slot="{ notifications, close }"
|
||||
enter="transform ease-out duration-300 transition"
|
||||
@@ -506,13 +508,32 @@ export default class App extends Vue {
|
||||
|
||||
<style>
|
||||
#Content {
|
||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||
padding-left: max(
|
||||
1.5rem,
|
||||
env(safe-area-inset-left),
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!-- QUICK NAV -->
|
||||
<nav
|
||||
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">
|
||||
<!-- Home Feed -->
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<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="ml-2">
|
||||
<router-link
|
||||
|
||||
@@ -35,6 +35,7 @@ import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logger, safeStringify } from "./utils/logger";
|
||||
import "./utils/safeAreaInset";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.safe-area-spacer {
|
||||
height: env(safe-area-inset-top);
|
||||
height: max(env(safe-area-inset-top), var(--safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||
<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">
|
||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||
|
||||
Reference in New Issue
Block a user