Skip to content
14 changes: 11 additions & 3 deletions src/mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import 'package:airqo/src/app/dashboard/repository/dashboard_repository.dart';
import 'package:airqo/src/app/dashboard/repository/forecast_repository.dart';
import 'package:airqo/src/app/dashboard/repository/country_repository.dart';
import 'package:airqo/src/app/learn/bloc/kya_bloc.dart';
import 'package:airqo/src/app/learn/repository/kya_repository.dart';
import 'package:airqo/src/app/learn/repository/learn_repository.dart';
import 'package:airqo/src/app/learn/services/learn_sync_service.dart';
import 'package:airqo/src/app/map/bloc/map_bloc.dart';
import 'package:airqo/src/app/map/repository/map_repository.dart';
import 'package:airqo/src/app/other/places/bloc/google_places_bloc.dart';
Expand Down Expand Up @@ -89,7 +90,7 @@ void main() async {
authRepository: authImpl,
socialAuthRepository: authImpl,
userRepository: UserImpl(),
kyaRepository: KyaImpl(),
kyaRepository: LearnRepositoryImpl(),
themeRepository: ThemeImpl(),
mapRepository: MapImpl(),
forecastRepository: ForecastImpl(),
Expand All @@ -113,7 +114,7 @@ class AirqoMobile extends StatelessWidget {
final UserRepository userRepository;
final MapRepository mapRepository;
final ThemeRepository themeRepository;
final KyaRepository kyaRepository;
final LearnRepository kyaRepository;
final GooglePlacesRepository googlePlacesRepository;
final DashboardRepository dashboardRepository;

Expand Down Expand Up @@ -248,9 +249,16 @@ class _DeciderState extends State<Decider> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) {
AutoUpdateService().initialize(navigatorKey);
_fetchCountries();
_initLearnGuestSession();
});
}

void _initLearnGuestSession() {
LearnSyncService.instance.ensureGuestSession().then((_) {
LearnSyncService.instance.syncPendingProgress();
}).catchError((_) {});
Comment thread
Mozart299 marked this conversation as resolved.
}

void _fetchCountries() async {
try {
await CountryRepository().fetchCountries();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ class LocationServiceManager with UiLoggy {

final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
timeLimit: Duration(seconds: 10),
timeLimit: Duration(seconds: 5),
),
);
final placemarks =
Expand Down
2 changes: 1 addition & 1 deletion src/mobile/lib/src/app/dashboard/widgets/nearby_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class _NearbyViewState extends State<NearbyView> with UiLoggy {

Future<void> _updateNearbyLocations() async {
if (_userPosition == null) {
loggy.warning('Cannot filter nearby locations: user position unavailable');
loggy.info('Skipping nearby filter: position not yet available');
return;
}

Expand Down
37 changes: 23 additions & 14 deletions src/mobile/lib/src/app/dashboard/widgets/view_selector.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:airqo/src/app/dashboard/models/country_model.dart';
import 'package:airqo/src/app/shared/widgets/country_button';
import 'package:airqo/src/app/shared/widgets/loading_widget.dart';
import 'package:airqo/src/app/shared/widgets/translated_text.dart';
import 'package:airqo/src/app/shared/widgets/translated_tooltip.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -169,20 +170,28 @@ class _ViewSelectorState extends State<ViewSelector> {
),
],
SizedBox(width: 8),
...sortedCountries.map((country) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildCountryButton(
context,
flag: country.flag,
name: country.countryName,
isSelected: widget.currentView == DashboardView.country &&
widget.selectedCountry == country.countryName,
onTap: () => widget.onViewChanged(DashboardView.country,
country: country.countryName),
isUserCountry: widget.userCountry?.toLowerCase() ==
country.countryName.toLowerCase(),
),
)),
if (widget.activeCountries == null) ...[
const SizedBox(width: 8),
ShimmerContainer(height: 40, borderRadius: 30, width: 90),
const SizedBox(width: 8),
ShimmerContainer(height: 40, borderRadius: 30, width: 110),
const SizedBox(width: 8),
ShimmerContainer(height: 40, borderRadius: 30, width: 100),
] else
...sortedCountries.map((country) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildCountryButton(
context,
flag: country.flag,
name: country.countryName,
isSelected: widget.currentView == DashboardView.country &&
widget.selectedCountry == country.countryName,
onTap: () => widget.onViewChanged(DashboardView.country,
country: country.countryName),
isUserCountry: widget.userCountry?.toLowerCase() ==
country.countryName.toLowerCase(),
),
)),
],
),
);
Expand Down
38 changes: 19 additions & 19 deletions src/mobile/lib/src/app/learn/bloc/kya_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:airqo/src/app/learn/models/lesson_response_model.dart';
import 'package:airqo/src/app/learn/repository/kya_repository.dart';
import 'package:airqo/src/app/learn/models/learn_v2_catalog.dart';
import 'package:airqo/src/app/learn/repository/learn_repository.dart';
import 'package:airqo/src/app/shared/services/cache_manager.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
Expand All @@ -9,36 +9,37 @@ part 'kya_event.dart';
part 'kya_state.dart';

class KyaBloc extends Bloc<KyaEvent, KyaState> with UiLoggy {
final KyaRepository repository;
final LearnRepository repository;
final CacheManager _cacheManager = CacheManager();

KyaBloc(this.repository) : super(KyaInitial()) {
on<LoadLessons>(_onLoadLessons);
on<RefreshLessons>(_onRefreshLessons);
}

Future<void> _onLoadLessons(LoadLessons event, Emitter<KyaState> emit) async {
Future<void> _onLoadLessons(
LoadLessons event, Emitter<KyaState> emit) async {
try {
emit(LessonsLoading());

final LessonResponseModel model =
await repository.fetchLessons(forceRefresh: event.forceRefresh);
final LearnV2CatalogResponse model =
await repository.fetchCatalog(forceRefresh: event.forceRefresh);

emit(LessonsLoaded(model));
} catch (e) {
loggy.error('Error loading lessons: $e');
loggy.error('Error loading Learn catalog: $e');

// Try to get cached data directly from the repository
try {
final LessonResponseModel? cachedModel = await _getCachedLessonsData();
final LearnV2CatalogResponse? cachedModel =
await _getCachedCatalog();

emit(LessonsLoadingError(
message: e.toString(),
cachedModel: cachedModel,
isOffline: !_cacheManager.isConnected,
));
} catch (cacheError) {
loggy.error('Error fetching cached lessons: $cacheError');
loggy.error('Error fetching cached catalog: $cacheError');
emit(LessonsLoadingError(
message: e.toString(),
isOffline: !_cacheManager.isConnected,
Expand All @@ -52,19 +53,18 @@ class KyaBloc extends Bloc<KyaEvent, KyaState> with UiLoggy {
emit(LessonsRefreshing(currentModel: event.currentModel));

try {
final LessonResponseModel model =
await repository.fetchLessons(forceRefresh: true);
final LearnV2CatalogResponse model =
await repository.fetchCatalog(forceRefresh: true);

emit(LessonsLoaded(model));
} catch (e) {
loggy.error('Error refreshing lessons: $e');
loggy.error('Error refreshing Learn catalog: $e');

if (event.currentModel != null) {
// Return to loaded state with existing data
emit(LessonsLoaded(event.currentModel!));
} else {
// Try to get cached data
final LessonResponseModel? cachedModel = await _getCachedLessonsData();
final LearnV2CatalogResponse? cachedModel =
await _getCachedCatalog();

emit(LessonsLoadingError(
message: e.toString(),
Expand All @@ -75,11 +75,11 @@ class KyaBloc extends Bloc<KyaEvent, KyaState> with UiLoggy {
}
}

Future<LessonResponseModel?> _getCachedLessonsData() async {
Future<LearnV2CatalogResponse?> _getCachedCatalog() async {
try {
return await (repository as KyaImpl).getCachedLessonsData();
return await repository.getCachedCatalog();
} catch (e) {
loggy.error('Error getting cached lessons data: $e');
loggy.error('Error getting cached Learn catalog: $e');
return null;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/mobile/lib/src/app/learn/bloc/kya_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ class LoadLessons extends KyaEvent {
}

class RefreshLessons extends KyaEvent {
final LessonResponseModel? currentModel;
final LearnV2CatalogResponse? currentModel;

const RefreshLessons({this.currentModel});

@override
List<Object?> get props => [currentModel];
}
14 changes: 7 additions & 7 deletions src/mobile/lib/src/app/learn/bloc/kya_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ class KyaInitial extends KyaState {}
class LessonsLoading extends KyaState {}

class LessonsRefreshing extends KyaState {
final LessonResponseModel? currentModel;
final LearnV2CatalogResponse? currentModel;

const LessonsRefreshing({this.currentModel});

@override
List<Object?> get props => [currentModel];
}

class LessonsLoaded extends KyaState {
final LessonResponseModel model;
final LearnV2CatalogResponse model;
final bool fromCache;

const LessonsLoaded(this.model, {this.fromCache = false});

@override
List<Object> get props => [model, fromCache];
}

class LessonsLoadingError extends KyaState {
final String message;
final LessonResponseModel? cachedModel;
final LearnV2CatalogResponse? cachedModel;
final bool isOffline;

const LessonsLoadingError({
Expand Down
51 changes: 48 additions & 3 deletions src/mobile/lib/src/app/learn/models/learn_course_structure.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:airqo/src/app/learn/models/learn_lesson_continuation.dart';
import 'package:airqo/src/app/learn/models/learn_v2_catalog.dart';
import 'package:airqo/src/app/learn/models/lesson_response_model.dart';
import 'package:airqo/src/app/learn/services/learn_lesson_experience_service.dart';
import 'package:airqo/src/app/learn/services/learn_progress_service.dart';
Expand All @@ -7,19 +8,24 @@ class LearnLessonSlot {
final String catalogId;
final String plainTitleKey;
final KyaLesson? apiLesson;
final LearnV2Lesson? v2Lesson;

const LearnLessonSlot({
required this.catalogId,
required this.plainTitleKey,
this.apiLesson,
this.v2Lesson,
});

String get progressKey => catalogId;

bool get hasContent => apiLesson != null && apiLesson!.tasks.isNotEmpty;
bool get hasContent => v2Lesson != null
? v2Lesson!.activities.isNotEmpty
: (apiLesson != null && apiLesson!.tasks.isNotEmpty);

/// Scripted demo flow activity count.
int get activityCount => LearnLessonExperienceService.demoActivityCount;
int get activityCount => v2Lesson != null
? v2Lesson!.activities.length
: LearnLessonExperienceService.demoActivityCount;
Comment thread
Mozart299 marked this conversation as resolved.
Outdated
}

class LearnUnitViewModel {
Expand Down Expand Up @@ -53,13 +59,15 @@ class LearnCourseViewModel {
final int courseNumber;
final String title;
final String plainTitleKey;
final String? coverImageUrl;
final List<LearnUnitViewModel> units;

const LearnCourseViewModel({
required this.id,
required this.courseNumber,
required this.title,
required this.plainTitleKey,
this.coverImageUrl,
required this.units,
});

Expand Down Expand Up @@ -272,6 +280,43 @@ class LearnCatalog {
];
}

static List<LearnCourseViewModel> buildFromV2Catalog(
List<LearnV2Course> v2Courses) {
return v2Courses.map((course) {
final coverImage = course.coverImageUrl ?? _deriveCoverImage(course);
return LearnCourseViewModel(
id: course.id,
courseNumber: course.courseNumber,
title: course.title,
plainTitleKey: course.plainTitleKey,
coverImageUrl: coverImage,
units: course.units.map((unit) {
return LearnUnitViewModel(
id: unit.id,
title: unit.title,
plainTitleKey: unit.title,
lessons: unit.lessons
.map((lesson) => LearnLessonSlot(
catalogId: lesson.id,
plainTitleKey: lesson.title,
v2Lesson: lesson,
))
.toList(),
);
}).toList(),
);
}).toList();
}

static String? _deriveCoverImage(LearnV2Course course) {
for (final unit in course.units) {
for (final lesson in unit.lessons) {
if (lesson.coverImageUrl != null) return lesson.coverImageUrl;
}
}
return null;
}

static bool isCourseUnlocked(
List<LearnCourseViewModel> courses,
int courseIndex,
Expand Down
22 changes: 21 additions & 1 deletion src/mobile/lib/src/app/learn/models/learn_lesson_activity.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
enum LearnActivityType { article, video, image, quiz }

enum LearnQuizFormat { singleChoice, multiChoice, ranking, freeText }
enum LearnQuizFormat {
singleChoice,
multiChoice,
ranking,
freeText;

String get apiKey => switch (this) {
LearnQuizFormat.singleChoice => 'single_choice',
LearnQuizFormat.multiChoice => 'multi_choice',
LearnQuizFormat.ranking => 'ranking',
LearnQuizFormat.freeText => 'free_text',
};

static LearnQuizFormat fromApiKey(String raw) => switch (raw) {
'single_choice' => LearnQuizFormat.singleChoice,
'multi_choice' => LearnQuizFormat.multiChoice,
'ranking' => LearnQuizFormat.ranking,
'free_text' => LearnQuizFormat.freeText,
_ => LearnQuizFormat.singleChoice,
};
}

class LearnArticlePayload {
final String body;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:airqo/src/app/learn/models/learn_course_structure.dart';
import 'package:airqo/src/app/learn/models/lesson_response_model.dart';

/// Optional next-lesson CTA shown on the lesson finish pane.
class LearnLessonContinuation {
Expand All @@ -25,7 +24,5 @@ class LearnLessonContinuation {
this.isNextUnit = false,
});

KyaLesson? get nextLesson => nextSlot.apiLesson;

String get nextProgressKey => nextSlot.progressKey;
}
Loading
Loading