Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added android/app/src/main/res/drawable/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ org.gradle.caching=true
org.gradle.parallel=true
org.gradle.daemon=true
android.enableJetifier=true
android.useAndroidX=true
android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
6 changes: 3 additions & 3 deletions assets/lang/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"settings": "Indstillinger",
"get": "Få",
"app_home.nav_bar_item.tasks": "Opgaver",
"app_home.nav_bar_item.about": "Om",
"app_home.nav_bar_item.data": "Data",
"app_home.nav_bar_item.devices": "Enheder",
"app_home.nav_bar_item.statistics": "Statistik",
"app_home.nav_bar_item.connections": "Forbindelser",
"app_home.nav_bar_item.home": "Hjem",
"invitation.invitations": "Invitationer",
"invitation.subtitle": "Her kan du finde alle de invitationer til studier, du er blevet inviteret til.",
"invitation.invited_to_study": "Invitation til studie",
Expand Down
6 changes: 3 additions & 3 deletions assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"get": "Get",
"scan": "Scan",
"app_home.nav_bar_item.tasks": "Tasks",
"app_home.nav_bar_item.about": "About",
"app_home.nav_bar_item.data": "Data",
"app_home.nav_bar_item.devices": "Devices",
"app_home.nav_bar_item.statistics": "Statistics",
"app_home.nav_bar_item.connections": "Connections",
"app_home.nav_bar_item.home": "Home",
"invitation.invitations": "Invitations",
"invitation.subtitle": "Here you can find all the invitations to studies that you have been invited to.",
"invitation.invited_to_study": "Study Invitation",
Expand Down
6 changes: 3 additions & 3 deletions assets/lang/es.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"app_home.nav_bar_item.tasks": "Tareas",
"app_home.nav_bar_item.about": "Acerca de",
"app_home.nav_bar_item.data": "Datos",
"app_home.nav_bar_item.devices": "Dispositivos",
"app_home.nav_bar_item.statistics": "Estadísticas",
"app_home.nav_bar_item.connections": "Conexiones",
"app_home.nav_bar_item.home": "Inicio",
"pages.task_list.title": "MIS TAREAS",
"pages.task_list.pending": "Pendientes",
"pages.task_list.completed": "Completadas",
Expand Down
54 changes: 36 additions & 18 deletions lib/carp_study_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,41 +65,41 @@ class CarpAppState extends State<CarpStudyApp> {
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) =>
HomePage(model: bloc.appViewModel.homePageViewModel, child: child),
CarpAppShell(model: bloc.appViewModel.homePageViewModel, child: child),
routes: [
// Home is just a landing slot — the top-level redirect always moves
// the user to the right place based on bloc state.
GoRoute(path: homeRoute, parentNavigatorKey: _shellNavigatorKey, redirect: (_, _) => StudyPage.route),
GoRoute(path: homeRoute, parentNavigatorKey: _shellNavigatorKey, redirect: (_, _) => HomePage.route),
GoRoute(
path: TaskListPage.route,
path: HomePage.route,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage(
child: TaskListPage(model: bloc.appViewModel.taskListPageViewModel),
key: state.pageKey,
child: HomePage(model: bloc.appViewModel.homePageViewModel),
transitionsBuilder: bottomNavigationBarAnimation,
),
),
GoRoute(
path: StudyPage.route,
path: TaskListPage.route,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage(
child: StudyPage(model: bloc.appViewModel.studyPageViewModel),
key: state.pageKey,
child: TaskListPage(model: bloc.appViewModel.taskListPageViewModel),
transitionsBuilder: bottomNavigationBarAnimation,
),
routes: [
// /study/consent — nested so the parent StudyPage stays mounted
// underneath. parentNavigatorKey escapes the shell so the bottom
// nav doesn't bleed through during consent.
GoRoute(
path: 'consent',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => InformedConsentPage(model: bloc.appViewModel.informedConsentViewModel),
),
],
),
// HomePage is the new study landing page; keep /study as a redirect
// alias so any legacy navigation lands on the Home tab.
GoRoute(
path: StudyPage.route,
parentNavigatorKey: _shellNavigatorKey,
redirect: (_, _) => HomePage.route,
),
GoRoute(
path: DataVisualizationPage.route,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: DataVisualizationPage(bloc.appViewModel.dataVisualizationPageViewModel),
transitionsBuilder: bottomNavigationBarAnimation,
),
Expand All @@ -108,6 +108,7 @@ class CarpAppState extends State<CarpStudyApp> {
path: DeviceListPage.route,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: DeviceListPage(model: bloc.appViewModel.devicesPageViewModel),
transitionsBuilder: bottomNavigationBarAnimation,
),
Expand All @@ -116,12 +117,20 @@ class CarpAppState extends State<CarpStudyApp> {
path: ProfilePage.route,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: ProfilePage(bloc.appViewModel.profilePageViewModel),
transitionsBuilder: bottomNavigationBarAnimation,
),
),
],
),
// Consent escapes the shell (root navigator) so the bottom nav doesn't
// bleed through while the user is completing informed consent.
GoRoute(
path: InformedConsentPage.route,
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => InformedConsentPage(model: bloc.appViewModel.informedConsentViewModel),
),
GoRoute(
path: StudyDetailsPage.route,
parentNavigatorKey: _rootNavigatorKey,
Expand Down Expand Up @@ -258,9 +267,18 @@ class _AppLocalizationsDelegate extends RPLocalizationsDelegate {
}
}

FadeTransition bottomNavigationBarAnimation(
/// Fade between bottom-nav tabs: the incoming page fades and gently scales in.
/// (On a tab switch the outgoing page is replaced, not popped, so only the
/// incoming page animates - both tabs share the same gray background.)
Widget bottomNavigationBarAnimation(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) => FadeTransition(opacity: animation, child: child);
) {
final fade = CurveTween(curve: Curves.easeOut).animate(animation);
return FadeTransition(
opacity: fade,
child: ScaleTransition(scale: Tween(begin: 0.98, end: 1.0).animate(fade), child: child),
);
}
4 changes: 2 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

Expand Down Expand Up @@ -101,6 +100,7 @@ part 'view_models/user_tasks.dart';

part 'carp_study_app.dart';
part 'ui/pages/informed_consent_page.dart';
part 'ui/pages/app_shell.dart';
part 'ui/pages/home_page.dart';
part 'ui/pages/home_page.install_health_connect_dialog.dart';
part 'ui/carp_study_style.dart';
Expand Down Expand Up @@ -146,11 +146,11 @@ part 'ui/widgets/location_usage_dialog.dart';

part 'ui/cards/activity_card.dart';
part 'ui/cards/anonymous_card.dart';
part 'ui/cards/connections_status_card.dart';
part 'ui/cards/distance_card.dart';
part 'ui/cards/heart_rate_card.dart';
part 'ui/cards/media_card.dart';
part 'ui/cards/mobility_card.dart';
part 'ui/cards/scoreboard_card.dart';
part 'ui/cards/steps_card.dart';
part 'ui/cards/study_progress_card.dart';
part 'ui/cards/survey_card.dart';
Expand Down
9 changes: 9 additions & 0 deletions lib/services/system_info_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ class SystemInfoService {
);
return result.canUpdate;
}

/// Open this app's store listing (Play Store on Android, App Store on iOS).
Future<void> openAppStore() async {
final info = await PackageInfo.fromPlatform();
final url = Platform.isAndroid
? Uri.parse('https://play.google.com/store/apps/details?id=${info.packageName}')
: Uri.parse('https://apps.apple.com/app/1569798025');
if (await canLaunchUrl(url)) await launchUrl(url);
}
}
158 changes: 158 additions & 0 deletions lib/ui/cards/connections_status_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
part of carp_study_app;

/// Home card summarising the connection state of all data sources (hardware
/// devices + online services, excluding the phone itself).
///
/// The connection data comes from [HomePageViewModel]; this widget only maps it
/// to visuals and owns the expand/collapse toggle.
// ponytail: labels hardcoded EN to match the rest of the static home page; i18n later.
class ConnectionsStatusCard extends StatefulWidget {
final HomePageViewModel model;
const ConnectionsStatusCard({required this.model, super.key});

@override
State<ConnectionsStatusCard> createState() => _ConnectionsStatusCardState();
}

class _ConnectionsStatusCardState extends State<ConnectionsStatusCard> {
static const _green = Color(0xFF22C55E);
static const _amber = Color(0xFFF59E0B);
static const _rose = Color(0xFFF43F5E);

bool _expanded = false;

@override
Widget build(BuildContext context) {
final colors = Theme.of(context).extension<CarpColors>()!;
final locale = RPLocalizations.of(context)!;
final model = widget.model;
final total = model.totalSourceCount;
final active = model.activeSourceCount;

final (accent, icon, title, badge) = switch (model.connectionState) {
HomeConnectionState.all => (_green, Icons.sync, 'Connected & sending data', 'LIVE'),
HomeConnectionState.partial => (_amber, Icons.sync_problem, 'Partially connected', 'ACTION'),
HomeConnectionState.none => (_rose, Icons.sync_disabled, 'No devices connected', 'SETUP'),
};

final sources = switch (model.connectionState) {
HomeConnectionState.all => 'All $total sources active',
HomeConnectionState.partial => '$active of $total sources active',
HomeConnectionState.none => 'No sources active',
};
final hint = switch (model.connectionState) {
HomeConnectionState.all => 'tap to view',
HomeConnectionState.partial => 'tap to fix',
HomeConnectionState.none => 'tap to set up',
};

return StudiesMaterial(
backgroundColor: colors.grey50!,
hasBorder: true,
borderColor: accent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => setState(() => _expanded = !_expanded),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: accent, borderRadius: BorderRadius.circular(14)),
child: Icon(icon, color: Colors.white),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(child: Text(title, style: fs20fw700.copyWith(color: colors.grey900))),
const SizedBox(width: 8),
_badge(accent, badge),
],
),
const SizedBox(height: 4),
Text('$sources • $hint', style: fs14fw600.copyWith(color: colors.grey600)),
],
),
),
Container(
decoration: BoxDecoration(color: colors.grey100, shape: BoxShape.circle),
padding: const EdgeInsets.all(4),
child: Icon(
_expanded ? Icons.keyboard_arrow_up : Icons.chevron_right,
color: colors.grey600,
),
),
],
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topCenter,
child: _expanded ? _details(context, colors, locale) : const SizedBox(width: double.infinity),
),
],
),
);
}

Widget _details(BuildContext context, CarpColors colors, RPLocalizations locale) {
final model = widget.model;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final d in model.connectionSources)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Icon(Icons.circle, size: 10, color: model.isSourceActive(d) ? _green : colors.grey400),
const SizedBox(width: 8),
Expanded(child: Text(locale.translate(d.typeName), style: fs16fw400.copyWith(color: colors.grey900))),
Text(
model.isSourceActive(d) ? 'ON' : 'OFF',
style: fs14fw600.copyWith(color: model.isSourceActive(d) ? _green : colors.grey500),
),
],
),
),
Divider(height: 1, color: colors.grey200),
InkWell(
onTap: () => context.go(DeviceListPage.route),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text('Manage in Connections', style: fs16fw600.copyWith(color: colors.primary)),
const SizedBox(width: 4),
Icon(Icons.chevron_right, size: 20, color: colors.primary),
],
),
),
),
],
);
}

Widget _badge(Color accent, String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(color: accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(100)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, size: 8, color: accent),
const SizedBox(width: 5),
Text(label, style: fs12fw600.copyWith(color: accent)),
],
),
);
}
}
Loading
Loading