diff --git a/index.html b/index.html
index 99e2c70..2a622ab 100644
--- a/index.html
+++ b/index.html
@@ -83,6 +83,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/offline.html b/public/offline.html
new file mode 100644
index 0000000..5b87cf0
--- /dev/null
+++ b/public/offline.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ TrustyNotes - Offline
+
+
+
+
+
📱
+
You're Offline
+
Don't worry! Your notes are safely stored and will sync when you're back online.
+
+
+
+
\ No newline at end of file
diff --git a/public/site.webmanifest b/public/site.webmanifest
index 45dc8a2..264175d 100644
--- a/public/site.webmanifest
+++ b/public/site.webmanifest
@@ -1 +1,41 @@
-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
+{
+ "name": "TrustyNotes",
+ "short_name": "TrustyNotes",
+ "description": "Secure, encrypted note-taking app",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#1e1e1e",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "New Note",
+ "short_name": "New",
+ "description": "Create a new note",
+ "url": "/?new=true",
+ "icons": [{ "src": "/icons/new-note.png", "sizes": "192x192" }]
+ }
+ ],
+ "screenshots": [
+ {
+ "src": "/screenshots/app.png",
+ "sizes": "1280x720",
+ "type": "image/png",
+ "platform": "wide",
+ "label": "TrustyNotes App Interface"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 72a9680..a6f926d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -41,6 +41,8 @@ import { SyncSettings as SyncSettingsType } from './types/sync';
import { WebStorageService } from './services/webStorage';
import './styles/richtext.css';
import { MobileNav } from './components/MobileNav';
+import { notifications } from '@mantine/notifications';
+import { InstallPrompt } from './components/InstallPrompt';
interface Note {
id?: number;
@@ -373,6 +375,26 @@ async function deleteNote(noteId: number) {
}
}
+ useEffect(() => {
+ WebStorageService.initializeOfflineSupport();
+
+ window.addEventListener('online', () => {
+ notifications.show({
+ title: 'Back Online',
+ message: 'Your changes will be synced automatically.',
+ color: 'green'
+ });
+ });
+
+ window.addEventListener('offline', () => {
+ notifications.show({
+ title: 'Offline Mode',
+ message: 'Changes will be saved locally and synced when online.',
+ color: 'yellow'
+ });
+ });
+ }, []);
+
return (
+ {isMobile && }
Promise;
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
+}
+
+export function InstallPrompt() {
+ const [deferredPrompt, setDeferredPrompt] = useState(null);
+ const [showIOSPrompt, setShowIOSPrompt] = useState(false);
+
+ useEffect(() => {
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !/CriOS/.test(navigator.userAgent);
+ const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
+
+ if (isIOS && !isStandalone) {
+ setShowIOSPrompt(true);
+ }
+
+ const handleBeforeInstall = (e: Event) => {
+ e.preventDefault();
+ setDeferredPrompt(e as BeforeInstallPromptEvent);
+ };
+
+ window.addEventListener('beforeinstallprompt', handleBeforeInstall);
+
+ return () => {
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
+ };
+ }, []);
+
+ const handleInstall = async () => {
+ if (!deferredPrompt) return;
+
+ deferredPrompt.prompt();
+ const { outcome } = await deferredPrompt.userChoice;
+
+ if (outcome === 'accepted') {
+ setDeferredPrompt(null);
+ }
+ };
+
+ if (!deferredPrompt && !showIOSPrompt) return null;
+
+ return (
+
+
+
+
+ Install TrustyNotes
+
+
+ {showIOSPrompt ? (
+ <>
+
+ Install TrustyNotes on your iOS device: tap the share button
+ and select "Add to Home Screen"
+
+
+ >
+ ) : (
+ <>
+
+ Install TrustyNotes for quick access and offline use
+
+ }
+ >
+ Install App
+
+ >
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MobileNav.tsx b/src/components/MobileNav.tsx
index 51d948d..5ac494b 100644
--- a/src/components/MobileNav.tsx
+++ b/src/components/MobileNav.tsx
@@ -1,4 +1,18 @@
-import { Group, ActionIcon, Drawer, Stack, Button, TextInput, Box, Text, Paper, Image, MantineColorScheme, Anchor } from '@mantine/core';
+import { useState, useEffect } from 'react';
+import {
+ Group,
+ ActionIcon,
+ Drawer,
+ Stack,
+ Button,
+ TextInput,
+ Box,
+ Text,
+ Paper,
+ Image,
+ MantineColorScheme,
+ Anchor
+} from '@mantine/core';
import {
IconPlus,
IconSearch,
@@ -8,7 +22,8 @@ import {
IconDownload,
IconUpload,
IconBrandGithub,
- IconTrash
+ IconTrash,
+ IconWifiOff
} from '@tabler/icons-react';
import { format } from 'date-fns';
@@ -53,64 +68,93 @@ export function MobileNav({
onSelectNote,
onDeleteNote
}: MobileNavProps) {
- const isDark = colorScheme === 'dark' || (colorScheme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
+
+ useEffect(() => {
+ const handleOnline = () => setIsOnline(true);
+ const handleOffline = () => setIsOnline(false);
+
+ window.addEventListener('online', handleOnline);
+ window.addEventListener('offline', handleOffline);
+
+ return () => {
+ window.removeEventListener('online', handleOnline);
+ window.removeEventListener('offline', handleOffline);
+ };
+ }, []);
+
+ const handleNoteSelect = (note: Note) => {
+ onSelectNote(note);
+ onClose();
+ };
return (
+
+
+
+
+ TrustyNotes
+
+ ×
+
+
-
- TrustyNotes
+ }
+ variant="light"
+ onClick={() => {
+ onNewNote();
+ onClose();
+ }}
+ style={{ flex: 1 }}
+ >
+ New Note
+
- }
- >
-
+
}
value={searchQuery}
onChange={(e) => onSearch(e.currentTarget.value)}
+ leftSection={}
/>
- }
- onClick={() => {
- onNewNote();
- onClose();
- }}
- fullWidth
- >
- New Note
-
-
+ {!isOnline && (
+
+
+
+ Offline Mode - Changes will sync when online
+
+
+ )}
+
{notes.map((note) => (
{
- onSelectNote(note);
- onClose();
- }}
+ onClick={() => handleNoteSelect(note)}
style={{
cursor: 'pointer',
backgroundColor: selectedNote?.id === note.id ?
- 'var(--mantine-color-blue-light)' : undefined,
+ 'var(--mantine-color-blue-light)' : undefined
}}
>
-
+
{note.title || 'Untitled'}
-
+
{format(note.updated_at, 'MMM d, yyyy HH:mm')}
@@ -131,54 +175,53 @@ export function MobileNav({
- : }
- onClick={onToggleTheme}
- fullWidth
- >
- {isDark ? 'Light Mode' : 'Dark Mode'}
-
-
- }
- onClick={() => {
- onShowSyncSettings();
- onClose();
- }}
- fullWidth
- >
- Sync Settings
-
-
-
- }
- onClick={onExport}
+
+
+ {colorScheme === 'dark' ? : }
+
+
- Export
-
- }
- onClick={onImport}
+
+
+
- Import
-
+
+
+
+
+
- }
- component="a"
- href="https://github.com/toolworks-dev/trusty-notes"
- target="_blank"
- fullWidth
- >
- GitHub
-
+
+
+
+
+
+
+
{
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => {
+ return cache.addAll(ASSETS_TO_CACHE);
+ })
+ );
+});
+
+self.addEventListener('fetch', (event) => {
+ if (API_ROUTES.some(route => event.request.url.startsWith(route))) {
+ return;
+ }
+
+ event.respondWith(
+ caches.match(event.request)
+ .then((response) => {
+ if (response) {
+ return response;
+ }
+
+ return fetch(event.request.clone())
+ .then((response) => {
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ return response;
+ }
+
+ const responseToCache = response.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(event.request, responseToCache);
+ });
+
+ return response;
+ })
+ .catch(() => {
+ if (event.request.mode === 'navigate') {
+ return caches.match('/offline.html');
+ }
+ return new Response('', {
+ status: 408,
+ statusText: 'Request timed out.'
+ });
+ });
+ })
+ );
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames
+ .filter((name) => name !== CACHE_NAME)
+ .map((name) => caches.delete(name))
+ );
+ })
+ );
+});
+
+self.addEventListener('message', (event) => {
+ if (event.data === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+});
\ No newline at end of file
diff --git a/src/services/webStorage.ts b/src/services/webStorage.ts
index 5a996bf..4b0eb03 100644
--- a/src/services/webStorage.ts
+++ b/src/services/webStorage.ts
@@ -108,6 +108,11 @@ export class WebStorageService {
}
static async syncWithServer(serverUrl: string, retries = 3): Promise {
+ if (!navigator.onLine) {
+ console.log('Device is offline, skipping sync');
+ return;
+ }
+
if (!this.crypto) {
throw new Error('Crypto not initialized');
}
@@ -231,4 +236,49 @@ export class WebStorageService {
localStorage.setItem(this.NOTES_KEY, JSON.stringify(filteredNotes));
}
+ static async isOnline(): Promise {
+ return navigator.onLine;
+ }
+
+ static async saveNoteWithOfflineSupport(note: Partial): Promise {
+ await this.saveNote(note);
+
+ if (!navigator.onLine) {
+ const offlinePendingSync = JSON.parse(
+ localStorage.getItem('offline_pending_sync') || '[]'
+ );
+ offlinePendingSync.push(note.id);
+ localStorage.setItem('offline_pending_sync', JSON.stringify(offlinePendingSync));
+ }
+ }
+
+ static async syncOfflineChanges(): Promise {
+ if (!navigator.onLine) return;
+
+ const pendingSync = JSON.parse(
+ localStorage.getItem('offline_pending_sync') || '[]'
+ );
+
+ if (pendingSync.length === 0) return;
+
+ const settings = await this.getSyncSettings();
+ if (settings?.auto_sync && settings?.seed_phrase) {
+ try {
+ await this.syncWithServer(settings.server_url);
+ localStorage.setItem('offline_pending_sync', '[]');
+ } catch (error) {
+ console.error('Failed to sync offline changes:', error);
+ }
+ }
+ }
+
+ static initializeOfflineSupport(): void {
+ window.addEventListener('online', () => {
+ this.syncOfflineChanges();
+ });
+
+ window.addEventListener('offline', () => {
+ console.log('App is offline. Changes will be synced when online.');
+ });
+ }
}
\ No newline at end of file