Compare commits
17 Commits
ios-contac
...
android-sa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dfb2fda27 | ||
|
|
d3aa2e40a0 | ||
|
|
08cda50f13 | ||
| 716a23e76b | |||
| 7f499a0fc0 | |||
| 1b343b598c | |||
|
|
e588e223bc | ||
|
|
02eead5609 | ||
| 1e203da9bb | |||
|
|
df49c80199 | ||
|
|
4d89042997 | ||
|
|
0c9ede9fc9 | ||
|
|
dc857f9119 | ||
|
|
3a8652fd8d | ||
|
|
c2949c4dbf | ||
|
|
4ba58145d0 | ||
|
|
8f5111d100 |
@@ -16,6 +16,7 @@ dependencies {
|
|||||||
implementation project(':capacitor-clipboard')
|
implementation project(':capacitor-clipboard')
|
||||||
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" />
|
||||||
|
|||||||
@@ -27,6 +27,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>
|
||||||
@@ -23,5 +23,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')
|
||||||
|
|||||||
69
doc/z-index-guide.md
Normal file
69
doc/z-index-guide.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Z-Index Guide — TimeSafari
|
||||||
|
|
||||||
|
**Author**: Development Team
|
||||||
|
**Date**: 2025-08-25T19:38:09-08:00
|
||||||
|
**Status**: 🎯 **ACTIVE** - Z-index layering standards
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
This document defines the z-index hierarchy for all UI components.
|
||||||
|
|
||||||
|
## Use/Run
|
||||||
|
Reference these values when implementing new components or modifying existing ones to maintain consistent layering.
|
||||||
|
|
||||||
|
## Z-Index Hierarchy
|
||||||
|
|
||||||
|
| Component | Z-Index | Usage |
|
||||||
|
|-----------|---------|-------|
|
||||||
|
| **Map** | `40` | Base map layer and map-related overlays |
|
||||||
|
| **QuickNav** | `50` | Quick navigation bottom bar |
|
||||||
|
| **Dialogs and Modals** | `100` | Modal dialogs, popups, and overlay content |
|
||||||
|
| **Notifications and Toasts** | `120` | System notifications, alerts, and toast messages |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Never exceed 120** - Keep the highest z-index reserved for critical notifications
|
||||||
|
2. **Use increments of 10** - Leave room for future additions between layers
|
||||||
|
3. **Document exceptions** - If you need a z-index outside this range, document the reason
|
||||||
|
4. **Test layering** - Verify z-index behavior across different screen sizes and devices
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- **Avoid arbitrary values** - Don't use random z-index numbers
|
||||||
|
- **Don't nest high z-index** - Keep child elements within their parent's z-index range
|
||||||
|
- **Consider stacking context** - Remember that `position: relative` creates new stacking contexts
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
| Owner | Task | Exit Criteria | Target Date |
|
||||||
|
|-------|------|---------------|-------------|
|
||||||
|
| Dev Team | Apply z-index classes to existing components | All components use defined z-index values | 2025-09-01 |
|
||||||
|
|
||||||
|
## Competence Hooks
|
||||||
|
|
||||||
|
- **Why this works**: Creates predictable layering hierarchy that prevents UI conflicts
|
||||||
|
- **Common pitfalls**: Using arbitrary z-index values or exceeding the defined range
|
||||||
|
- **Next skill unlock**: Learn about CSS stacking contexts and their impact on z-index
|
||||||
|
- **Teach-back**: Explain the z-index hierarchy to a team member without referencing this guide
|
||||||
|
|
||||||
|
## Collaboration Hooks
|
||||||
|
|
||||||
|
- **Reviewers**: Frontend team, UI/UX designers
|
||||||
|
- **Sign-off checklist**:
|
||||||
|
- [ ] All new components follow z-index guidelines
|
||||||
|
- [ ] Existing components updated to use defined values
|
||||||
|
- [ ] Cross-browser testing completed
|
||||||
|
- [ ] Mobile responsiveness verified
|
||||||
|
|
||||||
|
## Assumptions & Limits
|
||||||
|
|
||||||
|
- Assumes modern browser support for z-index
|
||||||
|
- Limited to 4 defined layers (expandable if needed)
|
||||||
|
- Requires team discipline to maintain consistency
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MDN Z-Index Documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index)
|
||||||
|
- [CSS Stacking Context Guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context)
|
||||||
@@ -18,6 +18,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,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):
|
||||||
@@ -96,6 +98,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:
|
||||||
@@ -134,6 +137,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"
|
||||||
|
|
||||||
@@ -147,6 +152,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
|
||||||
@@ -163,6 +169,6 @@ SPEC CHECKSUMS:
|
|||||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 60f54b19c5a7a07343ab5ba9e5db49019fd86aa0
|
PODFILE CHECKSUM: 5fa870b031c7c4e0733e2f96deaf81866c75ff7d
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -20,6 +20,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",
|
||||||
@@ -2346,6 +2347,14 @@
|
|||||||
"@capacitor/core": "^6.0.0"
|
"@capacitor/core": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/status-bar": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capawesome/capacitor-file-picker": {
|
"node_modules/@capawesome/capacitor-file-picker": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz",
|
||||||
|
|||||||
@@ -150,6 +150,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",
|
||||||
|
|||||||
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-[120] 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>
|
||||||
|
|||||||
@@ -14,4 +14,12 @@
|
|||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
background-color: #FFF !important;
|
background-color: #FFF !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
@apply z-[100] fixed inset-0 bg-black/50 flex justify-center items-center p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- similar to UserNameDialog -->
|
<!-- similar to UserNameDialog -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="visible" :class="overlayClasses">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div :class="dialogClasses">
|
<div class="dialog">
|
||||||
<h1 :class="titleClasses">{{ title }}</h1>
|
<h1 :class="titleClasses">{{ title }}</h1>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
Note that their name is only stored on this device.
|
Note that their name is only stored on this device.
|
||||||
@@ -61,20 +61,6 @@ export default class ContactNameDialog extends Vue {
|
|||||||
title = "Contact Name";
|
title = "Contact Name";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* CSS classes for the modal overlay backdrop
|
|
||||||
*/
|
|
||||||
get overlayClasses(): string {
|
|
||||||
return "z-index-50 fixed top-0 left-0 right-0 bottom-0 bg-black/50 flex justify-center items-center p-6";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CSS classes for the modal dialog container
|
|
||||||
*/
|
|
||||||
get dialogClasses(): string {
|
|
||||||
return "bg-white p-4 rounded-lg w-full max-w-[500px]";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS classes for the dialog title
|
* CSS classes for the dialog title
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -212,30 +212,7 @@ export default class FeedFilters extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogFeedFilters.dialog-overlay {
|
#dialogFeedFilters.dialog-overlay {
|
||||||
z-index: 100;
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -665,27 +665,3 @@ export default class GiftedDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -291,27 +291,3 @@ export default class GivenPrompts extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="isOpen" class="dialog-overlay">
|
||||||
v-if="isOpen"
|
<div class="dialog">
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
||||||
>
|
|
||||||
<div class="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog relative">
|
<div class="dialog relative">
|
||||||
<div class="text-lg text-center font-bold relative">
|
<div class="text-lg text-center font-bold relative">
|
||||||
<h1 id="ViewHeading" class="text-center font-bold">
|
<h1 id="ViewHeading" class="text-center font-bold">
|
||||||
@@ -931,32 +931,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add styles for diagnostic panel */
|
/* Add styles for diagnostic panel */
|
||||||
.diagnostic-panel {
|
.diagnostic-panel {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
|||||||
@@ -93,27 +93,3 @@ export default class InviteDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -312,28 +312,3 @@ export default class OfferDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
max-width: 500px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -307,27 +307,3 @@ export default class OnboardingDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 40;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Comprehensive error handling * * @author Matthew Raymer * @version 1.0.0 * @file
|
|||||||
PhotoDialog.vue */
|
PhotoDialog.vue */
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog relative">
|
<div class="dialog relative">
|
||||||
<div class="text-lg text-center font-light relative z-50">
|
<div class="text-lg text-center font-light relative z-50">
|
||||||
<div id="ViewHeading" :class="headingClasses">
|
<div id="ViewHeading" :class="headingClasses">
|
||||||
@@ -628,34 +628,6 @@ export default class PhotoDialog extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Dialog overlay styling */
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 60;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dialog container styling */
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Camera preview styling */
|
/* Camera preview styling */
|
||||||
.camera-preview {
|
.camera-preview {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -134,27 +134,3 @@ export default class UserNameDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -58,7 +58,8 @@
|
|||||||
v-if="!isRegistered"
|
v-if="!isRegistered"
|
||||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||||
:given-name="givenName"
|
:given-name="givenName"
|
||||||
message="Before you can publicly announce a new project or time commitment, a friend needs to register you."
|
message="Before you can publicly announce a new project or time commitment,
|
||||||
|
a friend needs to register you."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -257,11 +257,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 {
|
||||||
|
|||||||
@@ -831,26 +831,3 @@ export default class DIDView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="deep-link-error">
|
<!-- CONTENT -->
|
||||||
<div class="safe-area-spacer"></div>
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<h1>Invalid Deep Link</h1>
|
<h1>Invalid Deep Link</h1>
|
||||||
<div class="error-details">
|
<div class="error-details">
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -114,18 +114,6 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.deep-link-error {
|
|
||||||
padding-top: 60px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.safe-area-spacer {
|
|
||||||
height: env(safe-area-inset-top);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #ff4444;
|
color: #ff4444;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|||||||
@@ -1,95 +1,87 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<div
|
<div class="mb-4">
|
||||||
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"
|
<h1 class="text-2xl text-center font-semibold relative px-7">
|
||||||
>
|
Redirecting to Time Safari
|
||||||
<div class="mb-4">
|
</h1>
|
||||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
|
||||||
Redirecting to Time Safari
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div v-if="destinationUrl" class="space-y-4">
|
<div v-if="destinationUrl" class="space-y-4">
|
||||||
<!-- Platform-specific messaging -->
|
<!-- Platform-specific messaging -->
|
||||||
<div class="text-center text-gray-600 mb-4">
|
<div class="text-center text-gray-600 mb-4">
|
||||||
<p v-if="isMobile">
|
<p v-if="isMobile">
|
||||||
{{
|
{{
|
||||||
isIOS
|
isIOS
|
||||||
? "Opening Time Safari app on your iPhone..."
|
? "Opening Time Safari app on your iPhone..."
|
||||||
: "Opening Time Safari app on your Android device..."
|
: "Opening Time Safari app on your Android device..."
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<p v-else>Opening Time Safari app...</p>
|
<p v-else>Opening Time Safari app...</p>
|
||||||
<p class="text-sm mt-2">
|
<p class="text-sm mt-2">
|
||||||
<span v-if="isMobile"
|
<span v-if="isMobile"
|
||||||
>If the app doesn't open automatically, use one of these
|
>If the app doesn't open automatically, use one of these
|
||||||
options:</span
|
options:</span
|
||||||
>
|
|
||||||
<span v-else>Choose how you'd like to open this link:</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Deep Link Button -->
|
|
||||||
<div class="text-center">
|
|
||||||
<a
|
|
||||||
:href="deepLinkUrl || '#'"
|
|
||||||
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
@click="handleDeepLinkClick"
|
|
||||||
>
|
>
|
||||||
<span v-if="isMobile">Open in Time Safari App</span>
|
<span v-else>Choose how you'd like to open this link:</span>
|
||||||
<span v-else>Try Opening in Time Safari App</span>
|
</p>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Web Fallback Link -->
|
<!-- Deep Link Button -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a
|
<a
|
||||||
:href="webUrl || '#'"
|
:href="deepLinkUrl || '#'"
|
||||||
target="_blank"
|
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
@click="handleDeepLinkClick"
|
||||||
@click="handleWebFallbackClick"
|
|
||||||
>
|
|
||||||
<span v-if="isMobile">Open in Web Browser Instead</span>
|
|
||||||
<span v-else>Open in Web Browser</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Manual Instructions -->
|
|
||||||
<div class="text-center text-sm text-gray-500 mt-4">
|
|
||||||
<p v-if="isMobile">
|
|
||||||
Or manually open:
|
|
||||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
|
||||||
deepLinkUrl
|
|
||||||
}}</code>
|
|
||||||
</p>
|
|
||||||
<p v-else>
|
|
||||||
If you have the Time Safari app installed, you can also copy this
|
|
||||||
link:
|
|
||||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
|
||||||
deepLinkUrl
|
|
||||||
}}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Platform info for debugging -->
|
|
||||||
<div
|
|
||||||
v-if="isDevelopment"
|
|
||||||
class="text-center text-xs text-gray-400 mt-4"
|
|
||||||
>
|
>
|
||||||
<p>
|
<span v-if="isMobile">Open in Time Safari App</span>
|
||||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
<span v-else>Try Opening in Time Safari App</span>
|
||||||
</p>
|
</a>
|
||||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
<!-- Web Fallback Link -->
|
||||||
{{ pageError }}
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
:href="webUrl || '#'"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||||
|
@click="handleWebFallbackClick"
|
||||||
|
>
|
||||||
|
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||||
|
<span v-else>Open in Web Browser</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center text-gray-600">
|
<!-- Manual Instructions -->
|
||||||
<p>Processing redirect...</p>
|
<div class="text-center text-sm text-gray-500 mt-4">
|
||||||
|
<p v-if="isMobile">
|
||||||
|
Or manually open:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{ deepLinkUrl }}</code>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
If you have the Time Safari app installed, you can also copy this
|
||||||
|
link:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{ deepLinkUrl }}</code>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Platform info for debugging -->
|
||||||
|
<div
|
||||||
|
v-if="isDevelopment"
|
||||||
|
class="text-center text-xs text-gray-400 mt-4"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||||
|
</p>
|
||||||
|
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||||
|
{{ pageError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-gray-600">
|
||||||
|
<p>Processing redirect...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user