Add PWA and push notification support for mobile app deployment
Major features added: - Progressive Web App (PWA) manifest and service worker for offline support - Push notifications with VAPID authentication - Mobile-optimized UI with touch navigation fix - Admin panel with configurable settings - Geocache sound alerts - App icons in all required sizes Technical improvements: - Fixed mobile touch handling for navigation selection - Added remesh tool for track point standardization - Improved pathfinding algorithm for more efficient routes - WebSocket-based real-time multi-user tracking - Docker deployment with persistent data volumes Ready for APK generation via PWA2APK.com or Bubblewrap Full offline support with map tile caching Push notifications for geocache alerts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
-
9.claude/settings.local.json
-
6.gitignore
-
10.well-known/assetlinks.json
-
121BUILD_APK_INSTRUCTIONS.md
-
12Dockerfile
-
13generate-vapid-keys.js
-
82generate_icons.py
-
60geocaches.json
-
BINicon-128x128.png
-
BINicon-144x144.png
-
BINicon-152x152.png
-
BINicon-192x192.png
-
BINicon-384x384.png
-
BINicon-512x512.png
-
BINicon-72x72.png
-
BINicon-96x96.png
-
684index.html
-
94manifest.json
-
4001package-lock.json
-
3package.json
-
148server.js
-
214service-worker.js
-
54twa-manifest.json
@ -0,0 +1,6 @@ |
|||||
|
node_modules/ |
||||
|
.env |
||||
|
.claude/ |
||||
|
push-subscriptions.json |
||||
|
*.backup.* |
||||
|
.DS_Store |
||||
@ -0,0 +1,10 @@ |
|||||
|
[{ |
||||
|
"relation": ["delegate_permission/common.handle_all_urls"], |
||||
|
"target": { |
||||
|
"namespace": "android_app", |
||||
|
"package_name": "org.duckdns.bibbit.hikemap", |
||||
|
"sha256_cert_fingerprints": [ |
||||
|
"4B:99:EB:12:8C:5C:7B:9B:3C:2F:E5:5C:7A:5D:22:16:7C:A4:8B:28:95:DF:B8:A3:F5:7C:06:92:F0:73:79:36" |
||||
|
] |
||||
|
} |
||||
|
}] |
||||
@ -0,0 +1,121 @@ |
|||||
|
# HikeMap APK Build Instructions |
||||
|
|
||||
|
Your HikeMap PWA is ready to be converted to an APK! Here are three methods to create an installable Android app: |
||||
|
|
||||
|
## Method 1: Online Converter (Easiest - No coding required) |
||||
|
|
||||
|
### Using PWA2APK.com: |
||||
|
1. Visit https://pwa2apk.com |
||||
|
2. Enter your app URL: `https://maps.bibbit.duckdns.org` |
||||
|
3. Click "Start" |
||||
|
4. Fill in the form: |
||||
|
- App Name: HikeMap Trail Navigator |
||||
|
- Short Name: HikeMap |
||||
|
- Package ID: org.duckdns.bibbit.hikemap |
||||
|
5. Click "Generate APK" |
||||
|
6. Download the APK file |
||||
|
7. Share with users - they can install directly! |
||||
|
|
||||
|
### Using PWABuilder.com (Microsoft's Tool): |
||||
|
1. Visit https://www.pwabuilder.com |
||||
|
2. Enter URL: `https://maps.bibbit.duckdns.org` |
||||
|
3. Click "Start" |
||||
|
4. Review the PWA score (should be high!) |
||||
|
5. Click "Package for stores" |
||||
|
6. Select "Android" |
||||
|
7. Download the APK package |
||||
|
|
||||
|
## Method 2: Using Bubblewrap (Advanced - Full control) |
||||
|
|
||||
|
If you want to build locally with full customization: |
||||
|
|
||||
|
```bash |
||||
|
# Install required tools |
||||
|
sudo apt-get install openjdk-11-jdk android-sdk |
||||
|
|
||||
|
# Install Bubblewrap globally |
||||
|
npm install -g @bubblewrap/cli |
||||
|
|
||||
|
# Initialize your TWA project |
||||
|
bubblewrap init --manifest="https://maps.bibbit.duckdns.org/manifest.json" |
||||
|
|
||||
|
# Build the APK |
||||
|
bubblewrap build |
||||
|
|
||||
|
# The APK will be in: app-release-signed.apk |
||||
|
``` |
||||
|
|
||||
|
## Method 3: Android Studio (Most Control) |
||||
|
|
||||
|
1. Download Android Studio |
||||
|
2. Create new project → "Empty Activity" |
||||
|
3. Add TWA (Trusted Web Activity) support |
||||
|
4. Configure `AndroidManifest.xml` with your URL |
||||
|
5. Build → Generate Signed Bundle/APK |
||||
|
|
||||
|
## APK Features |
||||
|
|
||||
|
Your generated APK will have: |
||||
|
- ✅ Full offline support (Service Worker caching) |
||||
|
- ✅ Push notifications |
||||
|
- ✅ GPS location access |
||||
|
- ✅ Camera access (for future features) |
||||
|
- ✅ Install to home screen |
||||
|
- ✅ Runs in fullscreen (no browser UI) |
||||
|
- ✅ Auto-updates from your server |
||||
|
|
||||
|
## Sharing the APK |
||||
|
|
||||
|
Once you have the APK file: |
||||
|
|
||||
|
1. **Direct Install**: Users enable "Install from unknown sources" in Android settings |
||||
|
2. **Email/Message**: Send the APK file directly |
||||
|
3. **Download Link**: Host on your server at `https://maps.bibbit.duckdns.org/hikemap.apk` |
||||
|
4. **QR Code**: Generate QR code linking to the APK |
||||
|
|
||||
|
## Google Play Store (Optional) |
||||
|
|
||||
|
To publish on Play Store: |
||||
|
1. Create Google Play Developer account ($25 one-time fee) |
||||
|
2. Use the AAB (Android App Bundle) format instead of APK |
||||
|
3. Add the assetlinks.json file to your server |
||||
|
4. Submit for review |
||||
|
|
||||
|
## Testing the APK |
||||
|
|
||||
|
Before sharing: |
||||
|
1. Test on multiple Android versions (7.0+) |
||||
|
2. Verify GPS works properly |
||||
|
3. Test push notifications |
||||
|
4. Check offline functionality |
||||
|
5. Ensure all icons display correctly |
||||
|
|
||||
|
## Current Configuration |
||||
|
|
||||
|
Your app is configured with: |
||||
|
- Package ID: `org.duckdns.bibbit.hikemap` |
||||
|
- Version: 1.0.0 |
||||
|
- Min Android: 7.0 (API 24) |
||||
|
- Target Android: Latest |
||||
|
- Orientation: Portrait |
||||
|
- Theme Color: #4CAF50 |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
**"App not installed" error**: |
||||
|
- Enable "Install unknown apps" for your browser |
||||
|
- Uninstall any previous version first |
||||
|
|
||||
|
**Notifications not working**: |
||||
|
- Ensure HTTPS is working |
||||
|
- Check notification permissions in Android settings |
||||
|
|
||||
|
**GPS not working**: |
||||
|
- Grant location permission when prompted |
||||
|
- Check location services are enabled |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Quick Start (Recommended) |
||||
|
|
||||
|
For fastest results, use **PWA2APK.com** - it takes about 2 minutes and produces a ready-to-share APK file that your users can install immediately! |
||||
@ -0,0 +1,13 @@ |
|||||
|
const webpush = require('web-push'); |
||||
|
|
||||
|
// Generate VAPID keys
|
||||
|
const vapidKeys = webpush.generateVAPIDKeys(); |
||||
|
|
||||
|
console.log('Add these to your .env file:'); |
||||
|
console.log(''); |
||||
|
console.log(`VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`); |
||||
|
console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`); |
||||
|
console.log(`VAPID_EMAIL=mailto:admin@maps.bibbit.duckdns.org`); |
||||
|
console.log(''); |
||||
|
console.log('Public key for client-side:'); |
||||
|
console.log(vapidKeys.publicKey); |
||||
@ -0,0 +1,82 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
""" |
||||
|
Generate PWA icons from SVG or create simple placeholder icons |
||||
|
""" |
||||
|
|
||||
|
from PIL import Image, ImageDraw, ImageFont |
||||
|
import os |
||||
|
|
||||
|
def create_icon(size): |
||||
|
"""Create a simple app icon with the specified size.""" |
||||
|
# Create a new image with a green background |
||||
|
img = Image.new('RGBA', (size, size), color='#4CAF50') |
||||
|
draw = ImageDraw.Draw(img) |
||||
|
|
||||
|
# Draw a white mountain shape |
||||
|
mountain_color = 'white' |
||||
|
# Mountain 1 (left) |
||||
|
mountain1 = [ |
||||
|
(size * 0.1, size * 0.7), |
||||
|
(size * 0.35, size * 0.3), |
||||
|
(size * 0.6, size * 0.7) |
||||
|
] |
||||
|
draw.polygon(mountain1, fill=mountain_color) |
||||
|
|
||||
|
# Mountain 2 (right, slightly overlapping) |
||||
|
mountain2 = [ |
||||
|
(size * 0.4, size * 0.7), |
||||
|
(size * 0.65, size * 0.4), |
||||
|
(size * 0.9, size * 0.7) |
||||
|
] |
||||
|
draw.polygon(mountain2, fill='#E8F5E9') |
||||
|
|
||||
|
# Draw a location pin at the peak |
||||
|
pin_center = (int(size * 0.65), int(size * 0.4)) |
||||
|
pin_radius = int(size * 0.05) |
||||
|
|
||||
|
# Pin circle |
||||
|
draw.ellipse( |
||||
|
[pin_center[0] - pin_radius, pin_center[1] - pin_radius, |
||||
|
pin_center[0] + pin_radius, pin_center[1] + pin_radius], |
||||
|
fill='#FF5722' |
||||
|
) |
||||
|
|
||||
|
# Pin point |
||||
|
draw.polygon([ |
||||
|
(pin_center[0] - pin_radius, pin_center[1]), |
||||
|
(pin_center[0] + pin_radius, pin_center[1]), |
||||
|
(pin_center[0], pin_center[1] + pin_radius * 2) |
||||
|
], fill='#FF5722') |
||||
|
|
||||
|
# Add "HM" text at the bottom |
||||
|
try: |
||||
|
# Try to use a default font, fall back to PIL default if not available |
||||
|
font_size = int(size * 0.15) |
||||
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size) |
||||
|
except: |
||||
|
font = ImageFont.load_default() |
||||
|
|
||||
|
text = "HM" |
||||
|
bbox = draw.textbbox((0, 0), text, font=font) |
||||
|
text_width = bbox[2] - bbox[0] |
||||
|
text_height = bbox[3] - bbox[1] |
||||
|
text_x = (size - text_width) // 2 |
||||
|
text_y = int(size * 0.75) |
||||
|
|
||||
|
draw.text((text_x, text_y), text, fill='white', font=font) |
||||
|
|
||||
|
return img |
||||
|
|
||||
|
# Icon sizes needed for PWA |
||||
|
sizes = [72, 96, 128, 144, 152, 192, 384, 512] |
||||
|
|
||||
|
print("Generating HikeMap PWA icons...") |
||||
|
|
||||
|
for size in sizes: |
||||
|
icon = create_icon(size) |
||||
|
filename = f"icon-{size}x{size}.png" |
||||
|
icon.save(filename, "PNG") |
||||
|
print(f"Created {filename}") |
||||
|
|
||||
|
print("\nAll icons generated successfully!") |
||||
|
print("Icons feature a mountain landscape with location pin design.") |
||||
|
After Width: 128 | Height: 128 | Size: 1.2 KiB |
|
After Width: 144 | Height: 144 | Size: 1.4 KiB |
|
After Width: 152 | Height: 152 | Size: 1.4 KiB |
|
After Width: 192 | Height: 192 | Size: 1.8 KiB |
|
After Width: 384 | Height: 384 | Size: 3.7 KiB |
|
After Width: 512 | Height: 512 | Size: 5.1 KiB |
|
After Width: 72 | Height: 72 | Size: 721 B |
|
After Width: 96 | Height: 96 | Size: 947 B |
@ -0,0 +1,94 @@ |
|||||
|
{ |
||||
|
"name": "HikeMap Trail Navigator", |
||||
|
"short_name": "HikeMap", |
||||
|
"description": "GPS trail navigation, tracking, and geocaching app for hikers", |
||||
|
"start_url": "https://maps.bibbit.duckdns.org/", |
||||
|
"scope": "https://maps.bibbit.duckdns.org/", |
||||
|
"display": "standalone", |
||||
|
"theme_color": "#4CAF50", |
||||
|
"background_color": "#ffffff", |
||||
|
"orientation": "portrait-primary", |
||||
|
"categories": ["navigation", "sports", "travel"], |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "/icon-72x72.png", |
||||
|
"sizes": "72x72", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-96x96.png", |
||||
|
"sizes": "96x96", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-128x128.png", |
||||
|
"sizes": "128x128", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-144x144.png", |
||||
|
"sizes": "144x144", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-152x152.png", |
||||
|
"sizes": "152x152", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-192x192.png", |
||||
|
"sizes": "192x192", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any maskable" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-384x384.png", |
||||
|
"sizes": "384x384", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/icon-512x512.png", |
||||
|
"sizes": "512x512", |
||||
|
"type": "image/png", |
||||
|
"purpose": "any maskable" |
||||
|
} |
||||
|
], |
||||
|
"screenshots": [ |
||||
|
{ |
||||
|
"src": "/screenshot-1.png", |
||||
|
"sizes": "540x720", |
||||
|
"type": "image/png", |
||||
|
"label": "Trail navigation view" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/screenshot-2.png", |
||||
|
"sizes": "540x720", |
||||
|
"type": "image/png", |
||||
|
"label": "Track editing tools" |
||||
|
} |
||||
|
], |
||||
|
"shortcuts": [ |
||||
|
{ |
||||
|
"name": "Start Navigation", |
||||
|
"short_name": "Navigate", |
||||
|
"description": "Start navigation mode", |
||||
|
"url": "/?mode=navigate", |
||||
|
"icons": [{ "src": "/icon-96x96.png", "sizes": "96x96" }] |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Edit Tracks", |
||||
|
"short_name": "Edit", |
||||
|
"description": "Open track editor", |
||||
|
"url": "/?mode=edit", |
||||
|
"icons": [{ "src": "/icon-96x96.png", "sizes": "96x96" }] |
||||
|
} |
||||
|
], |
||||
|
"prefer_related_applications": false, |
||||
|
"related_applications": [] |
||||
|
} |
||||
4001
package-lock.json
File diff suppressed because it is too large
View File
@ -0,0 +1,214 @@ |
|||||
|
// HikeMap Service Worker
|
||||
|
const CACHE_NAME = 'hikemap-v1.0.0'; |
||||
|
const urlsToCache = [ |
||||
|
'/', |
||||
|
'/index.html', |
||||
|
'/manifest.json', |
||||
|
'/default.kml', |
||||
|
'/icon-192x192.png', |
||||
|
'/icon-512x512.png', |
||||
|
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', |
||||
|
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', |
||||
|
'https://unpkg.com/leaflet-rotate@0.2.8/dist/leaflet-rotate-src.js', |
||||
|
'https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css' |
||||
|
]; |
||||
|
|
||||
|
// Cache map tiles separately with a different strategy
|
||||
|
const MAP_TILE_CACHE = 'hikemap-tiles-v1'; |
||||
|
const GEOCACHE_CACHE = 'hikemap-geocaches-v1'; |
||||
|
|
||||
|
// Install event - cache essential files
|
||||
|
self.addEventListener('install', event => { |
||||
|
event.waitUntil( |
||||
|
caches.open(CACHE_NAME) |
||||
|
.then(cache => { |
||||
|
console.log('Opened cache'); |
||||
|
return cache.addAll(urlsToCache); |
||||
|
}) |
||||
|
.then(() => self.skipWaiting()) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
// Activate event - clean up old caches
|
||||
|
self.addEventListener('activate', event => { |
||||
|
event.waitUntil( |
||||
|
caches.keys().then(cacheNames => { |
||||
|
return Promise.all( |
||||
|
cacheNames.map(cacheName => { |
||||
|
if (cacheName !== CACHE_NAME && |
||||
|
cacheName !== MAP_TILE_CACHE && |
||||
|
cacheName !== GEOCACHE_CACHE) { |
||||
|
console.log('Deleting old cache:', cacheName); |
||||
|
return caches.delete(cacheName); |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
}).then(() => self.clients.claim()) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
// Fetch event - serve from cache when possible
|
||||
|
self.addEventListener('fetch', event => { |
||||
|
const url = new URL(event.request.url); |
||||
|
|
||||
|
// Handle map tiles with cache-first strategy
|
||||
|
if (url.hostname.includes('tile.openstreetmap.org') || |
||||
|
url.hostname.includes('mt0.google.com') || |
||||
|
url.hostname.includes('mt1.google.com')) { |
||||
|
event.respondWith( |
||||
|
caches.open(MAP_TILE_CACHE).then(cache => { |
||||
|
return cache.match(event.request).then(response => { |
||||
|
if (response) { |
||||
|
return response; |
||||
|
} |
||||
|
// Fetch and cache the tile
|
||||
|
return fetch(event.request).then(response => { |
||||
|
// Only cache successful responses
|
||||
|
if (response.status === 200) { |
||||
|
cache.put(event.request, response.clone()); |
||||
|
} |
||||
|
return response; |
||||
|
}).catch(() => { |
||||
|
// Return a placeholder tile if offline
|
||||
|
return new Response('', { status: 204 }); |
||||
|
}); |
||||
|
}); |
||||
|
}) |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Handle API calls with network-first strategy
|
||||
|
if (url.pathname.includes('/save-kml') || |
||||
|
url.pathname.includes('/geocaches')) { |
||||
|
event.respondWith( |
||||
|
fetch(event.request) |
||||
|
.then(response => { |
||||
|
// Cache successful API responses
|
||||
|
if (response.status === 200) { |
||||
|
const responseToCache = response.clone(); |
||||
|
caches.open(GEOCACHE_CACHE).then(cache => { |
||||
|
cache.put(event.request, responseToCache); |
||||
|
}); |
||||
|
} |
||||
|
return response; |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
// Fall back to cache for API calls
|
||||
|
return caches.match(event.request); |
||||
|
}) |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Default strategy: cache-first for assets
|
||||
|
event.respondWith( |
||||
|
caches.match(event.request) |
||||
|
.then(response => { |
||||
|
if (response) { |
||||
|
return response; |
||||
|
} |
||||
|
// Clone the request because it's a stream
|
||||
|
const fetchRequest = event.request.clone(); |
||||
|
|
||||
|
return fetch(fetchRequest).then(response => { |
||||
|
// Check if valid response
|
||||
|
if (!response || response.status !== 200 || response.type !== 'basic') { |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
// Clone the response because it's a stream
|
||||
|
const responseToCache = response.clone(); |
||||
|
|
||||
|
caches.open(CACHE_NAME).then(cache => { |
||||
|
cache.put(event.request, responseToCache); |
||||
|
}); |
||||
|
|
||||
|
return response; |
||||
|
}); |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
// Offline fallback
|
||||
|
if (event.request.destination === 'document') { |
||||
|
return caches.match('/index.html'); |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
// Background sync for uploading tracks when back online
|
||||
|
self.addEventListener('sync', event => { |
||||
|
if (event.tag === 'sync-tracks') { |
||||
|
event.waitUntil(syncTracks()); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
async function syncTracks() { |
||||
|
// This would sync any offline changes when connection is restored
|
||||
|
console.log('Syncing tracks with server...'); |
||||
|
// Implementation would go here
|
||||
|
} |
||||
|
|
||||
|
// Handle messages from the main app
|
||||
|
self.addEventListener('message', event => { |
||||
|
if (event.data && event.data.type === 'SKIP_WAITING') { |
||||
|
self.skipWaiting(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Handle push notifications
|
||||
|
self.addEventListener('push', event => { |
||||
|
if (!event.data) { |
||||
|
console.log('Push notification without data'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const options = event.data.json(); |
||||
|
|
||||
|
event.waitUntil( |
||||
|
self.registration.showNotification(options.title || 'HikeMap Alert', { |
||||
|
body: options.body || 'You have a new notification', |
||||
|
icon: options.icon || '/icon-192x192.png', |
||||
|
badge: options.badge || '/icon-72x72.png', |
||||
|
vibrate: [200, 100, 200], |
||||
|
tag: options.tag || 'hikemap-notification', |
||||
|
data: options.data || {}, |
||||
|
actions: [ |
||||
|
{ |
||||
|
action: 'view', |
||||
|
title: 'View', |
||||
|
icon: '/icon-72x72.png' |
||||
|
}, |
||||
|
{ |
||||
|
action: 'close', |
||||
|
title: 'Dismiss' |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
// Handle notification clicks
|
||||
|
self.addEventListener('notificationclick', event => { |
||||
|
event.notification.close(); |
||||
|
|
||||
|
if (event.action === 'close') { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Open or focus the app
|
||||
|
event.waitUntil( |
||||
|
clients.matchAll({ type: 'window' }).then(clientList => { |
||||
|
// If app is already open, focus it
|
||||
|
for (const client of clientList) { |
||||
|
if (client.url.includes('maps.bibbit.duckdns.org') && 'focus' in client) { |
||||
|
return client.focus(); |
||||
|
} |
||||
|
} |
||||
|
// If app is not open, open it
|
||||
|
if (clients.openWindow) { |
||||
|
return clients.openWindow('/'); |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
@ -0,0 +1,54 @@ |
|||||
|
{ |
||||
|
"packageId": "org.duckdns.bibbit.hikemap", |
||||
|
"host": "maps.bibbit.duckdns.org", |
||||
|
"name": "HikeMap Trail Navigator", |
||||
|
"launcherName": "HikeMap", |
||||
|
"display": "standalone", |
||||
|
"themeColor": "#4CAF50", |
||||
|
"navigationColor": "#4CAF50", |
||||
|
"backgroundColor": "#ffffff", |
||||
|
"enableNotifications": true, |
||||
|
"startUrl": "/", |
||||
|
"iconUrl": "https://maps.bibbit.duckdns.org/icon-512x512.png", |
||||
|
"maskableIconUrl": "https://maps.bibbit.duckdns.org/icon-512x512.png", |
||||
|
"splashScreenFadeOutDuration": 300, |
||||
|
"signingKey": { |
||||
|
"alias": "android", |
||||
|
"path": "./android.keystore", |
||||
|
"password": "hikemap2024", |
||||
|
"keyPassword": "hikemap2024" |
||||
|
}, |
||||
|
"appVersionName": "1.0.0", |
||||
|
"appVersionCode": 1, |
||||
|
"shortcuts": [ |
||||
|
{ |
||||
|
"name": "Navigate", |
||||
|
"shortName": "Navigate", |
||||
|
"url": "/?mode=navigate", |
||||
|
"chosenIconUrl": "https://maps.bibbit.duckdns.org/icon-96x96.png" |
||||
|
} |
||||
|
], |
||||
|
"generatorApp": "bubblewrap-cli", |
||||
|
"webManifestUrl": "https://maps.bibbit.duckdns.org/manifest.json", |
||||
|
"fallbackType": "customtabs", |
||||
|
"features": { |
||||
|
"locationDelegation": { |
||||
|
"enabled": true |
||||
|
}, |
||||
|
"playBilling": { |
||||
|
"enabled": false |
||||
|
} |
||||
|
}, |
||||
|
"alphaDependencies": { |
||||
|
"enabled": false |
||||
|
}, |
||||
|
"enableSiteSettingsShortcut": true, |
||||
|
"isChromeOSOnly": false, |
||||
|
"orientation": "portrait-primary", |
||||
|
"fingerprints": [ |
||||
|
{ |
||||
|
"value": "4B:99:EB:12:8C:5C:7B:9B:3C:2F:E5:5C:7A:5D:22:16:7C:A4:8B:28:95:DF:B8:A3:F5:7C:06:92:F0:73:79:36" |
||||
|
} |
||||
|
], |
||||
|
"appVersion": "1.0.0" |
||||
|
} |
||||