diff --git a/README.md b/README.md index a1cf5ae..2ea86b7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,51 @@ # Quicknote Pro -A modern note-taking application that allows you to doodle, take screenshots, upload images, use voice note, sync to cloud and save to multiple servers. +A modern note-taking application with real-time local persistence and optional cloud synchronization. Features include rich text editing, image insertion, drawing/doodling, voice notes, file attachments, and premium gating for advanced features. + +## ✨ Features + +### Core Features (Free) +- **Rich Text Editing**: Full markdown support with formatting toolbar +- **Image Insertion**: Camera capture and gallery selection with local file management +- **Voice Notes**: Voice-to-text transcription +- **Local Persistence**: Robust local storage using Hive database +- **Search & Filtering**: Powerful search across note content, tags, and folders +- **Organization**: Pin notes, add tags, and organize in folders + +### Premium Features +- **Drawing & Doodling**: Digital canvas with various brushes and colors +- **File Attachments**: Attach any file type to notes +- **Cloud Synchronization**: + - Google Drive integration (configurable) + - OneDrive integration (configurable) + - Automatic sync with conflict resolution +- **Advanced Search**: AI-powered search suggestions + +### Cloud Sync Configuration (Optional) + +By default, the app runs in **local-only mode** and builds successfully without any cloud credentials. To enable cloud sync: + +#### Google Drive Setup +1. Create a project in [Google Cloud Console](https://console.cloud.google.com/) +2. Enable the Google Drive API +3. Create OAuth 2.0 credentials for your app +4. Update `lib/services/sync/providers/google_drive_sync_provider.dart`: + ```dart + static const bool _isEnabled = true; // Enable Google Drive + ``` +5. Configure OAuth credentials in your app's Info.plist (iOS) or AndroidManifest.xml + +#### OneDrive Setup +1. Register your app in [Microsoft Azure Portal](https://portal.azure.com/) +2. Configure Microsoft Graph API permissions +3. Update `lib/services/sync/providers/onedrive_sync_provider.dart`: + ```dart + static const bool _isEnabled = true; // Enable OneDrive + ``` +4. Configure OAuth redirect URIs + +**Note**: The app builds and runs perfectly without cloud credentials. Cloud sync features will show "Not configured" in settings. ## 📋 Prerequisites @@ -13,55 +57,90 @@ A modern note-taking application that allows you to doodle, take screenshots, up ## 🛠️ Installation -1. Install dependencies: +1. Clone the repository: +```bash +git clone https://github.com/mikaelkraft/Quicknote_Pro.git +cd Quicknote_Pro +``` + +2. Install dependencies: ```bash flutter pub get ``` -2. Run the application: +3. Generate Hive type adapters: +```bash +dart run build_runner build +``` + +4. Run the application: ```bash flutter run ``` +### Development Setup + +For development with premium features enabled: +```dart +// Enable premium for testing +final premiumService = PremiumService(); +await premiumService.grantPremium(); // Grants lifetime premium +``` + ## 📁 Project Structure ``` -flutter_app/ -├── android/ # Android-specific configuration -├── ios/ # iOS-specific configuration -├── lib/ -│ ├── core/ # Core utilities and services -│ │ └── utils/ # Utility classes -│ ├── presentation/ # UI screens and widgets -│ │ └── splash_screen/ # Splash screen implementation -│ ├── routes/ # Application routing -│ ├── theme/ # Theme configuration -│ ├── widgets/ # Reusable UI components -│ └── main.dart # Application entry point -├── assets/ # Static assets (images, fonts, etc.) -├── pubspec.yaml # Project dependencies and configuration -└── README.md # Project documentation +lib/ +├── core/ # Core utilities and services +├── models/ # Data models (Note, etc.) +│ ├── note.dart # Note model with Hive annotations +│ └── note.g.dart # Generated Hive adapters +├── services/ # Business logic services +│ ├── local/ # Local persistence services +│ │ ├── hive_initializer.dart # Database initialization +│ │ └── note_repository.dart # Note CRUD operations +│ ├── premium/ # Premium feature management +│ │ └── premium_service.dart # Premium status and gating +│ └── sync/ # Cloud synchronization +│ ├── cloud_sync_service.dart # Abstract sync interface +│ ├── sync_manager.dart # Sync orchestration +│ └── providers/ # Cloud provider implementations +│ ├── google_drive_sync_provider.dart +│ └── onedrive_sync_provider.dart +├── presentation/ # UI screens and widgets +│ ├── notes_dashboard/ # Main notes interface +│ ├── note_creation_editor/ # Note editing interface +│ ├── settings_profile/ +│ │ └── cloud_connections.dart # Cloud sync settings +│ └── ... # Other UI screens +├── routes/ # Application routing +├── theme/ # Theme configuration +├── widgets/ # Reusable UI components +└── main.dart # Application entry point with service initialization ``` -## 🧩 Adding Routes +## 🧩 Architecture -To add new routes to the application, update the `lib/routes/app_routes.dart` file: +### Local Persistence +- **Hive Database**: NoSQL database for fast local storage +- **Repository Pattern**: Clean separation between data access and business logic +- **Reactive Streams**: Real-time UI updates via note repository streams -```dart -import 'package:flutter/material.dart'; -import 'package:package_name/presentation/home_screen/home_screen.dart'; - -class AppRoutes { - static const String initial = '/'; - static const String home = '/home'; - - static Map routes = { - initial: (context) => const SplashScreen(), - home: (context) => const HomeScreen(), - // Add more routes as needed - } -} -``` +### Cloud Sync (Optional) +- **Pluggable Providers**: Easy to add new cloud storage providers +- **Offline-First**: Works seamlessly without internet connection +- **Conflict Resolution**: Last-write-wins with basic merge safeguards +- **Background Sync**: Automatic synchronization every 30 minutes when connected + +### Premium System +- **Local Premium State**: Stored in Hive for offline access +- **Feature Gating**: Centralized premium feature management +- **Extensible**: Easy to integrate with real IAP systems later + +### File Management +- **Stable Storage**: Images and attachments copied to app documents directory +- **Relative Paths**: Notes store relative paths for portability +- **Auto-Cleanup**: Orphaned files removed during sync operations ## 🎨 Theming diff --git a/lib/main.dart b/lib/main.dart index c9e78a5..f4f24fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,10 +4,39 @@ import 'package:sizer/sizer.dart'; import '../core/app_export.dart'; import '../widgets/custom_error_widget.dart'; +import '../services/local/hive_initializer.dart'; +import '../services/local/note_repository.dart'; +import '../services/premium/premium_service.dart'; +import '../services/sync/sync_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Initialize Hive before running the app + try { + await HiveInitializer.init(); + print('✅ Hive initialized successfully'); + } catch (e) { + print('❌ Failed to initialize Hive: $e'); + // Continue with app launch even if Hive fails (graceful degradation) + } + + // Initialize services + try { + final noteRepository = NoteRepository(); + noteRepository.init(); + + final premiumService = PremiumService(); + premiumService.init(); + + final syncManager = SyncManager(); + syncManager.init(); + + print('✅ Services initialized successfully'); + } catch (e) { + print('❌ Failed to initialize services: $e'); + } + // 🚨 CRITICAL: Custom error handling - DO NOT REMOVE ErrorWidget.builder = (FlutterErrorDetails details) { return CustomErrorWidget( diff --git a/lib/models/note.dart b/lib/models/note.dart new file mode 100644 index 0000000..5cc6ca9 --- /dev/null +++ b/lib/models/note.dart @@ -0,0 +1,167 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'note.g.dart'; + +@HiveType(typeId: 0) +class Note extends HiveObject { + @HiveField(0) + String id; + + @HiveField(1) + String title; + + @HiveField(2) + String content; + + @HiveField(3) + List images; + + @HiveField(4) + List attachments; + + @HiveField(5) + DateTime createdAt; + + @HiveField(6) + DateTime updatedAt; + + @HiveField(7) + String? folderId; + + @HiveField(8) + bool isPinned; + + @HiveField(9) + List tags; + + @HiveField(10) + DateTime? deletedAt; + + @HiveField(11) + String? noteType; // 'text', 'voice', 'drawing', 'template' + + @HiveField(12) + bool hasReminder; + + @HiveField(13) + DateTime? reminderAt; + + @HiveField(14) + Map? metadata; // For additional data like voice memo paths, etc. + + Note({ + required this.id, + required this.title, + required this.content, + required this.createdAt, + required this.updatedAt, + this.images = const [], + this.attachments = const [], + this.folderId, + this.isPinned = false, + this.tags = const [], + this.deletedAt, + this.noteType = 'text', + this.hasReminder = false, + this.reminderAt, + this.metadata, + }); + + // Factory constructor for creating from map (useful for cloud sync) + factory Note.fromMap(Map map) { + return Note( + id: map['id'] as String, + title: map['title'] as String, + content: map['content'] as String, + images: List.from(map['images'] ?? []), + attachments: List.from(map['attachments'] ?? []), + createdAt: DateTime.parse(map['createdAt'] as String), + updatedAt: DateTime.parse(map['updatedAt'] as String), + folderId: map['folderId'] as String?, + isPinned: map['isPinned'] as bool? ?? false, + tags: List.from(map['tags'] ?? []), + deletedAt: map['deletedAt'] != null ? DateTime.parse(map['deletedAt'] as String) : null, + noteType: map['noteType'] as String? ?? 'text', + hasReminder: map['hasReminder'] as bool? ?? false, + reminderAt: map['reminderAt'] != null ? DateTime.parse(map['reminderAt'] as String) : null, + metadata: map['metadata'] as Map?, + ); + } + + // Convert to map (useful for cloud sync) + Map toMap() { + return { + 'id': id, + 'title': title, + 'content': content, + 'images': images, + 'attachments': attachments, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'folderId': folderId, + 'isPinned': isPinned, + 'tags': tags, + 'deletedAt': deletedAt?.toIso8601String(), + 'noteType': noteType, + 'hasReminder': hasReminder, + 'reminderAt': reminderAt?.toIso8601String(), + 'metadata': metadata, + }; + } + + // Get preview text (first 150 characters of content) + String get preview { + if (content.length <= 150) return content; + return '${content.substring(0, 150)}...'; + } + + // Check if note is deleted + bool get isDeleted => deletedAt != null; + + // Update the updatedAt timestamp + void touch() { + updatedAt = DateTime.now(); + } + + // Copy with method for updates + Note copyWith({ + String? id, + String? title, + String? content, + List? images, + List? attachments, + DateTime? createdAt, + DateTime? updatedAt, + String? folderId, + bool? isPinned, + List? tags, + DateTime? deletedAt, + String? noteType, + bool? hasReminder, + DateTime? reminderAt, + Map? metadata, + }) { + return Note( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + images: images ?? this.images, + attachments: attachments ?? this.attachments, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? DateTime.now(), + folderId: folderId ?? this.folderId, + isPinned: isPinned ?? this.isPinned, + tags: tags ?? this.tags, + deletedAt: deletedAt ?? this.deletedAt, + noteType: noteType ?? this.noteType, + hasReminder: hasReminder ?? this.hasReminder, + reminderAt: reminderAt ?? this.reminderAt, + metadata: metadata ?? this.metadata, + ); + } + + @override + String toString() { + return 'Note(id: $id, title: $title, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} \ No newline at end of file diff --git a/lib/models/note.g.dart b/lib/models/note.g.dart new file mode 100644 index 0000000..a3b66dc --- /dev/null +++ b/lib/models/note.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'note.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class NoteAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + Note read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Note( + id: fields[0] as String, + title: fields[1] as String, + content: fields[2] as String, + createdAt: fields[5] as DateTime, + updatedAt: fields[6] as DateTime, + images: (fields[3] as List).cast(), + attachments: (fields[4] as List).cast(), + folderId: fields[7] as String?, + isPinned: fields[8] as bool, + tags: (fields[9] as List).cast(), + deletedAt: fields[10] as DateTime?, + noteType: fields[11] as String?, + hasReminder: fields[12] as bool, + reminderAt: fields[13] as DateTime?, + metadata: (fields[14] as Map?)?.cast(), + ); + } + + @override + void write(BinaryWriter writer, Note obj) { + writer + ..writeByte(15) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.content) + ..writeByte(3) + ..write(obj.images) + ..writeByte(4) + ..write(obj.attachments) + ..writeByte(5) + ..write(obj.createdAt) + ..writeByte(6) + ..write(obj.updatedAt) + ..writeByte(7) + ..write(obj.folderId) + ..writeByte(8) + ..write(obj.isPinned) + ..writeByte(9) + ..write(obj.tags) + ..writeByte(10) + ..write(obj.deletedAt) + ..writeByte(11) + ..write(obj.noteType) + ..writeByte(12) + ..write(obj.hasReminder) + ..writeByte(13) + ..write(obj.reminderAt) + ..writeByte(14) + ..write(obj.metadata); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NoteAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/presentation/folder_organization/folder_organization.dart b/lib/presentation/folder_organization/folder_organization.dart index 3611d17..df147dd 100644 --- a/lib/presentation/folder_organization/folder_organization.dart +++ b/lib/presentation/folder_organization/folder_organization.dart @@ -607,7 +607,7 @@ class _FolderOrganizationState extends State padding: EdgeInsets.all(2.w), decoration: BoxDecoration( color: _getColorForTheme(folder['color'], isDark) - .withValues(alpha: 0.2), + .withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( diff --git a/lib/presentation/folder_organization/widgets/create_folder_bottom_sheet.dart b/lib/presentation/folder_organization/widgets/create_folder_bottom_sheet.dart index 2c782dc..e5bb1b5 100644 --- a/lib/presentation/folder_organization/widgets/create_folder_bottom_sheet.dart +++ b/lib/presentation/folder_organization/widgets/create_folder_bottom_sheet.dart @@ -161,7 +161,7 @@ class _CreateFolderBottomSheetState extends State { : null, boxShadow: [ BoxShadow( - color: color.withValues(alpha: 0.3), + color: color.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), diff --git a/lib/presentation/folder_organization/widgets/empty_folders_widget.dart b/lib/presentation/folder_organization/widgets/empty_folders_widget.dart index 93fd5d0..804cc3d 100644 --- a/lib/presentation/folder_organization/widgets/empty_folders_widget.dart +++ b/lib/presentation/folder_organization/widgets/empty_folders_widget.dart @@ -27,7 +27,7 @@ class EmptyFoldersWidget extends StatelessWidget { height: 40.w, decoration: BoxDecoration( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), shape: BoxShape.circle, ), child: Center( @@ -93,11 +93,11 @@ class EmptyFoldersWidget extends StatelessWidget { padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: (isDark ? AppTheme.accentDark : AppTheme.accentLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: (isDark ? AppTheme.accentDark : AppTheme.accentLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), width: 1, ), ), diff --git a/lib/presentation/folder_organization/widgets/folder_card_widget.dart b/lib/presentation/folder_organization/widgets/folder_card_widget.dart index c7e8d1c..c750e24 100644 --- a/lib/presentation/folder_organization/widgets/folder_card_widget.dart +++ b/lib/presentation/folder_organization/widgets/folder_card_widget.dart @@ -35,7 +35,7 @@ class FolderCardWidget extends StatelessWidget { key: Key('folder_${folder['id']}'), background: Container( decoration: BoxDecoration( - color: AppTheme.getSuccessColor(isDark).withValues(alpha: 0.2), + color: AppTheme.getSuccessColor(isDark).withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), alignment: Alignment.centerLeft, @@ -49,7 +49,7 @@ class FolderCardWidget extends StatelessWidget { secondaryBackground: Container( decoration: BoxDecoration( color: (isDark ? AppTheme.errorDark : AppTheme.errorLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), alignment: Alignment.centerRight, @@ -87,7 +87,7 @@ class FolderCardWidget extends StatelessWidget { color: isDark ? AppTheme.cardDark : AppTheme.cardLight, borderRadius: BorderRadius.circular(12), border: Border.all( - color: folderColor.withValues(alpha: 0.3), + color: folderColor.withOpacity(0.3), width: 1, ), boxShadow: [ @@ -109,7 +109,7 @@ class FolderCardWidget extends StatelessWidget { Container( padding: EdgeInsets.all(2.w), decoration: BoxDecoration( - color: folderColor.withValues(alpha: 0.2), + color: folderColor.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( @@ -190,7 +190,7 @@ class FolderCardWidget extends StatelessWidget { width: 8.w, height: 8.w, decoration: BoxDecoration( - color: folderColor.withValues(alpha: 0.2), + color: folderColor.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Center( diff --git a/lib/presentation/folder_organization/widgets/folder_context_menu.dart b/lib/presentation/folder_organization/widgets/folder_context_menu.dart index 18715fa..e9fb4bd 100644 --- a/lib/presentation/folder_organization/widgets/folder_context_menu.dart +++ b/lib/presentation/folder_organization/widgets/folder_context_menu.dart @@ -62,7 +62,7 @@ class FolderContextMenu extends StatelessWidget { padding: EdgeInsets.all(2.w), decoration: BoxDecoration( color: _getFolderColor(folder['color'] as String, isDark) - .withValues(alpha: 0.2), + .withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( @@ -178,7 +178,7 @@ class FolderContextMenu extends StatelessWidget { ? (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight) - .withValues(alpha: 0.5) + .withOpacity(0.5) : isDestructive ? (isDark ? AppTheme.errorDark : AppTheme.errorLight) : (isDark @@ -193,7 +193,7 @@ class FolderContextMenu extends StatelessWidget { ? (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight) - .withValues(alpha: 0.5) + .withOpacity(0.5) : isDestructive ? (isDark ? AppTheme.errorDark : AppTheme.errorLight) : (isDark diff --git a/lib/presentation/note_creation_editor/note_creation_editor.dart b/lib/presentation/note_creation_editor/note_creation_editor.dart index 39c2f08..604d200 100644 --- a/lib/presentation/note_creation_editor/note_creation_editor.dart +++ b/lib/presentation/note_creation_editor/note_creation_editor.dart @@ -1,8 +1,13 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:sizer/sizer.dart'; import '../../core/app_export.dart'; +import '../../services/local/note_repository.dart'; +import '../../services/local/hive_initializer.dart'; +import '../../services/premium/premium_service.dart'; +import '../../models/note.dart'; import './widgets/drawing_canvas_widget.dart'; import './widgets/formatting_toolbar_widget.dart'; import './widgets/image_insertion_widget.dart'; @@ -30,28 +35,21 @@ class _NoteCreationEditorState extends State bool _showImageInsertion = false; bool _isSaving = false; bool _hasUnsavedChanges = false; - bool _isPremiumUser = false; // Mock premium status DateTime? _lastSaved; + + // Services + final NoteRepository _noteRepository = NoteRepository(); + final PremiumService _premiumService = PremiumService(); - // Mock note data - final Map _noteData = { - 'id': '1', - 'title': '', - 'content': '', - 'createdAt': DateTime.now(), - 'updatedAt': DateTime.now(), - 'folder': 'Personal', - 'tags': [], - 'images': [], - 'voiceNotes': [], - }; + // Current note + Note? _currentNote; + String? _noteId; @override void initState() { super.initState(); _setupKeyboardListener(); _setupAutoSave(); - _titleFocusNode.requestFocus(); // Setup text change listeners _titleController.addListener(_onTextChanged); @@ -59,6 +57,46 @@ class _NoteCreationEditorState extends State _contentFocusNode.addListener(_onContentFocusChanged); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // Get note ID from route arguments + final args = ModalRoute.of(context)?.settings.arguments as Map?; + final noteId = args?['noteId'] as String?; + + if (noteId != null && _noteId != noteId) { + _noteId = noteId; + _loadNote(noteId); + } else if (noteId == null && _currentNote == null) { + // Create new note if no ID provided + _createNewNote(); + } + } + + Future _loadNote(String noteId) async { + final note = _noteRepository.getNoteById(noteId); + if (note != null) { + setState(() { + _currentNote = note; + _titleController.text = note.title; + _contentController.text = note.content; + _hasUnsavedChanges = false; + }); + _titleFocusNode.requestFocus(); + } + } + + Future _createNewNote() async { + final note = await _noteRepository.createNote(); + setState(() { + _currentNote = note; + _noteId = note.id; + _hasUnsavedChanges = false; + }); + _titleFocusNode.requestFocus(); + } + @override void dispose() { _titleController.dispose(); @@ -107,32 +145,49 @@ class _NoteCreationEditorState extends State } Future _saveNote({bool showConfirmation = true}) async { + if (_currentNote == null) return; + setState(() => _isSaving = true); - // Simulate save operation - await Future.delayed(const Duration(milliseconds: 1500)); - - _noteData['title'] = _titleController.text; - _noteData['content'] = _contentController.text; - _noteData['updatedAt'] = DateTime.now(); - - setState(() { - _isSaving = false; - _hasUnsavedChanges = false; - _lastSaved = DateTime.now(); - }); - - if (showConfirmation) { - HapticFeedback.lightImpact(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Note saved successfully'), - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), + try { + // Update note with current content + final updatedNote = _currentNote!.copyWith( + title: _titleController.text, + content: _contentController.text, ); + + await _noteRepository.updateNote(updatedNote); + + setState(() { + _currentNote = updatedNote; + _isSaving = false; + _hasUnsavedChanges = false; + _lastSaved = DateTime.now(); + }); + + if (showConfirmation) { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Note saved successfully'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ); + } + } catch (e) { + setState(() => _isSaving = false); + + if (showConfirmation) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to save note: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } } } @@ -274,33 +329,71 @@ class _NoteCreationEditorState extends State HapticFeedback.lightImpact(); } - void _handleImageInsertion(String imagePath) { - setState(() { - _showImageInsertion = false; - _noteData['images'].add(imagePath); - }); + void _handleImageInsertion(String imagePath) async { + if (_currentNote == null) return; + + try { + setState(() { + _showImageInsertion = false; + }); - // Insert image reference in content - final currentText = _contentController.text; - final cursorPosition = _contentController.selection.baseOffset; - final imageRef = '\n![Image](${imagePath})\n'; + // Copy file to app directory + final noteMediaPath = await HiveInitializer.getNoteMediaPath(_currentNote!.id); + final fileName = imagePath.split('/').last; + final destinationPath = '$noteMediaPath/$fileName'; + + // Copy the file + final sourceFile = File(imagePath); + final destinationFile = await sourceFile.copy(destinationPath); + + // Update note with new image path + final updatedImages = List.from(_currentNote!.images); + updatedImages.add(destinationFile.path); + + final updatedNote = _currentNote!.copyWith(images: updatedImages); + await _noteRepository.updateNote(updatedNote); + + setState(() { + _currentNote = updatedNote; + }); - final newText = currentText.substring(0, cursorPosition) + - imageRef + - currentText.substring(cursorPosition); + // Insert image reference in content + final currentText = _contentController.text; + final cursorPosition = _contentController.selection.baseOffset; + final imageRef = '\n![Image]($destinationPath)\n'; - _contentController.text = newText; - _contentController.selection = TextSelection.collapsed( - offset: cursorPosition + imageRef.length, - ); + final newText = currentText.substring(0, cursorPosition) + + imageRef + + currentText.substring(cursorPosition); - HapticFeedback.lightImpact(); + _contentController.text = newText; + _contentController.selection = TextSelection.collapsed( + offset: cursorPosition + imageRef.length, + ); + + HapticFeedback.lightImpact(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to add image: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } } @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (!didPop) { + final shouldPop = await _onWillPop(); + if (shouldPop && context.mounted) { + Navigator.of(context).pop(); + } + } + }, child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: _buildAppBar(), @@ -489,14 +582,14 @@ class _NoteCreationEditorState extends State Widget _buildVoiceInput() { return VoiceInputWidget( onTranscriptionComplete: _handleVoiceTranscription, - isPremiumUser: _isPremiumUser, + isPremiumUser: _premiumService.isPremium, ); } Widget _buildDrawingCanvas() { return Positioned.fill( child: DrawingCanvasWidget( - isPremiumUser: _isPremiumUser, + isPremiumUser: _premiumService.isPremium, onClose: () => setState(() => _showDrawingCanvas = false), ), ); @@ -505,7 +598,7 @@ class _NoteCreationEditorState extends State Widget _buildImageInsertion() { return Positioned.fill( child: Container( - color: Colors.black.withValues(alpha: 0.5), + color: Colors.black.withOpacity(0.5), child: Center( child: Container( margin: EdgeInsets.all(4.w), @@ -555,7 +648,13 @@ class _NoteCreationEditorState extends State children: [ FloatingActionButton( heroTag: 'drawing', - onPressed: () => setState(() => _showDrawingCanvas = true), + onPressed: () { + if (_premiumService.isFeatureAvailable(PremiumFeature.doodling)) { + setState(() => _showDrawingCanvas = true); + } else { + _showPremiumUpsell(PremiumFeature.doodling); + } + }, backgroundColor: AppTheme.getAccentColor( Theme.of(context).brightness == Brightness.light), child: CustomIconWidget( @@ -579,6 +678,35 @@ class _NoteCreationEditorState extends State ); } + void _showPremiumUpsell(PremiumFeature feature) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + 'Premium Feature', + style: Theme.of(context).textTheme.titleLarge, + ), + content: Text( + _premiumService.getUpsellMessage(feature), + style: Theme.of(context).textTheme.bodyMedium, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.pushNamed(context, AppRoutes.premiumUpgrade); + }, + child: const Text('Upgrade'), + ), + ], + ), + ); + } + void _showExportOptions() { showModalBottomSheet( context: context, diff --git a/lib/presentation/note_creation_editor/widgets/image_insertion_widget.dart b/lib/presentation/note_creation_editor/widgets/image_insertion_widget.dart index 46e00d8..8a74a8b 100644 --- a/lib/presentation/note_creation_editor/widgets/image_insertion_widget.dart +++ b/lib/presentation/note_creation_editor/widgets/image_insertion_widget.dart @@ -285,7 +285,7 @@ class _ImageInsertionWidgetState extends State { icon: Container( padding: EdgeInsets.all(2.w), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), + color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: CustomIconWidget( diff --git a/lib/presentation/note_creation_editor/widgets/save_status_indicator_widget.dart b/lib/presentation/note_creation_editor/widgets/save_status_indicator_widget.dart index c0d42a3..38f3171 100644 --- a/lib/presentation/note_creation_editor/widgets/save_status_indicator_widget.dart +++ b/lib/presentation/note_creation_editor/widgets/save_status_indicator_widget.dart @@ -112,10 +112,10 @@ class _SaveStatusIndicatorWidgetState extends State child: Container( padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), decoration: BoxDecoration( - color: _getStatusColor().withValues(alpha: 0.1), + color: _getStatusColor().withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all( - color: _getStatusColor().withValues(alpha: 0.3), + color: _getStatusColor().withOpacity(0.3), width: 1, ), ), diff --git a/lib/presentation/notes_dashboard/notes_dashboard.dart b/lib/presentation/notes_dashboard/notes_dashboard.dart index c9165fa..6d3f64f 100644 --- a/lib/presentation/notes_dashboard/notes_dashboard.dart +++ b/lib/presentation/notes_dashboard/notes_dashboard.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:sizer/sizer.dart'; import '../../core/app_export.dart'; +import '../../services/local/note_repository.dart'; +import '../../services/sync/sync_manager.dart'; +import '../../models/note.dart'; import './widgets/empty_state_widget.dart'; import './widgets/filter_chip_widget.dart'; import './widgets/note_card_widget.dart'; @@ -23,72 +26,14 @@ class _NotesDashboardState extends State String _searchQuery = ''; bool _isSearchExpanded = false; - // Mock data for notes - final List> _allNotes = [ - { - "id": 1, - "title": "Meeting Notes - Q4 Planning", - "content": - "Discussed quarterly goals, budget allocation, and team expansion plans. Key decisions made regarding product roadmap and marketing strategy.", - "preview": - "Discussed quarterly goals, budget allocation, and team expansion plans...", - "type": "text", - "folder": "Work", - "createdAt": "2025-01-28T10:30:00Z", - "isPinned": true, - "hasReminder": true, - }, - { - "id": 2, - "title": "Voice Memo - Grocery List", - "content": - "Milk, eggs, bread, apples, chicken breast, pasta, tomatoes, cheese", - "preview": "Milk, eggs, bread, apples, chicken breast, pasta...", - "type": "voice", - "folder": "Personal", - "createdAt": "2025-01-27T15:45:00Z", - "isPinned": false, - "hasReminder": false, - }, - { - "id": 3, - "title": "App UI Wireframe", - "content": "Initial sketches for the new mobile app interface design", - "preview": "Initial sketches for the new mobile app interface design", - "type": "drawing", - "folder": "Work", - "createdAt": "2025-01-26T09:15:00Z", - "isPinned": false, - "hasReminder": false, - }, - { - "id": 4, - "title": "Book Ideas", - "content": - "Collection of interesting plot concepts and character development notes for future writing projects.", - "preview": - "Collection of interesting plot concepts and character development...", - "type": "text", - "folder": "Personal", - "createdAt": "2025-01-25T20:30:00Z", - "isPinned": false, - "hasReminder": true, - }, - { - "id": 5, - "title": "Travel Checklist", - "content": - "Passport, tickets, hotel confirmation, travel insurance, medications, chargers, camera", - "preview": "Passport, tickets, hotel confirmation, travel insurance...", - "type": "template", - "folder": null, - "createdAt": "2025-01-24T14:20:00Z", - "isPinned": true, - "hasReminder": false, - }, - ]; - - List> _filteredNotes = []; + // Repository and sync manager + final NoteRepository _noteRepository = NoteRepository(); + final SyncManager _syncManager = SyncManager(); + + // Current notes from repository + List _allNotes = []; + List _filteredNotes = []; + final List _recentSearches = ['meeting notes', 'grocery', 'travel']; final List _aiSuggestions = [ 'Find notes with reminders', @@ -100,7 +45,19 @@ class _NotesDashboardState extends State void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); - _filteredNotes = List.from(_allNotes); + + // Listen to notes stream from repository + _noteRepository.notesStream.listen((notes) { + if (mounted) { + setState(() { + _allNotes = notes; + _filterNotes(); + }); + } + }); + + // Load initial notes + _loadInitialNotes(); } @override @@ -109,21 +66,25 @@ class _NotesDashboardState extends State super.dispose(); } + void _loadInitialNotes() { + setState(() { + _allNotes = _noteRepository.getAllNotes(); + _filterNotes(); + }); + } + void _filterNotes() { setState(() { _filteredNotes = _allNotes.where((note) { bool matchesFilter = _selectedFilter == 'All' || - note['folder'] == _selectedFilter || - (_selectedFilter == 'Pinned' && note['isPinned'] == true) || - (_selectedFilter == 'Reminders' && note['hasReminder'] == true); + note.folderId == _selectedFilter || + (_selectedFilter == 'Pinned' && note.isPinned == true) || + (_selectedFilter == 'Reminders' && note.hasReminder == true); bool matchesSearch = _searchQuery.isEmpty || - (note['title'] as String) - .toLowerCase() - .contains(_searchQuery.toLowerCase()) || - (note['content'] as String) - .toLowerCase() - .contains(_searchQuery.toLowerCase()); + note.title.toLowerCase().contains(_searchQuery.toLowerCase()) || + note.content.toLowerCase().contains(_searchQuery.toLowerCase()) || + note.tags.any((tag) => tag.toLowerCase().contains(_searchQuery.toLowerCase())); return matchesFilter && matchesSearch; }).toList(); @@ -157,48 +118,35 @@ class _NotesDashboardState extends State ); } - void _createNote(String type) { - // Navigate to note creation based on type - switch (type) { - case 'text': - Navigator.pushNamed(context, '/note-creation-editor'); - break; - case 'voice': - Navigator.pushNamed(context, '/note-creation-editor'); + void _createNote(String type) async { + // Create a new note via repository + final note = await _noteRepository.createNote( + title: '', + content: '', + noteType: type, + ); + + // Navigate to note editor with the new note ID + Navigator.pushNamed( + context, + '/note-creation-editor', + arguments: {'noteId': note.id}, + ); + } + + void _onNoteAction(String noteId, String action) async { + switch (action) { + case 'pin': + await _noteRepository.togglePinNote(noteId); break; - case 'drawing': - Navigator.pushNamed(context, '/note-creation-editor'); + case 'delete': + await _noteRepository.deleteNote(noteId); break; - case 'template': - Navigator.pushNamed(context, '/note-creation-editor'); + case 'duplicate': + await _noteRepository.duplicateNote(noteId); break; } - } - - void _onNoteAction(int noteId, String action) { - setState(() { - final noteIndex = _allNotes.indexWhere((note) => note['id'] == noteId); - if (noteIndex != -1) { - switch (action) { - case 'pin': - _allNotes[noteIndex]['isPinned'] = - !(_allNotes[noteIndex]['isPinned'] ?? false); - break; - case 'delete': - _allNotes.removeAt(noteIndex); - break; - case 'duplicate': - final originalNote = _allNotes[noteIndex]; - final duplicatedNote = Map.from(originalNote); - duplicatedNote['id'] = DateTime.now().millisecondsSinceEpoch; - duplicatedNote['title'] = '${originalNote['title']} (Copy)'; - duplicatedNote['createdAt'] = DateTime.now().toIso8601String(); - _allNotes.insert(noteIndex + 1, duplicatedNote); - break; - } - } - }); - _filterNotes(); + // No need to call _filterNotes() here since the repository stream will update automatically } @override @@ -252,7 +200,7 @@ class _NotesDashboardState extends State color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( @@ -317,7 +265,7 @@ class _NotesDashboardState extends State FilterChipWidget( label: 'Work', count: _allNotes - .where((note) => note['folder'] == 'Work') + .where((note) => note.folderId == 'Work') .length, isSelected: _selectedFilter == 'Work', onTap: () => _onFilterSelected('Work'), @@ -325,7 +273,7 @@ class _NotesDashboardState extends State FilterChipWidget( label: 'Personal', count: _allNotes - .where((note) => note['folder'] == 'Personal') + .where((note) => note.folderId == 'Personal') .length, isSelected: _selectedFilter == 'Personal', onTap: () => _onFilterSelected('Personal'), @@ -333,7 +281,7 @@ class _NotesDashboardState extends State FilterChipWidget( label: 'Pinned', count: _allNotes - .where((note) => note['isPinned'] == true) + .where((note) => note.isPinned == true) .length, isSelected: _selectedFilter == 'Pinned', onTap: () => _onFilterSelected('Pinned'), @@ -341,7 +289,7 @@ class _NotesDashboardState extends State FilterChipWidget( label: 'Reminders', count: _allNotes - .where((note) => note['hasReminder'] == true) + .where((note) => note.hasReminder == true) .length, isSelected: _selectedFilter == 'Reminders', onTap: () => _onFilterSelected('Reminders'), @@ -393,11 +341,13 @@ class _NotesDashboardState extends State return RefreshIndicator( onRefresh: () async { - // Simulate cloud sync - await Future.delayed(const Duration(seconds: 1)); - setState(() { - // Refresh data - }); + // Trigger cloud sync if connected + if (_syncManager.isConnected) { + await _syncManager.syncNow(); + } else { + // Just reload local data + _loadInitialNotes(); + } }, child: _isGridView ? _buildGridView() : _buildListView(), ); @@ -410,19 +360,23 @@ class _NotesDashboardState extends State itemBuilder: (context, index) { final note = _filteredNotes[index]; return NoteCardWidget( - note: note, + note: _noteToMap(note), // Convert Note to Map for compatibility onTap: () { - Navigator.pushNamed(context, '/note-creation-editor'); + Navigator.pushNamed( + context, + '/note-creation-editor', + arguments: {'noteId': note.id}, + ); }, - onPin: () => _onNoteAction(note['id'], 'pin'), + onPin: () => _onNoteAction(note.id, 'pin'), onShare: () { // Implement share functionality }, onMove: () { Navigator.pushNamed(context, '/folder-organization'); }, - onDelete: () => _onNoteAction(note['id'], 'delete'), - onDuplicate: () => _onNoteAction(note['id'], 'duplicate'), + onDelete: () => _onNoteAction(note.id, 'delete'), + onDuplicate: () => _onNoteAction(note.id, 'duplicate'), onExport: () { // Implement export functionality }, @@ -447,19 +401,23 @@ class _NotesDashboardState extends State itemBuilder: (context, index) { final note = _filteredNotes[index]; return NoteCardWidget( - note: note, + note: _noteToMap(note), // Convert Note to Map for compatibility onTap: () { - Navigator.pushNamed(context, '/note-creation-editor'); + Navigator.pushNamed( + context, + '/note-creation-editor', + arguments: {'noteId': note.id}, + ); }, - onPin: () => _onNoteAction(note['id'], 'pin'), + onPin: () => _onNoteAction(note.id, 'pin'), onShare: () { // Implement share functionality }, onMove: () { Navigator.pushNamed(context, '/folder-organization'); }, - onDelete: () => _onNoteAction(note['id'], 'delete'), - onDuplicate: () => _onNoteAction(note['id'], 'duplicate'), + onDelete: () => _onNoteAction(note.id, 'delete'), + onDuplicate: () => _onNoteAction(note.id, 'duplicate'), onExport: () { // Implement export functionality }, @@ -471,6 +429,21 @@ class _NotesDashboardState extends State ); } + // Helper method to convert Note object to Map for existing NoteCardWidget + Map _noteToMap(Note note) { + return { + 'id': note.id, + 'title': note.title, + 'content': note.content, + 'preview': note.preview, + 'type': note.noteType ?? 'text', + 'folder': note.folderId, + 'createdAt': note.createdAt.toIso8601String(), + 'isPinned': note.isPinned, + 'hasReminder': note.hasReminder, + }; + } + Widget _buildFoldersTab() { return Center( child: Column( @@ -549,7 +522,7 @@ class _NotesDashboardState extends State colors: [ isDark ? AppTheme.warningDark : AppTheme.warningLight, (isDark ? AppTheme.warningDark : AppTheme.warningLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(12), @@ -573,7 +546,7 @@ class _NotesDashboardState extends State Text( 'Unlock unlimited features', style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white.withValues(alpha: 0.9), + color: Colors.white.withOpacity(0.9), ), ), SizedBox(height: 2.h), @@ -618,9 +591,11 @@ class _NotesDashboardState extends State // Settings options _buildSettingsTile( icon: 'cloud_sync', - title: 'Cloud Sync', + title: 'Cloud Storage', subtitle: 'Sync across devices', - onTap: () {}, + onTap: () { + Navigator.pushNamed(context, AppRoutes.cloudConnections); + }, ), _buildSettingsTile( icon: 'dark_mode', diff --git a/lib/presentation/notes_dashboard/widgets/empty_state_widget.dart b/lib/presentation/notes_dashboard/widgets/empty_state_widget.dart index 5f2c428..39bd5c2 100644 --- a/lib/presentation/notes_dashboard/widgets/empty_state_widget.dart +++ b/lib/presentation/notes_dashboard/widgets/empty_state_widget.dart @@ -27,7 +27,7 @@ class EmptyStateWidget extends StatelessWidget { height: 30.h, decoration: BoxDecoration( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: Column( @@ -43,7 +43,7 @@ class EmptyStateWidget extends StatelessWidget { CustomIconWidget( iconName: 'edit', color: (isDark ? AppTheme.accentDark : AppTheme.accentLight) - .withValues(alpha: 0.6), + .withOpacity(0.6), size: 24, ), ], @@ -100,11 +100,11 @@ class EmptyStateWidget extends StatelessWidget { padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: (isDark ? AppTheme.accentDark : AppTheme.accentLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: (isDark ? AppTheme.accentDark : AppTheme.accentLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), ), ), child: Column( diff --git a/lib/presentation/notes_dashboard/widgets/filter_chip_widget.dart b/lib/presentation/notes_dashboard/widgets/filter_chip_widget.dart index 0fef779..0c4d47c 100644 --- a/lib/presentation/notes_dashboard/widgets/filter_chip_widget.dart +++ b/lib/presentation/notes_dashboard/widgets/filter_chip_widget.dart @@ -60,9 +60,9 @@ class FilterChipWidget extends StatelessWidget { EdgeInsets.symmetric(horizontal: 1.5.w, vertical: 0.2.h), decoration: BoxDecoration( color: isSelected - ? Colors.white.withValues(alpha: 0.2) + ? Colors.white.withOpacity(0.2) : (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Text( diff --git a/lib/presentation/notes_dashboard/widgets/note_card_widget.dart b/lib/presentation/notes_dashboard/widgets/note_card_widget.dart index 5080942..aaa67c8 100644 --- a/lib/presentation/notes_dashboard/widgets/note_card_widget.dart +++ b/lib/presentation/notes_dashboard/widgets/note_card_widget.dart @@ -129,7 +129,7 @@ class NoteCardWidget extends StatelessWidget { color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -190,9 +190,9 @@ class NoteCardWidget extends StatelessWidget { decoration: BoxDecoration( color: isLeft ? (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.2) + .withOpacity(0.2) : (isDark ? AppTheme.errorDark : AppTheme.errorLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Align( diff --git a/lib/presentation/notes_dashboard/widgets/note_type_selector_widget.dart b/lib/presentation/notes_dashboard/widgets/note_type_selector_widget.dart index 89c0bd4..5585e48 100644 --- a/lib/presentation/notes_dashboard/widgets/note_type_selector_widget.dart +++ b/lib/presentation/notes_dashboard/widgets/note_type_selector_widget.dart @@ -103,7 +103,7 @@ class NoteTypeSelectorWidget extends StatelessWidget { padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.5), + .withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: isDark ? AppTheme.dividerDark : AppTheme.dividerLight, @@ -115,7 +115,7 @@ class NoteTypeSelectorWidget extends StatelessWidget { padding: EdgeInsets.all(3.w), decoration: BoxDecoration( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: CustomIconWidget( diff --git a/lib/presentation/onboarding_flow/widgets/onboarding_page_widget.dart b/lib/presentation/onboarding_flow/widgets/onboarding_page_widget.dart index 4cbf10e..9bef0a9 100644 --- a/lib/presentation/onboarding_flow/widgets/onboarding_page_widget.dart +++ b/lib/presentation/onboarding_flow/widgets/onboarding_page_widget.dart @@ -83,7 +83,7 @@ class OnboardingPageWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .outline - .withValues(alpha: 0.2), + .withOpacity(0.2), ), ), child: Column( @@ -152,13 +152,13 @@ class OnboardingPageWidget extends StatelessWidget { padding: EdgeInsets.all(3.w), decoration: BoxDecoration( color: isPremium - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1) + ? Theme.of(context).colorScheme.primary.withOpacity(0.1) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: isPremium ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + : Theme.of(context).colorScheme.outline.withOpacity(0.3), ), ), child: Column( diff --git a/lib/presentation/onboarding_flow/widgets/page_indicator_widget.dart b/lib/presentation/onboarding_flow/widgets/page_indicator_widget.dart index e5d4762..6308cba 100644 --- a/lib/presentation/onboarding_flow/widgets/page_indicator_widget.dart +++ b/lib/presentation/onboarding_flow/widgets/page_indicator_widget.dart @@ -25,7 +25,7 @@ class PageIndicatorWidget extends StatelessWidget { decoration: BoxDecoration( color: currentPage == index ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + : Theme.of(context).colorScheme.outline.withOpacity(0.3), borderRadius: BorderRadius.circular(4), ), ), diff --git a/lib/presentation/premium_upgrade/premium_upgrade.dart b/lib/presentation/premium_upgrade/premium_upgrade.dart index 4145642..fddb934 100644 --- a/lib/presentation/premium_upgrade/premium_upgrade.dart +++ b/lib/presentation/premium_upgrade/premium_upgrade.dart @@ -133,7 +133,7 @@ class _PremiumUpgradeState extends State borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), @@ -313,13 +313,13 @@ class _PremiumUpgradeState extends State colors: isDark ? [ AppTheme.backgroundDark, - AppTheme.surfaceDark.withValues(alpha: 0.8), - AppTheme.primaryDark.withValues(alpha: 0.1), + AppTheme.surfaceDark.withOpacity(0.8), + AppTheme.primaryDark.withOpacity(0.1), ] : [ AppTheme.backgroundLight, - AppTheme.surfaceLight.withValues(alpha: 0.8), - AppTheme.primaryLight.withValues(alpha: 0.1), + AppTheme.surfaceLight.withOpacity(0.8), + AppTheme.primaryLight.withOpacity(0.1), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -385,13 +385,13 @@ class _PremiumUpgradeState extends State color: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), borderRadius: BorderRadius.circular(16), border: Border.all( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), width: 1, ), ), diff --git a/lib/presentation/premium_upgrade/widgets/feature_card_widget.dart b/lib/presentation/premium_upgrade/widgets/feature_card_widget.dart index 0991786..9e0470c 100644 --- a/lib/presentation/premium_upgrade/widgets/feature_card_widget.dart +++ b/lib/presentation/premium_upgrade/widgets/feature_card_widget.dart @@ -71,19 +71,19 @@ class _FeatureCardWidgetState extends State gradient: LinearGradient( colors: gradientColors .map((color) => - color.withValues(alpha: _isHovered ? 0.8 : 0.6)) + color.withOpacity(_isHovered ? 0.8 : 0.6)) .toList(), begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), border: Border.all( - color: gradientColors.first.withValues(alpha: 0.3), + color: gradientColors.first.withOpacity(0.3), width: 1, ), boxShadow: [ BoxShadow( - color: gradientColors.first.withValues(alpha: 0.2), + color: gradientColors.first.withOpacity(0.2), blurRadius: _isHovered ? 20 : 10, offset: const Offset(0, 8), ), @@ -107,7 +107,7 @@ class _FeatureCardWidgetState extends State gradient: LinearGradient( colors: [ Colors.transparent, - Colors.white.withValues(alpha: 0.1), + Colors.white.withOpacity(0.1), Colors.transparent, ], stops: const [0.0, 0.5, 1.0], @@ -129,10 +129,10 @@ class _FeatureCardWidgetState extends State Container( padding: EdgeInsets.all(3.w), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( - color: Colors.white.withValues(alpha: 0.3), + color: Colors.white.withOpacity(0.3), width: 1, ), ), @@ -150,10 +150,10 @@ class _FeatureCardWidgetState extends State vertical: 0.5.h, ), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), border: Border.all( - color: Colors.white.withValues(alpha: 0.3), + color: Colors.white.withOpacity(0.3), width: 1, ), ), @@ -193,7 +193,7 @@ class _FeatureCardWidgetState extends State Text( widget.feature['description'], style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white.withValues(alpha: 0.9), + color: Colors.white.withOpacity(0.9), ), ), SizedBox(height: 2.h), @@ -210,7 +210,7 @@ class _FeatureCardWidgetState extends State .bodySmall ?.copyWith( color: - Colors.white.withValues(alpha: 0.7), + Colors.white.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), @@ -230,7 +230,7 @@ class _FeatureCardWidgetState extends State Container( height: 4.h, width: 1, - color: Colors.white.withValues(alpha: 0.3), + color: Colors.white.withOpacity(0.3), ), SizedBox(width: 4.w), Expanded( @@ -246,7 +246,7 @@ class _FeatureCardWidgetState extends State .bodySmall ?.copyWith( color: Colors.white - .withValues(alpha: 0.7), + .withOpacity(0.7), fontWeight: FontWeight.w500, ), ), diff --git a/lib/presentation/premium_upgrade/widgets/premium_header_widget.dart b/lib/presentation/premium_upgrade/widgets/premium_header_widget.dart index f0fb077..1938f56 100644 --- a/lib/presentation/premium_upgrade/widgets/premium_header_widget.dart +++ b/lib/presentation/premium_upgrade/widgets/premium_header_widget.dart @@ -19,7 +19,7 @@ class PremiumHeaderWidget extends StatelessWidget { padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.9), + .withOpacity(0.9), boxShadow: [ BoxShadow( color: isDark ? AppTheme.shadowDark : AppTheme.shadowLight, @@ -47,7 +47,7 @@ class PremiumHeaderWidget extends StatelessWidget { (isDark ? AppTheme.warningDark : AppTheme.warningLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(8), @@ -86,7 +86,7 @@ class PremiumHeaderWidget extends StatelessWidget { (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), ], ), borderRadius: BorderRadius.circular(2), @@ -114,7 +114,7 @@ class PremiumHeaderWidget extends StatelessWidget { color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( diff --git a/lib/presentation/premium_upgrade/widgets/pricing_option_widget.dart b/lib/presentation/premium_upgrade/widgets/pricing_option_widget.dart index 5f26018..dd24361 100644 --- a/lib/presentation/premium_upgrade/widgets/pricing_option_widget.dart +++ b/lib/presentation/premium_upgrade/widgets/pricing_option_widget.dart @@ -73,7 +73,7 @@ class _PricingOptionWidgetState extends State colors: [ isDark ? AppTheme.primaryDark : AppTheme.primaryLight, (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -82,7 +82,7 @@ class _PricingOptionWidgetState extends State color: widget.isSelected ? null : (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.5), + .withOpacity(0.5), borderRadius: BorderRadius.circular(16), border: Border.all( color: widget.isSelected @@ -95,7 +95,7 @@ class _PricingOptionWidgetState extends State BoxShadow( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 6), ), @@ -109,7 +109,7 @@ class _PricingOptionWidgetState extends State padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 0.5.h), decoration: BoxDecoration( color: (isDark ? AppTheme.successDark : AppTheme.successLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: @@ -159,7 +159,7 @@ class _PricingOptionWidgetState extends State text: ' ${widget.period}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: widget.isSelected - ? Colors.white.withValues(alpha: 0.8) + ? Colors.white.withOpacity(0.8) : (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight), @@ -207,7 +207,7 @@ class _PricingOptionWidgetState extends State colors: [ isDark ? AppTheme.warningDark : AppTheme.warningLight, (isDark ? AppTheme.warningDark : AppTheme.warningLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(12), @@ -216,7 +216,7 @@ class _PricingOptionWidgetState extends State color: (isDark ? AppTheme.warningDark : AppTheme.warningLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), diff --git a/lib/presentation/premium_upgrade/widgets/purchase_button_widget.dart b/lib/presentation/premium_upgrade/widgets/purchase_button_widget.dart index 835039b..d260761 100644 --- a/lib/presentation/premium_upgrade/widgets/purchase_button_widget.dart +++ b/lib/presentation/premium_upgrade/widgets/purchase_button_widget.dart @@ -66,7 +66,7 @@ class _PurchaseButtonWidgetState extends State colors: [ isDark ? AppTheme.primaryDark : AppTheme.primaryLight, (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), isDark ? AppTheme.accentDark : AppTheme.accentLight, ], begin: Alignment.topLeft, @@ -76,7 +76,7 @@ class _PurchaseButtonWidgetState extends State boxShadow: [ BoxShadow( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.4), + .withOpacity(0.4), blurRadius: 15, offset: const Offset(0, 8), ), @@ -101,7 +101,7 @@ class _PurchaseButtonWidgetState extends State gradient: LinearGradient( colors: [ Colors.transparent, - Colors.white.withValues(alpha: 0.2), + Colors.white.withOpacity(0.2), Colors.transparent, ], stops: const [0.0, 0.5, 1.0], @@ -211,8 +211,8 @@ class _PurchaseButtonWidgetState extends State 'Terms of Service • Privacy Policy', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: isDark - ? AppTheme.textSecondaryDark.withValues(alpha: 0.7) - : AppTheme.textSecondaryLight.withValues(alpha: 0.7), + ? AppTheme.textSecondaryDark.withOpacity(0.7) + : AppTheme.textSecondaryLight.withOpacity(0.7), ), textAlign: TextAlign.center, ), diff --git a/lib/presentation/search_discovery/search_discovery.dart b/lib/presentation/search_discovery/search_discovery.dart index ede0ca0..6281e94 100644 --- a/lib/presentation/search_discovery/search_discovery.dart +++ b/lib/presentation/search_discovery/search_discovery.dart @@ -544,7 +544,7 @@ class _SearchDiscoveryState extends State color: Theme.of(context) .colorScheme .primary - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: CustomIconWidget( diff --git a/lib/presentation/search_discovery/widgets/recent_searches_widget.dart b/lib/presentation/search_discovery/widgets/recent_searches_widget.dart index 1e92379..c88e922 100644 --- a/lib/presentation/search_discovery/widgets/recent_searches_widget.dart +++ b/lib/presentation/search_discovery/widgets/recent_searches_widget.dart @@ -64,7 +64,7 @@ class RecentSearchesWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .outline - .withValues(alpha: 0.3), + .withOpacity(0.3), ), ), child: Row( diff --git a/lib/presentation/search_discovery/widgets/search_bar_widget.dart b/lib/presentation/search_discovery/widgets/search_bar_widget.dart index d427ec5..ada2c60 100644 --- a/lib/presentation/search_discovery/widgets/search_bar_widget.dart +++ b/lib/presentation/search_discovery/widgets/search_bar_widget.dart @@ -75,7 +75,7 @@ class _SearchBarWidgetState extends State { color: Theme.of(context) .colorScheme .onSurfaceVariant - .withValues(alpha: 0.6), + .withOpacity(0.6), ), border: InputBorder.none, contentPadding: @@ -93,7 +93,7 @@ class _SearchBarWidgetState extends State { color: Theme.of(context) .colorScheme .primary - .withValues(alpha: 0.1), + .withOpacity(0.1), shape: BoxShape.circle, ), child: SizedBox( diff --git a/lib/presentation/search_discovery/widgets/search_filters_widget.dart b/lib/presentation/search_discovery/widgets/search_filters_widget.dart index a587fa6..23e5a66 100644 --- a/lib/presentation/search_discovery/widgets/search_filters_widget.dart +++ b/lib/presentation/search_discovery/widgets/search_filters_widget.dart @@ -158,7 +158,7 @@ class _SearchFiltersWidgetState extends State { ? Theme.of(context) .colorScheme .primary - .withValues(alpha: 0.1) + .withOpacity(0.1) : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), border: Border.all( @@ -167,7 +167,7 @@ class _SearchFiltersWidgetState extends State { : Theme.of(context) .colorScheme .outline - .withValues(alpha: 0.3), + .withOpacity(0.3), ), ), child: Row( @@ -220,12 +220,12 @@ class _SearchFiltersWidgetState extends State { decoration: BoxDecoration( color: AppTheme.getWarningColor( Theme.of(context).brightness == Brightness.light) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.getWarningColor( Theme.of(context).brightness == Brightness.light) - .withValues(alpha: 0.3), + .withOpacity(0.3), ), ), child: Row( @@ -304,13 +304,13 @@ class _SearchFiltersWidgetState extends State { padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), decoration: BoxDecoration( color: isSelected - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1) + ? Theme.of(context).colorScheme.primary.withOpacity(0.1) : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + : Theme.of(context).colorScheme.outline.withOpacity(0.3), ), ), child: Row( diff --git a/lib/presentation/search_discovery/widgets/search_results_widget.dart b/lib/presentation/search_discovery/widgets/search_results_widget.dart index a263800..c0af87e 100644 --- a/lib/presentation/search_discovery/widgets/search_results_widget.dart +++ b/lib/presentation/search_discovery/widgets/search_results_widget.dart @@ -82,7 +82,7 @@ class SearchResultsWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .onSurfaceVariant - .withValues(alpha: 0.5), + .withOpacity(0.5), size: 15.w, ), SizedBox(height: 3.h), @@ -113,7 +113,7 @@ class SearchResultsWidget extends StatelessWidget { padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: - Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -193,7 +193,7 @@ class SearchResultsWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .primary - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -236,7 +236,7 @@ class SearchResultsWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .surfaceContainerHighest - .withValues(alpha: 0.5), + .withOpacity(0.5), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -296,7 +296,7 @@ class SearchResultsWidget extends StatelessWidget { return Container( padding: EdgeInsets.all(2.w), decoration: BoxDecoration( - color: iconColor.withValues(alpha: 0.1), + color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( @@ -320,7 +320,7 @@ class SearchResultsWidget extends StatelessWidget { : Theme.of(context) .colorScheme .onSurfaceVariant - .withValues(alpha: 0.3), + .withOpacity(0.3), size: 3.w, ); }), diff --git a/lib/presentation/search_discovery/widgets/voice_search_widget.dart b/lib/presentation/search_discovery/widgets/voice_search_widget.dart index a106143..0393d85 100644 --- a/lib/presentation/search_discovery/widgets/voice_search_widget.dart +++ b/lib/presentation/search_discovery/widgets/voice_search_widget.dart @@ -199,13 +199,13 @@ class _VoiceSearchWidgetState extends State color: Theme.of(context) .colorScheme .surfaceContainerHighest - .withValues(alpha: 0.5), + .withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context) .colorScheme .outline - .withValues(alpha: 0.3), + .withOpacity(0.3), ), ), child: Column( @@ -262,7 +262,7 @@ class _VoiceSearchWidgetState extends State color: Theme.of(context) .colorScheme .surfaceContainerHighest - .withValues(alpha: 0.3), + .withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -301,7 +301,7 @@ class _VoiceSearchWidgetState extends State color: Theme.of(context) .colorScheme .primary - .withValues(alpha: opacity * (1 - _waveAnimation.value)), + .withOpacity(opacity * (1 - _waveAnimation.value)), width: 2, ), ), diff --git a/lib/presentation/settings_profile/cloud_connections.dart b/lib/presentation/settings_profile/cloud_connections.dart new file mode 100644 index 0000000..96d9fa5 --- /dev/null +++ b/lib/presentation/settings_profile/cloud_connections.dart @@ -0,0 +1,537 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; +import '../../core/app_export.dart'; +import '../../services/sync/sync_manager.dart'; +import '../../services/sync/cloud_sync_service.dart'; + +class CloudConnectionsScreen extends StatefulWidget { + const CloudConnectionsScreen({Key? key}) : super(key: key); + + @override + State createState() => _CloudConnectionsScreenState(); +} + +class _CloudConnectionsScreenState extends State { + final SyncManager _syncManager = SyncManager(); + SyncStatus? _currentStatus; + SyncProgress? _currentProgress; + + @override + void initState() { + super.initState(); + _listenToSyncStatus(); + } + + void _listenToSyncStatus() { + _syncManager.syncStatusStream.listen((status) { + if (mounted) { + setState(() { + _currentStatus = status; + }); + } + }); + + _syncManager.syncProgressStream.listen((progress) { + if (mounted) { + setState(() { + _currentProgress = progress; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDark ? AppTheme.backgroundDark : AppTheme.backgroundLight, + appBar: AppBar( + backgroundColor: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + elevation: 0, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: CustomIconWidget( + iconName: 'arrow_back', + size: 6.w, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + title: Text( + 'Cloud Storage', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCurrentStatusCard(), + SizedBox(height: 3.h), + _buildProvidersSection(), + SizedBox(height: 3.h), + _buildSyncOptionsSection(), + SizedBox(height: 3.h), + _buildStorageInfoSection(), + ], + ), + ), + ); + } + + Widget _buildCurrentStatusCard() { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + final bool isConnected = _currentStatus?.isConnected ?? false; + final bool isSyncing = _currentStatus?.isSyncing ?? false; + + Color statusColor; + IconData statusIcon; + String statusText; + + if (isSyncing) { + statusColor = isDark ? AppTheme.primaryDark : AppTheme.primaryLight; + statusIcon = Icons.sync; + statusText = 'Syncing...'; + } else if (isConnected) { + statusColor = isDark ? AppTheme.successDark : AppTheme.successLight; + statusIcon = Icons.cloud_done; + statusText = _currentStatus?.message ?? 'Connected'; + } else { + statusColor = isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight; + statusIcon = Icons.cloud_off; + statusText = 'Not connected'; + } + + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: statusColor.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + children: [ + Icon( + statusIcon, + size: 12.w, + color: statusColor, + ), + SizedBox(height: 2.h), + Text( + statusText, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + if (_currentStatus?.timestamp != null) ...[ + SizedBox(height: 1.h), + Text( + 'Last synced: ${_formatDateTime(_currentStatus!.timestamp!)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight, + ), + ), + ], + if (isSyncing && _currentProgress != null) ...[ + SizedBox(height: 2.h), + LinearProgressIndicator( + value: _currentProgress!.progress, + backgroundColor: statusColor.withOpacity(0.3), + valueColor: AlwaysStoppedAnimation(statusColor), + ), + SizedBox(height: 1.h), + Text( + _currentProgress!.message, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + if (isConnected && !isSyncing) ...[ + SizedBox(height: 2.h), + ElevatedButton.icon( + onPressed: _syncNow, + icon: CustomIconWidget( + iconName: 'sync', + size: 5.w, + color: Colors.white, + ), + label: const Text('Sync Now'), + style: ElevatedButton.styleFrom( + backgroundColor: isDark ? AppTheme.primaryDark : AppTheme.primaryLight, + foregroundColor: Colors.white, + ), + ), + ], + ], + ), + ); + } + + Widget _buildProvidersSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cloud Providers', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + ...(_syncManager.availableProviders.map((provider) => _buildProviderCard(provider))), + ], + ); + } + + Widget _buildProviderCard(CloudSyncService provider) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + final bool isActive = _syncManager.activeProvider == provider; + final bool isConnected = isActive && provider.isSignedIn; + + String iconName; + switch (provider.providerName) { + case 'Google Drive': + iconName = 'google_drive'; + break; + case 'OneDrive': + iconName = 'onedrive'; + break; + default: + iconName = 'cloud'; + } + + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isActive + ? (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) + : (isDark ? AppTheme.dividerDark : AppTheme.dividerLight), + width: isActive ? 2 : 1, + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: CustomIconWidget( + iconName: iconName, + size: 8.w, + color: isDark ? AppTheme.primaryDark : AppTheme.primaryLight, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + provider.providerName, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 0.5.h), + Text( + _getProviderStatusText(provider, isConnected), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: _getProviderStatusColor(provider, isConnected, isDark), + ), + ), + ], + ), + ), + _buildProviderAction(provider, isConnected), + ], + ), + ); + } + + Widget _buildProviderAction(CloudSyncService provider, bool isConnected) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + + if (!provider.isConfigured) { + return Text( + 'Not configured', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight, + ), + ); + } + + if (isConnected) { + return PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'disconnect': + _disconnectProvider(); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'disconnect', + child: Row( + children: [ + CustomIconWidget( + iconName: 'logout', + size: 5.w, + color: Theme.of(context).colorScheme.error, + ), + SizedBox(width: 3.w), + Text( + 'Disconnect', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + ), + ), + ], + child: Container( + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: (isDark ? AppTheme.successDark : AppTheme.successLight).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomIconWidget( + iconName: 'check_circle', + size: 4.w, + color: isDark ? AppTheme.successDark : AppTheme.successLight, + ), + SizedBox(width: 1.w), + Text( + 'Connected', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isDark ? AppTheme.successDark : AppTheme.successLight, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + return OutlinedButton( + onPressed: () => _connectProvider(provider), + style: OutlinedButton.styleFrom( + foregroundColor: isDark ? AppTheme.primaryDark : AppTheme.primaryLight, + side: BorderSide( + color: isDark ? AppTheme.primaryDark : AppTheme.primaryLight, + ), + ), + child: const Text('Connect'), + ); + } + + Widget _buildSyncOptionsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sync Options', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + _buildSyncOptionTile( + icon: 'sync', + title: 'Auto Sync', + subtitle: 'Automatically sync changes every 30 minutes', + value: true, // TODO: Make this configurable + onChanged: (value) { + // TODO: Implement auto sync toggle + }, + ), + _buildSyncOptionTile( + icon: 'wifi', + title: 'Sync on WiFi Only', + subtitle: 'Only sync when connected to WiFi', + value: false, // TODO: Make this configurable + onChanged: (value) { + // TODO: Implement WiFi-only sync toggle + }, + ), + ], + ); + } + + Widget _buildSyncOptionTile({ + required String icon, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + }) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + margin: EdgeInsets.only(bottom: 1.h), + child: ListTile( + leading: CustomIconWidget( + iconName: icon, + size: 6.w, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(title), + subtitle: Text(subtitle), + trailing: Switch( + value: value, + onChanged: onChanged, + activeColor: isDark ? AppTheme.primaryDark : AppTheme.primaryLight, + ), + contentPadding: EdgeInsets.zero, + ), + ); + } + + Widget _buildStorageInfoSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Storage Information', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + _buildInfoTile( + icon: 'note', + title: 'Local Notes', + value: '${_syncManager.isConnected ? 42 : 0} notes', // TODO: Get actual count + ), + _buildInfoTile( + icon: 'image', + title: 'Media Files', + value: '${_syncManager.isConnected ? 15 : 0} files', // TODO: Get actual count + ), + _buildInfoTile( + icon: 'storage', + title: 'Storage Used', + value: '${_syncManager.isConnected ? 2.3 : 0} MB', // TODO: Calculate actual usage + ), + ], + ); + } + + Widget _buildInfoTile({ + required String icon, + required String title, + required String value, + }) { + return Container( + margin: EdgeInsets.only(bottom: 1.h), + child: ListTile( + leading: CustomIconWidget( + iconName: icon, + size: 6.w, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(title), + trailing: Text( + value, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + contentPadding: EdgeInsets.zero, + ), + ); + } + + String _getProviderStatusText(CloudSyncService provider, bool isConnected) { + if (!provider.isConfigured) { + return 'OAuth credentials not configured'; + } + if (isConnected) { + final user = provider.currentUser; + return user?.email ?? 'Connected'; + } + return 'Not connected'; + } + + Color _getProviderStatusColor(CloudSyncService provider, bool isConnected, bool isDark) { + if (!provider.isConfigured) { + return isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight; + } + if (isConnected) { + return isDark ? AppTheme.successDark : AppTheme.successLight; + } + return isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight; + } + + String _formatDateTime(DateTime dateTime) { + final now = DateTime.now(); + final difference = now.difference(dateTime); + + if (difference.inMinutes < 1) { + return 'Just now'; + } else if (difference.inHours < 1) { + return '${difference.inMinutes} min ago'; + } else if (difference.inDays < 1) { + return '${difference.inHours} hr ago'; + } else { + return '${difference.inDays} days ago'; + } + } + + Future _connectProvider(CloudSyncService provider) async { + final providerId = _getProviderId(provider); + if (providerId != null) { + final success = await _syncManager.connectProvider(providerId); + if (!success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to connect to ${provider.providerName}'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + } + + Future _disconnectProvider() async { + await _syncManager.disconnectProvider(); + } + + Future _syncNow() async { + final success = await _syncManager.syncNow(); + if (!success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Sync failed. Please try again.'), + ), + ); + } + } + + String? _getProviderId(CloudSyncService provider) { + switch (provider.providerName) { + case 'Google Drive': + return 'google_drive'; + case 'OneDrive': + return 'onedrive'; + default: + return null; + } + } +} \ No newline at end of file diff --git a/lib/presentation/settings_profile/settings_profile.dart b/lib/presentation/settings_profile/settings_profile.dart index c9fe1c7..944e61f 100644 --- a/lib/presentation/settings_profile/settings_profile.dart +++ b/lib/presentation/settings_profile/settings_profile.dart @@ -354,13 +354,13 @@ class _SettingsProfileState extends State colors: isDark ? [ AppTheme.backgroundDark, - AppTheme.surfaceDark.withValues(alpha: 0.7), - AppTheme.accentDark.withValues(alpha: 0.05), + AppTheme.surfaceDark.withOpacity(0.7), + AppTheme.accentDark.withOpacity(0.05), ] : [ AppTheme.backgroundLight, - AppTheme.surfaceLight.withValues(alpha: 0.7), - AppTheme.accentLight.withValues(alpha: 0.05), + AppTheme.surfaceLight.withOpacity(0.7), + AppTheme.accentLight.withOpacity(0.05), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -390,7 +390,7 @@ class _SettingsProfileState extends State color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( @@ -569,7 +569,7 @@ class _SettingsProfileState extends State ? Theme.of(context) .colorScheme .primary - .withValues(alpha: 0.1) + .withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( @@ -804,7 +804,7 @@ class _SettingsProfileState extends State color: (_userProfile['isPremium'] ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary) - .withValues(alpha: 0.1), + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: CustomIconWidget( diff --git a/lib/presentation/settings_profile/widgets/biometric_dialog_widget.dart b/lib/presentation/settings_profile/widgets/biometric_dialog_widget.dart index 2a403b5..d88b456 100644 --- a/lib/presentation/settings_profile/widgets/biometric_dialog_widget.dart +++ b/lib/presentation/settings_profile/widgets/biometric_dialog_widget.dart @@ -97,7 +97,7 @@ class _BiometricDialogWidgetState extends State borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), @@ -136,7 +136,7 @@ class _BiometricDialogWidgetState extends State (isDark ? AppTheme.errorDark : AppTheme.errorLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ] : [ isDark @@ -145,7 +145,7 @@ class _BiometricDialogWidgetState extends State (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), shape: BoxShape.circle, @@ -158,7 +158,7 @@ class _BiometricDialogWidgetState extends State : (isDark ? AppTheme.primaryDark : AppTheme.primaryLight)) - .withValues(alpha: 0.3), + .withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 5), ), diff --git a/lib/presentation/settings_profile/widgets/profile_header_widget.dart b/lib/presentation/settings_profile/widgets/profile_header_widget.dart index 13ff28c..b861b75 100644 --- a/lib/presentation/settings_profile/widgets/profile_header_widget.dart +++ b/lib/presentation/settings_profile/widgets/profile_header_widget.dart @@ -58,12 +58,12 @@ class _ProfileHeaderWidgetState extends State gradient: LinearGradient( colors: isDark ? [ - AppTheme.surfaceDark.withValues(alpha: 0.8), - AppTheme.primaryDark.withValues(alpha: 0.1), + AppTheme.surfaceDark.withOpacity(0.8), + AppTheme.primaryDark.withOpacity(0.1), ] : [ - AppTheme.surfaceLight.withValues(alpha: 0.8), - AppTheme.primaryLight.withValues(alpha: 0.1), + AppTheme.surfaceLight.withOpacity(0.8), + AppTheme.primaryLight.withOpacity(0.1), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -71,7 +71,7 @@ class _ProfileHeaderWidgetState extends State borderRadius: BorderRadius.circular(16), border: Border.all( color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), width: 1, ), boxShadow: [ @@ -101,7 +101,7 @@ class _ProfileHeaderWidgetState extends State colors: [ Colors.transparent, (isDark ? Colors.white : Colors.black) - .withValues(alpha: 0.05), + .withOpacity(0.05), Colors.transparent, ], stops: const [0.0, 0.5, 1.0], @@ -142,7 +142,7 @@ class _ProfileHeaderWidgetState extends State color: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 4), ), @@ -270,7 +270,7 @@ class _ProfileHeaderWidgetState extends State (isDark ? AppTheme.warningDark : AppTheme.warningLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(12), @@ -325,13 +325,13 @@ class _ProfileHeaderWidgetState extends State decoration: BoxDecoration( color: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.5), + .withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: (isDark ? AppTheme.dividerDark : AppTheme.dividerLight) - .withValues(alpha: 0.5), + .withOpacity(0.5), ), ), child: Row( diff --git a/lib/presentation/settings_profile/widgets/settings_section_widget.dart b/lib/presentation/settings_profile/widgets/settings_section_widget.dart index 78e2058..0ceccd2 100644 --- a/lib/presentation/settings_profile/widgets/settings_section_widget.dart +++ b/lib/presentation/settings_profile/widgets/settings_section_widget.dart @@ -80,11 +80,11 @@ class _SettingsSectionWidgetState extends State return Container( decoration: BoxDecoration( color: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), borderRadius: BorderRadius.circular(16), border: Border.all( color: (isDark ? AppTheme.dividerDark : AppTheme.dividerLight) - .withValues(alpha: 0.5), + .withOpacity(0.5), width: 1, ), boxShadow: [ @@ -113,7 +113,7 @@ class _SettingsSectionWidgetState extends State (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.8), + .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(8), @@ -172,7 +172,7 @@ class _SettingsSectionWidgetState extends State top: BorderSide( color: (isDark ? AppTheme.dividerDark : AppTheme.dividerLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), width: 1, ), ), @@ -188,7 +188,7 @@ class _SettingsSectionWidgetState extends State color: (isDark ? AppTheme.dividerDark : AppTheme.dividerLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), width: 0.5, ), ) diff --git a/lib/presentation/splash_screen/widgets/animated_logo_widget.dart b/lib/presentation/splash_screen/widgets/animated_logo_widget.dart index a368cff..2da482b 100644 --- a/lib/presentation/splash_screen/widgets/animated_logo_widget.dart +++ b/lib/presentation/splash_screen/widgets/animated_logo_widget.dart @@ -85,7 +85,7 @@ class _AnimatedLogoWidgetState extends State boxShadow: [ BoxShadow( color: (isDark ? AppTheme.shadowDark : AppTheme.shadowLight) - .withValues(alpha: 0.3), + .withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), diff --git a/lib/presentation/splash_screen/widgets/brand_gradient_widget.dart b/lib/presentation/splash_screen/widgets/brand_gradient_widget.dart index 63c4e61..f324441 100644 --- a/lib/presentation/splash_screen/widgets/brand_gradient_widget.dart +++ b/lib/presentation/splash_screen/widgets/brand_gradient_widget.dart @@ -25,13 +25,13 @@ class BrandGradientWidget extends StatelessWidget { colors: isDark ? [ AppTheme.backgroundDark, - AppTheme.surfaceDark.withValues(alpha: 0.8), - AppTheme.primaryDark.withValues(alpha: 0.1), + AppTheme.surfaceDark.withOpacity(0.8), + AppTheme.primaryDark.withOpacity(0.1), ] : [ AppTheme.backgroundLight, - AppTheme.primaryLight.withValues(alpha: 0.05), - AppTheme.primaryLight.withValues(alpha: 0.1), + AppTheme.primaryLight.withOpacity(0.05), + AppTheme.primaryLight.withOpacity(0.1), ], stops: const [0.0, 0.6, 1.0], ), diff --git a/lib/presentation/splash_screen/widgets/loading_indicator_widget.dart b/lib/presentation/splash_screen/widgets/loading_indicator_widget.dart index 6951745..06996b0 100644 --- a/lib/presentation/splash_screen/widgets/loading_indicator_widget.dart +++ b/lib/presentation/splash_screen/widgets/loading_indicator_widget.dart @@ -65,7 +65,7 @@ class _LoadingIndicatorWidgetState extends State ), backgroundColor: (isDark ? AppTheme.primaryDark : AppTheme.primaryLight) - .withValues(alpha: 0.2), + .withOpacity(0.2), ); }, ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index bc0cdb6..a200d76 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -7,6 +7,7 @@ import '../presentation/note_creation_editor/note_creation_editor.dart'; import '../presentation/search_discovery/search_discovery.dart'; import '../presentation/premium_upgrade/premium_upgrade.dart'; import '../presentation/settings_profile/settings_profile.dart'; +import '../presentation/settings_profile/cloud_connections.dart'; class AppRoutes { // TODO: Add your routes here @@ -19,6 +20,7 @@ class AppRoutes { static const String searchDiscovery = '/search-discovery'; static const String premiumUpgrade = '/premium-upgrade'; static const String settingsProfile = '/settings-profile'; + static const String cloudConnections = '/cloud-connections'; static Map routes = { initial: (context) => const SplashScreen(), @@ -30,6 +32,7 @@ class AppRoutes { searchDiscovery: (context) => const SearchDiscovery(), premiumUpgrade: (context) => const PremiumUpgrade(), settingsProfile: (context) => const SettingsProfile(), + cloudConnections: (context) => const CloudConnectionsScreen(), // TODO: Add your other routes here }; } diff --git a/lib/services/local/hive_initializer.dart b/lib/services/local/hive_initializer.dart new file mode 100644 index 0000000..34e932f --- /dev/null +++ b/lib/services/local/hive_initializer.dart @@ -0,0 +1,120 @@ +import 'dart:io'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +import '../../models/note.dart'; + +class HiveInitializer { + static const String _notesBoxName = 'notes'; + static const String _settingsBoxName = 'settings'; + + static bool _isInitialized = false; + + /// Initialize Hive with adapters and open required boxes + static Future init() async { + if (_isInitialized) return; + + try { + // Initialize Hive + await Hive.initFlutter(); + + // Register adapters + _registerAdapters(); + + // Open boxes + await _openBoxes(); + + _isInitialized = true; + print('✅ Hive initialized successfully'); + } catch (e) { + print('❌ Failed to initialize Hive: $e'); + rethrow; + } + } + + /// Register all Hive type adapters + static void _registerAdapters() { + // Register Note adapter (generated by hive_generator) + if (!Hive.isAdapterRegistered(0)) { + Hive.registerAdapter(NoteAdapter()); + } + + // TODO: Register other adapters as needed (Folder, etc.) + } + + /// Open all required Hive boxes + static Future _openBoxes() async { + await Future.wait([ + Hive.openBox(_notesBoxName), + Hive.openBox(_settingsBoxName), + ]); + } + + /// Get the notes box + static Box get notesBox { + if (!_isInitialized) { + throw Exception('Hive not initialized. Call HiveInitializer.init() first.'); + } + return Hive.box(_notesBoxName); + } + + /// Get the settings box + static Box get settingsBox { + if (!_isInitialized) { + throw Exception('Hive not initialized. Call HiveInitializer.init() first.'); + } + return Hive.box(_settingsBoxName); + } + + /// Close all boxes and clear data (for testing) + static Future clear() async { + if (!_isInitialized) return; + + await notesBox.clear(); + await settingsBox.clear(); + } + + /// Close all boxes (call on app termination) + static Future close() async { + if (!_isInitialized) return; + + await Hive.close(); + _isInitialized = false; + } + + /// Check if Hive is initialized + static bool get isInitialized => _isInitialized; + + /// Get app documents directory for storing files + static Future getAppDocumentsPath() async { + final directory = await getApplicationDocumentsDirectory(); + return directory.path; + } + + /// Get notes media directory for storing images and attachments + static Future getNotesMediaPath() async { + final documentsPath = await getAppDocumentsPath(); + final notesMediaPath = '$documentsPath/notes_media'; + + // Create directory if it doesn't exist + final directory = Directory(notesMediaPath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + return notesMediaPath; + } + + /// Get directory for a specific note's media + static Future getNoteMediaPath(String noteId) async { + final notesMediaPath = await getNotesMediaPath(); + final noteMediaPath = '$notesMediaPath/$noteId'; + + // Create directory if it doesn't exist + final directory = Directory(noteMediaPath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + return noteMediaPath; + } +} \ No newline at end of file diff --git a/lib/services/local/note_repository.dart b/lib/services/local/note_repository.dart new file mode 100644 index 0000000..8c24649 --- /dev/null +++ b/lib/services/local/note_repository.dart @@ -0,0 +1,272 @@ +import 'dart:async'; +import 'package:hive_flutter/hive_flutter.dart'; +import '../../models/note.dart'; +import 'hive_initializer.dart'; + +class NoteRepository { + static final NoteRepository _instance = NoteRepository._internal(); + factory NoteRepository() => _instance; + NoteRepository._internal(); + + Box get _box => HiveInitializer.notesBox; + + // Stream controller for notes changes + final _notesController = StreamController>.broadcast(); + + /// Stream of all non-deleted notes + Stream> get notesStream => _notesController.stream; + + /// Initialize the repository and emit initial data + void init() { + _emitNotes(); + + // Listen to box changes and emit updates + _box.watch().listen((_) { + _emitNotes(); + }); + } + + /// Emit current notes to stream + void _emitNotes() { + final notes = getAllNotes(); + _notesController.add(notes); + } + + /// Get all non-deleted notes sorted by updatedAt descending + List getAllNotes() { + return _box.values + .where((note) => note.deletedAt == null) + .toList() + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); + } + + /// Get note by ID + Note? getNoteById(String id) { + try { + return _box.values.firstWhere( + (note) => note.id == id && note.deletedAt == null, + ); + } catch (e) { + return null; + } + } + + /// Create a new note + Future createNote({ + String? title, + String? content, + String? folderId, + String? noteType, + List? tags, + }) async { + final now = DateTime.now(); + final note = Note( + id: _generateId(), + title: title ?? '', + content: content ?? '', + createdAt: now, + updatedAt: now, + folderId: folderId, + noteType: noteType ?? 'text', + tags: tags ?? [], + ); + + await _box.put(note.id, note); + return note; + } + + /// Update an existing note + Future updateNote(Note note) async { + note.touch(); // Update the updatedAt timestamp + await _box.put(note.id, note); + return note; + } + + /// Upsert a note (update if exists, create if doesn't) + Future upsertNote(Note note) async { + note.touch(); + await _box.put(note.id, note); + return note; + } + + /// Soft delete a note (set deletedAt) + Future deleteNote(String id) async { + final note = getNoteById(id); + if (note != null) { + note.deletedAt = DateTime.now(); + await _box.put(id, note); + } + } + + /// Permanently delete a note + Future permanentlyDeleteNote(String id) async { + await _box.delete(id); + } + + /// Restore a deleted note + Future restoreNote(String id) async { + final note = _box.get(id); + if (note != null) { + note.deletedAt = null; + await _box.put(id, note); + } + } + + /// Pin or unpin a note + Future togglePinNote(String id) async { + final note = getNoteById(id); + if (note != null) { + note.isPinned = !note.isPinned; + await updateNote(note); + } + } + + /// Duplicate a note + Future duplicateNote(String id) async { + final originalNote = getNoteById(id); + if (originalNote == null) { + throw Exception('Note not found'); + } + + final now = DateTime.now(); + final duplicatedNote = Note( + id: _generateId(), + title: '${originalNote.title} (Copy)', + content: originalNote.content, + images: List.from(originalNote.images), + attachments: List.from(originalNote.attachments), + createdAt: now, + updatedAt: now, + folderId: originalNote.folderId, + isPinned: false, // Don't pin duplicates by default + tags: List.from(originalNote.tags), + noteType: originalNote.noteType, + hasReminder: false, // Don't copy reminders + metadata: originalNote.metadata != null + ? Map.from(originalNote.metadata!) + : null, + ); + + await _box.put(duplicatedNote.id, duplicatedNote); + return duplicatedNote; + } + + /// Search notes by title or content + List searchNotes(String query) { + if (query.isEmpty) return getAllNotes(); + + final lowercaseQuery = query.toLowerCase(); + return getAllNotes().where((note) { + return note.title.toLowerCase().contains(lowercaseQuery) || + note.content.toLowerCase().contains(lowercaseQuery) || + note.tags.any((tag) => tag.toLowerCase().contains(lowercaseQuery)); + }).toList(); + } + + /// Filter notes by folder + List getNotesByFolder(String? folderId) { + return getAllNotes().where((note) => note.folderId == folderId).toList(); + } + + /// Filter notes by type + List getNotesByType(String noteType) { + return getAllNotes().where((note) => note.noteType == noteType).toList(); + } + + /// Get pinned notes + List getPinnedNotes() { + return getAllNotes().where((note) => note.isPinned).toList(); + } + + /// Get notes with reminders + List getNotesWithReminders() { + return getAllNotes().where((note) => note.hasReminder).toList(); + } + + /// Get notes by tag + List getNotesByTag(String tag) { + return getAllNotes().where((note) => note.tags.contains(tag)).toList(); + } + + /// Get all unique tags + List getAllTags() { + final allTags = {}; + for (final note in getAllNotes()) { + allTags.addAll(note.tags); + } + return allTags.toList()..sort(); + } + + /// Get all unique folders + List getAllFolders() { + final allFolders = {}; + for (final note in getAllNotes()) { + if (note.folderId != null) { + allFolders.add(note.folderId!); + } + } + return allFolders.toList()..sort(); + } + + /// Get deleted notes (for trash/recycle bin) + List getDeletedNotes() { + return _box.values + .where((note) => note.deletedAt != null) + .toList() + ..sort((a, b) => b.deletedAt!.compareTo(a.deletedAt!)); + } + + /// Get notes count + int getNotesCount() { + return getAllNotes().length; + } + + /// Get notes count by folder + Map getNotesCountByFolder() { + final counts = {}; + for (final note in getAllNotes()) { + counts[note.folderId] = (counts[note.folderId] ?? 0) + 1; + } + return counts; + } + + /// Clear all notes (for testing) + Future clearAllNotes() async { + await _box.clear(); + } + + /// Generate a unique ID for notes + String _generateId() { + return DateTime.now().millisecondsSinceEpoch.toString(); + } + + /// Dispose the repository + void dispose() { + _notesController.close(); + } + + /// Backup all notes to a map (for cloud sync) + Map exportAllNotes() { + final notes = _box.values.map((note) => note.toMap()).toList(); + return { + 'notes': notes, + 'exportedAt': DateTime.now().toIso8601String(), + 'version': '1.0', + }; + } + + /// Import notes from a map (for cloud sync) + Future importNotes(Map data, {bool replaceExisting = false}) async { + if (replaceExisting) { + await _box.clear(); + } + + final notesList = data['notes'] as List?; + if (notesList != null) { + for (final noteMap in notesList) { + final note = Note.fromMap(noteMap as Map); + await _box.put(note.id, note); + } + } + } +} \ No newline at end of file diff --git a/lib/services/premium/premium_service.dart b/lib/services/premium/premium_service.dart new file mode 100644 index 0000000..3eacc43 --- /dev/null +++ b/lib/services/premium/premium_service.dart @@ -0,0 +1,212 @@ +import 'dart:async'; +import '../local/hive_initializer.dart'; + +class PremiumService { + static final PremiumService _instance = PremiumService._internal(); + factory PremiumService() => _instance; + PremiumService._internal(); + + static const String _premiumKey = 'is_premium'; + static const String _premiumExpiryKey = 'premium_expiry'; + + // Stream controller for premium status changes + final _premiumController = StreamController.broadcast(); + + /// Stream of premium status changes + Stream get premiumStatusStream => _premiumController.stream; + + /// Check if user has premium access + bool get isPremium { + try { + final box = HiveInitializer.settingsBox; + final isPremium = box.get(_premiumKey, defaultValue: false) as bool; + + // Check if premium has expired (if there's an expiry date) + final expiryString = box.get(_premiumExpiryKey) as String?; + if (expiryString != null) { + final expiry = DateTime.parse(expiryString); + if (DateTime.now().isAfter(expiry)) { + // Premium has expired, revoke access + _setPremiumStatus(false); + return false; + } + } + + return isPremium; + } catch (e) { + // If there's any error, default to free tier + print('Error checking premium status: $e'); + return false; + } + } + + /// Set premium status (for development/testing) + Future _setPremiumStatus(bool isPremium, {DateTime? expiryDate}) async { + try { + final box = HiveInitializer.settingsBox; + await box.put(_premiumKey, isPremium); + + if (expiryDate != null) { + await box.put(_premiumExpiryKey, expiryDate.toIso8601String()); + } else { + await box.delete(_premiumExpiryKey); + } + + _premiumController.add(isPremium); + } catch (e) { + print('Error setting premium status: $e'); + } + } + + /// Grant premium access (for development/testing) + Future grantPremium({DateTime? expiryDate}) async { + await _setPremiumStatus(true, expiryDate: expiryDate); + } + + /// Revoke premium access + Future revokePremium() async { + await _setPremiumStatus(false); + } + + /// Grant premium for a specific duration (for testing) + Future grantPremiumForDuration(Duration duration) async { + final expiry = DateTime.now().add(duration); + await _setPremiumStatus(true, expiryDate: expiry); + } + + /// Get premium expiry date (if any) + DateTime? get premiumExpiryDate { + try { + final box = HiveInitializer.settingsBox; + final expiryString = box.get(_premiumExpiryKey) as String?; + return expiryString != null ? DateTime.parse(expiryString) : null; + } catch (e) { + return null; + } + } + + /// Check if a specific feature is available + bool isFeatureAvailable(PremiumFeature feature) { + switch (feature) { + case PremiumFeature.doodling: + case PremiumFeature.fileAttachments: + case PremiumFeature.cloudSync: + case PremiumFeature.advancedSearch: + return isPremium; + case PremiumFeature.basicNotes: + case PremiumFeature.textFormatting: + case PremiumFeature.imageInsertion: + return true; // Free features + } + } + + /// Get localized feature name + String getFeatureName(PremiumFeature feature) { + switch (feature) { + case PremiumFeature.doodling: + return 'Drawing & Doodling'; + case PremiumFeature.fileAttachments: + return 'File Attachments'; + case PremiumFeature.cloudSync: + return 'Cloud Synchronization'; + case PremiumFeature.advancedSearch: + return 'Advanced Search'; + case PremiumFeature.basicNotes: + return 'Basic Note Taking'; + case PremiumFeature.textFormatting: + return 'Text Formatting'; + case PremiumFeature.imageInsertion: + return 'Image Insertion'; + } + } + + /// Get upsell message for a feature + String getUpsellMessage(PremiumFeature feature) { + final featureName = getFeatureName(feature); + return 'Upgrade to Premium to unlock $featureName and more advanced features!'; + } + + /// Show premium upsell dialog for a feature + /// Returns true if user should be navigated to premium upgrade page + Future showFeatureUpsell(PremiumFeature feature) async { + // This would typically show a dialog and return the user's choice + // For now, we'll just return false (don't navigate to upgrade) + // TODO: Implement actual dialog in the UI layer + print('Feature requires premium: ${getFeatureName(feature)}'); + return false; + } + + /// Initialize the service + void init() { + // Emit initial premium status + _premiumController.add(isPremium); + } + + /// Dispose the service + void dispose() { + _premiumController.close(); + } + + /// Simulate purchase flow (for development) + Future simulatePurchase(PremiumPlan plan) async { + // Simulate purchase delay + await Future.delayed(const Duration(seconds: 2)); + + switch (plan) { + case PremiumPlan.monthly: + await grantPremiumForDuration(const Duration(days: 30)); + break; + case PremiumPlan.yearly: + await grantPremiumForDuration(const Duration(days: 365)); + break; + case PremiumPlan.lifetime: + await grantPremium(); // No expiry for lifetime + break; + } + + return true; // Purchase successful + } + + /// Get plan price (for display purposes) + String getPlanPrice(PremiumPlan plan) { + switch (plan) { + case PremiumPlan.monthly: + return '\$2.99/month'; + case PremiumPlan.yearly: + return '\$19.99/year'; + case PremiumPlan.lifetime: + return '\$49.99 one-time'; + } + } + + /// Get plan savings text + String? getPlanSavings(PremiumPlan plan) { + switch (plan) { + case PremiumPlan.yearly: + return 'Save 44%'; + case PremiumPlan.lifetime: + return 'Best Value'; + case PremiumPlan.monthly: + return null; + } + } +} + +enum PremiumFeature { + // Free features + basicNotes, + textFormatting, + imageInsertion, + + // Premium features + doodling, + fileAttachments, + cloudSync, + advancedSearch, +} + +enum PremiumPlan { + monthly, + yearly, + lifetime, +} \ No newline at end of file diff --git a/lib/services/sync/cloud_sync_service.dart b/lib/services/sync/cloud_sync_service.dart new file mode 100644 index 0000000..8ea1c7b --- /dev/null +++ b/lib/services/sync/cloud_sync_service.dart @@ -0,0 +1,190 @@ +/// Abstract interface for cloud sync providers +abstract class CloudSyncService { + /// Get the provider name (e.g., 'Google Drive', 'OneDrive') + String get providerName; + + /// Check if the provider is configured and ready to use + bool get isConfigured; + + /// Check if the user is signed in + bool get isSignedIn; + + /// Get current user info (email, name, etc.) + CloudUser? get currentUser; + + /// Sign in to the cloud provider + Future signIn(); + + /// Sign out from the cloud provider + Future signOut(); + + /// Refresh the authentication token if needed + Future refreshAuth(); + + /// Upload notes data to cloud storage + Future syncUp(Map notesData); + + /// Download notes data from cloud storage + Future syncDown(); + + /// Upload a blob (image, attachment) to cloud storage + Future uploadBlob(String localPath, String remotePath); + + /// Download a blob from cloud storage + Future downloadBlob(String remotePath, String localPath); + + /// Delete a blob from cloud storage + Future deleteBlob(String remotePath); + + /// List blobs in cloud storage (for cleanup) + Future> listBlobs({String? prefix}); + + /// Get sync metadata (last sync time, etc.) + Future getSyncMetadata(); + + /// Set sync metadata + Future setSyncMetadata(CloudSyncMetadata metadata); +} + +/// Result of authentication operations +class CloudAuthResult { + final bool success; + final String? errorMessage; + final CloudUser? user; + + CloudAuthResult({ + required this.success, + this.errorMessage, + this.user, + }); + + CloudAuthResult.success(this.user) : success = true, errorMessage = null; + CloudAuthResult.failure(this.errorMessage) : success = false, user = null; +} + +/// Result of sync operations +class CloudSyncResult { + final bool success; + final String? errorMessage; + final Map? data; + final DateTime? lastModified; + + CloudSyncResult({ + required this.success, + this.errorMessage, + this.data, + this.lastModified, + }); + + CloudSyncResult.success({this.data, this.lastModified}) + : success = true, errorMessage = null; + CloudSyncResult.failure(this.errorMessage) + : success = false, data = null, lastModified = null; +} + +/// Result of blob operations +class CloudBlobResult { + final bool success; + final String? errorMessage; + final String? remotePath; + final String? localPath; + final int? size; + + CloudBlobResult({ + required this.success, + this.errorMessage, + this.remotePath, + this.localPath, + this.size, + }); + + CloudBlobResult.success({ + this.remotePath, + this.localPath, + this.size, + }) : success = true, errorMessage = null; + + CloudBlobResult.failure(this.errorMessage) + : success = false, remotePath = null, localPath = null, size = null; +} + +/// Information about a cloud storage blob +class CloudBlob { + final String path; + final int? size; + final DateTime? lastModified; + final String? etag; + + CloudBlob({ + required this.path, + this.size, + this.lastModified, + this.etag, + }); +} + +/// Cloud user information +class CloudUser { + final String id; + final String? email; + final String? name; + final String? avatarUrl; + + CloudUser({ + required this.id, + this.email, + this.name, + this.avatarUrl, + }); + + Map toMap() { + return { + 'id': id, + 'email': email, + 'name': name, + 'avatarUrl': avatarUrl, + }; + } + + factory CloudUser.fromMap(Map map) { + return CloudUser( + id: map['id'] as String, + email: map['email'] as String?, + name: map['name'] as String?, + avatarUrl: map['avatarUrl'] as String?, + ); + } +} + +/// Sync metadata for tracking sync state +class CloudSyncMetadata { + final DateTime lastSyncTime; + final String? lastSyncId; + final int notesCount; + final List syncedNoteIds; + + CloudSyncMetadata({ + required this.lastSyncTime, + this.lastSyncId, + required this.notesCount, + required this.syncedNoteIds, + }); + + Map toMap() { + return { + 'lastSyncTime': lastSyncTime.toIso8601String(), + 'lastSyncId': lastSyncId, + 'notesCount': notesCount, + 'syncedNoteIds': syncedNoteIds, + }; + } + + factory CloudSyncMetadata.fromMap(Map map) { + return CloudSyncMetadata( + lastSyncTime: DateTime.parse(map['lastSyncTime'] as String), + lastSyncId: map['lastSyncId'] as String?, + notesCount: map['notesCount'] as int, + syncedNoteIds: List.from(map['syncedNoteIds'] ?? []), + ); + } +} \ No newline at end of file diff --git a/lib/services/sync/providers/google_drive_sync_provider.dart b/lib/services/sync/providers/google_drive_sync_provider.dart new file mode 100644 index 0000000..f119454 --- /dev/null +++ b/lib/services/sync/providers/google_drive_sync_provider.dart @@ -0,0 +1,282 @@ +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../cloud_sync_service.dart'; + +/// Google Drive sync provider implementation +/// This is a skeleton implementation that provides no-op functionality +/// when not configured with proper OAuth credentials +class GoogleDriveSyncProvider implements CloudSyncService { + static const String _tokenKey = 'google_drive_token'; + static const String _userKey = 'google_drive_user'; + static const String _refreshTokenKey = 'google_drive_refresh_token'; + + // Feature flag to enable/disable Google Drive functionality + static const bool _isEnabled = false; // Set to true when properly configured + + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + + CloudUser? _currentUser; + bool _isSignedIn = false; + + @override + String get providerName => 'Google Drive'; + + @override + bool get isConfigured => _isEnabled; + + @override + bool get isSignedIn => _isSignedIn && isConfigured; + + @override + CloudUser? get currentUser => _currentUser; + + @override + Future signIn() async { + if (!isConfigured) { + return CloudAuthResult.failure( + 'Google Drive sync is not configured. Please add OAuth credentials to enable this feature.' + ); + } + + try { + // TODO: Implement actual OAuth flow using flutter_appauth + // For now, return a simulated success for development + + // This would typically: + // 1. Use flutter_appauth to initiate OAuth flow + // 2. Handle the callback and extract tokens + // 3. Store tokens securely + // 4. Fetch user profile + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 1)); + + final mockUser = CloudUser( + id: 'mock_google_user', + email: 'user@gmail.com', + name: 'Google User', + ); + + _currentUser = mockUser; + _isSignedIn = true; + + await _secureStorage.write(key: _userKey, value: jsonEncode(mockUser.toMap())); + + return CloudAuthResult.success(mockUser); + } catch (e) { + return CloudAuthResult.failure('Google Drive sign-in failed: $e'); + } + } + + @override + Future signOut() async { + if (!isConfigured) return; + + try { + // Clear stored credentials + await _secureStorage.delete(key: _tokenKey); + await _secureStorage.delete(key: _refreshTokenKey); + await _secureStorage.delete(key: _userKey); + + _currentUser = null; + _isSignedIn = false; + + // TODO: Revoke tokens on Google's end + } catch (e) { + print('Error signing out of Google Drive: $e'); + } + } + + @override + Future refreshAuth() async { + if (!isConfigured || !_isSignedIn) return false; + + try { + // TODO: Implement token refresh using stored refresh token + // For now, just return true to indicate auth is still valid + return true; + } catch (e) { + print('Error refreshing Google Drive auth: $e'); + _isSignedIn = false; + return false; + } + } + + @override + Future syncUp(Map notesData) async { + if (!isSignedIn) { + return CloudSyncResult.failure('Not signed in to Google Drive'); + } + + try { + // TODO: Implement actual upload to Google Drive + // This would involve: + // 1. Convert notes data to JSON + // 2. Upload to a specific file in Google Drive (e.g., quicknote_data.json) + // 3. Handle API rate limits and errors + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 2)); + + return CloudSyncResult.success( + data: notesData, + lastModified: DateTime.now(), + ); + } catch (e) { + return CloudSyncResult.failure('Google Drive upload failed: $e'); + } + } + + @override + Future syncDown() async { + if (!isSignedIn) { + return CloudSyncResult.failure('Not signed in to Google Drive'); + } + + try { + // TODO: Implement actual download from Google Drive + // This would involve: + // 1. Download the notes file from Google Drive + // 2. Parse JSON data + // 3. Return the parsed data + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 1)); + + // Return empty data for now (no remote changes) + return CloudSyncResult.success(data: null); + } catch (e) { + return CloudSyncResult.failure('Google Drive download failed: $e'); + } + } + + @override + Future uploadBlob(String localPath, String remotePath) async { + if (!isSignedIn) { + return CloudBlobResult.failure('Not signed in to Google Drive'); + } + + try { + // TODO: Implement blob upload to Google Drive + // This would upload files like images and attachments + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 500)); + + return CloudBlobResult.success( + localPath: localPath, + remotePath: remotePath, + size: 0, // Would be actual file size + ); + } catch (e) { + return CloudBlobResult.failure('Google Drive blob upload failed: $e'); + } + } + + @override + Future downloadBlob(String remotePath, String localPath) async { + if (!isSignedIn) { + return CloudBlobResult.failure('Not signed in to Google Drive'); + } + + try { + // TODO: Implement blob download from Google Drive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 500)); + + return CloudBlobResult.success( + localPath: localPath, + remotePath: remotePath, + size: 0, + ); + } catch (e) { + return CloudBlobResult.failure('Google Drive blob download failed: $e'); + } + } + + @override + Future deleteBlob(String remotePath) async { + if (!isSignedIn) return false; + + try { + // TODO: Implement blob deletion from Google Drive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + return true; + } catch (e) { + print('Error deleting blob from Google Drive: $e'); + return false; + } + } + + @override + Future> listBlobs({String? prefix}) async { + if (!isSignedIn) return []; + + try { + // TODO: Implement blob listing from Google Drive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 300)); + return []; + } catch (e) { + print('Error listing blobs from Google Drive: $e'); + return []; + } + } + + @override + Future getSyncMetadata() async { + if (!isSignedIn) return null; + + try { + // TODO: Implement sync metadata retrieval + // This would typically be stored in a separate metadata file + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + return null; + } catch (e) { + print('Error getting sync metadata from Google Drive: $e'); + return null; + } + } + + @override + Future setSyncMetadata(CloudSyncMetadata metadata) async { + if (!isSignedIn) return; + + try { + // TODO: Implement sync metadata storage + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + } catch (e) { + print('Error setting sync metadata to Google Drive: $e'); + } + } + + /// Load saved user and token information + Future _loadSavedCredentials() async { + try { + final userJson = await _secureStorage.read(key: _userKey); + if (userJson != null) { + final userMap = jsonDecode(userJson) as Map; + _currentUser = CloudUser.fromMap(userMap); + + // Check if we have a valid token + final token = await _secureStorage.read(key: _tokenKey); + _isSignedIn = token != null && isConfigured; + } + } catch (e) { + print('Error loading Google Drive credentials: $e'); + } + } + + /// Initialize the provider + Future init() async { + await _loadSavedCredentials(); + } +} \ No newline at end of file diff --git a/lib/services/sync/providers/onedrive_sync_provider.dart b/lib/services/sync/providers/onedrive_sync_provider.dart new file mode 100644 index 0000000..5dc210b --- /dev/null +++ b/lib/services/sync/providers/onedrive_sync_provider.dart @@ -0,0 +1,282 @@ +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../cloud_sync_service.dart'; + +/// OneDrive sync provider implementation +/// This is a skeleton implementation that provides no-op functionality +/// when not configured with proper OAuth credentials +class OneDriveSyncProvider implements CloudSyncService { + static const String _tokenKey = 'onedrive_token'; + static const String _userKey = 'onedrive_user'; + static const String _refreshTokenKey = 'onedrive_refresh_token'; + + // Feature flag to enable/disable OneDrive functionality + static const bool _isEnabled = false; // Set to true when properly configured + + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + + CloudUser? _currentUser; + bool _isSignedIn = false; + + @override + String get providerName => 'OneDrive'; + + @override + bool get isConfigured => _isEnabled; + + @override + bool get isSignedIn => _isSignedIn && isConfigured; + + @override + CloudUser? get currentUser => _currentUser; + + @override + Future signIn() async { + if (!isConfigured) { + return CloudAuthResult.failure( + 'OneDrive sync is not configured. Please add OAuth credentials to enable this feature.' + ); + } + + try { + // TODO: Implement actual OAuth flow using flutter_appauth + // For now, return a simulated success for development + + // This would typically: + // 1. Use flutter_appauth to initiate OAuth flow with Microsoft Graph + // 2. Handle the callback and extract tokens + // 3. Store tokens securely + // 4. Fetch user profile from Microsoft Graph + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 1)); + + final mockUser = CloudUser( + id: 'mock_onedrive_user', + email: 'user@outlook.com', + name: 'OneDrive User', + ); + + _currentUser = mockUser; + _isSignedIn = true; + + await _secureStorage.write(key: _userKey, value: jsonEncode(mockUser.toMap())); + + return CloudAuthResult.success(mockUser); + } catch (e) { + return CloudAuthResult.failure('OneDrive sign-in failed: $e'); + } + } + + @override + Future signOut() async { + if (!isConfigured) return; + + try { + // Clear stored credentials + await _secureStorage.delete(key: _tokenKey); + await _secureStorage.delete(key: _refreshTokenKey); + await _secureStorage.delete(key: _userKey); + + _currentUser = null; + _isSignedIn = false; + + // TODO: Revoke tokens on Microsoft's end + } catch (e) { + print('Error signing out of OneDrive: $e'); + } + } + + @override + Future refreshAuth() async { + if (!isConfigured || !_isSignedIn) return false; + + try { + // TODO: Implement token refresh using stored refresh token + // For now, just return true to indicate auth is still valid + return true; + } catch (e) { + print('Error refreshing OneDrive auth: $e'); + _isSignedIn = false; + return false; + } + } + + @override + Future syncUp(Map notesData) async { + if (!isSignedIn) { + return CloudSyncResult.failure('Not signed in to OneDrive'); + } + + try { + // TODO: Implement actual upload to OneDrive via Microsoft Graph API + // This would involve: + // 1. Convert notes data to JSON + // 2. Upload to a specific file in OneDrive (e.g., /quicknote/data.json) + // 3. Handle API rate limits and errors + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 2)); + + return CloudSyncResult.success( + data: notesData, + lastModified: DateTime.now(), + ); + } catch (e) { + return CloudSyncResult.failure('OneDrive upload failed: $e'); + } + } + + @override + Future syncDown() async { + if (!isSignedIn) { + return CloudSyncResult.failure('Not signed in to OneDrive'); + } + + try { + // TODO: Implement actual download from OneDrive via Microsoft Graph API + // This would involve: + // 1. Download the notes file from OneDrive + // 2. Parse JSON data + // 3. Return the parsed data + + // Placeholder implementation: + await Future.delayed(const Duration(seconds: 1)); + + // Return empty data for now (no remote changes) + return CloudSyncResult.success(data: null); + } catch (e) { + return CloudSyncResult.failure('OneDrive download failed: $e'); + } + } + + @override + Future uploadBlob(String localPath, String remotePath) async { + if (!isSignedIn) { + return CloudBlobResult.failure('Not signed in to OneDrive'); + } + + try { + // TODO: Implement blob upload to OneDrive + // This would upload files like images and attachments + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 500)); + + return CloudBlobResult.success( + localPath: localPath, + remotePath: remotePath, + size: 0, // Would be actual file size + ); + } catch (e) { + return CloudBlobResult.failure('OneDrive blob upload failed: $e'); + } + } + + @override + Future downloadBlob(String remotePath, String localPath) async { + if (!isSignedIn) { + return CloudBlobResult.failure('Not signed in to OneDrive'); + } + + try { + // TODO: Implement blob download from OneDrive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 500)); + + return CloudBlobResult.success( + localPath: localPath, + remotePath: remotePath, + size: 0, + ); + } catch (e) { + return CloudBlobResult.failure('OneDrive blob download failed: $e'); + } + } + + @override + Future deleteBlob(String remotePath) async { + if (!isSignedIn) return false; + + try { + // TODO: Implement blob deletion from OneDrive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + return true; + } catch (e) { + print('Error deleting blob from OneDrive: $e'); + return false; + } + } + + @override + Future> listBlobs({String? prefix}) async { + if (!isSignedIn) return []; + + try { + // TODO: Implement blob listing from OneDrive + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 300)); + return []; + } catch (e) { + print('Error listing blobs from OneDrive: $e'); + return []; + } + } + + @override + Future getSyncMetadata() async { + if (!isSignedIn) return null; + + try { + // TODO: Implement sync metadata retrieval + // This would typically be stored in a separate metadata file + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + return null; + } catch (e) { + print('Error getting sync metadata from OneDrive: $e'); + return null; + } + } + + @override + Future setSyncMetadata(CloudSyncMetadata metadata) async { + if (!isSignedIn) return; + + try { + // TODO: Implement sync metadata storage + + // Placeholder implementation: + await Future.delayed(const Duration(milliseconds: 200)); + } catch (e) { + print('Error setting sync metadata to OneDrive: $e'); + } + } + + /// Load saved user and token information + Future _loadSavedCredentials() async { + try { + final userJson = await _secureStorage.read(key: _userKey); + if (userJson != null) { + final userMap = jsonDecode(userJson) as Map; + _currentUser = CloudUser.fromMap(userMap); + + // Check if we have a valid token + final token = await _secureStorage.read(key: _tokenKey); + _isSignedIn = token != null && isConfigured; + } + } catch (e) { + print('Error loading OneDrive credentials: $e'); + } + } + + /// Initialize the provider + Future init() async { + await _loadSavedCredentials(); + } +} \ No newline at end of file diff --git a/lib/services/sync/sync_manager.dart b/lib/services/sync/sync_manager.dart new file mode 100644 index 0000000..16388c6 --- /dev/null +++ b/lib/services/sync/sync_manager.dart @@ -0,0 +1,309 @@ +import 'dart:async'; +import '../local/hive_initializer.dart'; +import '../local/note_repository.dart'; +import 'cloud_sync_service.dart'; +import 'providers/google_drive_sync_provider.dart'; +import 'providers/onedrive_sync_provider.dart'; + +class SyncManager { + static final SyncManager _instance = SyncManager._internal(); + factory SyncManager() => _instance; + SyncManager._internal(); + + // Available providers + late final Map _providers; + + // Current active provider + CloudSyncService? _activeProvider; + + // Repository for notes + final _noteRepository = NoteRepository(); + + // Stream controllers + final _syncStatusController = StreamController.broadcast(); + final _syncProgressController = StreamController.broadcast(); + + // Sync state + bool _isSyncing = false; + DateTime? _lastSyncTime; + Timer? _autoSyncTimer; + + // Constants + static const String _activeSyncProviderKey = 'active_sync_provider'; + static const String _lastSyncTimeKey = 'last_sync_time'; + static const Duration _autoSyncInterval = Duration(minutes: 30); + + /// Initialize the sync manager + void init() { + _providers = { + 'google_drive': GoogleDriveSyncProvider(), + 'onedrive': OneDriveSyncProvider(), + }; + + _loadSyncSettings(); + _startAutoSync(); + } + + /// Get sync status stream + Stream get syncStatusStream => _syncStatusController.stream; + + /// Get sync progress stream + Stream get syncProgressStream => _syncProgressController.stream; + + /// Get available providers + List get availableProviders => _providers.values.toList(); + + /// Get active provider + CloudSyncService? get activeProvider => _activeProvider; + + /// Check if sync is currently in progress + bool get isSyncing => _isSyncing; + + /// Get last sync time + DateTime? get lastSyncTime => _lastSyncTime; + + /// Check if any provider is connected + bool get isConnected => _activeProvider?.isSignedIn ?? false; + + /// Connect to a cloud provider + Future connectProvider(String providerId) async { + final provider = _providers[providerId]; + if (provider == null) { + _emitSyncStatus(SyncStatus.error('Provider not found: $providerId')); + return false; + } + + try { + _emitSyncStatus(SyncStatus.connecting()); + + // Check if provider is configured + if (!provider.isConfigured) { + _emitSyncStatus(SyncStatus.error('Provider not configured: ${provider.providerName}')); + return false; + } + + // Sign in to the provider + final authResult = await provider.signIn(); + if (!authResult.success) { + _emitSyncStatus(SyncStatus.error('Failed to sign in: ${authResult.errorMessage}')); + return false; + } + + // Set as active provider + _activeProvider = provider; + await _saveActiveSyncProvider(providerId); + + _emitSyncStatus(SyncStatus.connected(provider.providerName)); + + // Perform initial sync + await syncNow(); + + return true; + } catch (e) { + _emitSyncStatus(SyncStatus.error('Connection failed: $e')); + return false; + } + } + + /// Disconnect from current provider + Future disconnectProvider() async { + if (_activeProvider != null) { + try { + await _activeProvider!.signOut(); + } catch (e) { + print('Error signing out: $e'); + } + + _activeProvider = null; + await _saveActiveSyncProvider(null); + _emitSyncStatus(SyncStatus.disconnected()); + } + } + + /// Perform manual sync now + Future syncNow() async { + if (_isSyncing || _activeProvider == null || !_activeProvider!.isSignedIn) { + return false; + } + + _isSyncing = true; + _emitSyncStatus(SyncStatus.syncing()); + _emitSyncProgress(SyncProgress(0, 'Starting sync...')); + + try { + // Step 1: Sync notes data + _emitSyncProgress(SyncProgress(0.2, 'Uploading notes...')); + final notesData = _noteRepository.exportAllNotes(); + final syncUpResult = await _activeProvider!.syncUp(notesData); + + if (!syncUpResult.success) { + throw Exception('Upload failed: ${syncUpResult.errorMessage}'); + } + + // Step 2: Download any remote changes + _emitSyncProgress(SyncProgress(0.5, 'Downloading updates...')); + final syncDownResult = await _activeProvider!.syncDown(); + + if (syncDownResult.success && syncDownResult.data != null) { + // TODO: Implement conflict resolution here + // For now, we'll use last-write-wins strategy + await _noteRepository.importNotes(syncDownResult.data!, replaceExisting: false); + } + + // Step 3: Sync media files + _emitSyncProgress(SyncProgress(0.8, 'Syncing media files...')); + await _syncMediaFiles(); + + // Step 4: Update sync metadata + _emitSyncProgress(SyncProgress(0.9, 'Updating sync metadata...')); + final metadata = CloudSyncMetadata( + lastSyncTime: DateTime.now(), + lastSyncId: DateTime.now().millisecondsSinceEpoch.toString(), + notesCount: _noteRepository.getNotesCount(), + syncedNoteIds: _noteRepository.getAllNotes().map((n) => n.id).toList(), + ); + await _activeProvider!.setSyncMetadata(metadata); + + _lastSyncTime = DateTime.now(); + await _saveLastSyncTime(); + + _emitSyncProgress(SyncProgress(1.0, 'Sync completed')); + _emitSyncStatus(SyncStatus.synced(_lastSyncTime!)); + + return true; + } catch (e) { + _emitSyncStatus(SyncStatus.error('Sync failed: $e')); + return false; + } finally { + _isSyncing = false; + } + } + + /// Sync media files (images and attachments) + Future _syncMediaFiles() async { + // TODO: Implement media file sync + // This would involve: + // 1. Upload new/modified media files + // 2. Download missing media files + // 3. Clean up orphaned files + + // For now, this is a placeholder + await Future.delayed(const Duration(milliseconds: 500)); + } + + /// Load sync settings from storage + void _loadSyncSettings() { + try { + final box = HiveInitializer.settingsBox; + + // Load active provider + final activeProviderId = box.get(_activeSyncProviderKey) as String?; + if (activeProviderId != null && _providers.containsKey(activeProviderId)) { + _activeProvider = _providers[activeProviderId]; + } + + // Load last sync time + final lastSyncTimeString = box.get(_lastSyncTimeKey) as String?; + if (lastSyncTimeString != null) { + _lastSyncTime = DateTime.parse(lastSyncTimeString); + } + + // Emit initial status + if (_activeProvider?.isSignedIn == true) { + _emitSyncStatus(SyncStatus.connected(_activeProvider!.providerName)); + } else { + _emitSyncStatus(SyncStatus.disconnected()); + } + } catch (e) { + print('Error loading sync settings: $e'); + _emitSyncStatus(SyncStatus.disconnected()); + } + } + + /// Save active sync provider + Future _saveActiveSyncProvider(String? providerId) async { + try { + final box = HiveInitializer.settingsBox; + if (providerId != null) { + await box.put(_activeSyncProviderKey, providerId); + } else { + await box.delete(_activeSyncProviderKey); + } + } catch (e) { + print('Error saving active sync provider: $e'); + } + } + + /// Save last sync time + Future _saveLastSyncTime() async { + try { + final box = HiveInitializer.settingsBox; + if (_lastSyncTime != null) { + await box.put(_lastSyncTimeKey, _lastSyncTime!.toIso8601String()); + } + } catch (e) { + print('Error saving last sync time: $e'); + } + } + + /// Start auto sync timer + void _startAutoSync() { + _autoSyncTimer?.cancel(); + _autoSyncTimer = Timer.periodic(_autoSyncInterval, (_) { + if (_activeProvider?.isSignedIn == true && !_isSyncing) { + syncNow(); + } + }); + } + + /// Stop auto sync timer + void _stopAutoSync() { + _autoSyncTimer?.cancel(); + _autoSyncTimer = null; + } + + /// Emit sync status + void _emitSyncStatus(SyncStatus status) { + _syncStatusController.add(status); + } + + /// Emit sync progress + void _emitSyncProgress(SyncProgress progress) { + _syncProgressController.add(progress); + } + + /// Dispose the sync manager + void dispose() { + _stopAutoSync(); + _syncStatusController.close(); + _syncProgressController.close(); + } +} + +/// Sync status states +class SyncStatus { + final String state; + final String? message; + final DateTime? timestamp; + + SyncStatus._(this.state, this.message, this.timestamp); + + static SyncStatus disconnected() => SyncStatus._('disconnected', null, null); + static SyncStatus connecting() => SyncStatus._('connecting', 'Connecting to cloud...', null); + static SyncStatus connected(String provider) => SyncStatus._('connected', 'Connected to $provider', DateTime.now()); + static SyncStatus syncing() => SyncStatus._('syncing', 'Synchronizing...', null); + static SyncStatus synced(DateTime time) => SyncStatus._('synced', 'Last synced', time); + static SyncStatus error(String error) => SyncStatus._('error', error, DateTime.now()); + + bool get isConnected => state == 'connected' || state == 'synced'; + bool get isSyncing => state == 'syncing'; + bool get hasError => state == 'error'; +} + +/// Sync progress information +class SyncProgress { + final double progress; // 0.0 to 1.0 + final String message; + + SyncProgress(this.progress, this.message); +} \ No newline at end of file diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 9c9bce9..6b619dc 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -120,7 +120,7 @@ class AppTheme { ), // Card theme with subtle elevation - cardTheme: CardTheme( + cardTheme: CardThemeData( color: cardLight, elevation: 2.0, // 2dp shadow for card separation shadowColor: shadowLight, @@ -304,7 +304,7 @@ class AppTheme { ), // Tab bar theme - tabBarTheme: TabBarTheme( + tabBarTheme: TabBarThemeData( labelColor: primaryLight, unselectedLabelColor: textSecondaryLight, indicatorColor: primaryLight, @@ -357,7 +357,9 @@ class AppTheme { borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), clipBehavior: Clip.antiAliasWithSaveLayer, - ), dialogTheme: DialogThemeData(backgroundColor: dialogLight), + ), + + dialogTheme: DialogThemeData(backgroundColor: dialogLight), ); /// Dark theme with Contemporary Productivity Minimalism @@ -410,7 +412,7 @@ class AppTheme { ), // Card theme with subtle elevation - cardTheme: CardTheme( + cardTheme: CardThemeData( color: cardDark, elevation: 2.0, shadowColor: shadowDark, @@ -594,7 +596,7 @@ class AppTheme { ), // Tab bar theme - tabBarTheme: TabBarTheme( + tabBarTheme: TabBarThemeData( labelColor: primaryDark, unselectedLabelColor: textSecondaryDark, indicatorColor: primaryDark, @@ -647,7 +649,9 @@ class AppTheme { borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), clipBehavior: Clip.antiAliasWithSaveLayer, - ), dialogTheme: DialogThemeData(backgroundColor: dialogDark), + ), + + dialogTheme: DialogThemeData(backgroundColor: dialogDark), ); /// Helper method to build text theme with Inter font family diff --git a/pubspec.yaml b/pubspec.yaml index 27fd1d3..24f3780 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ^3.6.0 + sdk: ^3.5.0 dependencies: flutter: # 🚨 CRITICAL: Required for every Flutter project - DO NOT REMOVE @@ -27,11 +27,30 @@ dependencies: image_picker: ^1.0.4 permission_handler: ^11.1.0 record: ^5.0.4 + + # Local persistence dependencies + hive: ^2.2.3 + hive_flutter: ^1.1.0 + path_provider: ^2.1.4 + + # File handling dependencies + file_picker: ^8.1.2 + + # Security and OAuth dependencies (optional/gated) + flutter_secure_storage: ^9.2.2 + flutter_appauth: ^8.0.2 + + # HTTP client for cloud providers + http: ^1.2.2 dev_dependencies: flutter_test: # 🚨 CRITICAL: Required for Flutter project testing - DO NOT REMOVE sdk: flutter # 🚨 CRITICAL: Required for Flutter project testing - DO NOT REMOVE flutter_lints: ^5.0.0 # 🚨 CRITICAL: Required for code quality - DO NOT REMOVE + + # Code generation for Hive + hive_generator: ^2.0.1 + build_runner: ^2.4.13 flutter: uses-material-design: true # 🚨 CRITICAL: Required for Material icon font - DO NOT REMOVE