feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features

Version: 1.1.0

Major changes:
- Implemented complete Flutter app structure with all core features
- Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features
- Integrated Supabase backend with authentication and data repositories
- Added offline support with Hive caching and local storage
- Implemented comprehensive routing with go_router
- Added location services with Google Maps integration
- Implemented notifications and home widget support
- Added voice recording capabilities and AI chat features
- Created comprehensive test suite and documentation
- Added Android and iOS platform configurations
- Implemented achievements system and social features
- Added calendar integration and bucket list functionality

This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
Tomas Dvorak
2026-01-04 14:33:54 +01:00
parent 1a29315672
commit 37ffb93923
210 changed files with 29417 additions and 477 deletions
@@ -0,0 +1,178 @@
import 'package:equatable/equatable.dart';
class Achievement extends Equatable {
final String id;
final String title;
final String description;
final String icon;
final AchievementType type;
final int? threshold;
final DateTime unlockedAt;
final bool isUnlocked;
const Achievement({
required this.id,
required this.title,
required this.description,
required this.icon,
required this.type,
this.threshold,
required this.unlockedAt,
this.isUnlocked = false,
});
Achievement copyWith({
String? id,
String? title,
String? description,
String? icon,
AchievementType? type,
int? threshold,
DateTime? unlockedAt,
bool? isUnlocked,
}) {
return Achievement(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
icon: icon ?? this.icon,
type: type ?? this.type,
threshold: threshold ?? this.threshold,
unlockedAt: unlockedAt ?? this.unlockedAt,
isUnlocked: isUnlocked ?? this.isUnlocked,
);
}
@override
List<Object?> get props => [
id,
title,
description,
icon,
type,
threshold,
unlockedAt,
isUnlocked,
];
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'icon': icon,
'type': type.toString(),
'threshold': threshold,
'unlocked_at': unlockedAt.toIso8601String(),
'is_unlocked': isUnlocked,
};
}
factory Achievement.fromJson(Map<String, dynamic> json) {
return Achievement(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
icon: json['icon'] as String,
type: AchievementType.values.firstWhere(
(e) => e.toString() == json['type'],
orElse: () => AchievementType.custom,
),
threshold: json['threshold'] as int?,
unlockedAt: json['unlocked_at'] != null
? DateTime.parse(json['unlocked_at'] as String)
: DateTime.now(),
isUnlocked: json['is_unlocked'] as bool? ?? false,
);
}
}
enum AchievementType {
firstGoal,
goalsCompleted5,
goalsCompleted10,
goalsCompleted20,
streak7Days,
streak30Days,
countdownStarted,
countdown25Percent,
countdown50Percent,
countdown75Percent,
countdownCompleted,
earlyBird,
nightOwl,
socialButterfly,
custom,
}
extension AchievementTypeExtension on AchievementType {
String get displayName {
switch (this) {
case AchievementType.firstGoal:
return 'First Goal';
case AchievementType.goalsCompleted5:
return '5 Goals';
case AchievementType.goalsCompleted10:
return '10 Goals';
case AchievementType.goalsCompleted20:
return '20 Goals';
case AchievementType.streak7Days:
return '7 Day Streak';
case AchievementType.streak30Days:
return '30 Day Streak';
case AchievementType.countdownStarted:
return 'Challenge Started';
case AchievementType.countdown25Percent:
return '25% Complete';
case AchievementType.countdown50Percent:
return '50% Complete';
case AchievementType.countdown75Percent:
return '75% Complete';
case AchievementType.countdownCompleted:
return 'Challenge Complete';
case AchievementType.earlyBird:
return 'Early Bird';
case AchievementType.nightOwl:
return 'Night Owl';
case AchievementType.socialButterfly:
return 'Social Butterfly';
case AchievementType.custom:
return 'Custom';
}
}
String get iconEmoji {
switch (this) {
case AchievementType.firstGoal:
return '🎯';
case AchievementType.goalsCompleted5:
return '';
case AchievementType.goalsCompleted10:
return '🌟';
case AchievementType.goalsCompleted20:
return '💫';
case AchievementType.streak7Days:
return '🔥';
case AchievementType.streak30Days:
return '🏆';
case AchievementType.countdownStarted:
return '🚀';
case AchievementType.countdown25Percent:
return '📊';
case AchievementType.countdown50Percent:
return '📈';
case AchievementType.countdown75Percent:
return '📉';
case AchievementType.countdownCompleted:
return '🎉';
case AchievementType.earlyBird:
return '🌅';
case AchievementType.nightOwl:
return '🌙';
case AchievementType.socialButterfly:
return '🦋';
case AchievementType.custom:
return '🏅';
}
}
}
@@ -0,0 +1,68 @@
part of 'cached_goal_model.dart';
class CachedGoalAdapter extends TypeAdapter<CachedGoal> {
@override
final int typeId = 0;
@override
CachedGoal read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CachedGoal(
id: fields[0] as String,
ownerId: fields[1] as String,
title: fields[2] as String,
description: fields[3] as String?,
progress: fields[4] as int,
locationLat: fields[5] as double?,
locationLng: fields[6] as double?,
locationName: fields[7] as String?,
imageUrl: fields[8] as String?,
completed: fields[9] as bool,
createdAt: fields[10] as DateTime,
updatedAt: fields[11] as DateTime,
isDirty: fields[12] as bool,
);
}
@override
void write(BinaryWriter writer, CachedGoal obj) {
writer.writeByte(13);
writer.writeByte(0);
writer.write(obj.id);
writer.writeByte(1);
writer.write(obj.ownerId);
writer.writeByte(2);
writer.write(obj.title);
writer.writeByte(3);
writer.write(obj.description);
writer.writeByte(4);
writer.write(obj.progress);
writer.writeByte(5);
writer.write(obj.locationLat);
writer.writeByte(6);
writer.write(obj.locationLng);
writer.writeByte(7);
writer.write(obj.locationName);
writer.writeByte(8);
writer.write(obj.imageUrl);
writer.writeByte(9);
writer.write(obj.completed);
writer.writeByte(10);
writer.write(obj.createdAt);
writer.writeByte(11);
writer.write(obj.updatedAt);
writer.writeByte(12);
writer.write(obj.isDirty);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CachedGoalAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
}
@@ -0,0 +1,127 @@
import 'package:hive/hive.dart';
part 'cached_goal.g.dart';
@HiveType(typeId: 0)
class CachedGoal extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String ownerId;
@HiveField(2)
final String title;
@HiveField(3)
final String? description;
@HiveField(4)
final int progress;
@HiveField(5)
final double? locationLat;
@HiveField(6)
final double? locationLng;
@HiveField(7)
final String? locationName;
@HiveField(8)
final String? imageUrl;
@HiveField(9)
final bool completed;
@HiveField(10)
final DateTime createdAt;
@HiveField(11)
final DateTime updatedAt;
@HiveField(12)
final bool isDirty;
CachedGoal({
required this.id,
required this.ownerId,
required this.title,
this.description,
required this.progress,
this.locationLat,
this.locationLng,
this.locationName,
this.imageUrl,
required this.completed,
required this.createdAt,
required this.updatedAt,
this.isDirty = false,
});
CachedGoal copyWith({
String? id,
String? ownerId,
String? title,
String? description,
int? progress,
double? locationLat,
double? locationLng,
String? locationName,
String? imageUrl,
bool? completed,
DateTime? createdAt,
DateTime? updatedAt,
bool? isDirty,
}) {
return CachedGoal(
id: id ?? this.id,
ownerId: ownerId ?? this.ownerId,
title: title ?? this.title,
description: description ?? this.description,
progress: progress ?? this.progress,
locationLat: locationLat ?? this.locationLat,
locationLng: locationLng ?? this.locationLng,
locationName: locationName ?? this.locationName,
imageUrl: imageUrl ?? this.imageUrl,
completed: completed ?? this.completed,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isDirty: isDirty ?? this.isDirty,
);
}
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 CachedGoal.fromJson(Map<String, dynamic> json) {
return CachedGoal(
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,
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,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
}
@@ -0,0 +1,83 @@
import 'package:equatable/equatable.dart';
class CalendarEntry extends Equatable {
final String id;
final String userId;
final String? goalId;
final DateTime entryDate;
final String title;
final String? note;
final String entryType; // e.g. progress, milestone, reflection
final DateTime createdAt;
const CalendarEntry({
required this.id,
required this.userId,
this.goalId,
required this.entryDate,
required this.title,
this.note,
required this.entryType,
required this.createdAt,
});
CalendarEntry copyWith({
String? id,
String? userId,
String? goalId,
DateTime? entryDate,
String? title,
String? note,
String? entryType,
DateTime? createdAt,
}) {
return CalendarEntry(
id: id ?? this.id,
userId: userId ?? this.userId,
goalId: goalId ?? this.goalId,
entryDate: entryDate ?? this.entryDate,
title: title ?? this.title,
note: note ?? this.note,
entryType: entryType ?? this.entryType,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'goal_id': goalId,
'entry_date': entryDate.toIso8601String().split('T').first,
'title': title,
'note': note,
'entry_type': entryType,
'created_at': createdAt.toIso8601String(),
};
}
factory CalendarEntry.fromJson(Map<String, dynamic> json) {
return CalendarEntry(
id: json['id'] as String,
userId: json['user_id'] as String,
goalId: json['goal_id'] as String?,
entryDate: DateTime.parse(json['entry_date'] as String),
title: json['title'] as String,
note: json['note'] as String?,
entryType: json['entry_type'] as String? ?? 'note',
createdAt: DateTime.parse(json['created_at'] as String),
);
}
@override
List<Object?> get props => [
id,
userId,
goalId,
entryDate,
title,
note,
entryType,
createdAt,
];
}
@@ -0,0 +1,126 @@
import 'package:uuid/uuid.dart';
enum MutationType {
createGoal,
updateGoal,
deleteGoal,
updateGoalProgress,
}
class OfflineMutation {
final String id;
final MutationType type;
final String? goalId;
final Map<String, dynamic>? data;
final DateTime createdAt;
final DateTime? syncedAt;
final bool isSynced;
OfflineMutation({
required this.id,
required this.type,
this.goalId,
this.data,
required this.createdAt,
this.syncedAt,
this.isSynced = false,
});
OfflineMutation copyWith({
String? id,
MutationType? type,
String? goalId,
Map<String, dynamic>? data,
DateTime? createdAt,
DateTime? syncedAt,
bool? isSynced,
}) {
return OfflineMutation(
id: id ?? this.id,
type: type ?? this.type,
goalId: goalId ?? this.goalId,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
syncedAt: syncedAt ?? this.syncedAt,
isSynced: isSynced ?? this.isSynced,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type.name,
'goal_id': goalId,
'data': data,
'created_at': createdAt.toIso8601String(),
'synced_at': syncedAt?.toIso8601String(),
'is_synced': isSynced,
};
}
factory OfflineMutation.fromJson(Map<String, dynamic> json) {
return OfflineMutation(
id: json['id'] as String,
type: MutationType.values.firstWhere(
(e) => e.name == json['type'] as String,
),
goalId: json['goal_id'] as String?,
data: json['data'] as Map<String, dynamic>?,
createdAt: DateTime.parse(json['created_at'] as String),
syncedAt: json['synced_at'] != null
? DateTime.parse(json['synced_at'] as String)
: null,
isSynced: json['is_synced'] as bool,
);
}
static OfflineMutation createGoalMutation({
required String goalId,
required Map<String, dynamic> goalData,
}) {
return OfflineMutation(
id: const Uuid().v4(),
type: MutationType.createGoal,
goalId: goalId,
data: goalData,
createdAt: DateTime.now(),
);
}
static OfflineMutation updateGoalMutation({
required String goalId,
required Map<String, dynamic> goalData,
}) {
return OfflineMutation(
id: const Uuid().v4(),
type: MutationType.updateGoal,
goalId: goalId,
data: goalData,
createdAt: DateTime.now(),
);
}
static OfflineMutation deleteGoalMutation({
required String goalId,
}) {
return OfflineMutation(
id: const Uuid().v4(),
type: MutationType.deleteGoal,
goalId: goalId,
createdAt: DateTime.now(),
);
}
static OfflineMutation updateProgressMutation({
required String goalId,
required int progress,
}) {
return OfflineMutation(
id: const Uuid().v4(),
type: MutationType.updateGoalProgress,
goalId: goalId,
data: {'progress': progress},
createdAt: DateTime.now(),
);
}
}
+28
View File
@@ -7,6 +7,10 @@ class User extends Equatable {
final String? avatarUrl;
final String? bio;
final bool isPublicProfile;
final String? twitterHandle;
final String? instagramHandle;
final String? tiktokHandle;
final String? websiteUrl;
final DateTime? countdownStartDate;
final DateTime? countdownEndDate;
final DateTime createdAt;
@@ -19,6 +23,10 @@ class User extends Equatable {
this.avatarUrl,
this.bio,
this.isPublicProfile = false,
this.twitterHandle,
this.instagramHandle,
this.tiktokHandle,
this.websiteUrl,
this.countdownStartDate,
this.countdownEndDate,
required this.createdAt,
@@ -44,6 +52,10 @@ class User extends Equatable {
String? avatarUrl,
String? bio,
bool? isPublicProfile,
String? twitterHandle,
String? instagramHandle,
String? tiktokHandle,
String? websiteUrl,
DateTime? countdownStartDate,
DateTime? countdownEndDate,
DateTime? createdAt,
@@ -56,6 +68,10 @@ class User extends Equatable {
avatarUrl: avatarUrl ?? this.avatarUrl,
bio: bio ?? this.bio,
isPublicProfile: isPublicProfile ?? this.isPublicProfile,
twitterHandle: twitterHandle ?? this.twitterHandle,
instagramHandle: instagramHandle ?? this.instagramHandle,
tiktokHandle: tiktokHandle ?? this.tiktokHandle,
websiteUrl: websiteUrl ?? this.websiteUrl,
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
createdAt: createdAt ?? this.createdAt,
@@ -71,6 +87,10 @@ class User extends Equatable {
avatarUrl,
bio,
isPublicProfile,
twitterHandle,
instagramHandle,
tiktokHandle,
websiteUrl,
countdownStartDate,
countdownEndDate,
createdAt,
@@ -85,6 +105,10 @@ class User extends Equatable {
'avatar_url': avatarUrl,
'bio': bio,
'is_public_profile': isPublicProfile,
'twitter_handle': twitterHandle,
'instagram_handle': instagramHandle,
'tiktok_handle': tiktokHandle,
'website_url': websiteUrl,
'countdown_start_date': countdownStartDate?.toIso8601String(),
'countdown_end_date': countdownEndDate?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
@@ -100,6 +124,10 @@ class User extends Equatable {
avatarUrl: json['avatar_url'] as String?,
bio: json['bio'] as String?,
isPublicProfile: json['is_public_profile'] as bool? ?? false,
twitterHandle: json['twitter_handle'] as String?,
instagramHandle: json['instagram_handle'] as String?,
tiktokHandle: json['tiktok_handle'] as String?,
websiteUrl: json['website_url'] as String?,
countdownStartDate: json['countdown_start_date'] != null
? DateTime.parse(json['countdown_start_date'] as String)
: null,