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-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" />
|
||||
|
||||
@@ -27,6 +27,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>
|
||||
@@ -23,5 +23,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')
|
||||
|
||||
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 '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
|
||||
|
||||
|
||||
@@ -19,6 +19,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):
|
||||
@@ -96,6 +98,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:
|
||||
@@ -134,6 +137,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"
|
||||
|
||||
@@ -147,6 +152,7 @@ SPEC CHECKSUMS:
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||
@@ -163,6 +169,6 @@ SPEC CHECKSUMS:
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: 60f54b19c5a7a07343ab5ba9e5db49019fd86aa0
|
||||
PODFILE CHECKSUM: 5fa870b031c7c4e0733e2f96deaf81866c75ff7d
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -20,6 +20,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",
|
||||
@@ -2346,6 +2347,14 @@
|
||||
"@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": {
|
||||
"version": "6.2.0",
|
||||
"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/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",
|
||||
|
||||
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-[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
|
||||
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>
|
||||
|
||||
@@ -14,4 +14,12 @@
|
||||
transform: translateX(100%);
|
||||
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 -->
|
||||
<template>
|
||||
<div v-if="visible" :class="overlayClasses">
|
||||
<div :class="dialogClasses">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 :class="titleClasses">{{ title }}</h1>
|
||||
{{ message }}
|
||||
Note that their name is only stored on this device.
|
||||
@@ -61,20 +61,6 @@ export default class ContactNameDialog extends Vue {
|
||||
title = "Contact Name";
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -212,30 +212,7 @@ export default class FeedFilters extends Vue {
|
||||
</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;
|
||||
}
|
||||
|
||||
#dialogFeedFilters.dialog-overlay {
|
||||
z-index: 100;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -665,27 +665,3 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
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">
|
||||
<div v-if="isOpen" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog relative">
|
||||
<div class="text-lg text-center font-bold relative">
|
||||
<h1 id="ViewHeading" class="text-center font-bold">
|
||||
@@ -931,32 +931,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
</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: 700px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Add styles for diagnostic panel */
|
||||
.diagnostic-panel {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -93,27 +93,3 @@ export default class InviteDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 */
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog relative">
|
||||
<div class="text-lg text-center font-light relative z-50">
|
||||
<div id="ViewHeading" :class="headingClasses">
|
||||
@@ -628,34 +628,6 @@ export default class PhotoDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
flex: 1;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -134,27 +134,3 @@ export default class UserNameDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</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 { 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 };
|
||||
@@ -58,7 +58,8 @@
|
||||
v-if="!isRegistered"
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
: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 -->
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -257,11 +257,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 {
|
||||
|
||||
@@ -831,26 +831,3 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<div class="deep-link-error">
|
||||
<div class="safe-area-spacer"></div>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<h1>Invalid Deep Link</h1>
|
||||
<div class="error-details">
|
||||
<div class="error-message">
|
||||
@@ -39,7 +39,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -114,18 +114,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
color: #ff4444;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -1,95 +1,87 @@
|
||||
<template>
|
||||
<!-- 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"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||
Redirecting to Time Safari
|
||||
</h1>
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl text-center font-semibold relative px-7">
|
||||
Redirecting to Time Safari
|
||||
</h1>
|
||||
|
||||
<div v-if="destinationUrl" class="space-y-4">
|
||||
<!-- Platform-specific messaging -->
|
||||
<div class="text-center text-gray-600 mb-4">
|
||||
<p v-if="isMobile">
|
||||
{{
|
||||
isIOS
|
||||
? "Opening Time Safari app on your iPhone..."
|
||||
: "Opening Time Safari app on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening Time Safari app...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
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"
|
||||
<div v-if="destinationUrl" class="space-y-4">
|
||||
<!-- Platform-specific messaging -->
|
||||
<div class="text-center text-gray-600 mb-4">
|
||||
<p v-if="isMobile">
|
||||
{{
|
||||
isIOS
|
||||
? "Opening Time Safari app on your iPhone..."
|
||||
: "Opening Time Safari app on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening Time Safari app...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
<span v-if="isMobile">Open in Time Safari App</span>
|
||||
<span v-else>Try Opening in Time Safari App</span>
|
||||
</a>
|
||||
</div>
|
||||
<span v-else>Choose how you'd like to open this link:</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Web Fallback Link -->
|
||||
<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>
|
||||
|
||||
<!-- 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"
|
||||
<!-- 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"
|
||||
>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||
</div>
|
||||
<span v-if="isMobile">Open in Time Safari App</span>
|
||||
<span v-else>Try Opening in Time Safari App</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||
{{ pageError }}
|
||||
<!-- Web Fallback Link -->
|
||||
<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 v-else class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
<!-- 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>
|
||||
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>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user