Application de Gestion de Notes Multi-Plateforme
Sommaire
- 1- Architecture du Projet
- 2- Implémentation Backend (Spring Boot)
- 3- Implémentation Backend (Spring Boot)
- 3.1- Configuration des dépendances (pom.xml)
- 3.2- Modèles de données
- 3.3- Authentification JWT
- 3.4- Contrôleur d'authentification
- 4- Implémentation Frontend (Flutter)
- 4.1- Structure du projet Flutter
- 4.2- Implémentation des principaux composants
- 4.2.1- Configuration du pubspec.yaml
- 4.2.2- Modèles de données
- 4.2.3- Services
- 4.2.4- Providers (State Management)
- 4.2.5- Écrans principaux
- 4.2.6- Widgets
- 4.2.7- Utilitaires
- 4.2.8- Point d'entrée principal
- 4.3- Structure du projet Flutter
- 4.3.1- Cours Flutter
Application de Gestion de Notes Multi-Plateforme
-
Architecture du Projet
-
Implémentation Backend (Spring Boot)
-
Implémentation Backend (Spring Boot)
-
Configuration des dépendances (pom.xml)
-
Modèles de données
User.java
Note.java
-
Authentification JWT
JwtAuthenticationFilter.java
-
Contrôleur d’authentification
AuthController.java
-
Implémentation Frontend (Flutter)
-
Structure du projet Flutter
-
Implémentation des principaux composants
-
Configuration du pubspec.yaml
-
Modèles de données
lib/models/note.dart
lib/models/tag.dart
-
Services
lib/services/api_service.dart
lib/services/local_db_service.dart
-
Providers (State Management)
lib/providers/notes_provider.dart
-
Écrans principaux
lib/screens/auth/login_screen.dart
lib/screens/notes/notes_list_screen.dart
-
Widgets
lib/widgets/note_card.dart
lib/widgets/markdown_preview.dart
-
Utilitaires
lib/utils/constants.dart
lib/utils/network_utils.dart
-
Point d'entrée principal
lib/main.dart
-
Structure du projet Flutter
-
notes-app/
├── backend/# API Spring Boot
│ ├── src/main/java/com/notes/
│ │ ├── controller/ # Contrôleurs REST
│ │ ├── service/ # Logique métier
│ │ ├── repository/ # Accès aux données
│ │ ├── model/ # Entités JPA
│ │ ├── security/ # Configuration JWT
│ │ └── dto/ # Objets de transfert
│ ├── src/main/resources/
│ │ ├── application.properties
│ │ └── data.sql # Données initiales
│ ├── Dockerfile
│ └── pom.xml
├── frontend/ # Application Flutter
│ ├── lib/
│ │ ├── models/ # Modèles de données
│ │ ├── services/ # Services API et base locale
│ │ ├── providers/ # State management
│ │ ├── screens/ # Écrans de l’application
│ │ ├── widgets/ # Composants réutilisables
│ │ └── utils/ # Utilitaires
│ ├── assets/
│ ├── pubspec.yaml
│ └── Dockerfile
├── docker-compose.yml # Configuration multi-conteneurs
└── README.md # Documentation
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE notes (
id BIGSERIAL PRIMARY KEY,
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content_md TEXT,
visibility VARCHAR(10) NOT NULL CHECK (visibility IN ('PRIVATE','SHARED','PUBLIC')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
label VARCHAR(100) UNIQUE NOT NULL
);
CREATE TABLE note_tag (
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (note_id, tag_id)
);
CREATE TABLE shares (
id BIGSERIAL PRIMARY KEY,
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
shared_with_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission VARCHAR(10) NOT NULL CHECK (permission IN ('READ'))
);
CREATE TABLE public_links (
id BIGSERIAL PRIMARY KEY,
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
url_token VARCHAR(128) UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NULL
);
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String passwordHash;
private LocalDateTime createdAt;
// Getters and setters
}
@Entity
@Table(name = "notes")
public class Note {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String contentMd;
@Enumerated(EnumType.STRING)
private Visibility visibility = Visibility.PRIVATE;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false)
private User owner;
@ManyToMany
@JoinTable(
name = "note_tags",
joinColumns = @JoinColumn(name = "note_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set tags = new HashSet<>();
// Getters and setters
}
public enum Visibility {
PRIVATE, SHARED, PUBLIC
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
Long userId = jwtTokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtTokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
@PostMapping("/register")
public ResponseEntity> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new ApiResponse(false, "Email is already taken!"));
}
User user = new User();
user.setEmail(signUpRequest.getEmail());
user.setPasswordHash(passwordEncoder.encode(signUpRequest.getPassword()));
user.setCreatedAt(LocalDateTime.now());
userRepository.save(user);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
signUpRequest.getEmail(),
signUpRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtTokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
}
-
frontend/
├── lib/
│ ├── main.dart
│ ├── models/
│ │ ├── note.dart
│ │ ├── tag.dart
│ │ ├── user.dart
│ │ ├── share.dart
│ │ └── api_response.dart
│ ├── services/
│ │ ├── api_service.dart
│ │ ├── auth_service.dart
│ │ ├── local_db_service.dart
│ │ └── sync_service.dart
│ ├── providers/
│ │ ├── auth_provider.dart
│ │ ├── notes_provider.dart
│ │ └── sync_provider.dart
│ ├── screens/
│ │ ├── auth/
│ │ │ ├── login_screen.dart
│ │ │ └── register_screen.dart
│ │ ├── notes/
│ │ │ ├── notes_list_screen.dart
│ │ │ ├── note_detail_screen.dart
│ │ │ ├── note_edit_screen.dart
│ │ │ └── share_info_screen.dart
│ │ └── splash_screen.dart
│ ├── widgets/
│ │ ├── note_card.dart
│ │ ├── tag_chip.dart
│ │ ├── markdown_preview.dart
│ │ ├── search_bar.dart
│ │ └── offline_indicator.dart
│ └── utils/
│ ├── constants.dart
│ ├── network_utils.dart
│ └── date_utils.dart
├── assets/
├── pubspec.yaml
└── Dockerfile
name: notes_app
description: A multi-platform note management application
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
http: ^1.1.0
shared_preferences: ^2.2.2
flutter_markdown: ^0.6.15
hive: ^2.2.3
hive_flutter: ^1.1.0
path_provider: ^2.0.15
provider: ^6.0.5
pull_to_refresh: ^2.0.0
connectivity_plus: ^4.0.2
intl: ^0.18.1
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: ^1.1.3
build_runner: ^2.4.6
flutter:
uses-material-design: true
assets:
- assets/
import 'package:hive/hive.dart';
part 'note.g.dart';
@HiveType(typeId: 0)
class Note {
@HiveField(0)
final String id;
@HiveField(1)
final String ownerId;
@HiveField(2)
final String title;
@HiveField(3)
final String contentMd;
@HiveField(4)
final String visibility; // PRIVATE, SHARED, PUBLIC
@HiveField(5)
final DateTime createdAt;
@HiveField(6)
final DateTime updatedAt;
@HiveField(7)
final List tags;
@HiveField(8)
final bool isSynced;
@HiveField(9)
final String? localId; // For offline operations
Note({
required this.id,
required this.ownerId,
required this.title,
required this.contentMd,
required this.visibility,
required this.createdAt,
required this.updatedAt,
this.tags = const [],
this.isSynced = true,
this.localId,
});
// Copy with method for updates
Note copyWith({
String? id,
String? ownerId,
String? title,
String? contentMd,
String? visibility,
DateTime? createdAt,
DateTime? updatedAt,
List? tags,
bool? isSynced,
String? localId,
}) {
return Note(
id: id ?? this.id,
ownerId: ownerId ?? this.ownerId,
title: title ?? this.title,
contentMd: contentMd ?? this.contentMd,
visibility: visibility ?? this.visibility,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
tags: tags ?? this.tags,
isSynced: isSynced ?? this.isSynced,
localId: localId ?? this.localId,
);
}
// Convert from JSON
factory Note.fromJson(Map json) {
return Note(
id: json['id'],
ownerId: json['ownerId'],
title: json['title'],
contentMd: json['contentMd'],
visibility: json['visibility'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
tags: (json['tags'] as List?)
?.map((tag) => Tag.fromJson(tag))
.toList() ?? [],
isSynced: true,
);
}
// Convert to JSON
Map toJson() {
return {
'id': id,
'ownerId': ownerId,
'title': title,
'contentMd': contentMd,
'visibility': visibility,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'tags': tags.map((tag) => tag.toJson()).toList(),
};
}
}
import 'package:hive/hive.dart';
part 'tag.g.dart';
@HiveType(typeId: 1)
class Tag {
@HiveField(0)
final String id;
@HiveField(1)
final String label;
Tag({
required this.id,
required this.label,
});
factory Tag.fromJson(Map json) {
return Tag(
id: json['id'],
label: json['label'],
);
}
Map toJson() {
return {
'id': id,
'label': label,
};
}
}
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/note.dart';
import '../models/tag.dart';
import '../utils/constants.dart';
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
String? _token;
Future setToken(String token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
}
Future getToken() async {
if (_token != null) return _token;
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('auth_token');
return _token;
}
Future clearToken() async {
_token = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_token');
}
Map _getHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_token',
};
}
Future> getNotes({String? search, List? tags}) async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/notes');
final response = await http.get(url, headers: _getHeaders());
if (response.statusCode == 200) {
final List data = json.decode(response.body);
return data.map((json) => Note.fromJson(json)).toList();
} else {
throw Exception('Failed to load notes');
}
}
Future getNote(String id) async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/notes/$id');
final response = await http.get(url, headers: _getHeaders());
if (response.statusCode == 200) {
return Note.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load note');
}
}
Future createNote(Note note) async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/notes');
final response = await http.post(
url,
headers: _getHeaders(),
body: json.encode(note.toJson()),
);
if (response.statusCode == 201) {
return Note.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create note');
}
}
Future updateNote(Note note) async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/notes/${note.id}');
final response = await http.put(
url,
headers: _getHeaders(),
body: json.encode(note.toJson()),
);
if (response.statusCode == 200) {
return Note.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to update note');
}
}
Future deleteNote(String id) async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/notes/$id');
final response = await http.delete(url, headers: _getHeaders());
if (response.statusCode != 204) {
throw Exception('Failed to delete note');
}
}
Future> getTags() async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/tags');
final response = await http.get(url, headers: _getHeaders());
if (response.statusCode == 200) {
final List data = json.decode(response.body);
return data.map((json) => Tag.fromJson(json)).toList();
} else {
throw Exception('Failed to load tags');
}
}
}
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import '../models/note.dart';
import '../models/tag.dart';
class LocalDbService {
static final LocalDbService _instance = LocalDbService._internal();
factory LocalDbService() => _instance;
LocalDbService._internal();
static const String notesBox = 'notes';
static const String pendingOpsBox = 'pending_operations';
Future init() async {
final appDocumentDir = await getApplicationDocumentsDirectory();
Hive.init(appDocumentDir.path);
// Register adapters
Hive.registerAdapter(NoteAdapter());
Hive.registerAdapter(TagAdapter());
// Open boxes
await Hive.openBox(notesBox);
await Hive.openBox
import 'package:flutter/foundation.dart';
import '../models/note.dart';
import '../services/api_service.dart';
import '../services/local_db_service.dart';
import '../utils/network_utils.dart';
class NotesProvider with ChangeNotifier {
final ApiService _apiService = ApiService();
final LocalDbService _localDbService = LocalDbService();
List _notes = [];
bool _isLoading = false;
String _error = '';
bool _isOffline = false;
List get notes => _notes;
bool get isLoading => _isLoading;
String get error => _error;
bool get isOffline => _isOffline;
NotesProvider() {
_checkConnectivity();
loadNotes();
}
Future _checkConnectivity() async {
_isOffline = !(await NetworkUtils.isConnected());
notifyListeners();
}
Future loadNotes() async {
_isLoading = true;
notifyListeners();
try {
if (await NetworkUtils.isConnected()) {
// Online: fetch from API
_notes = await _apiService.getNotes();
// Save to local DB
for (final note in _notes) {
await _localDbService.saveNote(note);
}
_isOffline = false;
} else {
// Offline: load from local DB
_notes = await _localDbService.getNotes();
_isOffline = true;
}
_error = '';
} catch (e) {
_error = e.toString();
// Fallback to local DB if online fetch fails
_notes = await _localDbService.getNotes();
}
_isLoading = false;
notifyListeners();
}
Future refreshNotes() async {
await loadNotes();
}
Future createNote(Note note) async {
try {
Note createdNote;
if (await NetworkUtils.isConnected()) {
// Online: create via API
createdNote = await _apiService.createNote(note);
await _localDbService.saveNote(createdNote);
} else {
// Offline: save locally and queue operation
final localId = DateTime.now().millisecondsSinceEpoch.toString();
final localNote = note.copyWith(
id: '', // Will be assigned by server later
isSynced: false,
localId: localId,
);
await _localDbService.saveNote(localNote);
await _localDbService.addPendingOperation(
'create',
note.toJson(),
);
createdNote = localNote;
_isOffline = true;
}
_notes.insert(0, createdNote);
notifyListeners();
return createdNote;
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}
Future updateNote(Note note) async {
try {
Note updatedNote;
if (await NetworkUtils.isConnected()) {
// Online: update via API
updatedNote = await _apiService.updateNote(note);
await _localDbService.saveNote(updatedNote);
} else {
// Offline: update locally and queue operation
final localNote = note.copyWith(isSynced: false);
await _localDbService.saveNote(localNote);
await _localDbService.addPendingOperation(
'update',
note.toJson(),
);
updatedNote = localNote;
_isOffline = true;
}
final index = _notes.indexWhere((n) => n.id == note.id);
if (index != -1) {
_notes[index] = updatedNote;
}
notifyListeners();
return updatedNote;
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}
Future deleteNote(String id) async {
try {
final note = _notes.firstWhere((n) => n.id == id);
if (await NetworkUtils.isConnected()) {
// Online: delete via API
await _apiService.deleteNote(id);
await _localDbService.deleteNote(id);
} else {
// Offline: mark for deletion and queue operation
await _localDbService.addPendingOperation(
'delete',
{'id': id},
);
_isOffline = true;
}
_notes.removeWhere((n) => n.id == id);
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
rethrow;
}
}
List searchNotes(String query) {
if (query.isEmpty) return _notes;
return _notes.where((note) {
return note.title.toLowerCase().contains(query.toLowerCase()) ||
note.contentMd.toLowerCase().contains(query.toLowerCase());
}).toList();
}
List filterNotesByTag(String tagLabel) {
return _notes.where((note) {
return note.tags.any((tag) => tag.label == tagLabel);
}).toList();
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/auth_provider.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
final _formKey = GlobalKey();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
Future _login() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
await Provider.of(context, listen: false).login(
_emailController.text,
_passwordController.text,
);
// Navigate to notes list on success
Navigator.pushReplacementNamed(context, '/notes');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login failed: $e')),
);
}
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _login,
child: const Text('Login'),
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/register');
},
child: const Text('Create an account'),
),
],
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../providers/notes_provider.dart';
import '../../widgets/note_card.dart';
import '../../widgets/offline_indicator.dart';
import '../../widgets/search_bar.dart';
class NotesListScreen extends StatefulWidget {
const NotesListScreen({Key? key}) : super(key: key);
@override
_NotesListScreenState createState() => _NotesListScreenState();
}
class _NotesListScreenState extends State {
final RefreshController _refreshController = RefreshController();
String _searchQuery = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of(context, listen: false).loadNotes();
});
}
Future _onRefresh() async {
try {
await Provider.of(context, listen: false).refreshNotes();
_refreshController.refreshCompleted();
} catch (e) {
_refreshController.refreshFailed();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
Navigator.pushNamed(context, '/edit-note');
},
),
],
),
body: Consumer(
builder: (context, notesProvider, child) {
final notes = _searchQuery.isEmpty
? notesProvider.notes
: notesProvider.searchNotes(_searchQuery);
return Column(
children: [
const OfflineIndicator(),
SearchBar(
onSearch: (query) {
setState(() => _searchQuery = query);
},
),
Expanded(
child: SmartRefresher(
controller: _refreshController,
onRefresh: _onRefresh,
child: notesProvider.isLoading
? const Center(child: CircularProgressIndicator())
: notes.isEmpty
? const Center(child: Text('No notes found'))
: ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
final note = notes[index];
return NoteCard(
note: note,
onTap: () {
Navigator.pushNamed(
context,
'/note-detail',
arguments: note.id,
);
},
);
},
),
),
),
],
);
},
),
);
}
}
import 'package:flutter/material.dart';
import '../models/note.dart';
import 'tag_chip.dart';
class NoteCard extends StatelessWidget {
final Note note;
final VoidCallback onTap;
const NoteCard({
Key? key,
required this.note,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
onTap: onTap,
title: Text(
note.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
note.contentMd,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (note.tags.isNotEmpty) ...[
const SizedBox(height: 4),
Wrap(
spacing: 4,
children: note.tags
.map((tag) => TagChip(label: tag.label))
.toList(),
),
],
],
),
trailing: !note.isSynced
? const Icon(Icons.sync_disabled, size: 16)
: null,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
class MarkdownPreview extends StatelessWidget {
final String text;
const MarkdownPreview({Key? key, required this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return Markdown(
data: text,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, height: 1.5),
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
h2: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
h3: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
);
}
}
class Constants {
static const String apiBaseUrl = 'http://your-backend-url:8080/api';
// Hive type IDs
static const int noteTypeId = 0;
static const int tagTypeId = 1;
}
import 'package:connectivity_plus/connectivity_plus.dart';
class NetworkUtils {
static Future isConnected() async {
final connectivityResult = await Connectivity().checkConnectivity();
return connectivityResult != ConnectivityResult.none;
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'providers/auth_provider.dart';
import 'providers/notes_provider.dart';
import 'screens/auth/login_screen.dart';
import 'screens/auth/register_screen.dart';
import 'screens/notes/notes_list_screen.dart';
import 'screens/notes/note_detail_screen.dart';
import 'screens/notes/note_edit_screen.dart';
import 'screens/notes/share_info_screen.dart';
import 'screens/splash_screen.dart';
import 'services/local_db_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await LocalDbService().init();
runApp(const NotesApp());
}
class NotesApp extends StatelessWidget {
const NotesApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => NotesProvider()),
],
child: MaterialApp(
title: 'Notes App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/splash',
routes: {
'/splash': (context) => const SplashScreen(),
'/login': (context) => const LoginScreen(),
'/register': (context) => const RegisterScreen(),
'/notes': (context) => const NotesListScreen(),
'/note-detail': (context) => const NoteDetailScreen(),
'/edit-note': (context) => const NoteEditScreen(),
'/share-info': (context) => const ShareInfoScreen(),
},
),
);
}
}