Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 114 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<String, WidgetBuilder> 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

Expand Down
29 changes: 29 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
167 changes: 167 additions & 0 deletions lib/models/note.dart
Original file line number Diff line number Diff line change
@@ -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<String> images;

@HiveField(4)
List<String> attachments;

@HiveField(5)
DateTime createdAt;

@HiveField(6)
DateTime updatedAt;

@HiveField(7)
String? folderId;

@HiveField(8)
bool isPinned;

@HiveField(9)
List<String> tags;

@HiveField(10)
DateTime? deletedAt;

@HiveField(11)
String? noteType; // 'text', 'voice', 'drawing', 'template'

@HiveField(12)
bool hasReminder;

@HiveField(13)
DateTime? reminderAt;

@HiveField(14)
Map<String, dynamic>? 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<String, dynamic> map) {
return Note(
id: map['id'] as String,
title: map['title'] as String,
content: map['content'] as String,
images: List<String>.from(map['images'] ?? []),
attachments: List<String>.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<String>.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<String, dynamic>?,
);
}

// Convert to map (useful for cloud sync)
Map<String, dynamic> 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<String>? images,
List<String>? attachments,
DateTime? createdAt,
DateTime? updatedAt,
String? folderId,
bool? isPinned,
List<String>? tags,
DateTime? deletedAt,
String? noteType,
bool? hasReminder,
DateTime? reminderAt,
Map<String, dynamic>? 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)';
}
}
Loading