Skip to content

Commit

Permalink
Updates to journey
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelcspeed committed Dec 29, 2024
1 parent 6b194c0 commit 5b33849
Show file tree
Hide file tree
Showing 18 changed files with 494 additions and 350 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ flutter pub run pigeon --input pigeon_conf.dart

To generate API and state management code with Riverpod:
```
dart run build_runner watch --delete-conflicting-outputs
dart run build_runner build --delete-conflicting-outputs
```

### Development and Production Configurations
Expand Down
2 changes: 2 additions & 0 deletions lib/constants/http/http_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class HTTPConstants {
static const String allStats = '/stats';
static const String me = 'me';
static const String searchTracks = 'search/tracks';
static const String journeys = 'journeys';

// MAINTENANCE END POINTS
static String maintenance = '${contentBaseUrl}maintenance';
Expand All @@ -98,4 +99,5 @@ class HTTPConstants {
static const String rate = '/rate';
static const String favorite = '/favorite';
static const String donate = '/donations/asks?random=true';

}
92 changes: 68 additions & 24 deletions lib/controllers/path_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import 'package:flutter/foundation.dart';
import 'package:medito/providers/shared_preference/shared_preference_provider.dart';
import 'package:medito/repositories/path_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/path/path_result.dart';
import '../services/task_update_service.dart';

part 'path_notifier.g.dart';

@riverpod
class PathNotifier extends _$PathNotifier {
late final TaskUpdateService _taskUpdateService;

@override
AsyncValue<List<Step>> build() {
AsyncValue<List<JourneyStep>> build() {
_taskUpdateService = TaskUpdateService(ref.read(sharedPreferencesProvider));
fetchPathData();
return const AsyncValue.loading();
}
Expand All @@ -18,32 +24,68 @@ class PathNotifier extends _$PathNotifier {
_updateState(result);
}

Future<void> syncPendingUpdates() async {
final updates = _taskUpdateService.getPendingUpdates();
if (updates.isEmpty) return;

final payload = TaskUpdatePayload(
updated: DateTime.now().millisecondsSinceEpoch ~/ 1000,
tasks: updates,
);

try {
await ref.read(pathRepositoryProvider).updateTaskProgress('1', payload);
await _taskUpdateService.clearUpdates();
// Refresh path data to get the latest state from server
await fetchPathData();
} catch (e) {
debugPrint('Failed to sync updates: $e');
}
}

Future<void> updateTaskCompletion(String taskId, bool isCompleted) async {
final currentState = state;
if (currentState is AsyncData<List<Step>>) {
state = AsyncValue.data(_updateTaskInState(
currentState.value,
taskId,
(task) => task.copyWith(isCompleted: isCompleted),
));
final currentState = state.value;
if (currentState == null) return;

var stepId = '';
for (final step in currentState) {
if (step.tasks.any((t) => t.id == taskId)) {
stepId = step.id;
break;
}
}

final result = await ref
.read(pathRepositoryProvider)
.updateTaskCompletion(taskId, isCompleted);
_updateState(result);
final update = TaskUpdate(
stepId: stepId,
taskId: taskId,
completedAt: [DateTime.now().millisecondsSinceEpoch ~/ 1000],
);

await _taskUpdateService.addUpdate(update);

// Update local state immediately
state = AsyncData(currentState.map((step) {
return step.copyWith(
tasks: step.tasks.map((task) {
if (task.id == taskId) {
return task.copyWith(isCompleted: isCompleted);
}
return task;
}).toList(),
);
}).toList());
}

Future<void> updateJournalEntry(
String taskId, String entryText, bool markAsCompleted) async {
final currentState = state;
if (currentState is AsyncData<List<Step>>) {
if (currentState is AsyncData<List<JourneyStep>>) {
state = AsyncValue.data(_updateTaskInState(
currentState.value,
taskId,
(task) => task.copyWith(
isCompleted: markAsCompleted,
data: JournalData(entryText: entryText),
data: JournalData(id: 'wow', entryText: entryText),
),
));
}
Expand All @@ -54,8 +96,8 @@ class PathNotifier extends _$PathNotifier {
_updateState(result);
}

List<Step> _updateTaskInState(
List<Step> steps,
List<JourneyStep> _updateTaskInState(
List<JourneyStep> steps,
String taskId,
Task Function(Task) updateFunction,
) {
Expand All @@ -69,28 +111,30 @@ class PathNotifier extends _$PathNotifier {
}).toList(),
);

// Check if all tasks in this step are completed
bool allTasksCompleted =
updatedStep.tasks.every((task) => task.isCompleted);
updatedStep = updatedStep.copyWith(isCompleted: allTasksCompleted);
// Check if all required tasks in this step are completed
bool allRequiredTasksCompleted = updatedStep.tasks
.where((task) => task.isRequired)
.every((task) => task.isCompleted);
updatedStep =
updatedStep.copyWith(isCompleted: allRequiredTasksCompleted);

return updatedStep;
}).toList();

// Unlock the next step if the current step is completed
for (int i = 0; i < updatedSteps.length - 1; i++) {
if (updatedSteps[i].isCompleted && !updatedSteps[i + 1].isUnlocked) {
updatedSteps[i + 1] = updatedSteps[i + 1].copyWith(isUnlocked: true);
if (updatedSteps[i].isCompleted && updatedSteps[i + 1].isLocked) {
updatedSteps[i + 1] = updatedSteps[i + 1].copyWith(isLocked: false);
break;
}
}

return updatedSteps;
}

void _updateState(PathResult result) {
void _updateState(JourneyResult result) {
state = switch (result) {
PathResultSuccess(steps: var steps) => AsyncValue.data(steps),
JourneyResultSuccess(steps: var steps) => AsyncValue.data(steps),
PathResultError(message: var errorMessage) =>
AsyncValue.error(errorMessage, StackTrace.current),
};
Expand Down
13 changes: 7 additions & 6 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
// ignore: depend_on_referenced_packages
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:medito/constants/strings/string_constants.dart';
import 'package:medito/controllers/path_notifier.dart';
import 'package:medito/providers/notification/reminder_provider.dart';
import 'package:medito/providers/player/audio_state_provider.dart';
import 'package:medito/providers/player/player_provider.dart';
Expand Down Expand Up @@ -37,7 +38,7 @@ void main() async {
await _runApp();
}

Future<void> initializeApp() async {
Future<void> initializeApp() async {
await initializeAudioService();
usePathUrlStrategy();
}
Expand Down Expand Up @@ -110,7 +111,7 @@ class _ParentWidgetState extends ConsumerState<ParentWidget>
Future<void> _initDeepLinks() async {
_appLinks = AppLinks();
debugPrint('[DEEPLINK] Setting up deep link handlers');

// Handle links
_linkSubscription = _appLinks.uriLinkStream.listen(
(uri) {
Expand All @@ -128,18 +129,18 @@ class _ParentWidgetState extends ConsumerState<ParentWidget>
debugPrint('[DEEPLINK] Scheme: ${uri.scheme}');
debugPrint('[DEEPLINK] Host: ${uri.host}');
debugPrint('[DEEPLINK] Path: ${uri.path}');

var path = '';
var id = '';

try {
if (uri.scheme == 'org.meditofoundation') {
path = uri.host;
id = uri.path.replaceFirst('/', '');
} else if (uri.scheme == 'https' && uri.host == 'medito.app') {
var pathSegments = uri.path.split('/')
..removeWhere((segment) => segment.isEmpty);

if (pathSegments.isNotEmpty) {
path = pathSegments[0];
id = pathSegments.length > 1 ? pathSegments[1] : '';
Expand Down Expand Up @@ -176,7 +177,6 @@ class _ParentWidgetState extends ConsumerState<ParentWidget>
Future.delayed(const Duration(seconds: 2), () {
handleNavigation(path, [id], context);
});

} catch (e) {
debugPrint('[DEEPLINK] Error handling deep link: $e');
scaffoldMessengerKey.currentState?.showSnackBar(
Expand Down Expand Up @@ -218,5 +218,6 @@ class _ParentWidgetState extends ConsumerState<ParentWidget>
void _onAppForegrounded() {
ref.read(reminderProvider).clearBadge();
ref.read(statsProvider.notifier).refresh();
ref.read(pathNotifierProvider.notifier).syncPendingUpdates();
}
}
49 changes: 31 additions & 18 deletions lib/mappers/path_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import 'package:flutter/foundation.dart';

import '../models/path/path_dto.dart';
import '../models/path/path_result.dart';

class PathMapper {
PathResult mapDtoToResult(PathDTO dto) {
JourneyResult mapDtoToResult(PathDTO dto) {
try {
var steps = dto.steps.map((stepDto) => _mapStepDtoToStep(stepDto)).toList();
return PathResultSuccess(steps);
var steps =
dto.steps.map((stepDto) => _mapStepDtoToStep(stepDto)).toList();
return JourneyResultSuccess(steps);
} catch (e) {
return PathResultError('Failed to map path data: ${e.toString()}');
}
debugPrint(e.toString());
return PathResultError.JourneyResultError(
'Failed to map path data: ${e.toString()}');
}
}

Step _mapStepDtoToStep(StepDTO dto) {
JourneyStep _mapStepDtoToStep(StepDTO dto) {
var tasks = dto.tasks.map((taskDto) => _mapTaskDtoToTask(taskDto)).toList();
return Step(
return JourneyStep(
id: dto.id,
title: dto.title,
tasks: tasks,
isUnlocked: dto.isUnlocked,
isCompleted: dto.isCompleted,
isLocked: dto.isLocked,
isCompleted: dto.isCompleted,
);
}

Expand All @@ -28,15 +33,16 @@ class PathMapper {
type: _mapStringToTaskType(dto.type),
title: dto.title,
isCompleted: dto.isCompleted,
lastUpdated: DateTime.fromMillisecondsSinceEpoch(dto.lastUpdated),
data: _mapTaskData(dto.type, dto.data),
lastUpdated: DateTime.fromMillisecondsSinceEpoch(dto.lastUpdated ?? 0),
data: _mapTaskData(dto.id, dto.type, dto.data),
isRequired: dto.isRequired,
);
}

TaskType _mapStringToTaskType(String type) {
switch (type) {
case 'meditation':
return TaskType.meditation;
case 'track':
return TaskType.track;
case 'journal':
return TaskType.journal;
case 'article':
Expand All @@ -46,14 +52,21 @@ class PathMapper {
}
}

TaskData _mapTaskData(String type, Map<String, dynamic> data) {
TaskData _mapTaskData(String id, String type, Map<String, dynamic> data) {
switch (type) {
case 'meditation':
return MeditationData(duration: data['duration'] as int);
case 'track':
var trackId = data['track_id'] as String? ?? '';
var duration = data['duration'] as int? ?? 0;

return TrackData(id: trackId, duration: duration);
case 'journal':
return JournalData(entryText: data['entryText'] as String);
var entryText = data['entryText'] as String? ?? '';

return JournalData(id: id, entryText: entryText);
case 'article':
return ArticleData(content: data['content'] as String);
var content = data['content'] as String? ?? '';

return ArticleData(id: id, content: content);
default:
throw ArgumentError('Unknown task type: $type');
}
Expand Down
14 changes: 10 additions & 4 deletions lib/models/path/path_dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ part 'path_dto.g.dart';
@freezed
class PathDTO with _$PathDTO {
const factory PathDTO({
required String id,
required String title,
required String description,
required List<StepDTO> steps,
}) = _PathDTO;

Expand All @@ -17,8 +20,9 @@ class StepDTO with _$StepDTO {
const factory StepDTO({
required String id,
required String title,
required String description,
@Default(true) bool isLocked,
required List<TaskDTO> tasks,
@Default(false) bool isUnlocked,
@Default(false) bool isCompleted,
}) = _StepDTO;

Expand All @@ -31,9 +35,11 @@ class TaskDTO with _$TaskDTO {
required String id,
required String type,
required String title,
required bool isCompleted,
required int lastUpdated,
required Map<String, dynamic> data,
required String description,
@Default(false) bool isRequired,
@Default(false) bool isCompleted,
int? lastUpdated,
@Default({}) Map<String, dynamic> data,
}) = _TaskDTO;

factory TaskDTO.fromJson(Map<String, dynamic> json) => _$TaskDTOFromJson(json);
Expand Down
Loading

0 comments on commit 5b33849

Please sign in to comment.