Added core data models, repositories, and utilities

This commit is contained in:
Tomas Dvorak
2026-01-03 18:37:48 +01:00
parent 1639de69d4
commit 572fbb971c
17 changed files with 1248 additions and 0 deletions
@@ -0,0 +1,56 @@
import 'package:equatable/equatable.dart';
class Activity extends Equatable {
final String id;
final String userId;
final String type;
final Map<String, dynamic>? payload;
final DateTime createdAt;
const Activity({
required this.id,
required this.userId,
required this.type,
this.payload,
required this.createdAt,
});
Activity copyWith({
String? id,
String? userId,
String? type,
Map<String, dynamic>? payload,
DateTime? createdAt,
}) {
return Activity(
id: id ?? this.id,
userId: userId ?? this.userId,
type: type ?? this.type,
payload: payload ?? this.payload,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'type': type,
'payload': payload,
'created_at': createdAt.toIso8601String(),
};
}
factory Activity.fromJson(Map<String, dynamic> json) {
return Activity(
id: json['id'] as String,
userId: json['user_id'] as String,
type: json['type'] as String,
payload: json['payload'] as Map<String, dynamic>?,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
@override
List<Object?> get props => [id, userId, type, payload, createdAt];
}
+34
View File
@@ -78,4 +78,38 @@ class Goal extends Equatable {
createdAt,
updatedAt,
];
Map<String, dynamic> toJson() {
return {
'id': id,
'owner_id': ownerId,
'title': title,
'description': description,
'progress': progress,
'location_lat': locationLat,
'location_lng': locationLng,
'location_name': locationName,
'image_url': imageUrl,
'completed': completed,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
factory Goal.fromJson(Map<String, dynamic> json) {
return Goal(
id: json['id'] as String,
ownerId: json['owner_id'] as String,
title: json['title'] as String,
description: json['description'] as String?,
progress: json['progress'] as int? ?? 0,
locationLat: json['location_lat'] as double?,
locationLng: json['location_lng'] as double?,
locationName: json['location_name'] as String?,
imageUrl: json['image_url'] as String?,
completed: json['completed'] as bool? ?? false,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
}
@@ -0,0 +1,62 @@
import 'package:equatable/equatable.dart';
class GoalStep extends Equatable {
final String id;
final String goalId;
final String title;
final bool isDone;
final int orderIndex;
final DateTime createdAt;
const GoalStep({
required this.id,
required this.goalId,
required this.title,
required this.isDone,
required this.orderIndex,
required this.createdAt,
});
GoalStep copyWith({
String? id,
String? goalId,
String? title,
bool? isDone,
int? orderIndex,
DateTime? createdAt,
}) {
return GoalStep(
id: id ?? this.id,
goalId: goalId ?? this.goalId,
title: title ?? this.title,
isDone: isDone ?? this.isDone,
orderIndex: orderIndex ?? this.orderIndex,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'goal_id': goalId,
'title': title,
'is_done': isDone,
'order_index': orderIndex,
'created_at': createdAt.toIso8601String(),
};
}
factory GoalStep.fromJson(Map<String, dynamic> json) {
return GoalStep(
id: json['id'] as String,
goalId: json['goal_id'] as String,
title: json['title'] as String,
isDone: json['is_done'] as bool? ?? false,
orderIndex: json['order_index'] as int? ?? 0,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
@override
List<Object?> get props => [id, goalId, title, isDone, orderIndex, createdAt];
}
+34
View File
@@ -76,4 +76,38 @@ class User extends Equatable {
createdAt,
updatedAt,
];
Map<String, dynamic> toJson() {
return {
'id': id,
'username': username,
'email': email,
'avatar_url': avatarUrl,
'bio': bio,
'is_public_profile': isPublicProfile,
'countdown_start_date': countdownStartDate?.toIso8601String(),
'countdown_end_date': countdownEndDate?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
username: json['username'] as String,
email: json['email'] as String,
avatarUrl: json['avatar_url'] as String?,
bio: json['bio'] as String?,
isPublicProfile: json['is_public_profile'] as bool? ?? false,
countdownStartDate: json['countdown_start_date'] != null
? DateTime.parse(json['countdown_start_date'] as String)
: null,
countdownEndDate: json['countdown_end_date'] != null
? DateTime.parse(json['countdown_end_date'] as String)
: null,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
}
@@ -0,0 +1,62 @@
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../models/user_model.dart' as app;
import '../../core/utils/date_time_utils.dart';
import '../../core/errors/failure.dart';
class CountdownRepository {
final supabase.SupabaseClient _client;
CountdownRepository(this._client);
Future<app.User> startCountdown(String userId) async {
try {
final startDate = DateTime.now();
final endDate = DateTimeUtils.calculateEndDate(startDate);
final response = await _client
.from('users')
.update({
'countdown_start_date': startDate.toIso8601String(),
'countdown_end_date': endDate.toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
})
.eq('id', userId)
.select()
.single();
return app.User.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<app.User> getCountdownInfo(String userId) async {
try {
final response = await _client
.from('users')
.select()
.eq('id', userId)
.single();
return app.User.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<bool> hasCountdownStarted(String userId) async {
try {
final user = await getCountdownInfo(userId);
return user.countdownStartDate != null;
} catch (e) {
throw _handleError(e);
}
}
Failure _handleError(dynamic error) {
if (error is supabase.PostgrestException) {
return ServerFailure(error.message);
}
return UnknownFailure(error.toString());
}
}
@@ -0,0 +1,153 @@
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../models/goal_model.dart';
import '../models/goal_step_model.dart';
import '../../core/errors/failure.dart';
class GoalsRepository {
final supabase.SupabaseClient _client;
static const int maxGoals = 20;
GoalsRepository(this._client);
Future<List<Goal>> getGoals(String userId) async {
try {
final response = await _client
.from('goals')
.select()
.eq('owner_id', userId)
.order('created_at', ascending: false);
return (response as List).map((json) => Goal.fromJson(json)).toList();
} catch (e) {
throw _handleError(e);
}
}
Future<Goal> getGoal(String goalId) async {
try {
final response = await _client
.from('goals')
.select()
.eq('id', goalId)
.single();
return Goal.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<Goal> createGoal(Goal goal) async {
try {
final response = await _client
.from('goals')
.insert(goal.toJson())
.select()
.single();
return Goal.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<Goal> updateGoal(Goal goal) async {
try {
final updates = goal.toJson();
updates['updated_at'] = DateTime.now().toIso8601String();
final response = await _client
.from('goals')
.update(updates)
.eq('id', goal.id)
.select()
.single();
return Goal.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<void> deleteGoal(String goalId) async {
try {
await _client.from('goals').delete().eq('id', goalId);
} catch (e) {
throw _handleError(e);
}
}
Future<List<GoalStep>> getGoalSteps(String goalId) async {
try {
final response = await _client
.from('goal_steps')
.select()
.eq('goal_id', goalId)
.order('order_index', ascending: true);
return (response as List).map((json) => GoalStep.fromJson(json)).toList();
} catch (e) {
throw _handleError(e);
}
}
Future<GoalStep> createGoalStep(GoalStep step) async {
try {
final response = await _client
.from('goal_steps')
.insert(step.toJson())
.select()
.single();
return GoalStep.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<GoalStep> updateGoalStep(GoalStep step) async {
try {
final response = await _client
.from('goal_steps')
.update(step.toJson())
.eq('id', step.id)
.select()
.single();
return GoalStep.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<void> deleteGoalStep(String stepId) async {
try {
await _client.from('goal_steps').delete().eq('id', stepId);
} catch (e) {
throw _handleError(e);
}
}
Future<int> getGoalsCount(String userId) async {
try {
final response = await _client
.from('goals')
.select('id')
.eq('owner_id', userId);
return (response as List).length;
} catch (e) {
throw _handleError(e);
}
}
Failure _handleError(dynamic error) {
if (error is supabase.PostgrestException) {
if (error.code == '23505') {
return const ValidationFailure('A goal with this title already exists');
}
return ServerFailure(error.message);
}
return UnknownFailure(error.toString());
}
}
@@ -0,0 +1,156 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz_data;
class NotificationsRepository {
final FlutterLocalNotificationsPlugin _notificationsPlugin;
NotificationsRepository() : _notificationsPlugin = FlutterLocalNotificationsPlugin() {
_initialize();
}
Future<void> _initialize() async {
tz_data.initializeTimeZones();
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings();
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notificationsPlugin.initialize(initSettings);
}
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
const androidDetails = AndroidNotificationDetails(
'lifetimer_channel',
'LifeTimer Notifications',
channelDescription: 'Notifications for LifeTimer app',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notificationsPlugin.show(
id,
title,
body,
notificationDetails,
payload: payload,
);
}
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
}) async {
const androidDetails = AndroidNotificationDetails(
'lifetimer_channel',
'LifeTimer Notifications',
channelDescription: 'Notifications for LifeTimer app',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notificationsPlugin.zonedSchedule(
id,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
notificationDetails,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: payload,
);
}
Future<void> scheduleDailyReminder({
required int id,
required String title,
required String body,
required int hour,
required int minute,
}) async {
const androidDetails = AndroidNotificationDetails(
'lifetimer_daily_channel',
'Daily Reminders',
channelDescription: 'Daily reminder notifications',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notificationsPlugin.zonedSchedule(
id,
title,
body,
_nextInstanceOfTime(hour, minute),
notificationDetails,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
payload: 'daily_reminder',
);
}
Future<void> cancelNotification(int id) async {
await _notificationsPlugin.cancel(id);
}
Future<void> cancelAllNotifications() async {
await _notificationsPlugin.cancelAll();
}
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
return await _notificationsPlugin.pendingNotificationRequests();
}
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
final scheduledDate = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
hour,
minute,
0,
);
if (scheduledDate.isBefore(now)) {
return scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
}
@@ -0,0 +1,154 @@
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../models/user_model.dart' as app;
import '../models/activity_model.dart';
import '../../core/errors/failure.dart';
class SocialRepository {
final supabase.SupabaseClient _client;
SocialRepository(this._client);
Future<void> followUser(String userId, String targetUserId) async {
try {
await _client.from('followers').insert({
'user_id': targetUserId,
'follower_id': userId,
'created_at': DateTime.now().toIso8601String(),
});
} catch (e) {
throw _handleError(e);
}
}
Future<void> unfollowUser(String userId, String targetUserId) async {
try {
await _client
.from('followers')
.delete()
.eq('user_id', targetUserId)
.eq('follower_id', userId);
} catch (e) {
throw _handleError(e);
}
}
Future<bool> isFollowing(String userId, String targetUserId) async {
try {
final response = await _client
.from('followers')
.select('id')
.eq('user_id', targetUserId)
.eq('follower_id', userId)
.maybeSingle();
return response != null;
} catch (e) {
throw _handleError(e);
}
}
Future<List<app.User>> getFollowers(String userId) async {
try {
final response = await _client
.from('followers')
.select('follower_id, users!followers_follower_id_fkey(*)')
.eq('user_id', userId);
return (response as List)
.map((json) => app.User.fromJson(json['users']))
.toList();
} catch (e) {
throw _handleError(e);
}
}
Future<List<app.User>> getFollowing(String userId) async {
try {
final response = await _client
.from('followers')
.select('user_id, users!followers_user_id_fkey(*)')
.eq('follower_id', userId);
return (response as List)
.map((json) => app.User.fromJson(json['users']))
.toList();
} catch (e) {
throw _handleError(e);
}
}
Future<List<Activity>> getActivityFeed(String userId) async {
try {
final response = await _client
.from('activities')
.select()
.eq('user_id', userId)
.order('created_at', ascending: false)
.limit(50);
return (response as List).map((json) => Activity.fromJson(json)).toList();
} catch (e) {
throw _handleError(e);
}
}
Future<Activity> logActivity({
required String userId,
required String type,
Map<String, dynamic>? payload,
}) async {
try {
final response = await _client
.from('activities')
.insert({
'user_id': userId,
'type': type,
'payload': payload,
'created_at': DateTime.now().toIso8601String(),
})
.select()
.single();
return Activity.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<List<app.User>> getLeaderboard({
required String sortBy,
int limit = 50,
}) async {
try {
String orderBy;
switch (sortBy) {
case 'goals_completed':
orderBy = 'goals_completed_count';
break;
case 'streak':
orderBy = 'streak_days';
break;
default:
orderBy = 'created_at';
}
final response = await _client
.from('users')
.select()
.eq('is_public_profile', true)
.order(orderBy, ascending: false)
.limit(limit);
return (response as List).map((json) => app.User.fromJson(json)).toList();
} catch (e) {
throw _handleError(e);
}
}
Failure _handleError(dynamic error) {
if (error is supabase.PostgrestException) {
return ServerFailure(error.message);
}
return UnknownFailure(error.toString());
}
}
@@ -0,0 +1,83 @@
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../models/user_model.dart' as app;
import '../../core/errors/failure.dart';
class UserRepository {
final supabase.SupabaseClient _client;
UserRepository(this._client);
Future<app.User> getProfile(String userId) async {
try {
final response = await _client
.from('users')
.select()
.eq('id', userId)
.single();
return app.User.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<app.User> updateProfile({
required String userId,
String? username,
String? avatarUrl,
String? bio,
bool? isPublicProfile,
}) async {
try {
final updates = <String, dynamic>{};
if (username != null) updates['username'] = username;
if (avatarUrl != null) updates['avatar_url'] = avatarUrl;
if (bio != null) updates['bio'] = bio;
if (isPublicProfile != null) updates['is_public_profile'] = isPublicProfile;
updates['updated_at'] = DateTime.now().toIso8601String();
final response = await _client
.from('users')
.update(updates)
.eq('id', userId)
.select()
.single();
return app.User.fromJson(response);
} catch (e) {
throw _handleError(e);
}
}
Future<bool> isUsernameAvailable(String username) async {
try {
final response = await _client
.from('users')
.select('id')
.eq('username', username)
.maybeSingle();
return response == null;
} catch (e) {
throw _handleError(e);
}
}
Future<void> deleteAccount(String userId) async {
try {
await _client.from('users').delete().eq('id', userId);
} catch (e) {
throw _handleError(e);
}
}
Failure _handleError(dynamic error) {
if (error is supabase.PostgrestException) {
if (error.code == '23505') {
return const ValidationFailure('Username already taken');
}
return ServerFailure(error.message);
}
return UnknownFailure(error.toString());
}
}