mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-05 12:22:56 +00:00
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:
@@ -0,0 +1,158 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
class AnalyticsService {
|
||||
static final AnalyticsService _instance = AnalyticsService._internal();
|
||||
factory AnalyticsService() => _instance;
|
||||
AnalyticsService._internal();
|
||||
|
||||
bool _isInitialized = false;
|
||||
final Map<String, dynamic> _userProperties = {};
|
||||
|
||||
Future<void> initialize() async {
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
void setUserId(String userId) {
|
||||
_userProperties['user_id'] = userId;
|
||||
}
|
||||
|
||||
void setUserProperty(String name, dynamic value) {
|
||||
_userProperties[name] = value;
|
||||
}
|
||||
|
||||
void logEvent(String eventName, {Map<String, dynamic>? parameters}) {
|
||||
if (!_isInitialized) {
|
||||
developer.log(
|
||||
'Analytics not initialized. Event: $eventName',
|
||||
name: 'AnalyticsService',
|
||||
level: 900, // warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final eventData = {
|
||||
'event_name': eventName,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
..._userProperties,
|
||||
if (parameters != null) ...parameters,
|
||||
};
|
||||
|
||||
developer.log(
|
||||
'Analytics Event: $eventData',
|
||||
name: 'AnalyticsService',
|
||||
level: 800, // info
|
||||
);
|
||||
}
|
||||
|
||||
void logSignUp({required String method}) {
|
||||
logEvent('sign_up', parameters: {
|
||||
'method': method,
|
||||
});
|
||||
}
|
||||
|
||||
void logSignIn({required String method}) {
|
||||
logEvent('sign_in', parameters: {
|
||||
'method': method,
|
||||
});
|
||||
}
|
||||
|
||||
void logSignOut() {
|
||||
logEvent('sign_out');
|
||||
}
|
||||
|
||||
void logGoalCreated({required String goalId, required String hasLocation, required String hasImage}) {
|
||||
logEvent('goal_created', parameters: {
|
||||
'goal_id': goalId,
|
||||
'has_location': hasLocation,
|
||||
'has_image': hasImage,
|
||||
});
|
||||
}
|
||||
|
||||
void logGoalUpdated({required String goalId}) {
|
||||
logEvent('goal_updated', parameters: {
|
||||
'goal_id': goalId,
|
||||
});
|
||||
}
|
||||
|
||||
void logGoalCompleted({required String goalId, required int daysInChallenge}) {
|
||||
logEvent('goal_completed', parameters: {
|
||||
'goal_id': goalId,
|
||||
'days_in_challenge': daysInChallenge,
|
||||
});
|
||||
}
|
||||
|
||||
void logGoalDeleted({required String goalId}) {
|
||||
logEvent('goal_deleted', parameters: {
|
||||
'goal_id': goalId,
|
||||
});
|
||||
}
|
||||
|
||||
void logCountdownStarted({required String startDate, required String endDate}) {
|
||||
logEvent('countdown_started', parameters: {
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
});
|
||||
}
|
||||
|
||||
void logCountdownViewed() {
|
||||
logEvent('countdown_viewed');
|
||||
}
|
||||
|
||||
void logProfileUpdated({required String fieldsUpdated}) {
|
||||
logEvent('profile_updated', parameters: {
|
||||
'fields_updated': fieldsUpdated,
|
||||
});
|
||||
}
|
||||
|
||||
void logProfileVisibilityChanged({required bool isPublic}) {
|
||||
logEvent('profile_visibility_changed', parameters: {
|
||||
'is_public': isPublic,
|
||||
});
|
||||
}
|
||||
|
||||
void logOnboardingCompleted() {
|
||||
logEvent('onboarding_completed');
|
||||
}
|
||||
|
||||
void logOnboardingStepCompleted({required String stepName}) {
|
||||
logEvent('onboarding_step_completed', parameters: {
|
||||
'step_name': stepName,
|
||||
});
|
||||
}
|
||||
|
||||
void logSettingsChanged({required String settingName, required String value}) {
|
||||
logEvent('settings_changed', parameters: {
|
||||
'setting_name': settingName,
|
||||
'value': value,
|
||||
});
|
||||
}
|
||||
|
||||
void logNotificationEnabled({required String notificationType}) {
|
||||
logEvent('notification_enabled', parameters: {
|
||||
'notification_type': notificationType,
|
||||
});
|
||||
}
|
||||
|
||||
void logNotificationDisabled({required String notificationType}) {
|
||||
logEvent('notification_disabled', parameters: {
|
||||
'notification_type': notificationType,
|
||||
});
|
||||
}
|
||||
|
||||
void logError({required String error, String? context}) {
|
||||
logEvent('error', parameters: {
|
||||
'error_message': error,
|
||||
if (context != null) 'context': context,
|
||||
});
|
||||
}
|
||||
|
||||
void logScreenView({required String screenName}) {
|
||||
logEvent('screen_view', parameters: {
|
||||
'screen_name': screenName,
|
||||
});
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_userProperties.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
enum NotificationFrequency { daily, weekly, custom }
|
||||
|
||||
enum NotificationType {
|
||||
countdownReminder,
|
||||
milestoneReminder,
|
||||
streakReminder,
|
||||
countdownCheckpoint,
|
||||
}
|
||||
|
||||
class NotificationService {
|
||||
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final StreamController<String?> _onNotificationClickController =
|
||||
StreamController<String?>.broadcast();
|
||||
bool _isInitialized = false;
|
||||
|
||||
Stream<String?> get onNotificationClick => _onNotificationClickController.stream;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
tz.initializeTimeZones();
|
||||
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||
_onNotificationClickController.add(response.payload);
|
||||
},
|
||||
);
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<bool> requestPermissions() async {
|
||||
final android = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
final result = await android?.requestNotificationsPermission();
|
||||
|
||||
return result ?? true;
|
||||
}
|
||||
|
||||
Future<void> scheduleCountdownReminder({
|
||||
required NotificationFrequency frequency,
|
||||
required String title,
|
||||
required String body,
|
||||
int hour = 9,
|
||||
int minute = 0,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'countdown_reminders',
|
||||
'Countdown Reminders',
|
||||
channelDescription: 'Reminders for your 1356-day countdown',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
switch (frequency) {
|
||||
case NotificationFrequency.daily:
|
||||
await _notificationsPlugin.zonedSchedule(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title,
|
||||
body,
|
||||
_nextInstanceOfTime(hour, minute),
|
||||
notificationDetails,
|
||||
uiLocalNotificationDateInterpretation:
|
||||
UILocalNotificationDateInterpretation.absoluteTime,
|
||||
matchDateTimeComponents: DateTimeComponents.time,
|
||||
);
|
||||
break;
|
||||
case NotificationFrequency.weekly:
|
||||
await _notificationsPlugin.zonedSchedule(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title,
|
||||
body,
|
||||
_nextInstanceOfDayAndTime(hour, minute),
|
||||
notificationDetails,
|
||||
uiLocalNotificationDateInterpretation:
|
||||
UILocalNotificationDateInterpretation.absoluteTime,
|
||||
matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime,
|
||||
);
|
||||
break;
|
||||
case NotificationFrequency.custom:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> scheduleMilestoneReminder({
|
||||
required String goalId,
|
||||
required String goalTitle,
|
||||
required DateTime dueDate,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'milestone_reminders',
|
||||
'Milestone Reminders',
|
||||
channelDescription: 'Reminders for your goal milestones',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.zonedSchedule(
|
||||
int.parse(goalId.replaceAll('-', '')),
|
||||
'Milestone Due Soon',
|
||||
'Your goal "$goalTitle" has an upcoming milestone!',
|
||||
tz.TZDateTime.from(dueDate, tz.local).subtract(const Duration(days: 1)),
|
||||
notificationDetails,
|
||||
uiLocalNotificationDateInterpretation:
|
||||
UILocalNotificationDateInterpretation.absoluteTime,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> scheduleStreakReminder({
|
||||
required int streakDays,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'streak_reminders',
|
||||
'Streak Reminders',
|
||||
channelDescription: 'Celebrations for your active streaks',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'🔥 $streakDays Day Streak!',
|
||||
'Keep going! You\'re on fire!',
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> scheduleCountdownCheckpoint({
|
||||
required String checkpointType,
|
||||
required DateTime checkpointDate,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'countdown_checkpoints',
|
||||
'Countdown Checkpoints',
|
||||
channelDescription: 'Important countdown milestones',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
String title;
|
||||
String body;
|
||||
|
||||
switch (checkpointType) {
|
||||
case '50_percent':
|
||||
title = 'Halfway There! 🎉';
|
||||
body = 'You\'ve completed 50% of your 1356-day journey!';
|
||||
break;
|
||||
case '25_percent':
|
||||
title = '25% Remaining ⏰';
|
||||
body = 'Only 25% of your challenge remains. Make it count!';
|
||||
break;
|
||||
default:
|
||||
title = 'Countdown Milestone';
|
||||
body = 'An important milestone in your journey has been reached!';
|
||||
}
|
||||
|
||||
await _notificationsPlugin.zonedSchedule(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title,
|
||||
body,
|
||||
tz.TZDateTime.from(checkpointDate, tz.local),
|
||||
notificationDetails,
|
||||
uiLocalNotificationDateInterpretation:
|
||||
UILocalNotificationDateInterpretation.absoluteTime,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancelAll() async {
|
||||
await _notificationsPlugin.cancelAll();
|
||||
}
|
||||
|
||||
Future<void> cancel(int id) async {
|
||||
await _notificationsPlugin.cancel(id);
|
||||
}
|
||||
|
||||
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
|
||||
final now = tz.TZDateTime.now(tz.local);
|
||||
var scheduledDate = tz.TZDateTime(
|
||||
tz.local,
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
);
|
||||
|
||||
if (scheduledDate.isBefore(now)) {
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
return scheduledDate;
|
||||
}
|
||||
|
||||
tz.TZDateTime _nextInstanceOfDayAndTime(int hour, int minute) {
|
||||
final now = tz.TZDateTime.now(tz.local);
|
||||
var scheduledDate = tz.TZDateTime(
|
||||
tz.local,
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
);
|
||||
|
||||
if (scheduledDate.isBefore(now)) {
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
return scheduledDate;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_onNotificationClickController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user