From a7a133bb0aa46de8dff3602222e5d3051bcecf78 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 01/83] feat: api-setup --- .../sks_chart/data/models/sks_chart_data.dart | 17 ++++++++++++ .../data/repository/sks_chart_repository.dart | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/features/sks_chart/data/models/sks_chart_data.dart create mode 100644 lib/features/sks_chart/data/repository/sks_chart_repository.dart diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart new file mode 100644 index 00000000..ae820b0b --- /dev/null +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -0,0 +1,17 @@ + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "sks_chart_data.freezed.dart"; +part "sks_chart_data.g.dart"; + +@freezed +class SksChartData with _$SksChartData { + const factory SksChartData({ + required int activeUsers, + required int movingAverage21, + required DateTime externalTimestamp +}) = _SksChartData; + + factory SksChartData.fromJson(Map json) => + _$SksChartDataFromJson(json); +} \ No newline at end of file diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart new file mode 100644 index 00000000..63c1b47a --- /dev/null +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -0,0 +1,26 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../api_base_rest/client/dio_client.dart"; +import "../../../../config/env.dart"; +import "../models/sks_chart_data.dart"; + +part "sks_chart_repository.g.dart"; + +@riverpod +Future> getLatestChartData(Ref ref) async { + final dio = ref.watch(restClientProvider); + final latestChartDataUrl = "${Env.sksUrl}/sks-users/today/"; + final response = await dio.get(latestChartDataUrl); + final data = response.data as List; + final chartDataList = data + .map( + (entry) => SksChartData.fromJson(entry as Map),) + .where((e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) + .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + .toIList(); + + return chartDataList; +} From 69789f0b24a45b71df1178f6030bdc8d1c5d3268 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 02/83] feat: temp navigation setup --- lib/features/navigator/app_router.dart | 5 +++ .../navigator/utils/navigation_commands.dart | 3 ++ .../presentation/sks_chart_screen.dart | 39 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 9 +++-- lib/utils/datetime_utils.dart | 11 ++++++ 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index fb9efc22..44c5d47d 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -15,6 +15,7 @@ import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; import "../sks-menu/presentation/sks_menu_screen.dart"; +import "../sks_chart/presentation/sks_chart_screen.dart"; import "root_view.dart"; part "app_router.g.dart"; @@ -78,6 +79,10 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), + AutoRoute( + path: "/sks-chart", + page: SksChartRoute.page, + ), _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 2cbe1df4..7dc66fd0 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -76,4 +76,7 @@ extension NavigationX on WidgetRef { Future navigateToSksMenu() async { await _router.push(const SksMenuRoute()); } + Future navigateToSksChart() async { + await _router.push(const SksChartRoute()); + } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart new file mode 100644 index 00000000..c9619a78 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -0,0 +1,39 @@ +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../utils/datetime_utils.dart"; +import "../data/repository/sks_chart_repository.dart"; + + +@RoutePage() +class SksChartView extends ConsumerWidget { + const SksChartView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + + return Scaffold( + body: asyncChartData.when( + data: (chartDataList) { + if (chartDataList.isEmpty) { + return const Center(child: Text("No data available")); + } + return ListView.builder( + itemCount: chartDataList.length, + itemBuilder: (context, index) { + final data = chartDataList[index]; + return ListTile( + title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + subtitle: Text("Value: ${data.activeUsers}"), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 6758ac7f..80ee7856 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../navigator/utils/navigation_commands.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -14,7 +15,8 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: _SksButton.new, + data: (sksUsersData) => + _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); @@ -22,8 +24,9 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key}); + const _SksButton(this.sksUserData, {super.key, required this.onTap}); + final VoidCallback onTap; final SksUserData sksUserData; @override @@ -31,7 +34,7 @@ class _SksButton extends StatelessWidget { return Padding( padding: SksConfig.outerPadding, child: GestureDetector( - onTap: () {}, + onTap: onTap, child: Row( children: [ Container( diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 88588e93..7e51ca37 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); From a047687b3d37f72efc1db255d950226c119e402e Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 03/83] chore: apply linter rules --- .../navigator/utils/navigation_commands.dart | 1 + .../sks_chart/data/models/sks_chart_data.dart | 9 ++++----- .../data/repository/sks_chart_repository.dart | 16 ++++++++++++---- .../sks_chart/presentation/sks_chart_screen.dart | 5 +++-- .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 7dc66fd0..37d2452c 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -76,6 +76,7 @@ extension NavigationX on WidgetRef { Future navigateToSksMenu() async { await _router.push(const SksMenuRoute()); } + Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae820b0b..ae0b3441 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,4 +1,3 @@ - import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -9,9 +8,9 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; - factory SksChartData.fromJson(Map json) => + factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); -} \ No newline at end of file +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 63c1b47a..cef81e3a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,10 +16,18 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map),) - .where((e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) - .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + (entry) => SksChartData.fromJson(entry as Map), + ) + .where( + (e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= + 25, + ) + .map( + (e) => e = e.copyWith( + externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), + ), + ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index c9619a78..45f3b9ef 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../utils/datetime_utils.dart"; import "../data/repository/sks_chart_repository.dart"; - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -25,7 +24,9 @@ class SksChartView extends ConsumerWidget { itemBuilder: (context, index) { final data = chartDataList[index]; return ListTile( - title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + title: Text( + "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + ), subtitle: Text("Value: ${data.activeUsers}"), ); }, diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 80ee7856..a1562974 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -24,7 +24,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key, required this.onTap}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 7e51ca37..cd1ef2e9 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From 55f0e1f1ca0d356e76c7943eab1a211243e45299 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 04/83] feat: api-setup --- .../sks_chart/data/models/sks_chart_data.dart | 17 ++++++++++++ .../data/repository/sks_chart_repository.dart | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/features/sks_chart/data/models/sks_chart_data.dart create mode 100644 lib/features/sks_chart/data/repository/sks_chart_repository.dart diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart new file mode 100644 index 00000000..ae820b0b --- /dev/null +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -0,0 +1,17 @@ + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "sks_chart_data.freezed.dart"; +part "sks_chart_data.g.dart"; + +@freezed +class SksChartData with _$SksChartData { + const factory SksChartData({ + required int activeUsers, + required int movingAverage21, + required DateTime externalTimestamp +}) = _SksChartData; + + factory SksChartData.fromJson(Map json) => + _$SksChartDataFromJson(json); +} \ No newline at end of file diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart new file mode 100644 index 00000000..63c1b47a --- /dev/null +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -0,0 +1,26 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../api_base_rest/client/dio_client.dart"; +import "../../../../config/env.dart"; +import "../models/sks_chart_data.dart"; + +part "sks_chart_repository.g.dart"; + +@riverpod +Future> getLatestChartData(Ref ref) async { + final dio = ref.watch(restClientProvider); + final latestChartDataUrl = "${Env.sksUrl}/sks-users/today/"; + final response = await dio.get(latestChartDataUrl); + final data = response.data as List; + final chartDataList = data + .map( + (entry) => SksChartData.fromJson(entry as Map),) + .where((e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) + .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + .toIList(); + + return chartDataList; +} From c43ec620788146f2b05b124399654035218eeb80 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 05/83] feat: temp navigation setup --- lib/features/navigator/app_router.dart | 5 +++ .../navigator/utils/navigation_commands.dart | 3 ++ .../presentation/sks_chart_screen.dart | 39 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 9 +++-- lib/utils/datetime_utils.dart | 11 ++++++ 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index fb9efc22..44c5d47d 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -15,6 +15,7 @@ import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; import "../sks-menu/presentation/sks_menu_screen.dart"; +import "../sks_chart/presentation/sks_chart_screen.dart"; import "root_view.dart"; part "app_router.g.dart"; @@ -78,6 +79,10 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), + AutoRoute( + path: "/sks-chart", + page: SksChartRoute.page, + ), _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 25d75b52..596a67be 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,4 +80,7 @@ extension NavigationX on WidgetRef { await _router.push(SksMenuRoute()); } } + Future navigateToSksChart() async { + await _router.push(const SksChartRoute()); + } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart new file mode 100644 index 00000000..c9619a78 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -0,0 +1,39 @@ +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../utils/datetime_utils.dart"; +import "../data/repository/sks_chart_repository.dart"; + + +@RoutePage() +class SksChartView extends ConsumerWidget { + const SksChartView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + + return Scaffold( + body: asyncChartData.when( + data: (chartDataList) { + if (chartDataList.isEmpty) { + return const Center(child: Text("No data available")); + } + return ListView.builder( + itemCount: chartDataList.length, + itemBuilder: (context, index) { + final data = chartDataList[index]; + return ListTile( + title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + subtitle: Text("Value: ${data.activeUsers}"), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 6758ac7f..80ee7856 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../navigator/utils/navigation_commands.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -14,7 +15,8 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: _SksButton.new, + data: (sksUsersData) => + _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); @@ -22,8 +24,9 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key}); + const _SksButton(this.sksUserData, {super.key, required this.onTap}); + final VoidCallback onTap; final SksUserData sksUserData; @override @@ -31,7 +34,7 @@ class _SksButton extends StatelessWidget { return Padding( padding: SksConfig.outerPadding, child: GestureDetector( - onTap: () {}, + onTap: onTap, child: Row( children: [ Container( diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 88588e93..7e51ca37 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); From 344365f59bafb214f17ab6282f7bbdf317178c39 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 06/83] chore: apply linter rules --- .../navigator/utils/navigation_commands.dart | 1 + .../sks_chart/data/models/sks_chart_data.dart | 9 ++++----- .../data/repository/sks_chart_repository.dart | 16 ++++++++++++---- .../sks_chart/presentation/sks_chart_screen.dart | 5 +++-- .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 596a67be..e823d871 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,6 +80,7 @@ extension NavigationX on WidgetRef { await _router.push(SksMenuRoute()); } } + Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae820b0b..ae0b3441 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,4 +1,3 @@ - import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -9,9 +8,9 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; - factory SksChartData.fromJson(Map json) => + factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); -} \ No newline at end of file +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 63c1b47a..cef81e3a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,10 +16,18 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map),) - .where((e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) - .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + (entry) => SksChartData.fromJson(entry as Map), + ) + .where( + (e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= + 25, + ) + .map( + (e) => e = e.copyWith( + externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), + ), + ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index c9619a78..45f3b9ef 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../utils/datetime_utils.dart"; import "../data/repository/sks_chart_repository.dart"; - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -25,7 +24,9 @@ class SksChartView extends ConsumerWidget { itemBuilder: (context, index) { final data = chartDataList[index]; return ListTile( - title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + title: Text( + "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + ), subtitle: Text("Value: ${data.activeUsers}"), ); }, diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 80ee7856..a1562974 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -24,7 +24,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key, required this.onTap}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 7e51ca37..cd1ef2e9 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From d64669b0cf31c7bf622f81da4ebddc3f06ed3874 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 23:18:19 +0100 Subject: [PATCH 07/83] feat: add sks chart --- .../presentation/sks_chart_screen.dart | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 45f3b9ef..e4b96293 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,10 +1,15 @@ import "package:auto_route/annotations.dart"; +import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../utils/datetime_utils.dart"; +import "../../../theme/app_theme.dart"; +import "../../about_us_view/utils/custom_license_dialog.dart"; import "../data/repository/sks_chart_repository.dart"; + +// TODO: after click the hover should be int instead of double + @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -14,26 +19,49 @@ class SksChartView extends ConsumerWidget { final asyncChartData = ref.watch(getLatestChartDataProvider); return Scaffold( - body: asyncChartData.when( - data: (chartDataList) { - if (chartDataList.isEmpty) { - return const Center(child: Text("No data available")); - } - return ListView.builder( - itemCount: chartDataList.length, - itemBuilder: (context, index) { - final data = chartDataList[index]; - return ListTile( - title: Text( - "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 250), + child: BarChart( + BarChartData( + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: 75, + barGroups: [ + BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), + BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), + BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), + BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), + BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) + ,],), + ], + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + leftTitles: const AxisTitles( + axisNameWidget: Text("Ilość osób"), + axisNameSize: 40, + ), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + style: context.textTheme.body.copyWith(fontSize: 12), + (value / 100) + .toStringAsFixed(2) + .replaceRange(2, 3, ":")); + }, + ), ), - subtitle: Text("Value: ${data.activeUsers}"), - ); - }, - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ), + ), + ), ), ); } From 21406577f748900718e2b2891cccc8301df48c78 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:26:18 +0100 Subject: [PATCH 08/83] feat: add sks chart --- .../sks_chart/data/models/sks_chart_data.dart | 11 + .../data/repository/sks_chart_repository.dart | 7 +- .../presentation/sks_chart_screen.dart | 245 +++++++++++++++--- .../widgets/sks_user_data_button.dart | 19 +- lib/l10n/app_pl.arb | 6 +- lib/utils/datetime_utils.dart | 5 + pubspec.yaml | 1 + 7 files changed, 242 insertions(+), 52 deletions(-) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae0b3441..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -14,3 +15,13 @@ class SksChartData with _$SksChartData { factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); } + +extension SksChartDataIListX on IList { + double get maxNumberOfUsers { + return map( + (data) => data.activeUsers > data.movingAverage21 + ? data.activeUsers + : data.movingAverage21, + ).reduce((a, b) => a > b ? a : b).toDouble(); + } +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index cef81e3a..40c743d8 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -21,12 +21,7 @@ Future> getLatestChartData(Ref ref) async { .where( (e) => e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 25, - ) - .map( - (e) => e = e.copyWith( - externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), - ), + 15, ) .toIList(); diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index e4b96293..922a8585 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,15 +1,18 @@ import "package:auto_route/annotations.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../theme/app_theme.dart"; -import "../../about_us_view/utils/custom_license_dialog.dart"; +import "../../../theme/colors.dart"; +import "../../../utils/context_extensions.dart"; +import "../../../utils/datetime_utils.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; - -// TODO: after click the hover should be int instead of double - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -17,47 +20,139 @@ class SksChartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ListView( + children: [ + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center(child: LineHandle()), + ), + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 12, + ), + child: Center( + child: Text( + context.localize.sks_chart_title, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: _SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _LegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + _LegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ); + } +} + +class _SksChart extends StatelessWidget { + const _SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 250), - child: BarChart( - BarChartData( - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + backgroundColor: context.colorTheme.whiteSoap, + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + borderData: FlBorderData( + show: false, + ), + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final int index = entry.key; + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: index, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + leftTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - maxY: 75, - barGroups: [ - BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), - BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), - BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), - BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), - BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) - ,],), - ], - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - leftTitles: const AxisTitles( - axisNameWidget: Text("Ilość osób"), - axisNameSize: 40, - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - style: context.textTheme.body.copyWith(fontSize: 12), - (value / 100) - .toStringAsFixed(2) - .replaceRange(2, 3, ":")); - }, - ), - ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + asyncChartData.value![value.toInt()].externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, ), ), ), @@ -66,3 +161,67 @@ class SksChartView extends ConsumerWidget { ); } } + +class _LegendItem extends StatelessWidget { + const _LegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [4], + radius: const Radius.circular(8), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: 18, + height: 18, + ), + ) + else + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: isPredicted ? [4] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index a1562974..0f8dfeeb 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../navigator/utils/navigation_commands.dart"; +import "../../../sks_chart/presentation/sks_chart_screen.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -16,7 +16,22 @@ class SksUserDataButton extends ConsumerWidget { return asyncSksUserData.when( data: (sksUsersData) => - _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + _SksButton( + sksUsersData, + onTap: () async => showModalBottomSheet( + context: context, + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * + FilterConfig.bottomSheetHeightFactor, + ), + isScrollControlled: true, + builder: (BuildContext context) => UncontrolledProviderScope( + container: ProviderScope.containerOf(context), + child: const SksChartView(), + ), + ), + ), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d6bd970b..d49da36c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -146,5 +146,9 @@ "rest_header": "Pozostałe", "settings": "Ustawienia", "about_the_app": "O aplikacji", - "other_view" : "Inne" + "other_view" : "Inne", + "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", + "sks_chart_legend_users" : "Zmierzona liczba osób", + "sks_chart_legend_forecast" : "Prognozowana liczba osób", + "sks_chart_number_of_users" : "Liczba osób" } \ No newline at end of file diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index cd1ef2e9..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,6 +30,11 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } + String toHourMinuteString() { + final DateFormat hourFormat = DateFormat("HH:mm"); + return hourFormat.format(this); + } + // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); diff --git a/pubspec.yaml b/pubspec.yaml index f519cee3..ab94aaa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: upgrader: ^11.3.0 in_app_review: ^2.0.9 flutter_map_animations: ^0.7.1 + dotted_border: ^2.1.0 dev_dependencies: flutter_test: From 044969dc57ded21fc196a9f4c84f28015687f018 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:34:40 +0100 Subject: [PATCH 09/83] chore: code cleanup --- .../navigator/utils/navigation_commands.dart | 4 ---- .../sks_chart/data/models/sks_chart_data.dart | 1 + lib/utils/datetime_utils.dart | 12 ------------ 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 27020e59..e823d871 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -84,8 +84,4 @@ extension NavigationX on WidgetRef { Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } - - Future navigateToSksChart() async { - await _router.push(const SksChartRoute()); - } } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index 30728e69..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index a89258d3..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,18 +30,6 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } - String toDayDateHourString() { - final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); - final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); - final DateFormat hourFormat = DateFormat("HH:mm"); - final String day = dayFormat.format(this); - final String capitalizedDay = - day[0].toUpperCase() + day.substring(1).toLowerCase(); - final String date = dateFormat.format(this); - final String hour = hourFormat.format(this); - return "$capitalizedDay, $date $hour"; - } - String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this); From 3b736e8ec38cd3157c20ba2b64bd80bf2750e628 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:05:23 +0100 Subject: [PATCH 10/83] chore: code separation --- lib/config/ui_config.dart | 9 + .../parking_chart/widgets/chart_widget.dart | 9 +- .../presentation/sks_menu_screen.dart | 4 +- .../sks_chart_bar_touch_data.dart | 28 +++ .../chart_elements/sks_chart_border_data.dart | 8 + .../chart_elements/sks_chart_grid_data.dart | 9 + .../chart_elements/sks_chart_labels.dart | 57 ++++++ .../chart_elements/sks_chart_legend_item.dart | 50 +++++ .../sks_chart/presentation/sks_chart.dart | 90 +++++++++ .../presentation/sks_chart_screen.dart | 182 ++---------------- lib/widgets/chart_elements.dart | 5 + 11 files changed, 276 insertions(+), 175 deletions(-) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart.dart create mode 100644 lib/widgets/chart_elements.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 6298e6b1..632a15e2 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -198,6 +198,15 @@ abstract class SksConfig { static const outerPadding = EdgeInsets.only(right: 12, bottom: 2); } +abstract class SksChartConfig { + static const borderDashArray = 4.0; + static const borderRadius = 8.0; + static const paddingLarge = 16.0; + static const paddingMedium = 12.0; + static const paddingExtraSmall = 4.0; + static const legendItemSize = 18.0; +} + abstract class NavigationTabViewConfig { static const universalPadding = 12.0; static const radius = 8.0; diff --git a/lib/features/parking_chart/widgets/chart_widget.dart b/lib/features/parking_chart/widgets/chart_widget.dart index 221ea8a8..5eea93f2 100644 --- a/lib/features/parking_chart/widgets/chart_widget.dart +++ b/lib/features/parking_chart/widgets/chart_widget.dart @@ -3,6 +3,7 @@ import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "../../../theme/app_theme.dart"; +import "../../../widgets/chart_elements.dart"; import "../../parkings_view/models/parking.dart"; import "../chart_elements/chart_border.dart"; import "../chart_elements/chart_grid.dart"; @@ -30,8 +31,8 @@ class ChartWidget extends StatelessWidget { borderData: ChartBorder(context), gridData: ChartGrid(context), titlesData: FlTitlesData( - rightTitles: const _HideLabels(), - topTitles: const _HideLabels(), + rightTitles: const HideLabels(), + topTitles: const HideLabels(), bottomTitles: BottomLabels(context), leftTitles: LeftLabels(context), ), @@ -64,7 +65,3 @@ class ChartWidget extends StatelessWidget { ); } } - -class _HideLabels extends AxisTitles { - const _HideLabels() : super(sideTitles: const SideTitles()); -} diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index 40c5d753..433cc644 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -52,9 +52,9 @@ class _SksMenuView extends StatelessWidget { final SksMenuResponse sksMenuData; @override Widget build(BuildContext context) { - if (!sksMenuData.isMenuOnline) { + /*if (!sksMenuData.isMenuOnline) { return const _SKSMenuLottieAnimation(); - } + }*/ return Scaffold( appBar: DetailViewAppBar( actions: const [ diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart new file mode 100644 index 00000000..587e1ab8 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart @@ -0,0 +1,28 @@ +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../theme/app_theme.dart"; + +class SksChartBarTouchData extends BarTouchData { + SksChartBarTouchData(BuildContext context) + : super( + enabled: true, + touchTooltipData: _SksChartTooltipData(context), + ); +} + +class _SksChartTooltipData extends BarTouchTooltipData { + _SksChartTooltipData(BuildContext context) + : super( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + rod.toY.toStringAsFixed(0), + context.textTheme.title + .copyWith(color: context.colorTheme.whiteSoap), + ); + }, + getTooltipColor: (barChartGroup) { + return context.colorTheme.orangePomegranade; + }, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart new file mode 100644 index 00000000..f0c99c01 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart @@ -0,0 +1,8 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartBorderData extends FlBorderData { + SksChartBorderData() + : super( + show: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart new file mode 100644 index 00000000..c3c2cb6f --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart @@ -0,0 +1,9 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartGridData extends FlGridData { + const SksChartGridData() + : super( + drawHorizontalLine: false, + drawVerticalLine: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart new file mode 100644 index 00000000..02e7fec1 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart @@ -0,0 +1,57 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../../utils/datetime_utils.dart"; +import "../../data/models/sks_chart_data.dart"; + +class SksChartRightTiles extends AxisTitles { + SksChartRightTiles(BuildContext context) + : super( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ); +} + +class SksChartBottomTitles extends AxisTitles { + SksChartBottomTitles(BuildContext context, IList chartData) + : super( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only( + top: SksChartConfig.paddingExtraSmall * 2, + ), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + chartData[value.toInt()] + .externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, + ), + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart new file mode 100644 index 00000000..acaa6532 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart @@ -0,0 +1,50 @@ +import "package:dotted_border/dotted_border.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; + +class SksChartLegendItem extends StatelessWidget { + const SksChartLegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [SksChartConfig.borderDashArray], + radius: const Radius.circular(SksChartConfig.borderRadius), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + ), + ) + else + Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all( + Radius.circular(SksChartConfig.borderRadius), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: SksChartConfig.paddingLarge), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart new file mode 100644 index 00000000..b43523be --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -0,0 +1,90 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../theme/colors.dart"; +import "../../../widgets/chart_elements.dart"; +import "../data/models/sks_chart_data.dart"; +import "chart_elements/sks_chart_bar_touch_data.dart"; +import "chart_elements/sks_chart_border_data.dart"; +import "chart_elements/sks_chart_grid_data.dart"; +import "chart_elements/sks_chart_labels.dart"; + +class SksChart extends StatelessWidget { + const SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: entry.key, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + alignment: BarChartAlignment.spaceAround, + backgroundColor: context.colorTheme.whiteSoap, + titlesData: FlTitlesData( + topTitles: const HideLabels(), + leftTitles: const HideLabels(), + rightTitles: SksChartRightTiles(context), + bottomTitles: SksChartBottomTitles( + context, + asyncChartData.value ?? const IList.empty(), + ), + ), + barTouchData: SksChartBarTouchData(context), + gridData: const SksChartGridData(), + borderData: SksChartBorderData(), + ), + ), + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(SksChartConfig.borderRadius), + ), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: + isPredicted ? [SksChartConfig.borderDashArray.toInt()] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 922a8585..88a61cd9 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,17 +1,15 @@ import "package:auto_route/annotations.dart"; -import "package:dotted_border/dotted_border.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; import "../../../utils/context_extensions.dart"; -import "../../../utils/datetime_utils.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; @RoutePage() class SksChartView extends ConsumerWidget { @@ -23,23 +21,26 @@ class SksChartView extends ConsumerWidget { final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), child: ListView( children: [ const Padding( - padding: EdgeInsets.only(top: 16), + padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), child: Center(child: LineHandle()), ), Padding( padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 12, + left: SksChartConfig.paddingMedium, + right: SksChartConfig.paddingMedium, + top: SksChartConfig.paddingMedium, ), child: Center( child: Text( context.localize.sks_chart_title, - style: context.textTheme.headline, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), textAlign: TextAlign.center, ), ), @@ -49,7 +50,7 @@ class SksChartView extends ConsumerWidget { top: 50, left: 25, ), - child: _SksChart( + child: SksChart( maxNumberOfUsers: maxNumberOfUsers, asyncChartData: asyncChartData, ), @@ -57,11 +58,11 @@ class SksChartView extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_users, isPredicted: false, ), - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_forecast, isPredicted: true, ), @@ -72,156 +73,3 @@ class SksChartView extends ConsumerWidget { ); } } - -class _SksChart extends StatelessWidget { - const _SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - swapAnimationDuration: Duration.zero, - BarChartData( - backgroundColor: context.colorTheme.whiteSoap, - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, - ), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - borderData: FlBorderData( - show: false, - ), - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final int index = entry.key; - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: index, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - topTitles: const AxisTitles(), - leftTitles: const AxisTitles(), - rightTitles: AxisTitles( - axisNameWidget: Text( - context.localize.sks_chart_number_of_users, - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ), - axisNameSize: 35, - sideTitles: SideTitles( - showTitles: true, - reservedSize: 25, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ); - }, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 35, - getTitlesWidget: (double value, TitleMeta meta) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - asyncChartData.value![value.toInt()].externalTimestamp - .toLocal() - .toHourMinuteString(), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LegendItem extends StatelessWidget { - const _LegendItem({required this.text, required this.isPredicted}); - - final String text; - final bool isPredicted; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - if (isPredicted) - DottedBorder( - borderType: BorderType.RRect, - dashPattern: const [4], - radius: const Radius.circular(8), - padding: EdgeInsets.zero, - // ignore: sized_box_for_whitespace - child: Container( - width: 18, - height: 18, - ), - ) - else - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - text, - style: context.textTheme.body, - ), - ), - ], - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: isPredicted ? [4] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} diff --git a/lib/widgets/chart_elements.dart b/lib/widgets/chart_elements.dart new file mode 100644 index 00000000..b4bd09af --- /dev/null +++ b/lib/widgets/chart_elements.dart @@ -0,0 +1,5 @@ +import "package:fl_chart/fl_chart.dart"; + +class HideLabels extends AxisTitles { + const HideLabels() : super(sideTitles: const SideTitles()); +} From 3f495f0f587b0cb61f4022561df56438985dd737 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:08:22 +0100 Subject: [PATCH 11/83] chore: remove navigation --- lib/features/navigator/app_router.dart | 4 ---- lib/features/navigator/utils/navigation_commands.dart | 4 ---- lib/features/sks_chart/presentation/sks_chart_screen.dart | 2 -- .../presentation/widgets/sks_user_data_button.dart | 4 +--- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index 44c5d47d..54367d38 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -79,10 +79,6 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), - AutoRoute( - path: "/sks-chart", - page: SksChartRoute.page, - ), _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 37d2452c..2cbe1df4 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -76,8 +76,4 @@ extension NavigationX on WidgetRef { Future navigateToSksMenu() async { await _router.push(const SksMenuRoute()); } - - Future navigateToSksChart() async { - await _router.push(const SksChartRoute()); - } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 88a61cd9..fc1c3660 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,4 +1,3 @@ -import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -11,7 +10,6 @@ import "../data/repository/sks_chart_repository.dart"; import "chart_elements/sks_chart_legend_item.dart"; import "sks_chart.dart"; -@RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 0f8dfeeb..9fda8812 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -15,9 +15,7 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: (sksUsersData) => - // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), - _SksButton( + data: (sksUsersData) => _SksButton( sksUsersData, onTap: () async => showModalBottomSheet( context: context, From a4e352696f14206510e61df6c010bdc999d4ff23 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:33:56 +0100 Subject: [PATCH 12/83] chore : final formatting --- lib/features/navigator/app_router.dart | 2 +- .../presentation/sks_chart_screen.dart | 73 ---------------- .../presentation/sks_chart_sheet.dart | 84 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 7 +- 4 files changed, 87 insertions(+), 79 deletions(-) delete mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart_sheet.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index 54367d38..08999a17 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -15,7 +15,7 @@ import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; import "../sks-menu/presentation/sks_menu_screen.dart"; -import "../sks_chart/presentation/sks_chart_screen.dart"; +import "../sks_chart/presentation/sks_chart_sheet.dart"; import "root_view.dart"; part "app_router.g.dart"; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart deleted file mode 100644 index fc1c3660..00000000 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../utils/context_extensions.dart"; -import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../data/models/sks_chart_data.dart"; -import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; - -class SksChartView extends ConsumerWidget { - const SksChartView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asyncChartData = ref.watch(getLatestChartDataProvider); - final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: ListView( - children: [ - const Padding( - padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), - child: Center(child: LineHandle()), - ), - Padding( - padding: const EdgeInsets.only( - left: SksChartConfig.paddingMedium, - right: SksChartConfig.paddingMedium, - top: SksChartConfig.paddingMedium, - ), - child: Center( - child: Text( - context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, - ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart new file mode 100644 index 00000000..a2606ec7 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -0,0 +1,84 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../utils/context_extensions.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; +import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; + +class SksChartSheet extends ConsumerWidget { + const SksChartSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(SksChartConfig.paddingLarge), + child: _SksSheetHeader(), + ), + Expanded( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SksChartLegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + SksChartLegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SksSheetHeader extends StatelessWidget { + const _SksSheetHeader(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const LineHandle(), + const SizedBox(height: 8), + Text( + context.localize.sks_chart_title, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 9fda8812..ac5cd8d7 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../sks_chart/presentation/sks_chart_screen.dart"; +import "../../../sks_chart/presentation/sks_chart_sheet.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -24,10 +24,7 @@ class SksUserDataButton extends ConsumerWidget { FilterConfig.bottomSheetHeightFactor, ), isScrollControlled: true, - builder: (BuildContext context) => UncontrolledProviderScope( - container: ProviderScope.containerOf(context), - child: const SksChartView(), - ), + builder: (BuildContext context) => const SksChartSheet(), ), ), error: (error, stackTrace) => const SizedBox.shrink(), From ae92b4ebffeb780e132f7df67d2ae849bd38dc00 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:38:09 +0100 Subject: [PATCH 13/83] chore : change sks menu package name to reflect convention --- lib/features/navigator/app_router.dart | 3 +-- .../data/models/dish_category_enum.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_data.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_response.dart | 0 .../data/repository/sks_menu_repository.dart | 0 .../{sks-menu => sks_menu}/presentation/sks_menu_screen.dart | 4 ++-- .../presentation/widgets/sks_menu_data_source_link.dart | 0 .../presentation/widgets/sks_menu_header.dart | 0 .../presentation/widgets/sks_menu_section.dart | 0 .../presentation/widgets/sks_menu_tiles.dart | 0 10 files changed, 3 insertions(+), 4 deletions(-) rename lib/features/{sks-menu => sks_menu}/data/models/dish_category_enum.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_data.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_response.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/repository/sks_menu_repository.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/sks_menu_screen.dart (98%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_data_source_link.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_header.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_section.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_tiles.dart (100%) diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index 08999a17..1ec5006a 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -14,8 +14,7 @@ import "../navigation_tab_view/navigation_tab_view.dart"; import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; -import "../sks-menu/presentation/sks_menu_screen.dart"; -import "../sks_chart/presentation/sks_chart_sheet.dart"; +import "../sks_menu/presentation/sks_menu_screen.dart"; import "root_view.dart"; part "app_router.g.dart"; diff --git a/lib/features/sks-menu/data/models/dish_category_enum.dart b/lib/features/sks_menu/data/models/dish_category_enum.dart similarity index 100% rename from lib/features/sks-menu/data/models/dish_category_enum.dart rename to lib/features/sks_menu/data/models/dish_category_enum.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_data.dart b/lib/features/sks_menu/data/models/sks_menu_data.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_data.dart rename to lib/features/sks_menu/data/models/sks_menu_data.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_response.dart b/lib/features/sks_menu/data/models/sks_menu_response.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_response.dart rename to lib/features/sks_menu/data/models/sks_menu_response.dart diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks_menu/data/repository/sks_menu_repository.dart similarity index 100% rename from lib/features/sks-menu/data/repository/sks_menu_repository.dart rename to lib/features/sks_menu/data/repository/sks_menu_repository.dart diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart similarity index 98% rename from lib/features/sks-menu/presentation/sks_menu_screen.dart rename to lib/features/sks_menu/presentation/sks_menu_screen.dart index 433cc644..40c5d753 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks_menu/presentation/sks_menu_screen.dart @@ -52,9 +52,9 @@ class _SksMenuView extends StatelessWidget { final SksMenuResponse sksMenuData; @override Widget build(BuildContext context) { - /*if (!sksMenuData.isMenuOnline) { + if (!sksMenuData.isMenuOnline) { return const _SKSMenuLottieAnimation(); - }*/ + } return Scaffold( appBar: DetailViewAppBar( actions: const [ diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_header.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_header.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_header.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_section.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_section.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_section.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart From d5b93aacef09e3273277dbb0e278cc9afea9b64f Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:47:33 +0100 Subject: [PATCH 14/83] chore : change fl chart version & fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index b43523be..e117d58a 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - swapAnimationDuration: Duration.zero, + duration : Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() diff --git a/pubspec.yaml b/pubspec.yaml index 2428ca6d..34c4be99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 collection: ^1.19.1 - fl_chart: ^0.69.0 + fl_chart: ^0.69.2 permission_handler: ^11.3.1 flutter_widget_from_html_core: ^0.15.2 html: ^0.15.4 From 1bb6826804cc440102c0b580fd6637d18d64ece6 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:52:38 +0100 Subject: [PATCH 15/83] chore : fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index e117d58a..225d6158 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - duration : Duration.zero, + duration: Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() From 7dbaa07d894bff23beb4b98c100a56f8370473d6 Mon Sep 17 00:00:00 2001 From: Rafal Rejek <42467911+Rejfi@users.noreply.github.com> Date: Wed, 4 Dec 2024 22:41:28 +0100 Subject: [PATCH 16/83] feat(guide): add send your idea link at guide tile (#459) * Add mechanism to open default email app with predefined email address and subject. * Update text * Add internalization and use launch instead of _openUrl --------- Co-authored-by: Rafal Rejek --- lib/features/guide_view/guide_view.dart | 31 +++++++++++++++++++++---- lib/l10n/app_pl.arb | 3 +++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/features/guide_view/guide_view.dart b/lib/features/guide_view/guide_view.dart index b3cf7c0f..b0da88a3 100644 --- a/lib/features/guide_view/guide_view.dart +++ b/lib/features/guide_view/guide_view.dart @@ -6,6 +6,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../widgets/my_error_widget.dart"; import "../../config/ui_config.dart"; import "../../utils/context_extensions.dart"; +import "../../utils/launch_url_util.dart"; import "../../widgets/search_box_app_bar.dart"; import "../../widgets/wide_tile_card.dart"; import "../departments_view/widgets/departments_view_loading.dart"; @@ -58,7 +59,10 @@ class _GuideViewContent extends ConsumerWidget { AsyncValue(:final IList value) => GuideGrid( children: [ for (final item in value) GuideTile(item), - const _GuideInfo(), + _GuideInfo( + emailAddress: "kn.solvro@pwr.edu.pl", + subject: context.localize.guide_subject_default_content, + ), ].lock, ), _ => const Padding( @@ -69,14 +73,31 @@ class _GuideViewContent extends ConsumerWidget { } } -class _GuideInfo extends StatelessWidget { - const _GuideInfo(); +class _GuideInfo extends ConsumerWidget { + final String emailAddress; + final String? subject; + late final Uri emailLaunchUri; + + _GuideInfo({ + required this.emailAddress, + this.subject, + }) { + emailLaunchUri = Uri( + scheme: "mailto", + path: emailAddress, + query: "subject=$subject", + ); + } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return WideTileCard( title: context.localize.hi_student, - subtitle: context.localize.guide_development_info, + subtitle: context.localize.guide_ideas_info, + secondSubtitle: context.localize.guide_click_here, + onTap: () async { + await ref.launch(emailLaunchUri.toString()); + }, ); } } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0335b6cc..607c3c75 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -105,6 +105,9 @@ "student_councils": "Samorząd", "hi_student": "Hej studencie!", "guide_development_info": "Pamiętaj, że przewodnik jest wciąż w fazie rozwoju :)", + "guide_ideas_info": "Masz ciekawy pomysł? Podziel się z nami!", + "guide_click_here": "Zgłoś swój pomysł", + "guide_subject_default_content": "Pomsył na rozwój ToPWR", "streak_counter": "Używasz ToPWR nieprzerwanie od {days, plural, =1{1 dnia} other{{days} dni}}🔥", "@streak_counter": { "description": "A message with a single parameter", From c76538af38da11d920559f8bca667b1251e58426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Thu, 5 Dec 2024 23:59:43 +0100 Subject: [PATCH 17/83] feat: change nav actions color --- lib/features/home_view/widgets/nav_actions_section.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/home_view/widgets/nav_actions_section.dart b/lib/features/home_view/widgets/nav_actions_section.dart index f181cfbf..a6571438 100644 --- a/lib/features/home_view/widgets/nav_actions_section.dart +++ b/lib/features/home_view/widgets/nav_actions_section.dart @@ -60,7 +60,7 @@ class _NavActionButton extends StatelessWidget { child: Ink( decoration: BoxDecoration( shape: BoxShape.circle, - gradient: context.colorTheme.toPwrGradient, + color: context.colorTheme.orangePomegranade, ), child: InkWell( onTap: onTap, From 46b2b36284a359973518c9f4dd0308d5e465b925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliwier=20Dygda=C5=82owicz?= <70859223+thesun901@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:50:19 +0100 Subject: [PATCH 18/83] fix: eliminate rotate map error (#465) map_controller.dart: prevent _controllerCompleter from completing AnimatedMapController again after rotating --- lib/features/map_view/controllers/map_controller.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/features/map_view/controllers/map_controller.dart b/lib/features/map_view/controllers/map_controller.dart index 652eaf30..3169191f 100644 --- a/lib/features/map_view/controllers/map_controller.dart +++ b/lib/features/map_view/controllers/map_controller.dart @@ -17,7 +17,9 @@ class MyMapController { final _controllerCompleter = Completer(); Future get _controller => _controllerCompleter.future; void completeController(AnimatedMapController controller) { - _controllerCompleter.complete(controller); + if (!_controllerCompleter.isCompleted) { + _controllerCompleter.complete(controller); + } } Future zoomOnMarker(T item) async { From aa6c7d82319a36178b8fac9d103fafcf5e4c6c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20=C5=BBurawski?= <92211556+BombardierBulge@users.noreply.github.com> Date: Sun, 8 Dec 2024 18:45:15 +0100 Subject: [PATCH 19/83] fix: shadows on filter's screen (#475) --- lib/features/home_view/home_view.dart | 1 - .../widgets/buildings_section/buildings_section.dart | 8 +++++--- lib/features/home_view/widgets/paddings.dart | 9 +++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/features/home_view/home_view.dart b/lib/features/home_view/home_view.dart index 5c7d651b..d0f1eef7 100644 --- a/lib/features/home_view/home_view.dart +++ b/lib/features/home_view/home_view.dart @@ -46,7 +46,6 @@ class HomeView extends StatelessWidget { padding: EdgeInsets.only( left: horizontalPadding, // Align with the top bar right: safeAreaInsets.right, - bottom: HomeViewConfig.bottomPadding, ), child: KeepAliveHomeViewProviders( child: ListView.separated( diff --git a/lib/features/home_view/widgets/buildings_section/buildings_section.dart b/lib/features/home_view/widgets/buildings_section/buildings_section.dart index 2a1f888a..5ef96c76 100644 --- a/lib/features/home_view/widgets/buildings_section/buildings_section.dart +++ b/lib/features/home_view/widgets/buildings_section/buildings_section.dart @@ -37,9 +37,11 @@ class _BuildingsList extends ConsumerWidget { return switch (state) { AsyncError(:final error) => MyErrorWidget(error), AsyncValue(:final IList value) => SmallHorizontalPadding( - child: SizedBox( - height: 120, - child: _DataListBuildingsTiles(value), + child: MediumBottomPadding( + child: SizedBox( + height: 120, + child: _DataListBuildingsTiles(value), + ), ), ), _ => const MediumLeftPadding( diff --git a/lib/features/home_view/widgets/paddings.dart b/lib/features/home_view/widgets/paddings.dart index 8acb0a2f..908863a9 100644 --- a/lib/features/home_view/widgets/paddings.dart +++ b/lib/features/home_view/widgets/paddings.dart @@ -29,3 +29,12 @@ class MediumHorizontalPadding extends Padding { ), ); } + +class MediumBottomPadding extends Padding { + const MediumBottomPadding({super.key, super.child}) + : super( + padding: const EdgeInsets.only( + bottom: HomeViewConfig.paddingMedium, + ), + ); +} From cbf1c771046e7e112879456112b0b7cfc2117f4c Mon Sep 17 00:00:00 2001 From: Bartosh <101900992+24bartixx@users.noreply.github.com> Date: Sun, 8 Dec 2024 21:30:13 +0100 Subject: [PATCH 20/83] feat(digital-guide): create digital guide screen (#442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add temp button on main screen * feat: add simple view and navigation * feat: initially implement UI in building detail view * feat: add building info section UI * feat: add extenstion tiles, adjust building info section, lint rules * fix: lint rules * fix: review fixes * feat: rename building_details_view package to digital_guide_view and add initial api for digital guide without authorization handling * feat: add authorization for digital guide API (with token) and add loading animation * feat: fetch building data and present general info in detail view * fix: add variables for digital guide to example.env file * feat: create reamdme md file about API * feat: implement partially utilities and surrounding expansion tiles * fix: minor improvements after reviews * fix: improve readme.md for the API * refractor: remove image repository and fetch imageUrl in digital guide repository * chore: add TODO and reflection comments as follow-up to reviews * refractor: skip method extraction in amenities expansion tile * refractor: feature catalog structure * refractor: combine imageUrl and digitalGuideResponse into on class and remove error handling from fetching imageUrl * fix: add error handling while fetching image url for other data to display * docs: update and improve readme.md for the feature * fix: follow mock on Figma for accesibility button * fix: reorganize assets svg * refractor: replace SliverChildListDeletage with SliverChildBuilderDeletage * refractor: replace Columns with slivers * fix: lint rules and formatting * fix: ensure type safety * fix: post merge fix --------- Co-authored-by: Szymon Kowaliński --- assets/svg/digital_guide/storey.svg | 1 + example.env | 4 +- lib/config/env.dart | 5 + lib/config/ui_config.dart | 15 ++ .../amenities_expansion_tile_content.dart | 64 +++++++++ .../data/models/digital_guide_response.dart | 93 +++++++++++++ .../digital_guide_response_extended.dart | 62 +++++++++ .../repository/digital_guide_repository.dart | 51 +++++++ .../presentation/digital_guide_view.dart | 131 ++++++++++++++++++ .../widgets/accessibility_button.dart | 32 +++++ .../digital_guide_data_source_link.dart | 32 +++++ .../digital_guide_features_section.dart | 93 +++++++++++++ .../widgets/headlines_section.dart | 30 ++++ .../widgets/report_change_button.dart | 37 +++++ .../localization_expansion_tile_content.dart | 8 ++ lib/features/digital_guide_view/readme.md | 17 +++ .../data/models/surrounding_response.dart | 37 +++++ .../repository/surrounding_repository.dart | 19 +++ .../surroundings_expansion_tile_content.dart | 54 ++++++++ .../widgets/science_clubs_section.dart | 8 ++ lib/features/navigator/app_router.dart | 5 + .../navigator/utils/navigation_commands.dart | 4 + .../widgets/sks_menu_data_source_link.dart | 4 + lib/l10n/app_pl.arb | 36 ++++- lib/utils/determine_contact_icon.dart | 3 +- lib/widgets/detail_views/contact_section.dart | 15 +- pubspec.yaml | 3 +- 27 files changed, 852 insertions(+), 11 deletions(-) create mode 100644 assets/svg/digital_guide/storey.svg create mode 100644 lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart create mode 100644 lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart create mode 100644 lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart create mode 100644 lib/features/digital_guide_view/general_info/data/repository/digital_guide_repository.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_features_section.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart create mode 100644 lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart create mode 100644 lib/features/digital_guide_view/localization/presentation/localization_expansion_tile_content.dart create mode 100644 lib/features/digital_guide_view/readme.md create mode 100644 lib/features/digital_guide_view/surrounding/data/models/surrounding_response.dart create mode 100644 lib/features/digital_guide_view/surrounding/data/repository/surrounding_repository.dart create mode 100644 lib/features/digital_guide_view/surrounding/presentation/surroundings_expansion_tile_content.dart diff --git a/assets/svg/digital_guide/storey.svg b/assets/svg/digital_guide/storey.svg new file mode 100644 index 00000000..5d715829 --- /dev/null +++ b/assets/svg/digital_guide/storey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example.env b/example.env index 9ada0206..5afa411d 100644 --- a/example.env +++ b/example.env @@ -3,4 +3,6 @@ ASSETS_URL="https://<...>" IPARKING_URL="https://<...>" WIREDASH_ID="<...>" WIREDASH_SECRET="<...>" -SKS_URL="<...>" \ No newline at end of file +SKS_URL="<...>" +DIGITAL_GUIDE_URL="<...>" +DIGITAL_GUIDE_AUTHORIZATION_TOKEN="<...>" \ No newline at end of file diff --git a/lib/config/env.dart b/lib/config/env.dart index b3ba5328..b836a7bc 100644 --- a/lib/config/env.dart +++ b/lib/config/env.dart @@ -27,4 +27,9 @@ abstract class Env { static final String wiredashSecret = _Env.wiredashSecret; @EnviedField() static final String sksUrl = _Env.sksUrl; + @EnviedField() + static final String digitalGuideUrl = _Env.digitalGuideUrl; + @EnviedField() + static final String digitalGuideAuthorizationToken = + _Env.digitalGuideAuthorizationToken; } diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 58fc4bfa..64d44bed 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -9,6 +9,12 @@ abstract class MyAppConfig { "\u{a9} 2024 Koło Naukowe Solvro, Politechnika Wrocławska"; } +abstract class AppWidgetsConfig { + static const paddingMedium = + EdgeInsets.symmetric(horizontal: 16, vertical: 12); + static const borderRadiusMedium = 8.0; +} + abstract class SplashScreenConfig { static const additionalWaitDuration = Duration(seconds: 1); static const animationDuration = Duration(milliseconds: 800); @@ -201,6 +207,15 @@ abstract class NavigationTabViewConfig { static const navIconSize = 30.0; } +abstract class DigitalGuideConfig { + static const symetricalPaddingBig = + EdgeInsets.symmetric(vertical: 24, horizontal: 24); + static const borderRadiusMedium = 8.0; + static const heightSmall = 8.0; + static const heightBig = 24.0; + static const heightHuge = 48.0; +} + abstract class AlertDialogConfig { static const horizontalPadding = 14.0; static const verticalPadding = 20.0; diff --git a/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart b/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart new file mode 100644 index 00000000..e0fc7ede --- /dev/null +++ b/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart @@ -0,0 +1,64 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/widgets.dart"; + +import "../../../../gen/assets.gen.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../../utils/determine_contact_icon.dart"; +import "../../../../widgets/detail_views/contact_section.dart"; +import "../../general_info/data/models/digital_guide_response_extended.dart"; + +class AmenitiesExpansionTileContent extends StatelessWidget { + const AmenitiesExpansionTileContent({ + required this.digitalGuideResponseExtended, + }); + + final DigitalGuideResponseExtended digitalGuideResponseExtended; + + @override + Widget build(BuildContext context) { + return ContactSection( + list: [ + if (digitalGuideResponseExtended.canAssistanceDog) + ContactIconsModel( + text: context.localize.assistance_dog, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.isInductionLoop) + ContactIconsModel( + text: context.localize.induction_loop, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.isMicroNavigationSystem) + ContactIconsModel( + text: context.localize.micronavigation_system, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.areGuidancePaths) + ContactIconsModel( + text: context.localize.guidance_paths, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.areBrailleBoards) + ContactIconsModel( + text: context.localize.information_boards_with_braille_description, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.areLargeFontBoards) + ContactIconsModel( + text: context.localize.information_boards_with_large_font, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.isSignLanguageInterpreter) + ContactIconsModel( + text: context.localize.sign_language_interpreter, + icon: Assets.svg.contactIcons.compass, + ), + if (digitalGuideResponseExtended.areEmergencyChairs) + ContactIconsModel( + text: context.localize.emergency_chairs, + icon: Assets.svg.contactIcons.compass, + ), + ].lock, + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart new file mode 100644 index 00000000..5fc48050 --- /dev/null +++ b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart @@ -0,0 +1,93 @@ +// ignore_for_file: invalid_annotation_target + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "digital_guide_response.freezed.dart"; +part "digital_guide_response.g.dart"; + +@freezed +class DigitalGuideResponse with _$DigitalGuideResponse { + const factory DigitalGuideResponse({ + required int id, + required DigitalGuideTranslations translations, + @JsonKey(name: "number_of_storeys") required int numberOfStoreys, + @JsonKey( + name: "is_possibility_to_enter_with_assistance_dog", + fromJson: _stringToBool, + ) + required bool canAssistanceDog, + @JsonKey( + name: "is_induction_loop", + fromJson: _stringToBool, + ) + required bool isInductionLoop, + @JsonKey( + name: "is_micronavigation_system", + fromJson: _stringToBool, + ) + required bool isMicroNavigationSystem, + @JsonKey( + name: "are_guidance_paths", + fromJson: _stringToBool, + ) + required bool areGuidancePaths, + @JsonKey( + name: "are_information_boards_with_braille_description", + fromJson: _stringToBool, + ) + required bool areBrailleBoards, + @JsonKey( + name: "are_information_boards_with_large_font", + fromJson: _stringToBool, + ) + required bool areLargeFontBoards, + @JsonKey( + name: "is_sign_language_interpreter", + fromJson: _stringToBool, + ) + required bool isSignLanguageInterpreter, + @JsonKey( + name: "are_emergency_chairs", + fromJson: _stringToBool, + ) + required bool areEmergencyChairs, + @JsonKey(name: "telephone_number", fromJson: _formatTelephoneNumber) + required String telephoneNumber, + @JsonKey(name: "surrounding") required int surroundingId, + required List images, + String? imageUrl, + }) = _DigitalGuideResponse; + + factory DigitalGuideResponse.fromJson(Map json) => + _$DigitalGuideResponseFromJson(json); +} + +@freezed +class DigitalGuideTranslations with _$DigitalGuideTranslations { + const factory DigitalGuideTranslations({ + @JsonKey(name: "pl") required DigitalGuideTranslation plTranslation, + }) = _DigitalGuideTranslations; + + factory DigitalGuideTranslations.fromJson(Map json) => + _$DigitalGuideTranslationsFromJson(json); +} + +@freezed +class DigitalGuideTranslation with _$DigitalGuideTranslation { + const factory DigitalGuideTranslation({ + required String name, + @JsonKey(name: "extended_name") required String extendedName, + required String address, + }) = _DigitalGuideTranslation; + + factory DigitalGuideTranslation.fromJson(Map json) => + _$DigitalGuideTranslationFromJson(json); +} + +bool _stringToBool(String value) { + return value == "True"; +} + +String _formatTelephoneNumber(String telephoneNumber) { + return telephoneNumber.replaceAll("

", "").replaceAll("

", ""); +} diff --git a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart new file mode 100644 index 00000000..7320e0ad --- /dev/null +++ b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart @@ -0,0 +1,62 @@ +import "dart:core"; + +import "digital_guide_response.dart"; + +class DigitalGuideResponseExtended { + const DigitalGuideResponseExtended({ + required this.id, + required this.translations, + required this.numberOfStoreys, + required this.canAssistanceDog, + required this.isInductionLoop, + required this.isMicroNavigationSystem, + required this.areGuidancePaths, + required this.areBrailleBoards, + required this.areLargeFontBoards, + required this.isSignLanguageInterpreter, + required this.areEmergencyChairs, + required this.telephoneNumber, + required this.surroundingId, + required this.images, + required this.imageUrl, + }); + + final int id; + final DigitalGuideTranslations translations; + final int numberOfStoreys; + final bool canAssistanceDog; + final bool isInductionLoop; + final bool isMicroNavigationSystem; + final bool areGuidancePaths; + final bool areBrailleBoards; + final bool areLargeFontBoards; + final bool isSignLanguageInterpreter; + final bool areEmergencyChairs; + final String telephoneNumber; + final int surroundingId; + final List images; + final String? imageUrl; + + factory DigitalGuideResponseExtended.fromDigitalGuideResponse({ + required DigitalGuideResponse digitalGuideResponse, + required String? imageUrl, + }) { + return DigitalGuideResponseExtended( + id: digitalGuideResponse.id, + translations: digitalGuideResponse.translations, + numberOfStoreys: digitalGuideResponse.numberOfStoreys, + canAssistanceDog: digitalGuideResponse.canAssistanceDog, + isInductionLoop: digitalGuideResponse.isInductionLoop, + isMicroNavigationSystem: digitalGuideResponse.isMicroNavigationSystem, + areGuidancePaths: digitalGuideResponse.areGuidancePaths, + areBrailleBoards: digitalGuideResponse.areBrailleBoards, + areLargeFontBoards: digitalGuideResponse.areLargeFontBoards, + isSignLanguageInterpreter: digitalGuideResponse.isSignLanguageInterpreter, + areEmergencyChairs: digitalGuideResponse.areEmergencyChairs, + telephoneNumber: digitalGuideResponse.telephoneNumber, + surroundingId: digitalGuideResponse.surroundingId, + images: digitalGuideResponse.images, + imageUrl: imageUrl, + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/data/repository/digital_guide_repository.dart b/lib/features/digital_guide_view/general_info/data/repository/digital_guide_repository.dart new file mode 100644 index 00000000..ebe4a17f --- /dev/null +++ b/lib/features/digital_guide_view/general_info/data/repository/digital_guide_repository.dart @@ -0,0 +1,51 @@ +import "package:flutter/foundation.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../../api_base_rest/client/dio_client.dart"; +import "../../../../../config/env.dart"; +import "../models/digital_guide_response.dart"; +import "../models/digital_guide_response_extended.dart"; + +part "digital_guide_repository.g.dart"; + +@riverpod +Future getDigitalGuideData( + Ref ref, + int id, +) async { + final digitalGuideUrl = "${Env.digitalGuideUrl}/buildings/$id"; + final dio = ref.read(restClientProvider); + dio.options.headers["Authorization"] = + "Token ${Env.digitalGuideAuthorizationToken}"; + final response = await dio.get(digitalGuideUrl); + final digitalGuideResponse = + DigitalGuideResponse.fromJson(response.data as Map); + final imageUrl = await getImageUrl(ref, digitalGuideResponse.images[0]); + return DigitalGuideResponseExtended.fromDigitalGuideResponse( + digitalGuideResponse: digitalGuideResponse, + imageUrl: imageUrl, + ); +} + +@riverpod +Future getImageUrl(Ref ref, int id) async { + final digitalGuideUrl = "${Env.digitalGuideUrl}/images/$id"; + final dio = ref.read(restClientProvider); + dio.options.headers["Authorization"] = + "Token ${Env.digitalGuideAuthorizationToken}"; + + final response = await dio.get(digitalGuideUrl); + + // if only fetching image url fails I want data to be presented anyway + if (response.data is! Map) { + debugPrint("Failed to fetch image url!"); + return null; + } + + final Map responseData = + response.data as Map; + final imageUrl = responseData["image_960w"]; + + return imageUrl; +} diff --git a/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart b/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart new file mode 100644 index 00000000..eda567fc --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart @@ -0,0 +1,131 @@ +import "package:auto_route/annotations.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../gen/assets.gen.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../../utils/determine_contact_icon.dart"; +import "../../../../widgets/detail_views/contact_section.dart"; +import "../../../../widgets/detail_views/detail_view_app_bar.dart"; +import "../../../../widgets/my_cached_image.dart"; +import "../../../../widgets/my_error_widget.dart"; +import "../data/models/digital_guide_response_extended.dart"; +import "../data/repository/digital_guide_repository.dart"; +import "widgets/accessibility_button.dart"; +import "widgets/digital_guide_data_source_link.dart"; +import "widgets/digital_guide_features_section.dart"; +import "widgets/headlines_section.dart"; +import "widgets/report_change_button.dart"; + +@RoutePage() +class DigitalGuideView extends ConsumerWidget { + const DigitalGuideView({ + @PathParam("id") required this.id, + }); + + final int id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncDigitalGuideData = ref.watch(getDigitalGuideDataProvider(id)); + // question: Should the app bar appear during loading or when there's an error? + // Now it doesn't, neither does it appear on SKS menu screen + return asyncDigitalGuideData.when( + data: _DigitalGuideView.new, + error: (error, stackTrace) => MyErrorWidget(error), + // TODO(Bartosh): shimmer loading + loading: () => const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } +} + +class _DigitalGuideView extends ConsumerWidget { + const _DigitalGuideView(this.digitalGuideResponseExtended); + + final DigitalGuideResponseExtended digitalGuideResponseExtended; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final widgets1 = [ + const SizedBox(height: DigitalGuideConfig.heightSmall), + MyCachedImage( + digitalGuideResponseExtended.imageUrl, + ), + HeadlinesSection( + // There is only Polish language translation in external API + // In the future we must think how to handle multiple translations in UI + // For now it can be temporarily dealt with in the data layer + name: digitalGuideResponseExtended.translations.plTranslation.name, + description: digitalGuideResponseExtended + .translations.plTranslation.extendedName, + ), + ContactSection( + list: IList([ + ContactIconsModel( + text: digitalGuideResponseExtended + .translations.plTranslation.address + .replaceAll("ulica", "ul."), + icon: Assets.svg.contactIcons.compass, + ), + ContactIconsModel( + text: digitalGuideResponseExtended.telephoneNumber, + icon: Assets.svg.contactIcons.phone, + // TODO(Bartosh): url not working, nothing happens + url: + "tel:+48${digitalGuideResponseExtended.telephoneNumber.replaceAll("

", "").replaceAll("

", "")}", + ), + ContactIconsModel( + text: context.localize + .storeys(digitalGuideResponseExtended.numberOfStoreys), + icon: Assets.svg.digitalGuide.storey, + ), + ]), + ), + const SizedBox(height: DigitalGuideConfig.heightBig), + ]; + + final widgets2 = [ + const SizedBox(height: DigitalGuideConfig.heightBig), + DigitalGuideDataSourceLink(), + ReportChangeButton(), + const SizedBox(height: DigitalGuideConfig.heightHuge), + ]; + + return Scaffold( + appBar: DetailViewAppBar( + actions: [ + AccessibilityButton(), + ], + ), + body: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return widgets1[index]; + }, + childCount: widgets1.length, + ), + ), + DigitalGuideFeaturesSection( + digitalGuideResponseExtended: digitalGuideResponseExtended, + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return widgets2[index]; + }, + childCount: widgets2.length, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart new file mode 100644 index 00000000..8951fa11 --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart @@ -0,0 +1,32 @@ +import "package:flutter/material.dart"; + +import "../../../../../config/ui_config.dart"; +import "../../../../../theme/app_theme.dart"; + +class AccessibilityButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + DigitalGuideConfig.borderRadiusMedium, + ), + side: BorderSide( + color: context.colorTheme.greyPigeon, + ), + ), + backgroundColor: context.colorTheme.greyLight, + minimumSize: const Size(56, 32), + ), + child: const Icon( + Icons.accessible, + color: Colors.black, + ), + ), + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart new file mode 100644 index 00000000..f3a19e5e --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart @@ -0,0 +1,32 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../../../theme/app_theme.dart"; +import "../../../../../utils/context_extensions.dart"; + +class DigitalGuideDataSourceLink extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + return Text.rich( + TextSpan( + text: "${context.localize.data_come_from_website}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: context.localize.digital_guide_website, + style: context.textTheme.bodyOrange.copyWith( + decoration: TextDecoration.underline, + decorationColor: context.colorTheme.orangePomegranade, + fontWeight: FontWeight.bold, + ), + // TODO(Bartosh): on tap url handling -> webbrowser launch + ), + ], + ), + textAlign: TextAlign.center, + style: context.textTheme.body, + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_features_section.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_features_section.dart new file mode 100644 index 00000000..704a7bf5 --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_features_section.dart @@ -0,0 +1,93 @@ +import "package:flutter/material.dart"; +import "package:flutter/widgets.dart"; + +import "../../../../../utils/context_extensions.dart"; +import "../../../../../widgets/my_expansion_tile.dart"; +import "../../../amenities/presentation/amenities_expansion_tile_content.dart"; +import "../../../localization/presentation/localization_expansion_tile_content.dart"; +import "../../../surrounding/presentation/surroundings_expansion_tile_content.dart"; +import "../../data/models/digital_guide_response_extended.dart"; + +typedef TileContent = ({String title, List content}); + +class DigitalGuideFeaturesSection extends StatelessWidget { + const DigitalGuideFeaturesSection({ + required this.digitalGuideResponseExtended, + }); + + final DigitalGuideResponseExtended digitalGuideResponseExtended; + + @override + Widget build(BuildContext context) { + final items = [ + ( + title: context.localize.localization, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.amenities, + content: [ + AmenitiesExpansionTileContent( + digitalGuideResponseExtended: digitalGuideResponseExtended, + ), + ], + ), + ( + title: context.localize.surroundings, + content: [ + SurroundingsExpansionTileContent( + digitalGuideResponseExtended: digitalGuideResponseExtended, + ), + ], + ), + ( + title: context.localize.transport, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.entrances, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.elevators, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.toilets, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.micro_navigation, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.building_structure, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.room_information, + content: [LocalizationExpansionTileContent()], + ), + ( + title: context.localize.evacuation, + content: [LocalizationExpansionTileContent()], + ), + ]; + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = items[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: MyExpansionTile( + title: item.title, + children: item.content, + ), + ); + }, + childCount: items.length, + ), + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart new file mode 100644 index 00000000..77452d0c --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart @@ -0,0 +1,30 @@ +import "package:flutter/widgets.dart"; + +import "../../../../../config/ui_config.dart"; + +class HeadlinesSection extends StatelessWidget { + const HeadlinesSection({ + required this.name, + required this.description, + }); + + final String name; + final String description; + + @override + Widget build(BuildContext context) { + return Padding( + padding: DigitalGuideConfig.symetricalPaddingBig, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), + ), + Text(description), + ], + ), + ); + } +} diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart new file mode 100644 index 00000000..623d5518 --- /dev/null +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart @@ -0,0 +1,37 @@ +import "package:flutter/material.dart"; + +import "../../../../../config/ui_config.dart"; +import "../../../../../theme/app_theme.dart"; +import "../../../../../utils/context_extensions.dart"; + +class ReportChangeButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Padding( + padding: AppWidgetsConfig.paddingMedium, + child: Column( + children: [ + Text(context.localize.change_report_title), + const SizedBox(height: 8), + ElevatedButton( + // TODO(Bartosh): handle action + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: context.colorTheme.blueAzure, + padding: AppWidgetsConfig.paddingMedium, + minimumSize: const Size(144, 40), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppWidgetsConfig.borderRadiusMedium), + ), + ), + child: Text( + context.localize.change_report_button, + style: TextStyle(color: context.colorTheme.whiteSoap), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/digital_guide_view/localization/presentation/localization_expansion_tile_content.dart b/lib/features/digital_guide_view/localization/presentation/localization_expansion_tile_content.dart new file mode 100644 index 00000000..8f8dcec2 --- /dev/null +++ b/lib/features/digital_guide_view/localization/presentation/localization_expansion_tile_content.dart @@ -0,0 +1,8 @@ +import "package:flutter/widgets.dart"; + +class LocalizationExpansionTileContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const Text("Lorem ipsum text siuuuu Cristiano Rolando"); + } +} diff --git a/lib/features/digital_guide_view/readme.md b/lib/features/digital_guide_view/readme.md new file mode 100644 index 00000000..e8b7f135 --- /dev/null +++ b/lib/features/digital_guide_view/readme.md @@ -0,0 +1,17 @@ +# Requirements +* Two variables should be added to .env + * DIGITAL_GUIDE_URL + * DIGITAL_GUIDE_AUTHORIZATION_TOKEN + +# Tips +* All HTTP requests must include the authorization token ("Token ${Env.digitalGuideAuthorizationToken"}) +* The complete list of endpoints is available after logging under [this](https://przewodnik.pwr.edu.pl/swagger/) link + +# Used endpoints +1) Building data and image + * /general_info/data/repository/digita_guide_repository.dart + * DIGITAL_GUIDE_URL/buildings/{id} + * DIGITAL_GUIDE_URL/images/{id} +2) Surroundings data + * /surrounding/data/repository/surrounding_repository.dart + * DIGITAL_GUIDE_URL/surroundings/{id} diff --git a/lib/features/digital_guide_view/surrounding/data/models/surrounding_response.dart b/lib/features/digital_guide_view/surrounding/data/models/surrounding_response.dart new file mode 100644 index 00000000..765636cc --- /dev/null +++ b/lib/features/digital_guide_view/surrounding/data/models/surrounding_response.dart @@ -0,0 +1,37 @@ +// ignore_for_file: invalid_annotation_target + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "surrounding_response.freezed.dart"; +part "surrounding_response.g.dart"; + +@freezed +class SurroundingResponse with _$SurroundingResponse { + const factory SurroundingResponse({ + required SurroundingResponseTranslations translations, + }) = _SurroundingResponse; + + factory SurroundingResponse.fromJson(Map json) => + _$SurroundingResponseFromJson(json); +} + +@freezed +class SurroundingResponseTranslations with _$SurroundingResponseTranslations { + const factory SurroundingResponseTranslations({ + @JsonKey(name: "pl") required SurroundingResponseTranslation translationPl, + }) = _SurroundingResponseTranslations; + + factory SurroundingResponseTranslations.fromJson(Map json) => + _$SurroundingResponseTranslationsFromJson(json); +} + +@freezed +class SurroundingResponseTranslation with _$SurroundingResponseTranslation { + const factory SurroundingResponseTranslation({ + @JsonKey(name: "are_parking_spaces_comment") + required String parkingSpacesComment, + }) = _SurroundingResponseTranslation; + + factory SurroundingResponseTranslation.fromJson(Map json) => + _$SurroundingResponseTranslationFromJson(json); +} diff --git a/lib/features/digital_guide_view/surrounding/data/repository/surrounding_repository.dart b/lib/features/digital_guide_view/surrounding/data/repository/surrounding_repository.dart new file mode 100644 index 00000000..3b57dd78 --- /dev/null +++ b/lib/features/digital_guide_view/surrounding/data/repository/surrounding_repository.dart @@ -0,0 +1,19 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../../api_base_rest/client/dio_client.dart"; +import "../../../../../config/env.dart"; +import "../models/surrounding_response.dart"; + +part "surrounding_repository.g.dart"; + +@riverpod +Future getSurroundingData(Ref ref, int id) async { + final surroundingUrl = "${Env.digitalGuideUrl}/surroundings/$id"; + final dio = ref.read(restClientProvider); + dio.options.headers["Authorization"] = + "Token ${Env.digitalGuideAuthorizationToken}"; + final response = await dio.get(surroundingUrl); + + return SurroundingResponse.fromJson(response.data as Map); +} diff --git a/lib/features/digital_guide_view/surrounding/presentation/surroundings_expansion_tile_content.dart b/lib/features/digital_guide_view/surrounding/presentation/surroundings_expansion_tile_content.dart new file mode 100644 index 00000000..7bc4e436 --- /dev/null +++ b/lib/features/digital_guide_view/surrounding/presentation/surroundings_expansion_tile_content.dart @@ -0,0 +1,54 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../../utils/context_extensions.dart"; +import "../../../../widgets/my_error_widget.dart"; +import "../../general_info/data/models/digital_guide_response_extended.dart"; +import "../data/models/surrounding_response.dart"; +import "../data/repository/surrounding_repository.dart"; + +class SurroundingsExpansionTileContent extends ConsumerWidget { + const SurroundingsExpansionTileContent({ + required this.digitalGuideResponseExtended, + }); + + final DigitalGuideResponseExtended digitalGuideResponseExtended; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncSurroundingData = ref.watch( + getSurroundingDataProvider(digitalGuideResponseExtended.surroundingId), + ); + + return asyncSurroundingData.when( + data: (surroundingData) => _SurroundingExpansionTileContent( + surroundingResponse: surroundingData, + ), + error: (error, stackTrace) => MyErrorWidget(error), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + ); + } +} + +class _SurroundingExpansionTileContent extends ConsumerWidget { + const _SurroundingExpansionTileContent({ + required this.surroundingResponse, + }); + + final SurroundingResponse surroundingResponse; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + children: [ + Text( + context.localize.parking_location( + surroundingResponse.translations.translationPl.parkingSpacesComment, + ), + ), + ], + ); + } +} diff --git a/lib/features/home_view/widgets/science_clubs_section.dart b/lib/features/home_view/widgets/science_clubs_section.dart index 133d3a50..b7b57859 100644 --- a/lib/features/home_view/widgets/science_clubs_section.dart +++ b/lib/features/home_view/widgets/science_clubs_section.dart @@ -1,3 +1,5 @@ +import "dart:async"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -24,6 +26,12 @@ class ScienceClubsSection extends ConsumerWidget { actionTitle: context.localize.list, onClick: ref.navigateScienceClubs, ), + FilledButton( + onPressed: () { + unawaited(ref.navigateDigitalGuide(101)); + }, + child: const Text("Navigate to digital guide screen!"), + ), const _ScienceClubsList(), ], ); diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index d2cc6ae9..ab86a511 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -7,6 +7,7 @@ import "../about_us_view/about_us_view.dart"; import "../buildings_view/buildings_view.dart"; import "../department_detail_view/department_detail_view.dart"; import "../departments_view/departments_view.dart"; +import "../digital_guide_view/general_info/presentation/digital_guide_view.dart"; import "../guide_detail_view/guide_detail_view.dart"; import "../guide_view/guide_view.dart"; import "../home_view/home_view.dart"; @@ -94,6 +95,10 @@ class AppRouter extends RootStackRouter { path: "/sci-clubs/:id", page: ScienceClubDetailRoute.page, ), + AutoRoute( + path: "/digital-guide/:id", + page: DigitalGuideRoute.page, + ), ]; } diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 2cbe1df4..5c492ab6 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -76,4 +76,8 @@ extension NavigationX on WidgetRef { Future navigateToSksMenu() async { await _router.push(const SksMenuRoute()); } + + Future navigateDigitalGuide(int id) async { + await _router.push(DigitalGuideRoute(id: id)); + } } diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart index eaebd5bf..21c917b6 100644 --- a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart @@ -17,12 +17,16 @@ class SksMenuDataSourceLink extends ConsumerWidget { return Text.rich( TextSpan( text: "${context.localize.data_come_from_website}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), children: [ TextSpan( text: SksMenuConfig.sksDataSource.replaceFirst("https://", "www."), style: context.textTheme.bodyOrange.copyWith( decoration: TextDecoration.underline, decorationColor: context.colorTheme.orangePomegranade, + fontWeight: FontWeight.bold, ), recognizer: TapGestureRecognizer() ..onTap = () async => ref.launch(SksMenuConfig.sksDataSource), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 607c3c75..2eda3387 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -150,8 +150,42 @@ "settings": "Ustawienia", "about_the_app": "O aplikacji", "other_view" : "Inne", + "map" : "Mapa", + "change_report_title" : "Coś się zmieniło?", + "change_report_button" : "Zgłoś zmianę", + "localization" : "Lokalizacja", + "amenities" : "Udogodnienia", + "surroundings": "Otoczenie", + "transport" : "Dojazd", + "entrances" : "Wejścia", + "elevators" : "Windy", + "toilets" : "Toilets", + "micro_navigation" : "Mikronawigacja", + "building_structure" : "Struktura budynku", + "room_information" : "Pomieszczenia", + "evacuation" : "Ewakuacja", + "storeys" : "{number, plural, =1{{number} piętro} few{{number} piętra} other{{number} pięter}}", + "@storeys" : { + "description" : "number of storeys with one parameter", + "placeholders": { + "number": { + "type": "int", + "example": "10" + } + } + }, + "assistance_dog" : "Do budynku i wszystkich jego pomieszczeń można wejść z psem asystującym i psem przewodnikiem", + "induction_loop": "W budynku jest/są pętle indukcyjne", + "micronavigation_system": "W budynku zostały zainstalowane urządzenia systemu nawigacyjno-informacyjnego", + "guidance_paths": "W budynku zastosowane zostały ścieżki naprowadzające (dotykowe)", + "information_boards_with_braille_description": "W budynku znajdują się czytelne tablice informacyjne zawierające opisy w alfabecie Braille'a", + "information_boards_with_large_font": "W budynku znajdują się czytelne tablice informacyjne zawierające napisy w dużej czcionce", + "sign_language_interpreter": "W budynku zapewniona jest możliwość skorzystania z usług tłumacza języka migowego", + "emergency_chairs": "W budynku zamieszczone zostały krzesła ewakuacyjne", + "parking_location" : "Miejsca parkingowe znajdują się {location}", + "digital_guide_website" : "www.przewodnik.pwr.edu.pl", "sks_old_menu": "Zobacz ostatnie menu", "sks_menu_closed" : "SKS Menu jest teraz niedostępne", "confirm": "Zatwierdź", "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz." -} \ No newline at end of file +} diff --git a/lib/utils/determine_contact_icon.dart b/lib/utils/determine_contact_icon.dart index 5e7c9807..c948a412 100644 --- a/lib/utils/determine_contact_icon.dart +++ b/lib/utils/determine_contact_icon.dart @@ -13,7 +13,8 @@ class ContactIconsModel { ContactIconsModel({ String? text, this.url, - }) : icon = url.determineIcon(), + String? icon, + }) : icon = icon ?? url.determineIcon(), order = url.determineIconOrder(), text = text ?? url; } diff --git a/lib/widgets/detail_views/contact_section.dart b/lib/widgets/detail_views/contact_section.dart index 24827dc0..d8b8c639 100644 --- a/lib/widgets/detail_views/contact_section.dart +++ b/lib/widgets/detail_views/contact_section.dart @@ -1,6 +1,6 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/cupertino.dart"; import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../theme/app_theme.dart"; @@ -9,10 +9,10 @@ import "../../utils/launch_url_util.dart"; import "contact_icon_widget.dart"; class ContactSection extends StatelessWidget { - const ContactSection({super.key, required this.list, required this.title}); + const ContactSection({super.key, required this.list, this.title}); final IList list; - final String title; + final String? title; @override Widget build(BuildContext context) { @@ -23,8 +23,10 @@ class ContactSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: context.textTheme.headline), - const SizedBox(height: 16), + if (title != null) ...[ + Text(title!, style: context.textTheme.headline), + const SizedBox(height: 16), + ], for (final item in sorted) Padding( padding: const EdgeInsets.only(bottom: 16), @@ -59,11 +61,10 @@ class _ContactIcon extends ConsumerWidget { const SizedBox(width: 16), Expanded( child: RichText( - overflow: TextOverflow.ellipsis, - maxLines: 2, text: TextSpan( text: text, style: context.textTheme.bodyOrange.copyWith( + color: url.isNotEmpty ? null : Colors.black, decoration: url.isNotEmpty ? TextDecoration.underline : TextDecoration.none, diff --git a/pubspec.yaml b/pubspec.yaml index a38932a5..143a7767 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -145,6 +145,7 @@ flutter: - assets/svg/contact_icons/x.svg - assets/svg/contact_icons/tiktok.svg - assets/svg/contact_icons/discord.svg + - assets/svg/digital_guide/storey.svg - assets/animations/error.json - assets/animations/search.json - assets/animations/offline.json @@ -223,4 +224,4 @@ flutter_launcher_icons: image_path: "assets/png/app_icon.png" macos: generate: true - image_path: "assets/png/app_icon.png" + image_path: "assets/png/app_icon.png" \ No newline at end of file From d04ae042bf99d98259bf30face42975a3d84f5ba Mon Sep 17 00:00:00 2001 From: Maja Mroczek <152724796+mmzek@users.noreply.github.com> Date: Mon, 9 Dec 2024 00:27:03 +0100 Subject: [PATCH 21/83] feat(sks-menu): add "see previously available menu" (#461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Mikołaj Jałocha <76820915+mikolaj-jalocha@users.noreply.github.com> Co-authored-by: Szymon Kowaliński Co-authored-by: mikolaj-jalocha --- .../home_view/widgets/parkings_section.dart | 4 + .../presentation/sks_menu_screen.dart | 124 ++++++++++++------ lib/l10n/app_pl.arb | 1 + lib/shared_api_clients/sks_api_client.dart | 16 +++ 4 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 lib/shared_api_clients/sks_api_client.dart diff --git a/lib/features/home_view/widgets/parkings_section.dart b/lib/features/home_view/widgets/parkings_section.dart index 7e483173..b534a8c4 100644 --- a/lib/features/home_view/widgets/parkings_section.dart +++ b/lib/features/home_view/widgets/parkings_section.dart @@ -25,6 +25,10 @@ class ParkingsSection extends ConsumerWidget { actionTitle: context.localize.map_button, onClick: ref.navigateParkings, ), + FilledButton( + onPressed: ref.navigateToSksMenu, + child: const Text("navigate to sks menu"), + ), const _ParkingsList(), ], ); diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index c06e5622..3f8a9255 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -2,7 +2,8 @@ import "dart:core"; import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:logger/logger.dart"; import "package:lottie/lottie.dart"; @@ -20,22 +21,28 @@ import "widgets/sks_menu_header.dart"; import "widgets/sks_menu_section.dart"; @RoutePage() -class SksMenuView extends ConsumerWidget { +class SksMenuView extends HookConsumerWidget { const SksMenuView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final asyncSksMenuData = ref.watch(getSksMenuDataProvider); + final isLastMenuButtonClicked = useState(false); return asyncSksMenuData.when( - data: (sksMenuData) => _SksMenuView( - asyncSksMenuData.value ?? - SksMenuResponse( - isMenuOnline: false, - lastUpdate: DateTime.now(), - meals: List.empty(), - ), - ), + data: (sksMenuData) { + if (!sksMenuData.isMenuOnline && !isLastMenuButtonClicked.value) { + return _SKSMenuLottieAnimation( + onShowLastMenuTap: () { + isLastMenuButtonClicked.value = true; + }, + ); + } + return _SksMenuView( + sksMenuData: sksMenuData, + isLastMenuButtonClicked: isLastMenuButtonClicked.value, + ); + }, error: (error, stackTrace) => _SKSMenuLottieAnimation(error: error), loading: () => const Scaffold( body: Center( @@ -47,12 +54,17 @@ class SksMenuView extends ConsumerWidget { } class _SksMenuView extends StatelessWidget { - const _SksMenuView(this.sksMenuData); + const _SksMenuView({ + required this.sksMenuData, + required this.isLastMenuButtonClicked, + }); final SksMenuResponse sksMenuData; + final bool isLastMenuButtonClicked; + @override Widget build(BuildContext context) { - if (!sksMenuData.isMenuOnline) { + if (!isLastMenuButtonClicked && !sksMenuData.isMenuOnline) { return const _SKSMenuLottieAnimation(); } return Scaffold( @@ -84,50 +96,86 @@ class _SksMenuView extends StatelessWidget { } } -class _SKSMenuLottieAnimation extends StatelessWidget { +class _SKSMenuLottieAnimation extends HookWidget { const _SKSMenuLottieAnimation({ this.error, + this.onShowLastMenuTap, }); final Object? error; + final VoidCallback? onShowLastMenuTap; + @override Widget build(BuildContext context) { - Logger().e(error.toString()); + final isAnimationCompleted = useState(false); + + if (error != null) { + Logger().e(error.toString()); + } + return Scaffold( appBar: DetailViewAppBar( actions: const [ SksUserDataButton(), ], ), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, + body: Stack( + alignment: Alignment.center, children: [ - SizedBox.square( - dimension: 200, - child: Lottie.asset( - Assets.animations.sksClosed, - fit: BoxFit.cover, - repeat: false, - frameRate: const FrameRate(LottieAnimationConfig.frameRate), - renderCache: RenderCache.drawingCommands, - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 200), + if (isAnimationCompleted.value) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + context.localize.sks_menu_closed, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + if (error != null && isAnimationCompleted.value) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + error.toString(), + style: context.textTheme.titleGrey, + textAlign: TextAlign.center, + ), + ), + if (onShowLastMenuTap != null && isAnimationCompleted.value) + Padding( + padding: const EdgeInsets.only(top: 16), + child: ElevatedButton( + onPressed: onShowLastMenuTap, + child: Text( + context.localize.sks_show_last_menu, + style: context.textTheme.lightTitle, + textAlign: TextAlign.center, + ), + ), + ), + ], ), Align( - child: Text( - context.localize.sks_menu_closed, - style: context.textTheme.headline, - textAlign: TextAlign.center, - ), - ), - if (error != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - error.toString(), - style: context.textTheme.titleGrey, - textAlign: TextAlign.center, + child: SizedBox.square( + dimension: 200, + child: Lottie.asset( + Assets.animations.sksClosed, + fit: BoxFit.cover, + repeat: false, + frameRate: const FrameRate(LottieAnimationConfig.frameRate), + renderCache: RenderCache.drawingCommands, + onLoaded: (composition) { + final totalDuration = composition.duration; + Future.delayed(totalDuration, () { + isAnimationCompleted.value = true; + }); + }, ), ), + ), ], ), ); diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2eda3387..47fe15f2 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -186,6 +186,7 @@ "digital_guide_website" : "www.przewodnik.pwr.edu.pl", "sks_old_menu": "Zobacz ostatnie menu", "sks_menu_closed" : "SKS Menu jest teraz niedostępne", + "sks_show_last_menu" : "Pokaż ostatnio dostępne menu", "confirm": "Zatwierdź", "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz." } diff --git a/lib/shared_api_clients/sks_api_client.dart b/lib/shared_api_clients/sks_api_client.dart new file mode 100644 index 00000000..96579d64 --- /dev/null +++ b/lib/shared_api_clients/sks_api_client.dart @@ -0,0 +1,16 @@ +import "package:dio/dio.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../config/env.dart"; + +part "sks_api_client.g.dart"; + +@riverpod +Dio sksClient(Ref ref) { + return Dio( + BaseOptions( + baseUrl: Env.sksUrl, + ), + ); +} From b6bc65409384efbf2b65773d331c23b80ef08618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Ja=C5=82ocha?= <76820915+mikolaj-jalocha@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:12:12 +0100 Subject: [PATCH 22/83] docs: update .env section to include digital guide's variables (#478) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 97815799..beb7a6d7 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ IPARKING_URL="https://.pl" WIREDASH_ID="<...>" # can be left empty WIREDASH_SECRET="<...>" # can be left empty SKS_URL="https://<...>/api/v1" +DIGITAL_GUIDE_URL="https://<...>/api" +DIGITAL_GUIDE_AUTHORIZATION_TOKEN="<...>" ``` If you need our server url please write us an email [kn.solvro@pwr.edu.pl](mailto:kn.solvro@pwr.edu.pl) or contact us via our [website](https://solvro.pwr.edu.pl/contact/) From ea39e2bb07104f8d556b21b524f08624e5f3f4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20=C5=BBurawski?= <92211556+BombardierBulge@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:15:02 +0100 Subject: [PATCH 23/83] feat(sks-menu): add loading screen (#427) Create loading screen for SKS Menu --- lib/config/ui_config.dart | 1 + .../presentation/sks_menu_screen.dart | 3 +- .../presentation/widgets/sks_menu_header.dart | 1 + .../widgets/sks_menu_view_loading.dart | 96 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 64d44bed..91891a1b 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -189,6 +189,7 @@ abstract class MyTooltipConfig { abstract class SksMenuConfig { static const borderRadius = 8.0; static const paddingSmall = 8.0; + static const paddingMedium = 12.0; static const paddingLarge = 16.0; static const sksDataSource = "https://sks.pwr.edu.pl/menu"; } diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index 3f8a9255..bcfeb73d 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -19,6 +19,7 @@ import "../data/repository/sks_menu_repository.dart"; import "widgets/sks_menu_data_source_link.dart"; import "widgets/sks_menu_header.dart"; import "widgets/sks_menu_section.dart"; +import "widgets/sks_menu_view_loading.dart"; @RoutePage() class SksMenuView extends HookConsumerWidget { @@ -46,7 +47,7 @@ class SksMenuView extends HookConsumerWidget { error: (error, stackTrace) => _SKSMenuLottieAnimation(error: error), loading: () => const Scaffold( body: Center( - child: CircularProgressIndicator(), + child: SksMenuViewLoading(), ), ), ); diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart b/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart index 537efc99..9e5af60f 100644 --- a/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart +++ b/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart @@ -1,4 +1,5 @@ import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart b/lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart new file mode 100644 index 00000000..41d34ee1 --- /dev/null +++ b/lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart @@ -0,0 +1,96 @@ +import "package:flutter/material.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../widgets/loading_widgets/scrolable_loader_builder.dart"; +import "../../../../widgets/loading_widgets/shimmer_loading.dart"; + +class SksMenuViewLoading extends StatelessWidget { + const SksMenuViewLoading({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Shimmer( + linearGradient: shimmerGradient, + child: Column( + children: [ + _SksMenuHeaderLoading(), + Expanded( + child: _SksMenuTilesLoading(), + ), + ], + ), + ); + } +} + +class _SksMenuTilesLoading extends StatelessWidget { + const _SksMenuTilesLoading(); + static const groupElements = 3; + @override + Widget build(BuildContext context) { + return ScrollableLoaderBuilder( + itemsSpacing: 4, + mainAxisItemSize: 14, + scrollDirection: Axis.vertical, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: + const EdgeInsets.symmetric(vertical: SksMenuConfig.paddingMedium), + child: ShimmerLoadingItem( + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, _) { + return const _LoadingTitle(); + }, + separatorBuilder: (context, _) => const SizedBox(), + itemCount: groupElements, + ), + ), + ); + }, + ); + } +} + +class _SksMenuHeaderLoading extends StatelessWidget { + const _SksMenuHeaderLoading(); + @override + Widget build(BuildContext context) { + return ShimmerLoadingItem( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(SksMenuConfig.borderRadius), + ), + width: double.infinity, + height: 250, + ), + ); + } +} + +class _LoadingTitle extends StatelessWidget { + const _LoadingTitle(); + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB( + SksMenuConfig.paddingLarge, + 0, + SksMenuConfig.paddingLarge, + SksMenuConfig.paddingMedium, + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(SksMenuConfig.borderRadius), + ), + width: double.infinity, + height: 50, + ), + ); + } +} From d6883e14370512909ae3ea275bade2878f3975d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Mon, 9 Dec 2024 16:18:24 +0100 Subject: [PATCH 24/83] feat(sks-menu): adjust loading sks view --- .../presentation/sks_menu_screen.dart | 98 ++++++++++--------- .../widgets/sks_menu_data_source_link.dart | 42 ++++---- lib/widgets/my_text_button.dart | 16 ++- 3 files changed, 89 insertions(+), 67 deletions(-) diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index bcfeb73d..4fbba10d 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -12,6 +12,7 @@ import "../../../config/ui_config.dart"; import "../../../gen/assets.gen.dart"; import "../../../utils/context_extensions.dart"; import "../../../widgets/detail_views/detail_view_app_bar.dart"; +import "../../../widgets/my_text_button.dart"; import "../../home_view/widgets/paddings.dart"; import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; import "../data/models/sks_menu_response.dart"; @@ -113,55 +114,21 @@ class _SKSMenuLottieAnimation extends HookWidget { if (error != null) { Logger().e(error.toString()); } + final animationSize = MediaQuery.sizeOf(context).width * 0.6; return Scaffold( + backgroundColor: context.colorTheme.greyLight, appBar: DetailViewAppBar( actions: const [ SksUserDataButton(), ], ), - body: Stack( - alignment: Alignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 200), - if (isAnimationCompleted.value) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - context.localize.sks_menu_closed, - style: context.textTheme.headline, - textAlign: TextAlign.center, - ), - ), - if (error != null && isAnimationCompleted.value) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - error.toString(), - style: context.textTheme.titleGrey, - textAlign: TextAlign.center, - ), - ), - if (onShowLastMenuTap != null && isAnimationCompleted.value) - Padding( - padding: const EdgeInsets.only(top: 16), - child: ElevatedButton( - onPressed: onShowLastMenuTap, - child: Text( - context.localize.sks_show_last_menu, - style: context.textTheme.lightTitle, - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - Align( - child: SizedBox.square( - dimension: 200, + body: Center( + child: Column( + children: [ + const Spacer(), + SizedBox.square( + dimension: animationSize, child: Lottie.asset( Assets.animations.sksClosed, fit: BoxFit.cover, @@ -170,14 +137,55 @@ class _SKSMenuLottieAnimation extends HookWidget { renderCache: RenderCache.drawingCommands, onLoaded: (composition) { final totalDuration = composition.duration; - Future.delayed(totalDuration, () { + Future.delayed( + totalDuration * + 0.8, // in my opinion the animation is a bit boring at the end, so we can show the texts a bit earlier + () { isAnimationCompleted.value = true; }); }, ), ), - ), - ], + Opacity( + opacity: isAnimationCompleted.value ? 1 : 0, + child: Transform.translate( + offset: Offset( + 0, + -(animationSize * + 0.10), // the animation has some extra space at the bottom + ), + child: Column( + children: [ + Text( + context.localize.sks_menu_closed, + style: context.textTheme.headline.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + if (error != null) + Text( + error.toString(), + style: context.textTheme.titleGrey, + textAlign: TextAlign.center, + ), + if (onShowLastMenuTap != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: MyTextButton( + actionTitle: context.localize.sks_show_last_menu, + onClick: onShowLastMenuTap, + showBorder: true, + color: context.colorTheme.blueAzure, + ), + ), + ], + ), + ), + ), + const Spacer(flex: 2), + ], + ), ), ); } diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart index 21c917b6..1d7a770b 100644 --- a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart @@ -14,27 +14,31 @@ class SksMenuDataSourceLink extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Text.rich( - TextSpan( - text: "${context.localize.data_come_from_website}: ", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - children: [ - TextSpan( - text: SksMenuConfig.sksDataSource.replaceFirst("https://", "www."), - style: context.textTheme.bodyOrange.copyWith( - decoration: TextDecoration.underline, - decorationColor: context.colorTheme.orangePomegranade, - fontWeight: FontWeight.bold, - ), - recognizer: TapGestureRecognizer() - ..onTap = () async => ref.launch(SksMenuConfig.sksDataSource), + return Padding( + padding: const EdgeInsets.all(16), + child: Text.rich( + TextSpan( + text: "${context.localize.data_come_from_website}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, ), - ], + children: [ + TextSpan( + text: + SksMenuConfig.sksDataSource.replaceFirst("https://", "www."), + style: context.textTheme.bodyOrange.copyWith( + decoration: TextDecoration.underline, + decorationColor: context.colorTheme.orangePomegranade, + fontWeight: FontWeight.bold, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async => ref.launch(SksMenuConfig.sksDataSource), + ), + ], + ), + textAlign: TextAlign.center, + style: context.textTheme.body, ), - textAlign: TextAlign.center, - style: context.textTheme.body, ); } } diff --git a/lib/widgets/my_text_button.dart b/lib/widgets/my_text_button.dart index cca32eba..812e3404 100644 --- a/lib/widgets/my_text_button.dart +++ b/lib/widgets/my_text_button.dart @@ -7,25 +7,35 @@ class MyTextButton extends StatelessWidget { super.key, this.onClick, required this.actionTitle, + this.showBorder = false, + this.color, }); final VoidCallback? onClick; final String actionTitle; - + final bool showBorder; + final Color? color; @override Widget build(BuildContext context) { return TextButton( onPressed: onClick, style: TextButton.styleFrom( padding: const EdgeInsets.all(12), + side: showBorder + ? BorderSide( + color: color ?? context.colorTheme.orangePomegranade, + ) + : null, ), child: Text( actionTitle, style: onClick == null ? context.textTheme.boldBodyOrange.copyWith( - color: context.colorTheme.greyPigeon, + color: color ?? context.colorTheme.greyPigeon, ) - : context.textTheme.boldBodyOrange, + : context.textTheme.boldBodyOrange.copyWith( + color: color ?? context.colorTheme.orangePomegranade, + ), ), ); } From a6620b9a69a120152c95a4288354703286fadf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Mon, 9 Dec 2024 21:32:23 +0100 Subject: [PATCH 25/83] feat(sks-menu): support technical messages --- .../data/models/dish_category_enum.dart | 6 ++-- .../data/models/sks_menu_response.dart | 13 +++++++ .../data/repository/sks_menu_repository.dart | 21 ++++++++++-- .../presentation/sks_menu_screen.dart | 14 ++++---- .../widgets/technical_message.dart | 34 +++++++++++++++++++ lib/l10n/app_pl.arb | 3 +- 6 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 lib/features/sks-menu/presentation/widgets/technical_message.dart diff --git a/lib/features/sks-menu/data/models/dish_category_enum.dart b/lib/features/sks-menu/data/models/dish_category_enum.dart index c78d7d8d..abbc4127 100644 --- a/lib/features/sks-menu/data/models/dish_category_enum.dart +++ b/lib/features/sks-menu/data/models/dish_category_enum.dart @@ -10,7 +10,8 @@ enum DishCategory { vegetarianDish, meatDish, sideDish, - drink; + drink, + technicalInfo; @override String toString() => name; @@ -25,7 +26,8 @@ extension GetLocalizedNameX on DishCategory { context.localize.sks_menu_vegetarian_dishes, DishCategory.meatDish => context.localize.sks_menu_meat_dishes, DishCategory.sideDish => context.localize.sks_menu_side_dishes, - DishCategory.drink => context.localize.sks_menu_drinks + DishCategory.drink => context.localize.sks_menu_drinks, + DishCategory.technicalInfo => context.localize.sks_menu_technical_info, }; } } diff --git a/lib/features/sks-menu/data/models/sks_menu_response.dart b/lib/features/sks-menu/data/models/sks_menu_response.dart index e17ed4aa..b97ec8be 100644 --- a/lib/features/sks-menu/data/models/sks_menu_response.dart +++ b/lib/features/sks-menu/data/models/sks_menu_response.dart @@ -16,3 +16,16 @@ class SksMenuResponse with _$SksMenuResponse { factory SksMenuResponse.fromJson(Map json) => _$SksMenuResponseFromJson(json); } + +@freezed +class ExtendedSksMenuResponse with _$ExtendedSksMenuResponse { + const factory ExtendedSksMenuResponse({ + required bool isMenuOnline, + required DateTime lastUpdate, + required List meals, + required List technicalInfos, + }) = _ExtendedSksMenuResponse; + + factory ExtendedSksMenuResponse.fromJson(Map json) => + _$ExtendedSksMenuResponseFromJson(json); +} diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks-menu/data/repository/sks_menu_repository.dart index 8cd7956a..8d9313cf 100644 --- a/lib/features/sks-menu/data/repository/sks_menu_repository.dart +++ b/lib/features/sks-menu/data/repository/sks_menu_repository.dart @@ -3,12 +3,13 @@ import "package:riverpod_annotation/riverpod_annotation.dart"; import "../../../../api_base_rest/client/dio_client.dart"; import "../../../../config/env.dart"; +import "../models/dish_category_enum.dart"; import "../models/sks_menu_response.dart"; part "sks_menu_repository.g.dart"; @riverpod -Future getSksMenuData(Ref ref) async { +Future getSksMenuData(Ref ref) async { final mealsUrl = "${Env.sksUrl}/meals/current"; final dio = ref.read(restClientProvider); @@ -16,5 +17,21 @@ Future getSksMenuData(Ref ref) async { final SksMenuResponse sksMenuResponse = SksMenuResponse.fromJson(response.data as Map); - return sksMenuResponse; + final trueMeals = sksMenuResponse.meals + .where((e) => e.category != DishCategory.technicalInfo) + .toList(); + final technicalInfos = sksMenuResponse.meals + .where((e) => e.category == DishCategory.technicalInfo) + .map((e) => e.name) + .toList(); + + return ExtendedSksMenuResponse( + isMenuOnline: sksMenuResponse.isMenuOnline, + lastUpdate: sksMenuResponse.lastUpdate, + meals: trueMeals, + technicalInfos: [ + ...technicalInfos, + "DZISIAJ BĘDZIE CZYNNE DO 14:00 !!!?!?", + ], + ); } diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index 4fbba10d..5b405076 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -13,7 +13,6 @@ import "../../../gen/assets.gen.dart"; import "../../../utils/context_extensions.dart"; import "../../../widgets/detail_views/detail_view_app_bar.dart"; import "../../../widgets/my_text_button.dart"; -import "../../home_view/widgets/paddings.dart"; import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; import "../data/models/sks_menu_response.dart"; import "../data/repository/sks_menu_repository.dart"; @@ -21,6 +20,7 @@ import "widgets/sks_menu_data_source_link.dart"; import "widgets/sks_menu_header.dart"; import "widgets/sks_menu_section.dart"; import "widgets/sks_menu_view_loading.dart"; +import "widgets/technical_message.dart"; @RoutePage() class SksMenuView extends HookConsumerWidget { @@ -61,7 +61,7 @@ class _SksMenuView extends StatelessWidget { required this.isLastMenuButtonClicked, }); - final SksMenuResponse sksMenuData; + final ExtendedSksMenuResponse sksMenuData; final bool isLastMenuButtonClicked; @override @@ -77,16 +77,14 @@ class _SksMenuView extends StatelessWidget { ), body: ListView( children: [ + for (final technicalInfo in sksMenuData.technicalInfos) + TechnicalMessage(message: technicalInfo), SksMenuHeader( dateTimeOfLastUpdate: sksMenuData.lastUpdate.toIso8601String(), ), Padding( - padding: const EdgeInsets.symmetric( - vertical: HomeViewConfig.paddingMedium, - ), - child: MediumHorizontalPadding( - child: SksMenuSection(sksMenuData.meals), - ), + padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), + child: SksMenuSection(sksMenuData.meals), ), const SksMenuDataSourceLink(), const SizedBox( diff --git a/lib/features/sks-menu/presentation/widgets/technical_message.dart b/lib/features/sks-menu/presentation/widgets/technical_message.dart new file mode 100644 index 00000000..7755bab7 --- /dev/null +++ b/lib/features/sks-menu/presentation/widgets/technical_message.dart @@ -0,0 +1,34 @@ +import "package:flutter/material.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../data/models/dish_category_enum.dart"; + +class TechnicalMessage extends StatelessWidget { + const TechnicalMessage({super.key, required this.message}); + final String message; + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all( + HomeViewConfig.paddingMedium, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: ColoredBox( + color: context.colorTheme.orangePomegranade, + child: ListTile( + title: Text( + DishCategory.technicalInfo.getLocalizedName(context), + style: context.textTheme.titleWhite, + ), + subtitle: Text( + message, + style: context.textTheme.bodyWhite, + ), + ), + ), + ), + ); + } +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 47fe15f2..9dca48ae 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -188,5 +188,6 @@ "sks_menu_closed" : "SKS Menu jest teraz niedostępne", "sks_show_last_menu" : "Pokaż ostatnio dostępne menu", "confirm": "Zatwierdź", - "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz." + "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz.", + "sks_menu_technical_info": "KOMUNIKAT" } From 2aa9510f5f13c4e48cc3b9da7d55eb97a24b8c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Mon, 9 Dec 2024 21:40:31 +0100 Subject: [PATCH 26/83] feat(sks-menu): add offline menu viewing note alert --- .../data/repository/sks_menu_repository.dart | 1 - .../sks-menu/presentation/sks_menu_screen.dart | 8 +++++++- .../presentation/widgets/technical_message.dart | 13 ++++++++++--- lib/l10n/app_pl.arb | 4 +++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks-menu/data/repository/sks_menu_repository.dart index 8d9313cf..91e8e008 100644 --- a/lib/features/sks-menu/data/repository/sks_menu_repository.dart +++ b/lib/features/sks-menu/data/repository/sks_menu_repository.dart @@ -31,7 +31,6 @@ Future getSksMenuData(Ref ref) async { meals: trueMeals, technicalInfos: [ ...technicalInfos, - "DZISIAJ BĘDZIE CZYNNE DO 14:00 !!!?!?", ], ); } diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index 5b405076..d8e2159e 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -77,6 +77,12 @@ class _SksMenuView extends StatelessWidget { ), body: ListView( children: [ + if (!sksMenuData.isMenuOnline) + TechnicalMessage( + color: context.colorTheme.blueAzure, + title: context.localize.sks_note, + message: context.localize.sks_menu_you_see_last_menu, + ), for (final technicalInfo in sksMenuData.technicalInfos) TechnicalMessage(message: technicalInfo), SksMenuHeader( @@ -115,7 +121,7 @@ class _SKSMenuLottieAnimation extends HookWidget { final animationSize = MediaQuery.sizeOf(context).width * 0.6; return Scaffold( - backgroundColor: context.colorTheme.greyLight, + backgroundColor: context.colorTheme.whiteSoap, appBar: DetailViewAppBar( actions: const [ SksUserDataButton(), diff --git a/lib/features/sks-menu/presentation/widgets/technical_message.dart b/lib/features/sks-menu/presentation/widgets/technical_message.dart index 7755bab7..9d5fc195 100644 --- a/lib/features/sks-menu/presentation/widgets/technical_message.dart +++ b/lib/features/sks-menu/presentation/widgets/technical_message.dart @@ -5,8 +5,15 @@ import "../../../../theme/app_theme.dart"; import "../../data/models/dish_category_enum.dart"; class TechnicalMessage extends StatelessWidget { - const TechnicalMessage({super.key, required this.message}); + const TechnicalMessage({ + super.key, + required this.message, + this.title, + this.color, + }); final String message; + final String? title; + final Color? color; @override Widget build(BuildContext context) { return Padding( @@ -16,10 +23,10 @@ class TechnicalMessage extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(8), child: ColoredBox( - color: context.colorTheme.orangePomegranade, + color: color ?? context.colorTheme.orangePomegranade, child: ListTile( title: Text( - DishCategory.technicalInfo.getLocalizedName(context), + title ?? DishCategory.technicalInfo.getLocalizedName(context), style: context.textTheme.titleWhite, ), subtitle: Text( diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9dca48ae..cd3b466e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -189,5 +189,7 @@ "sks_show_last_menu" : "Pokaż ostatnio dostępne menu", "confirm": "Zatwierdź", "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz.", - "sks_menu_technical_info": "KOMUNIKAT" + "sks_menu_technical_info": "KOMUNIKAT", + "sks_note": "UWAGA", + "sks_menu_you_see_last_menu": "Aktualne menu SKS jest niedostępne. Przeglądasz ostatnio dostępną wersję." } From 7d7ef88572989d6ecb69be14baa8d5d314186397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Mon, 9 Dec 2024 23:23:31 +0100 Subject: [PATCH 27/83] feat(sks-menu): add rest caching --- lib/api_base_rest/cache/cache.dart | 28 +++++++++++-------- lib/api_base_rest/cache/cache_manager.dart | 2 +- .../data/repository/sks_menu_repository.dart | 16 +++++++---- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/api_base_rest/cache/cache.dart b/lib/api_base_rest/cache/cache.dart index fd531ceb..390a71e6 100644 --- a/lib/api_base_rest/cache/cache.dart +++ b/lib/api_base_rest/cache/cache.dart @@ -2,6 +2,7 @@ import "dart:convert"; import "dart:typed_data"; import "package:flutter_riverpod/flutter_riverpod.dart"; + import "../client/dio_client.dart"; import "cache_manager.dart"; @@ -9,29 +10,32 @@ extension DataCachingX on Ref { Future getAndCacheData( String fullUrl, int ttlDays, - T Function(Map json) fromJson, - bool Function() extraValidityCheck, - ) async { + T Function(Map json) fromJson, { + // returns true if the data is still valid + required bool Function(T cachedData) extraValidityCheck, + }) async { final cacheManager = watch(restCacheManagerProvider(ttlDays)); final cachedFile = await cacheManager.getFileFromCache(fullUrl); - if (cachedFile != null && extraValidityCheck()) { + if (cachedFile != null) { final cachedData = await cachedFile.file.readAsString(); final data = fromJson( jsonDecode(cachedData) as Map, ); - return data; + if (extraValidityCheck(data)) { + return data; + } } final dio = watch(restClientProvider); final response = await dio.get(fullUrl); final sksData = fromJson(response.data as Map); - - await cacheManager.putFile( - fullUrl, - Uint8List.fromList(jsonEncode(response.data).codeUnits), - fileExtension: CacheManagerConfig.jsonExtesion, - ); - + if (extraValidityCheck(sksData)) { + await cacheManager.putFile( + fullUrl, + Uint8List.fromList(utf8.encode(jsonEncode(response.data))), + fileExtension: CacheManagerConfig.jsonExtesion, + ); + } return sksData; } } diff --git a/lib/api_base_rest/cache/cache_manager.dart b/lib/api_base_rest/cache/cache_manager.dart index d3af2082..ad783f0e 100644 --- a/lib/api_base_rest/cache/cache_manager.dart +++ b/lib/api_base_rest/cache/cache_manager.dart @@ -16,5 +16,5 @@ CacheManager restCacheManager(Ref ref, int ttlDays) { } abstract class CacheManagerConfig { - static const jsonExtesion = ".json"; + static const jsonExtesion = "json"; } diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks-menu/data/repository/sks_menu_repository.dart index 91e8e008..68b4381e 100644 --- a/lib/features/sks-menu/data/repository/sks_menu_repository.dart +++ b/lib/features/sks-menu/data/repository/sks_menu_repository.dart @@ -1,8 +1,9 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; -import "../../../../api_base_rest/client/dio_client.dart"; +import "../../../../api_base_rest/cache/cache.dart"; import "../../../../config/env.dart"; +import "../../../../utils/datetime_utils.dart"; import "../models/dish_category_enum.dart"; import "../models/sks_menu_response.dart"; @@ -11,15 +12,20 @@ part "sks_menu_repository.g.dart"; @riverpod Future getSksMenuData(Ref ref) async { final mealsUrl = "${Env.sksUrl}/meals/current"; + const ttlDays = 1; - final dio = ref.read(restClientProvider); - final response = await dio.get(mealsUrl); - final SksMenuResponse sksMenuResponse = - SksMenuResponse.fromJson(response.data as Map); + final sksMenuResponse = await ref.getAndCacheData( + mealsUrl, + ttlDays, + SksMenuResponse.fromJson, + extraValidityCheck: (data) => + data.isMenuOnline && DateTime.now().date == data.lastUpdate.date, + ); final trueMeals = sksMenuResponse.meals .where((e) => e.category != DishCategory.technicalInfo) .toList(); + final technicalInfos = sksMenuResponse.meals .where((e) => e.category == DishCategory.technicalInfo) .map((e) => e.name) From b2aab7aa31429d3c05024fc48186592c300af2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Tue, 10 Dec 2024 00:09:05 +0100 Subject: [PATCH 28/83] feat(sks-menu): improve error handling --- lib/api_base_rest/cache/cache.dart | 12 +++-- lib/api_base_rest/client/offline_error.dart | 50 +++++++++++++++++++ .../data/repository/sks_menu_repository.dart | 13 +++-- .../presentation/sks_menu_screen.dart | 33 +++++------- lib/l10n/app_pl.arb | 12 ++++- lib/widgets/my_error_widget.dart | 7 +++ 6 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 lib/api_base_rest/client/offline_error.dart diff --git a/lib/api_base_rest/cache/cache.dart b/lib/api_base_rest/cache/cache.dart index 390a71e6..8ddddd28 100644 --- a/lib/api_base_rest/cache/cache.dart +++ b/lib/api_base_rest/cache/cache.dart @@ -1,9 +1,10 @@ import "dart:convert"; import "dart:typed_data"; +import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../client/dio_client.dart"; +import "../client/offline_error.dart"; import "cache_manager.dart"; extension DataCachingX on Ref { @@ -13,6 +14,8 @@ extension DataCachingX on Ref { T Function(Map json) fromJson, { // returns true if the data is still valid required bool Function(T cachedData) extraValidityCheck, + required String Function(BuildContext context) localizedOfflineMessage, + VoidCallback? onRetry, }) async { final cacheManager = watch(restCacheManagerProvider(ttlDays)); @@ -26,8 +29,11 @@ extension DataCachingX on Ref { return data; } } - final dio = watch(restClientProvider); - final response = await dio.get(fullUrl); + final response = await safeGetWatch( + fullUrl, + localizedMessage: localizedOfflineMessage, + onRetry: onRetry, + ); final sksData = fromJson(response.data as Map); if (extraValidityCheck(sksData)) { await cacheManager.putFile( diff --git a/lib/api_base_rest/client/offline_error.dart b/lib/api_base_rest/client/offline_error.dart new file mode 100644 index 00000000..03f3bb26 --- /dev/null +++ b/lib/api_base_rest/client/offline_error.dart @@ -0,0 +1,50 @@ +import "package:dio/dio.dart"; +import "package:flutter/widgets.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "dio_client.dart"; + +class RestFrameworkOfflineException implements Exception { + final String message; + final String Function(BuildContext context) localizedMessage; + final VoidCallback? onRetry; + + RestFrameworkOfflineException({ + required this.localizedMessage, + this.onRetry, + this.message = "No Internet connection", + }); + + @override + String toString() => "RestFrameworkOfflineException: $message"; +} + +extension DioSafeRequestsX on Ref { + Future> safeRequest( + Future> Function() request, { + required String Function(BuildContext context) localizedMessage, + VoidCallback? onRetry, + }) async { + try { + return await request(); + } on DioException catch (_) { + throw RestFrameworkOfflineException( + localizedMessage: localizedMessage, + onRetry: onRetry, + ); + } + } + + Future> safeGetWatch( + String url, { + required String Function(BuildContext context) localizedMessage, + VoidCallback? onRetry, + }) async { + final dio = watch(restClientProvider); + return safeRequest( + () => dio.get(url), + localizedMessage: localizedMessage, + onRetry: onRetry, + ); + } +} diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks-menu/data/repository/sks_menu_repository.dart index 68b4381e..29197a07 100644 --- a/lib/features/sks-menu/data/repository/sks_menu_repository.dart +++ b/lib/features/sks-menu/data/repository/sks_menu_repository.dart @@ -3,6 +3,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart"; import "../../../../api_base_rest/cache/cache.dart"; import "../../../../config/env.dart"; +import "../../../../utils/context_extensions.dart"; import "../../../../utils/datetime_utils.dart"; import "../models/dish_category_enum.dart"; import "../models/sks_menu_response.dart"; @@ -19,7 +20,13 @@ Future getSksMenuData(Ref ref) async { ttlDays, SksMenuResponse.fromJson, extraValidityCheck: (data) => - data.isMenuOnline && DateTime.now().date == data.lastUpdate.date, + data.isMenuOnline && + DateTime.now().date.isSameDay(data.lastUpdate.date), + localizedOfflineMessage: (context) => + context.localize.my_offline_error_message( + context.localize.sks_menu, + ), + onRetry: () => ref.invalidateSelf(), ); final trueMeals = sksMenuResponse.meals @@ -35,8 +42,6 @@ Future getSksMenuData(Ref ref) async { isMenuOnline: sksMenuResponse.isMenuOnline, lastUpdate: sksMenuResponse.lastUpdate, meals: trueMeals, - technicalInfos: [ - ...technicalInfos, - ], + technicalInfos: technicalInfos, ); } diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index d8e2159e..b8a741ba 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -4,7 +4,6 @@ import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:logger/logger.dart"; import "package:lottie/lottie.dart"; import "../../../../theme/app_theme.dart"; @@ -12,6 +11,7 @@ import "../../../config/ui_config.dart"; import "../../../gen/assets.gen.dart"; import "../../../utils/context_extensions.dart"; import "../../../widgets/detail_views/detail_view_app_bar.dart"; +import "../../../widgets/my_error_widget.dart"; import "../../../widgets/my_text_button.dart"; import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; import "../data/models/sks_menu_response.dart"; @@ -34,7 +34,7 @@ class SksMenuView extends HookConsumerWidget { return asyncSksMenuData.when( data: (sksMenuData) { if (!sksMenuData.isMenuOnline && !isLastMenuButtonClicked.value) { - return _SKSMenuLottieAnimation( + return _SKSMenuUnavailableAnimation( onShowLastMenuTap: () { isLastMenuButtonClicked.value = true; }, @@ -45,7 +45,14 @@ class SksMenuView extends HookConsumerWidget { isLastMenuButtonClicked: isLastMenuButtonClicked.value, ); }, - error: (error, stackTrace) => _SKSMenuLottieAnimation(error: error), + error: (error, stackTrace) => Scaffold( + appBar: DetailViewAppBar( + actions: const [ + SksUserDataButton(), + ], + ), + body: MyErrorWidget(error), + ), loading: () => const Scaffold( body: Center( child: SksMenuViewLoading(), @@ -67,7 +74,7 @@ class _SksMenuView extends StatelessWidget { @override Widget build(BuildContext context) { if (!isLastMenuButtonClicked && !sksMenuData.isMenuOnline) { - return const _SKSMenuLottieAnimation(); + return const _SKSMenuUnavailableAnimation(); } return Scaffold( appBar: DetailViewAppBar( @@ -102,22 +109,14 @@ class _SksMenuView extends StatelessWidget { } } -class _SKSMenuLottieAnimation extends HookWidget { - const _SKSMenuLottieAnimation({ - this.error, - this.onShowLastMenuTap, - }); +class _SKSMenuUnavailableAnimation extends HookWidget { + const _SKSMenuUnavailableAnimation({this.onShowLastMenuTap}); - final Object? error; final VoidCallback? onShowLastMenuTap; @override Widget build(BuildContext context) { final isAnimationCompleted = useState(false); - - if (error != null) { - Logger().e(error.toString()); - } final animationSize = MediaQuery.sizeOf(context).width * 0.6; return Scaffold( @@ -167,12 +166,6 @@ class _SKSMenuLottieAnimation extends HookWidget { ), textAlign: TextAlign.center, ), - if (error != null) - Text( - error.toString(), - style: context.textTheme.titleGrey, - textAlign: TextAlign.center, - ), if (onShowLastMenuTap != null) Padding( padding: const EdgeInsets.only(top: 12), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index cd3b466e..d167e43c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -191,5 +191,15 @@ "push_notifications_dialog_info": "Obecnie nie korzystamy z powiadomień push, ale planujemy dodać je w przyszłości. Możesz wyrazić na nie zgodę już teraz.", "sks_menu_technical_info": "KOMUNIKAT", "sks_note": "UWAGA", - "sks_menu_you_see_last_menu": "Aktualne menu SKS jest niedostępne. Przeglądasz ostatnio dostępną wersję." + "sks_menu_you_see_last_menu": "Aktualne menu SKS jest niedostępne. Przeglądasz ostatnio dostępną wersję.", + "my_offline_error_message": "Wystąpił błąd podczas pobierania danych dotyczących {data_type}", + "@my_offline_error_message": { + "description": "An error message with a single parameter", + "placeholders": { + "data_type": { + "type": "String", + "example": "wydziałów" + } + } + } } diff --git a/lib/widgets/my_error_widget.dart b/lib/widgets/my_error_widget.dart index ff33a81a..0ee7a628 100644 --- a/lib/widgets/my_error_widget.dart +++ b/lib/widgets/my_error_widget.dart @@ -5,7 +5,9 @@ import "package:logger/logger.dart"; import "package:lottie/lottie.dart"; import "../api_base/query_adapter.dart"; +import "../api_base_rest/client/offline_error.dart"; import "../config/ui_config.dart"; +import "../features/offline_messages/widgets/general_offline_message.dart"; import "../features/offline_messages/widgets/grapgql_offline_message.dart"; import "../features/parkings_view/api_client/iparking_commands.dart"; import "../features/parkings_view/widgets/offline_parkings_view.dart"; @@ -32,6 +34,11 @@ class MyErrorWidget extends HookWidget { return switch (error) { ParkingsOfflineException() => const OfflineParkingsView(), GqlOfflineException(:final ttlKey) => OfflineGraphQLMessage(ttlKey), + RestFrameworkOfflineException(:final localizedMessage, :final onRetry) => + OfflineMessage( + localizedMessage(context), + onRefresh: onRetry, + ), _ => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, From d92bf9e93d71bf7e01f19e318e83610d65330a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Tue, 10 Dec 2024 01:30:24 +0100 Subject: [PATCH 29/83] feat(sks-menu): add pull to refresh --- lib/api_base_rest/cache/cache.dart | 8 ++ .../data/models/sks_menu_response.dart | 5 +- .../data/repository/sks_menu_repository.dart | 75 ++++++++++--------- .../presentation/sks_menu_screen.dart | 61 +++++++++------ .../widgets/sks_menu_section.dart | 2 +- .../widgets/technical_message.dart | 13 +++- 6 files changed, 98 insertions(+), 66 deletions(-) diff --git a/lib/api_base_rest/cache/cache.dart b/lib/api_base_rest/cache/cache.dart index 8ddddd28..f230f4f2 100644 --- a/lib/api_base_rest/cache/cache.dart +++ b/lib/api_base_rest/cache/cache.dart @@ -8,6 +8,14 @@ import "../client/offline_error.dart"; import "cache_manager.dart"; extension DataCachingX on Ref { + Future clearCache( + String fullUrl, + int ttlDays, + ) async { + final cacheManager = watch(restCacheManagerProvider(ttlDays)); + await cacheManager.removeFile(fullUrl); + } + Future getAndCacheData( String fullUrl, int ttlDays, diff --git a/lib/features/sks-menu/data/models/sks_menu_response.dart b/lib/features/sks-menu/data/models/sks_menu_response.dart index b97ec8be..32c5063e 100644 --- a/lib/features/sks-menu/data/models/sks_menu_response.dart +++ b/lib/features/sks-menu/data/models/sks_menu_response.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "sks_menu_data.dart"; @@ -22,8 +23,8 @@ class ExtendedSksMenuResponse with _$ExtendedSksMenuResponse { const factory ExtendedSksMenuResponse({ required bool isMenuOnline, required DateTime lastUpdate, - required List meals, - required List technicalInfos, + required IList meals, + required IList technicalInfos, }) = _ExtendedSksMenuResponse; factory ExtendedSksMenuResponse.fromJson(Map json) => diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks-menu/data/repository/sks_menu_repository.dart index 29197a07..e28eb940 100644 --- a/lib/features/sks-menu/data/repository/sks_menu_repository.dart +++ b/lib/features/sks-menu/data/repository/sks_menu_repository.dart @@ -1,47 +1,52 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; import "../../../../api_base_rest/cache/cache.dart"; import "../../../../config/env.dart"; -import "../../../../utils/context_extensions.dart"; import "../../../../utils/datetime_utils.dart"; +import "../../presentation/sks_menu_screen.dart"; import "../models/dish_category_enum.dart"; import "../models/sks_menu_response.dart"; part "sks_menu_repository.g.dart"; @riverpod -Future getSksMenuData(Ref ref) async { - final mealsUrl = "${Env.sksUrl}/meals/current"; - const ttlDays = 1; - - final sksMenuResponse = await ref.getAndCacheData( - mealsUrl, - ttlDays, - SksMenuResponse.fromJson, - extraValidityCheck: (data) => - data.isMenuOnline && - DateTime.now().date.isSameDay(data.lastUpdate.date), - localizedOfflineMessage: (context) => - context.localize.my_offline_error_message( - context.localize.sks_menu, - ), - onRetry: () => ref.invalidateSelf(), - ); - - final trueMeals = sksMenuResponse.meals - .where((e) => e.category != DishCategory.technicalInfo) - .toList(); - - final technicalInfos = sksMenuResponse.meals - .where((e) => e.category == DishCategory.technicalInfo) - .map((e) => e.name) - .toList(); - - return ExtendedSksMenuResponse( - isMenuOnline: sksMenuResponse.isMenuOnline, - lastUpdate: sksMenuResponse.lastUpdate, - meals: trueMeals, - technicalInfos: technicalInfos, - ); +class SksMenuRepository extends _$SksMenuRepository { + static final _mealsUrl = "${Env.sksUrl}/meals/current"; + static const _ttlDays = 1; + + Future clearCache() async { + return ref.clearCache(_mealsUrl, _ttlDays); + } + + @override + Future build() async { + final sksMenuResponse = await ref.getAndCacheData( + _mealsUrl, + _ttlDays, + SksMenuResponse.fromJson, + extraValidityCheck: (data) { + return data.isMenuOnline && + DateTime.now().date.isSameDay(data.lastUpdate.date); + }, + localizedOfflineMessage: SksMenuView.localizedOfflineMessage, + onRetry: () => ref.invalidateSelf(), + ); + + final trueMeals = sksMenuResponse.meals + .where((e) => e.category != DishCategory.technicalInfo) + .toIList(); + + final technicalInfos = sksMenuResponse.meals + .where((e) => e.category == DishCategory.technicalInfo) + .map((e) => e.name) + .toIList(); + + return ExtendedSksMenuResponse( + isMenuOnline: sksMenuResponse.isMenuOnline, + lastUpdate: sksMenuResponse.lastUpdate, + meals: trueMeals, + technicalInfos: technicalInfos, + ); + } } diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart index b8a741ba..049f0c02 100644 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -26,9 +26,15 @@ import "widgets/technical_message.dart"; class SksMenuView extends HookConsumerWidget { const SksMenuView({super.key}); + static String localizedOfflineMessage(BuildContext context) { + return context.localize.my_offline_error_message( + context.localize.sks_menu, + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { - final asyncSksMenuData = ref.watch(getSksMenuDataProvider); + final asyncSksMenuData = ref.watch(sksMenuRepositoryProvider); final isLastMenuButtonClicked = useState(false); return asyncSksMenuData.when( @@ -62,7 +68,7 @@ class SksMenuView extends HookConsumerWidget { } } -class _SksMenuView extends StatelessWidget { +class _SksMenuView extends ConsumerWidget { const _SksMenuView({ required this.sksMenuData, required this.isLastMenuButtonClicked, @@ -72,7 +78,7 @@ class _SksMenuView extends StatelessWidget { final bool isLastMenuButtonClicked; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { if (!isLastMenuButtonClicked && !sksMenuData.isMenuOnline) { return const _SKSMenuUnavailableAnimation(); } @@ -82,28 +88,35 @@ class _SksMenuView extends StatelessWidget { SksUserDataButton(), ], ), - body: ListView( - children: [ - if (!sksMenuData.isMenuOnline) - TechnicalMessage( - color: context.colorTheme.blueAzure, - title: context.localize.sks_note, - message: context.localize.sks_menu_you_see_last_menu, + body: RefreshIndicator( + onRefresh: () async { + await ref.read(sksMenuRepositoryProvider.notifier).clearCache(); + return ref.refresh(sksMenuRepositoryProvider.future); + }, + color: context.colorTheme.orangePomegranade, + child: ListView( + children: [ + if (!sksMenuData.isMenuOnline) + TechnicalMessage( + alertType: AlertType.info, + title: context.localize.sks_note, + message: context.localize.sks_menu_you_see_last_menu, + ), + for (final technicalInfo in sksMenuData.technicalInfos) + TechnicalMessage(message: technicalInfo), + SksMenuHeader( + dateTimeOfLastUpdate: sksMenuData.lastUpdate.toIso8601String(), ), - for (final technicalInfo in sksMenuData.technicalInfos) - TechnicalMessage(message: technicalInfo), - SksMenuHeader( - dateTimeOfLastUpdate: sksMenuData.lastUpdate.toIso8601String(), - ), - Padding( - padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), - child: SksMenuSection(sksMenuData.meals), - ), - const SksMenuDataSourceLink(), - const SizedBox( - height: ScienceClubsViewConfig.mediumPadding, - ), - ], + Padding( + padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), + child: SksMenuSection(sksMenuData.meals), + ), + const SksMenuDataSourceLink(), + const SizedBox( + height: ScienceClubsViewConfig.mediumPadding, + ), + ], + ), ), ); } diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart b/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart index f397a28b..6deed691 100644 --- a/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart +++ b/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart @@ -9,7 +9,7 @@ import "sks_menu_tiles.dart"; class SksMenuSection extends StatelessWidget { const SksMenuSection(this.data, {super.key}); - final List data; + final IList data; @override Widget build(BuildContext context) { diff --git a/lib/features/sks-menu/presentation/widgets/technical_message.dart b/lib/features/sks-menu/presentation/widgets/technical_message.dart index 9d5fc195..90727919 100644 --- a/lib/features/sks-menu/presentation/widgets/technical_message.dart +++ b/lib/features/sks-menu/presentation/widgets/technical_message.dart @@ -4,16 +4,18 @@ import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; import "../../data/models/dish_category_enum.dart"; +enum AlertType { info, error } + class TechnicalMessage extends StatelessWidget { const TechnicalMessage({ super.key, required this.message, this.title, - this.color, + this.alertType = AlertType.error, }); final String message; final String? title; - final Color? color; + final AlertType alertType; @override Widget build(BuildContext context) { return Padding( @@ -21,9 +23,12 @@ class TechnicalMessage extends StatelessWidget { HomeViewConfig.paddingMedium, ), child: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(AppWidgetsConfig.borderRadiusMedium), child: ColoredBox( - color: color ?? context.colorTheme.orangePomegranade, + color: alertType == AlertType.error + ? context.colorTheme.orangePomegranade + : context.colorTheme.blueAzure, child: ListTile( title: Text( title ?? DishCategory.technicalInfo.getLocalizedName(context), From 4b002c59b2ceff847bf731dd02358f6ab0f31e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kowali=C5=84ski?= Date: Tue, 10 Dec 2024 19:25:54 +0100 Subject: [PATCH 30/83] chore: bump build --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 143a7767..b2dea573 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.5+60 +version: 1.1.6+61 environment: sdk: ^3.5.3 @@ -224,4 +224,4 @@ flutter_launcher_icons: image_path: "assets/png/app_icon.png" macos: generate: true - image_path: "assets/png/app_icon.png" \ No newline at end of file + image_path: "assets/png/app_icon.png" From 489b8a0efb6221dcad8e368b495823623fcde790 Mon Sep 17 00:00:00 2001 From: Bartosh <101900992+24bartixx@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:16:57 +0100 Subject: [PATCH 31/83] feat(digita-guide): add report button functionality - open email app (#484) * feat: add report button functionality - open email app * fix: add horizontal padding * fix: padding * fix: toast * fix: adjsut toast color and gravity * fix: lint rules * refractor: utilize existing extension * refractor: move function that opens email app to a lambda expression * feat: handle digital guide source info tap * refractor: move emailUrl string from a direct declaration to a final variable * fix: formatting --- lib/config/ui_config.dart | 1 + .../digital_guide_data_source_link.dart | 50 ++++++++++++------- .../widgets/report_change_button.dart | 38 +++++++++++--- lib/l10n/app_pl.arb | 7 ++- pubspec.yaml | 1 + 5 files changed, 71 insertions(+), 26 deletions(-) diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 91891a1b..434d1c1c 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -215,6 +215,7 @@ abstract class DigitalGuideConfig { static const heightSmall = 8.0; static const heightBig = 24.0; static const heightHuge = 48.0; + static const paddingMedium = 16.0; } abstract class AlertDialogConfig { diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart index f3a19e5e..e91e8da9 100644 --- a/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/digital_guide_data_source_link.dart @@ -1,32 +1,46 @@ +import "package:flutter/gestures.dart"; import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../../../config/ui_config.dart"; import "../../../../../theme/app_theme.dart"; import "../../../../../utils/context_extensions.dart"; +import "../../../../../utils/launch_url_util.dart"; class DigitalGuideDataSourceLink extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Text.rich( - TextSpan( - text: "${context.localize.data_come_from_website}: ", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - children: [ - TextSpan( - text: context.localize.digital_guide_website, - style: context.textTheme.bodyOrange.copyWith( - decoration: TextDecoration.underline, - decorationColor: context.colorTheme.orangePomegranade, - fontWeight: FontWeight.bold, - ), - // TODO(Bartosh): on tap url handling -> webbrowser launch + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: DigitalGuideConfig.paddingMedium, + ), + child: Text.rich( + TextSpan( + text: "${context.localize.data_come_from_website}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, ), - ], + children: [ + TextSpan( + text: context.localize.digital_guide_website, + style: context.textTheme.bodyOrange.copyWith( + decoration: TextDecoration.underline, + decorationColor: context.colorTheme.orangePomegranade, + fontWeight: FontWeight.bold, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await ref.launch( + context.localize.digital_guide_website + .replaceAll("www.", "https://"), + ); + }, + ), + ], + ), + textAlign: TextAlign.center, + style: context.textTheme.body, ), - textAlign: TextAlign.center, - style: context.textTheme.body, ); } } diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart index 623d5518..06deadf5 100644 --- a/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart @@ -1,21 +1,47 @@ +import "dart:async"; + import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:fluttertoast/fluttertoast.dart"; import "../../../../../config/ui_config.dart"; import "../../../../../theme/app_theme.dart"; import "../../../../../utils/context_extensions.dart"; +import "../../../../../utils/launch_url_util.dart"; -class ReportChangeButton extends StatelessWidget { +class ReportChangeButton extends ConsumerWidget { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: AppWidgetsConfig.paddingMedium, child: Column( children: [ - Text(context.localize.change_report_title), + Text(context.localize.report_change_title), const SizedBox(height: 8), ElevatedButton( - // TODO(Bartosh): handle action - onPressed: () {}, + onPressed: () async { + final errorMessageToast = + context.localize.report_change_error_toast_message; + final backgroundColorToast = context.colorTheme.greyLight; + final textColorToast = context.colorTheme.blackMirage; + + final emailUrl = + "mailto:${context.localize.report_change_email}?subject=${Uri.encodeComponent(context.localize.report_change_subject)}"; + + if (!await ref.launch( + emailUrl, + )) { + unawaited( + Fluttertoast.showToast( + msg: errorMessageToast, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: backgroundColorToast, + textColor: textColorToast, + ), + ); + } + }, style: ElevatedButton.styleFrom( backgroundColor: context.colorTheme.blueAzure, padding: AppWidgetsConfig.paddingMedium, @@ -26,7 +52,7 @@ class ReportChangeButton extends StatelessWidget { ), ), child: Text( - context.localize.change_report_button, + context.localize.report_change_button, style: TextStyle(color: context.colorTheme.whiteSoap), ), ), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d167e43c..d82c95c1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -151,8 +151,11 @@ "about_the_app": "O aplikacji", "other_view" : "Inne", "map" : "Mapa", - "change_report_title" : "Coś się zmieniło?", - "change_report_button" : "Zgłoś zmianę", + "report_change_title" : "Coś się zmieniło?", + "report_change_button" : "Zgłoś zmianę", + "report_change_email" : "kn.solvro@pwr.edu.pl", + "report_change_subject" : "Sugestia zmiany - ToPWR", + "report_change_error_toast_message" : "Nie można otworzyć aplikacji mailowej", "localization" : "Lokalizacja", "amenities" : "Udogodnienia", "surroundings": "Otoczenie", diff --git a/pubspec.yaml b/pubspec.yaml index b2dea573..19ddbfea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: upgrader: ^11.3.0 in_app_review: ^2.0.9 flutter_map_animations: ^0.7.1 + fluttertoast: ^8.2.8 dev_dependencies: flutter_test: From 90f9e89ac85dce438d0fbb38f3f7a6a1ac356736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Ja=C5=82ocha?= <76820915+mikolaj-jalocha@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:15:59 +0100 Subject: [PATCH 32/83] feat(launch url util): toast message on error for all urls (#487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SKS Menu (#387) * Feat: create sks menu screen - prototype * feat: create sks menu screen * feat(sks-menu): improve spacings and font * fix(sks-menu): fix sks url link * fix(sks-menu): remove bottom nav bar * feat (sks-menu): add data layer * chore (sks-menu): code cleanup * feat (sks-menu): pr fix --------- Co-authored-by: Szymon Kowaliński * feat(launch-url-util): toast message each time url couldn't be opened. --------- Co-authored-by: Szymon Kowaliński --- .../widgets/report_change_button.dart | 21 +------------------ lib/l10n/app_pl.arb | 2 +- lib/utils/launch_url_util.dart | 17 +++++++++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart index 06deadf5..00bed0a4 100644 --- a/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/report_change_button.dart @@ -2,7 +2,6 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:fluttertoast/fluttertoast.dart"; import "../../../../../config/ui_config.dart"; import "../../../../../theme/app_theme.dart"; @@ -20,27 +19,9 @@ class ReportChangeButton extends ConsumerWidget { const SizedBox(height: 8), ElevatedButton( onPressed: () async { - final errorMessageToast = - context.localize.report_change_error_toast_message; - final backgroundColorToast = context.colorTheme.greyLight; - final textColorToast = context.colorTheme.blackMirage; - final emailUrl = "mailto:${context.localize.report_change_email}?subject=${Uri.encodeComponent(context.localize.report_change_subject)}"; - - if (!await ref.launch( - emailUrl, - )) { - unawaited( - Fluttertoast.showToast( - msg: errorMessageToast, - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.BOTTOM, - backgroundColor: backgroundColorToast, - textColor: textColorToast, - ), - ); - } + unawaited(ref.launch(emailUrl)); }, style: ElevatedButton.styleFrom( backgroundColor: context.colorTheme.blueAzure, diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d82c95c1..04d424f1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -155,7 +155,7 @@ "report_change_button" : "Zgłoś zmianę", "report_change_email" : "kn.solvro@pwr.edu.pl", "report_change_subject" : "Sugestia zmiany - ToPWR", - "report_change_error_toast_message" : "Nie można otworzyć aplikacji mailowej", + "report_change_error_toast_message" : "Wystąpił błąd przy otwieraniu hiperłącza, sprawdź uprawnienia aplikacji", "localization" : "Lokalizacja", "amenities" : "Udogodnienia", "surroundings": "Otoczenie", diff --git a/lib/utils/launch_url_util.dart b/lib/utils/launch_url_util.dart index 636f47fe..0b3fc6ea 100644 --- a/lib/utils/launch_url_util.dart +++ b/lib/utils/launch_url_util.dart @@ -1,10 +1,13 @@ import "dart:async"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:fluttertoast/fluttertoast.dart"; import "package:url_launcher/url_launcher.dart"; import "../config/url_config.dart"; import "../features/navigator/utils/navigation_commands.dart"; +import "../theme/colors.dart"; +import "context_extensions.dart"; extension LaunchUrlUtilX on WidgetRef? { Future launch(String uriStr) async { @@ -19,6 +22,20 @@ extension LaunchUrlUtilX on WidgetRef? { if (await canLaunchUrl(uri)) { return launchUrl(uri); } + + final String toastMsg = + this?.context.localize.report_change_error_toast_message ?? ""; + + if (toastMsg.isNotEmpty) { + await Fluttertoast.showToast( + msg: toastMsg, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: ColorsConsts.greyLight, + textColor: ColorsConsts.blackMirage, + ); + } + return false; } } From c7a45ebbeffc95c80a3353f19a85da484e7fec4c Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 33/83] feat: api-setup --- .../sks_chart/data/models/sks_chart_data.dart | 17 ++++++++++++ .../data/repository/sks_chart_repository.dart | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/features/sks_chart/data/models/sks_chart_data.dart create mode 100644 lib/features/sks_chart/data/repository/sks_chart_repository.dart diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart new file mode 100644 index 00000000..ae820b0b --- /dev/null +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -0,0 +1,17 @@ + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "sks_chart_data.freezed.dart"; +part "sks_chart_data.g.dart"; + +@freezed +class SksChartData with _$SksChartData { + const factory SksChartData({ + required int activeUsers, + required int movingAverage21, + required DateTime externalTimestamp +}) = _SksChartData; + + factory SksChartData.fromJson(Map json) => + _$SksChartDataFromJson(json); +} \ No newline at end of file diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart new file mode 100644 index 00000000..63c1b47a --- /dev/null +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -0,0 +1,26 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../api_base_rest/client/dio_client.dart"; +import "../../../../config/env.dart"; +import "../models/sks_chart_data.dart"; + +part "sks_chart_repository.g.dart"; + +@riverpod +Future> getLatestChartData(Ref ref) async { + final dio = ref.watch(restClientProvider); + final latestChartDataUrl = "${Env.sksUrl}/sks-users/today/"; + final response = await dio.get(latestChartDataUrl); + final data = response.data as List; + final chartDataList = data + .map( + (entry) => SksChartData.fromJson(entry as Map),) + .where((e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) + .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + .toIList(); + + return chartDataList; +} From 5a25f84140f54c2a14ebbb9ddbf48430f3054f6f Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 34/83] feat: temp navigation setup --- .../navigator/utils/navigation_commands.dart | 3 ++ .../presentation/sks_chart_screen.dart | 39 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 9 +++-- lib/utils/datetime_utils.dart | 11 ++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 5c492ab6..ccd8e3cf 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,4 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { + await _router.push(const SksChartRoute()); + } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart new file mode 100644 index 00000000..c9619a78 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -0,0 +1,39 @@ +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../utils/datetime_utils.dart"; +import "../data/repository/sks_chart_repository.dart"; + + +@RoutePage() +class SksChartView extends ConsumerWidget { + const SksChartView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + + return Scaffold( + body: asyncChartData.when( + data: (chartDataList) { + if (chartDataList.isEmpty) { + return const Center(child: Text("No data available")); + } + return ListView.builder( + itemCount: chartDataList.length, + itemBuilder: (context, index) { + final data = chartDataList[index]; + return ListTile( + title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + subtitle: Text("Value: ${data.activeUsers}"), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 6758ac7f..80ee7856 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../navigator/utils/navigation_commands.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -14,7 +15,8 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: _SksButton.new, + data: (sksUsersData) => + _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); @@ -22,8 +24,9 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key}); + const _SksButton(this.sksUserData, {super.key, required this.onTap}); + final VoidCallback onTap; final SksUserData sksUserData; @override @@ -31,7 +34,7 @@ class _SksButton extends StatelessWidget { return Padding( padding: SksConfig.outerPadding, child: GestureDetector( - onTap: () {}, + onTap: onTap, child: Row( children: [ Container( diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 88588e93..7e51ca37 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); From 89d373d9288bdccdc65c98de8b005bcfc48b7af2 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 35/83] chore: apply linter rules --- .../navigator/utils/navigation_commands.dart | 1 + .../sks_chart/data/models/sks_chart_data.dart | 9 ++++----- .../data/repository/sks_chart_repository.dart | 16 ++++++++++++---- .../sks_chart/presentation/sks_chart_screen.dart | 5 +++-- .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index ccd8e3cf..6677921b 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,6 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae820b0b..ae0b3441 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,4 +1,3 @@ - import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -9,9 +8,9 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; - factory SksChartData.fromJson(Map json) => + factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); -} \ No newline at end of file +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 63c1b47a..cef81e3a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,10 +16,18 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map),) - .where((e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) - .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + (entry) => SksChartData.fromJson(entry as Map), + ) + .where( + (e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= + 25, + ) + .map( + (e) => e = e.copyWith( + externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), + ), + ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index c9619a78..45f3b9ef 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../utils/datetime_utils.dart"; import "../data/repository/sks_chart_repository.dart"; - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -25,7 +24,9 @@ class SksChartView extends ConsumerWidget { itemBuilder: (context, index) { final data = chartDataList[index]; return ListTile( - title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + title: Text( + "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + ), subtitle: Text("Value: ${data.activeUsers}"), ); }, diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 80ee7856..a1562974 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -24,7 +24,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key, required this.onTap}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 7e51ca37..cd1ef2e9 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From 56a10394a9898a037c4a4143832f47fbd74bf2ed Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 23:18:19 +0100 Subject: [PATCH 36/83] feat: add sks chart --- .../presentation/sks_chart_screen.dart | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 45f3b9ef..e4b96293 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,10 +1,15 @@ import "package:auto_route/annotations.dart"; +import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../utils/datetime_utils.dart"; +import "../../../theme/app_theme.dart"; +import "../../about_us_view/utils/custom_license_dialog.dart"; import "../data/repository/sks_chart_repository.dart"; + +// TODO: after click the hover should be int instead of double + @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -14,26 +19,49 @@ class SksChartView extends ConsumerWidget { final asyncChartData = ref.watch(getLatestChartDataProvider); return Scaffold( - body: asyncChartData.when( - data: (chartDataList) { - if (chartDataList.isEmpty) { - return const Center(child: Text("No data available")); - } - return ListView.builder( - itemCount: chartDataList.length, - itemBuilder: (context, index) { - final data = chartDataList[index]; - return ListTile( - title: Text( - "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 250), + child: BarChart( + BarChartData( + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: 75, + barGroups: [ + BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), + BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), + BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), + BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), + BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) + ,],), + ], + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + leftTitles: const AxisTitles( + axisNameWidget: Text("Ilość osób"), + axisNameSize: 40, + ), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + style: context.textTheme.body.copyWith(fontSize: 12), + (value / 100) + .toStringAsFixed(2) + .replaceRange(2, 3, ":")); + }, + ), ), - subtitle: Text("Value: ${data.activeUsers}"), - ); - }, - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ), + ), + ), ), ); } From 36765d0a9d80e93ae3eff66438a04b23526d59da Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:26:18 +0100 Subject: [PATCH 37/83] feat: add sks chart --- .../sks_chart/data/models/sks_chart_data.dart | 11 + .../data/repository/sks_chart_repository.dart | 7 +- .../presentation/sks_chart_screen.dart | 245 +++++++++++++++--- .../widgets/sks_user_data_button.dart | 19 +- lib/l10n/app_pl.arb | 5 + lib/utils/datetime_utils.dart | 5 + pubspec.yaml | 1 + 7 files changed, 242 insertions(+), 51 deletions(-) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae0b3441..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -14,3 +15,13 @@ class SksChartData with _$SksChartData { factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); } + +extension SksChartDataIListX on IList { + double get maxNumberOfUsers { + return map( + (data) => data.activeUsers > data.movingAverage21 + ? data.activeUsers + : data.movingAverage21, + ).reduce((a, b) => a > b ? a : b).toDouble(); + } +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index cef81e3a..40c743d8 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -21,12 +21,7 @@ Future> getLatestChartData(Ref ref) async { .where( (e) => e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 25, - ) - .map( - (e) => e = e.copyWith( - externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), - ), + 15, ) .toIList(); diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index e4b96293..922a8585 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,15 +1,18 @@ import "package:auto_route/annotations.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../theme/app_theme.dart"; -import "../../about_us_view/utils/custom_license_dialog.dart"; +import "../../../theme/colors.dart"; +import "../../../utils/context_extensions.dart"; +import "../../../utils/datetime_utils.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; - -// TODO: after click the hover should be int instead of double - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -17,47 +20,139 @@ class SksChartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ListView( + children: [ + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center(child: LineHandle()), + ), + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 12, + ), + child: Center( + child: Text( + context.localize.sks_chart_title, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: _SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _LegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + _LegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ); + } +} + +class _SksChart extends StatelessWidget { + const _SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 250), - child: BarChart( - BarChartData( - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + backgroundColor: context.colorTheme.whiteSoap, + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + borderData: FlBorderData( + show: false, + ), + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final int index = entry.key; + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: index, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + leftTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - maxY: 75, - barGroups: [ - BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), - BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), - BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), - BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), - BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) - ,],), - ], - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - leftTitles: const AxisTitles( - axisNameWidget: Text("Ilość osób"), - axisNameSize: 40, - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - style: context.textTheme.body.copyWith(fontSize: 12), - (value / 100) - .toStringAsFixed(2) - .replaceRange(2, 3, ":")); - }, - ), - ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + asyncChartData.value![value.toInt()].externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, ), ), ), @@ -66,3 +161,67 @@ class SksChartView extends ConsumerWidget { ); } } + +class _LegendItem extends StatelessWidget { + const _LegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [4], + radius: const Radius.circular(8), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: 18, + height: 18, + ), + ) + else + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: isPredicted ? [4] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index a1562974..0f8dfeeb 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../navigator/utils/navigation_commands.dart"; +import "../../../sks_chart/presentation/sks_chart_screen.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -16,7 +16,22 @@ class SksUserDataButton extends ConsumerWidget { return asyncSksUserData.when( data: (sksUsersData) => - _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + _SksButton( + sksUsersData, + onTap: () async => showModalBottomSheet( + context: context, + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * + FilterConfig.bottomSheetHeightFactor, + ), + isScrollControlled: true, + builder: (BuildContext context) => UncontrolledProviderScope( + container: ProviderScope.containerOf(context), + child: const SksChartView(), + ), + ), + ), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 04d424f1..c2af3777 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -150,6 +150,11 @@ "settings": "Ustawienia", "about_the_app": "O aplikacji", "other_view" : "Inne", + "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", + "sks_chart_legend_users" : "Zmierzona liczba osób", + "sks_chart_legend_forecast" : "Prognozowana liczba osób", + "sks_chart_number_of_users" : "Liczba osób" + "other_view" : "Inne", "map" : "Mapa", "report_change_title" : "Coś się zmieniło?", "report_change_button" : "Zgłoś zmianę", diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index cd1ef2e9..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,6 +30,11 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } + String toHourMinuteString() { + final DateFormat hourFormat = DateFormat("HH:mm"); + return hourFormat.format(this); + } + // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); diff --git a/pubspec.yaml b/pubspec.yaml index 19ddbfea..3dbbc3fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: in_app_review: ^2.0.9 flutter_map_animations: ^0.7.1 fluttertoast: ^8.2.8 + dotted_border: ^2.1.0 dev_dependencies: flutter_test: From 03fdb4534e1bcf9164e82ab71f7fea421deab14b Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 38/83] feat: temp navigation setup --- lib/utils/datetime_utils.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 18b1f035..9f062db5 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); From 023c5089c0915355bc700348e54c5aae4799a6b3 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 39/83] chore: apply linter rules --- lib/utils/datetime_utils.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 9f062db5..a89258d3 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From ad169306f8616f92a3d6aa6bd01ecb5557e55256 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:34:40 +0100 Subject: [PATCH 40/83] chore: code cleanup --- .../navigator/utils/navigation_commands.dart | 4 ---- lib/utils/datetime_utils.dart | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 6677921b..5c492ab6 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,8 +80,4 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } - - Future navigateToSksChart() async { - await _router.push(const SksChartRoute()); - } } diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index a89258d3..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,18 +30,6 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } - String toDayDateHourString() { - final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); - final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); - final DateFormat hourFormat = DateFormat("HH:mm"); - final String day = dayFormat.format(this); - final String capitalizedDay = - day[0].toUpperCase() + day.substring(1).toLowerCase(); - final String date = dateFormat.format(this); - final String hour = hourFormat.format(this); - return "$capitalizedDay, $date $hour"; - } - String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this); From da8d768732f8b4007531b3d3282e1a2b18e226ce Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:05:23 +0100 Subject: [PATCH 41/83] chore: code separation --- lib/config/ui_config.dart | 9 + .../parking_chart/widgets/chart_widget.dart | 9 +- .../sks_chart_bar_touch_data.dart | 28 +++ .../chart_elements/sks_chart_border_data.dart | 8 + .../chart_elements/sks_chart_grid_data.dart | 9 + .../chart_elements/sks_chart_labels.dart | 57 ++++++ .../chart_elements/sks_chart_legend_item.dart | 50 +++++ .../sks_chart/presentation/sks_chart.dart | 90 +++++++++ .../presentation/sks_chart_screen.dart | 182 ++---------------- lib/widgets/chart_elements.dart | 5 + 10 files changed, 274 insertions(+), 173 deletions(-) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart.dart create mode 100644 lib/widgets/chart_elements.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 434d1c1c..a3859e33 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -202,6 +202,15 @@ abstract class SksConfig { static const outerPadding = EdgeInsets.only(right: 12, bottom: 2); } +abstract class SksChartConfig { + static const borderDashArray = 4.0; + static const borderRadius = 8.0; + static const paddingLarge = 16.0; + static const paddingMedium = 12.0; + static const paddingExtraSmall = 4.0; + static const legendItemSize = 18.0; +} + abstract class NavigationTabViewConfig { static const universalPadding = 12.0; static const radius = 8.0; diff --git a/lib/features/parking_chart/widgets/chart_widget.dart b/lib/features/parking_chart/widgets/chart_widget.dart index 221ea8a8..5eea93f2 100644 --- a/lib/features/parking_chart/widgets/chart_widget.dart +++ b/lib/features/parking_chart/widgets/chart_widget.dart @@ -3,6 +3,7 @@ import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "../../../theme/app_theme.dart"; +import "../../../widgets/chart_elements.dart"; import "../../parkings_view/models/parking.dart"; import "../chart_elements/chart_border.dart"; import "../chart_elements/chart_grid.dart"; @@ -30,8 +31,8 @@ class ChartWidget extends StatelessWidget { borderData: ChartBorder(context), gridData: ChartGrid(context), titlesData: FlTitlesData( - rightTitles: const _HideLabels(), - topTitles: const _HideLabels(), + rightTitles: const HideLabels(), + topTitles: const HideLabels(), bottomTitles: BottomLabels(context), leftTitles: LeftLabels(context), ), @@ -64,7 +65,3 @@ class ChartWidget extends StatelessWidget { ); } } - -class _HideLabels extends AxisTitles { - const _HideLabels() : super(sideTitles: const SideTitles()); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart new file mode 100644 index 00000000..587e1ab8 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart @@ -0,0 +1,28 @@ +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../theme/app_theme.dart"; + +class SksChartBarTouchData extends BarTouchData { + SksChartBarTouchData(BuildContext context) + : super( + enabled: true, + touchTooltipData: _SksChartTooltipData(context), + ); +} + +class _SksChartTooltipData extends BarTouchTooltipData { + _SksChartTooltipData(BuildContext context) + : super( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + rod.toY.toStringAsFixed(0), + context.textTheme.title + .copyWith(color: context.colorTheme.whiteSoap), + ); + }, + getTooltipColor: (barChartGroup) { + return context.colorTheme.orangePomegranade; + }, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart new file mode 100644 index 00000000..f0c99c01 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart @@ -0,0 +1,8 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartBorderData extends FlBorderData { + SksChartBorderData() + : super( + show: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart new file mode 100644 index 00000000..c3c2cb6f --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart @@ -0,0 +1,9 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartGridData extends FlGridData { + const SksChartGridData() + : super( + drawHorizontalLine: false, + drawVerticalLine: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart new file mode 100644 index 00000000..02e7fec1 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart @@ -0,0 +1,57 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../../utils/datetime_utils.dart"; +import "../../data/models/sks_chart_data.dart"; + +class SksChartRightTiles extends AxisTitles { + SksChartRightTiles(BuildContext context) + : super( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ); +} + +class SksChartBottomTitles extends AxisTitles { + SksChartBottomTitles(BuildContext context, IList chartData) + : super( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only( + top: SksChartConfig.paddingExtraSmall * 2, + ), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + chartData[value.toInt()] + .externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, + ), + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart new file mode 100644 index 00000000..acaa6532 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart @@ -0,0 +1,50 @@ +import "package:dotted_border/dotted_border.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; + +class SksChartLegendItem extends StatelessWidget { + const SksChartLegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [SksChartConfig.borderDashArray], + radius: const Radius.circular(SksChartConfig.borderRadius), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + ), + ) + else + Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all( + Radius.circular(SksChartConfig.borderRadius), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: SksChartConfig.paddingLarge), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart new file mode 100644 index 00000000..b43523be --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -0,0 +1,90 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../theme/colors.dart"; +import "../../../widgets/chart_elements.dart"; +import "../data/models/sks_chart_data.dart"; +import "chart_elements/sks_chart_bar_touch_data.dart"; +import "chart_elements/sks_chart_border_data.dart"; +import "chart_elements/sks_chart_grid_data.dart"; +import "chart_elements/sks_chart_labels.dart"; + +class SksChart extends StatelessWidget { + const SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: entry.key, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + alignment: BarChartAlignment.spaceAround, + backgroundColor: context.colorTheme.whiteSoap, + titlesData: FlTitlesData( + topTitles: const HideLabels(), + leftTitles: const HideLabels(), + rightTitles: SksChartRightTiles(context), + bottomTitles: SksChartBottomTitles( + context, + asyncChartData.value ?? const IList.empty(), + ), + ), + barTouchData: SksChartBarTouchData(context), + gridData: const SksChartGridData(), + borderData: SksChartBorderData(), + ), + ), + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(SksChartConfig.borderRadius), + ), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: + isPredicted ? [SksChartConfig.borderDashArray.toInt()] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 922a8585..88a61cd9 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,17 +1,15 @@ import "package:auto_route/annotations.dart"; -import "package:dotted_border/dotted_border.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; import "../../../utils/context_extensions.dart"; -import "../../../utils/datetime_utils.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; @RoutePage() class SksChartView extends ConsumerWidget { @@ -23,23 +21,26 @@ class SksChartView extends ConsumerWidget { final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), child: ListView( children: [ const Padding( - padding: EdgeInsets.only(top: 16), + padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), child: Center(child: LineHandle()), ), Padding( padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 12, + left: SksChartConfig.paddingMedium, + right: SksChartConfig.paddingMedium, + top: SksChartConfig.paddingMedium, ), child: Center( child: Text( context.localize.sks_chart_title, - style: context.textTheme.headline, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), textAlign: TextAlign.center, ), ), @@ -49,7 +50,7 @@ class SksChartView extends ConsumerWidget { top: 50, left: 25, ), - child: _SksChart( + child: SksChart( maxNumberOfUsers: maxNumberOfUsers, asyncChartData: asyncChartData, ), @@ -57,11 +58,11 @@ class SksChartView extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_users, isPredicted: false, ), - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_forecast, isPredicted: true, ), @@ -72,156 +73,3 @@ class SksChartView extends ConsumerWidget { ); } } - -class _SksChart extends StatelessWidget { - const _SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - swapAnimationDuration: Duration.zero, - BarChartData( - backgroundColor: context.colorTheme.whiteSoap, - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, - ), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - borderData: FlBorderData( - show: false, - ), - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final int index = entry.key; - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: index, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - topTitles: const AxisTitles(), - leftTitles: const AxisTitles(), - rightTitles: AxisTitles( - axisNameWidget: Text( - context.localize.sks_chart_number_of_users, - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ), - axisNameSize: 35, - sideTitles: SideTitles( - showTitles: true, - reservedSize: 25, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ); - }, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 35, - getTitlesWidget: (double value, TitleMeta meta) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - asyncChartData.value![value.toInt()].externalTimestamp - .toLocal() - .toHourMinuteString(), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LegendItem extends StatelessWidget { - const _LegendItem({required this.text, required this.isPredicted}); - - final String text; - final bool isPredicted; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - if (isPredicted) - DottedBorder( - borderType: BorderType.RRect, - dashPattern: const [4], - radius: const Radius.circular(8), - padding: EdgeInsets.zero, - // ignore: sized_box_for_whitespace - child: Container( - width: 18, - height: 18, - ), - ) - else - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - text, - style: context.textTheme.body, - ), - ), - ], - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: isPredicted ? [4] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} diff --git a/lib/widgets/chart_elements.dart b/lib/widgets/chart_elements.dart new file mode 100644 index 00000000..b4bd09af --- /dev/null +++ b/lib/widgets/chart_elements.dart @@ -0,0 +1,5 @@ +import "package:fl_chart/fl_chart.dart"; + +class HideLabels extends AxisTitles { + const HideLabels() : super(sideTitles: const SideTitles()); +} From ba26bd0cb7b8d1030fc5d9b758d86dd43ad66b18 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:08:22 +0100 Subject: [PATCH 42/83] chore: remove navigation --- lib/features/sks_chart/presentation/sks_chart_screen.dart | 2 -- .../presentation/widgets/sks_user_data_button.dart | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 88a61cd9..fc1c3660 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,4 +1,3 @@ -import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -11,7 +10,6 @@ import "../data/repository/sks_chart_repository.dart"; import "chart_elements/sks_chart_legend_item.dart"; import "sks_chart.dart"; -@RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 0f8dfeeb..9fda8812 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -15,9 +15,7 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: (sksUsersData) => - // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), - _SksButton( + data: (sksUsersData) => _SksButton( sksUsersData, onTap: () async => showModalBottomSheet( context: context, From 855f6ef457ac9b29621ce9d8a011137b98e2413e Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:33:56 +0100 Subject: [PATCH 43/83] chore : final formatting --- .../presentation/sks_chart_screen.dart | 73 ---------------- .../presentation/sks_chart_sheet.dart | 84 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 7 +- 3 files changed, 86 insertions(+), 78 deletions(-) delete mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart_sheet.dart diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart deleted file mode 100644 index fc1c3660..00000000 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../utils/context_extensions.dart"; -import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../data/models/sks_chart_data.dart"; -import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; - -class SksChartView extends ConsumerWidget { - const SksChartView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asyncChartData = ref.watch(getLatestChartDataProvider); - final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: ListView( - children: [ - const Padding( - padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), - child: Center(child: LineHandle()), - ), - Padding( - padding: const EdgeInsets.only( - left: SksChartConfig.paddingMedium, - right: SksChartConfig.paddingMedium, - top: SksChartConfig.paddingMedium, - ), - child: Center( - child: Text( - context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, - ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart new file mode 100644 index 00000000..a2606ec7 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -0,0 +1,84 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../utils/context_extensions.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; +import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; + +class SksChartSheet extends ConsumerWidget { + const SksChartSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(SksChartConfig.paddingLarge), + child: _SksSheetHeader(), + ), + Expanded( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SksChartLegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + SksChartLegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SksSheetHeader extends StatelessWidget { + const _SksSheetHeader(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const LineHandle(), + const SizedBox(height: 8), + Text( + context.localize.sks_chart_title, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 9fda8812..ac5cd8d7 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../sks_chart/presentation/sks_chart_screen.dart"; +import "../../../sks_chart/presentation/sks_chart_sheet.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -24,10 +24,7 @@ class SksUserDataButton extends ConsumerWidget { FilterConfig.bottomSheetHeightFactor, ), isScrollControlled: true, - builder: (BuildContext context) => UncontrolledProviderScope( - container: ProviderScope.containerOf(context), - child: const SksChartView(), - ), + builder: (BuildContext context) => const SksChartSheet(), ), ), error: (error, stackTrace) => const SizedBox.shrink(), From f20590dffb508e390cab12e8d9a94446342ba821 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:38:09 +0100 Subject: [PATCH 44/83] chore : change sks menu package name to reflect convention --- .../{sks-menu => sks_menu}/data/models/dish_category_enum.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_data.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_response.dart | 0 .../data/repository/sks_menu_repository.dart | 0 .../{sks-menu => sks_menu}/presentation/sks_menu_screen.dart | 0 .../presentation/widgets/sks_menu_data_source_link.dart | 0 .../presentation/widgets/sks_menu_header.dart | 0 .../presentation/widgets/sks_menu_section.dart | 0 .../presentation/widgets/sks_menu_tiles.dart | 0 .../presentation/widgets/sks_menu_view_loading.dart | 0 .../presentation/widgets/technical_message.dart | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename lib/features/{sks-menu => sks_menu}/data/models/dish_category_enum.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_data.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_response.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/repository/sks_menu_repository.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/sks_menu_screen.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_data_source_link.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_header.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_section.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_tiles.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_view_loading.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/technical_message.dart (100%) diff --git a/lib/features/sks-menu/data/models/dish_category_enum.dart b/lib/features/sks_menu/data/models/dish_category_enum.dart similarity index 100% rename from lib/features/sks-menu/data/models/dish_category_enum.dart rename to lib/features/sks_menu/data/models/dish_category_enum.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_data.dart b/lib/features/sks_menu/data/models/sks_menu_data.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_data.dart rename to lib/features/sks_menu/data/models/sks_menu_data.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_response.dart b/lib/features/sks_menu/data/models/sks_menu_response.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_response.dart rename to lib/features/sks_menu/data/models/sks_menu_response.dart diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks_menu/data/repository/sks_menu_repository.dart similarity index 100% rename from lib/features/sks-menu/data/repository/sks_menu_repository.dart rename to lib/features/sks_menu/data/repository/sks_menu_repository.dart diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart similarity index 100% rename from lib/features/sks-menu/presentation/sks_menu_screen.dart rename to lib/features/sks_menu/presentation/sks_menu_screen.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_header.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_header.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_header.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_section.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_section.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_section.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_view_loading.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_view_loading.dart diff --git a/lib/features/sks-menu/presentation/widgets/technical_message.dart b/lib/features/sks_menu/presentation/widgets/technical_message.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/technical_message.dart rename to lib/features/sks_menu/presentation/widgets/technical_message.dart From 40173ef764356125e427c5e52967bf2b8fcfe1b5 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:47:33 +0100 Subject: [PATCH 45/83] chore : change fl chart version & fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index b43523be..e117d58a 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - swapAnimationDuration: Duration.zero, + duration : Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() diff --git a/pubspec.yaml b/pubspec.yaml index 3dbbc3fb..af55a2ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 collection: ^1.19.1 - fl_chart: ^0.69.0 + fl_chart: ^0.69.2 permission_handler: ^11.3.1 flutter_widget_from_html_core: ^0.15.2 html: ^0.15.4 From fafaa473655e5658f354d96245f7836cf40b9ee8 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:52:38 +0100 Subject: [PATCH 46/83] chore : fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index e117d58a..225d6158 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - duration : Duration.zero, + duration: Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() From 71cdaf01f4fd076469bcd412ce58152049d0a758 Mon Sep 17 00:00:00 2001 From: Bartosh <101900992+24bartixx@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:04:17 +0100 Subject: [PATCH 47/83] feat(digital-guide): add amenities section (#486) What has been done - change the storey icon - add proper icons to the amenities section - display list of phone numbers https://private-user-images.githubusercontent.com/101900992/394956735-704d7640-b599-4219-b81e-d995f3dd4dc2.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzQyNzE2NzMsIm5iZiI6MTczNDI3MTM3MywicGF0aCI6Ii8xMDE5MDA5OTIvMzk0OTU2NzM1LTcwNGQ3NjQwLWI1OTktNDIxOS1iODFlLWQ5OTVmM2RkNGRjMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMjE1JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTIxNVQxNDAyNTNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xMWRjNGZlN2QzNDkyZTI4NzQ3NjdmYjI5NTY0MDhmMTVhYTZlYjNhYjY4NmUwMDFkMmIyYzRmZmExYTdlNjY3JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.JF834C-sPqG-gj5CS3Lyka-I5meViuquf3aN81Or_JE --- assets/svg/digital_guide/assistance_dog.svg | 8 +++++ assets/svg/digital_guide/braille.svg | 18 ++++++++++++ assets/svg/digital_guide/emergency_chairs.svg | 12 ++++++++ assets/svg/digital_guide/induction_loop.svg | 10 +++++++ assets/svg/digital_guide/large_font.svg | 10 +++++++ assets/svg/digital_guide/micronavigation.svg | 11 +++++++ .../svg/digital_guide/orientation_paths.svg | 29 +++++++++++++++++++ assets/svg/digital_guide/sign_language.svg | 4 +++ assets/svg/digital_guide/storey.svg | 4 ++- .../amenities_expansion_tile_content.dart | 18 ++++++------ .../data/models/digital_guide_response.dart | 11 ++++--- .../digital_guide_response_extended.dart | 6 ++-- .../presentation/digital_guide_view.dart | 12 ++++---- .../widgets/accessibility_button.dart | 10 +++---- .../widgets/headlines_section.dart | 2 +- .../widgets/science_clubs_section.dart | 2 +- lib/l10n/app_pl.arb | 2 +- pubspec.yaml | 8 +++++ 18 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 assets/svg/digital_guide/assistance_dog.svg create mode 100644 assets/svg/digital_guide/braille.svg create mode 100644 assets/svg/digital_guide/emergency_chairs.svg create mode 100644 assets/svg/digital_guide/induction_loop.svg create mode 100644 assets/svg/digital_guide/large_font.svg create mode 100644 assets/svg/digital_guide/micronavigation.svg create mode 100644 assets/svg/digital_guide/orientation_paths.svg create mode 100644 assets/svg/digital_guide/sign_language.svg diff --git a/assets/svg/digital_guide/assistance_dog.svg b/assets/svg/digital_guide/assistance_dog.svg new file mode 100644 index 00000000..c52f6078 --- /dev/null +++ b/assets/svg/digital_guide/assistance_dog.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/svg/digital_guide/braille.svg b/assets/svg/digital_guide/braille.svg new file mode 100644 index 00000000..3b1505b5 --- /dev/null +++ b/assets/svg/digital_guide/braille.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/digital_guide/emergency_chairs.svg b/assets/svg/digital_guide/emergency_chairs.svg new file mode 100644 index 00000000..dff36401 --- /dev/null +++ b/assets/svg/digital_guide/emergency_chairs.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/svg/digital_guide/induction_loop.svg b/assets/svg/digital_guide/induction_loop.svg new file mode 100644 index 00000000..5e3c12f9 --- /dev/null +++ b/assets/svg/digital_guide/induction_loop.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/digital_guide/large_font.svg b/assets/svg/digital_guide/large_font.svg new file mode 100644 index 00000000..7a0261e4 --- /dev/null +++ b/assets/svg/digital_guide/large_font.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/digital_guide/micronavigation.svg b/assets/svg/digital_guide/micronavigation.svg new file mode 100644 index 00000000..a6dc67ad --- /dev/null +++ b/assets/svg/digital_guide/micronavigation.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/svg/digital_guide/orientation_paths.svg b/assets/svg/digital_guide/orientation_paths.svg new file mode 100644 index 00000000..18a5bc15 --- /dev/null +++ b/assets/svg/digital_guide/orientation_paths.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/digital_guide/sign_language.svg b/assets/svg/digital_guide/sign_language.svg new file mode 100644 index 00000000..3ede508f --- /dev/null +++ b/assets/svg/digital_guide/sign_language.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/svg/digital_guide/storey.svg b/assets/svg/digital_guide/storey.svg index 5d715829..2f23acc9 100644 --- a/assets/svg/digital_guide/storey.svg +++ b/assets/svg/digital_guide/storey.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart b/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart index e0fc7ede..dc3ca6ab 100644 --- a/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart +++ b/lib/features/digital_guide_view/amenities/presentation/amenities_expansion_tile_content.dart @@ -21,42 +21,42 @@ class AmenitiesExpansionTileContent extends StatelessWidget { if (digitalGuideResponseExtended.canAssistanceDog) ContactIconsModel( text: context.localize.assistance_dog, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.assistanceDog, ), if (digitalGuideResponseExtended.isInductionLoop) ContactIconsModel( text: context.localize.induction_loop, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.inductionLoop, ), if (digitalGuideResponseExtended.isMicroNavigationSystem) ContactIconsModel( text: context.localize.micronavigation_system, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.micronavigation, ), if (digitalGuideResponseExtended.areGuidancePaths) ContactIconsModel( - text: context.localize.guidance_paths, - icon: Assets.svg.contactIcons.compass, + text: context.localize.orientation_paths, + icon: Assets.svg.digitalGuide.orientationPaths, ), if (digitalGuideResponseExtended.areBrailleBoards) ContactIconsModel( text: context.localize.information_boards_with_braille_description, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.braille, ), if (digitalGuideResponseExtended.areLargeFontBoards) ContactIconsModel( text: context.localize.information_boards_with_large_font, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.largeFont, ), if (digitalGuideResponseExtended.isSignLanguageInterpreter) ContactIconsModel( text: context.localize.sign_language_interpreter, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.signLanguage, ), if (digitalGuideResponseExtended.areEmergencyChairs) ContactIconsModel( text: context.localize.emergency_chairs, - icon: Assets.svg.contactIcons.compass, + icon: Assets.svg.digitalGuide.emergencyChairs, ), ].lock, ); diff --git a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart index 5fc48050..09766d6b 100644 --- a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart +++ b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response.dart @@ -51,8 +51,8 @@ class DigitalGuideResponse with _$DigitalGuideResponse { fromJson: _stringToBool, ) required bool areEmergencyChairs, - @JsonKey(name: "telephone_number", fromJson: _formatTelephoneNumber) - required String telephoneNumber, + @JsonKey(name: "telephone_number", fromJson: _formatPhoneNumbers) + required List phoneNumbers, @JsonKey(name: "surrounding") required int surroundingId, required List images, String? imageUrl, @@ -88,6 +88,9 @@ bool _stringToBool(String value) { return value == "True"; } -String _formatTelephoneNumber(String telephoneNumber) { - return telephoneNumber.replaceAll("

", "").replaceAll("

", ""); +List _formatPhoneNumbers(String phoneNumber) { + final matches = RegExp(r"\d{9}").allMatches( + phoneNumber.replaceAll("+48", "").replaceAll(RegExp(r"\D"), ""), + ); + return matches.map((match) => match.group(0)!).toList(); } diff --git a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart index 7320e0ad..4ede202f 100644 --- a/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart +++ b/lib/features/digital_guide_view/general_info/data/models/digital_guide_response_extended.dart @@ -15,7 +15,7 @@ class DigitalGuideResponseExtended { required this.areLargeFontBoards, required this.isSignLanguageInterpreter, required this.areEmergencyChairs, - required this.telephoneNumber, + required this.phoneNumbers, required this.surroundingId, required this.images, required this.imageUrl, @@ -32,7 +32,7 @@ class DigitalGuideResponseExtended { final bool areLargeFontBoards; final bool isSignLanguageInterpreter; final bool areEmergencyChairs; - final String telephoneNumber; + final List phoneNumbers; final int surroundingId; final List images; final String? imageUrl; @@ -53,7 +53,7 @@ class DigitalGuideResponseExtended { areLargeFontBoards: digitalGuideResponse.areLargeFontBoards, isSignLanguageInterpreter: digitalGuideResponse.isSignLanguageInterpreter, areEmergencyChairs: digitalGuideResponse.areEmergencyChairs, - telephoneNumber: digitalGuideResponse.telephoneNumber, + phoneNumbers: digitalGuideResponse.phoneNumbers, surroundingId: digitalGuideResponse.surroundingId, images: digitalGuideResponse.images, imageUrl: imageUrl, diff --git a/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart b/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart index eda567fc..38da56b8 100644 --- a/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart +++ b/lib/features/digital_guide_view/general_info/presentation/digital_guide_view.dart @@ -73,12 +73,12 @@ class _DigitalGuideView extends ConsumerWidget { .replaceAll("ulica", "ul."), icon: Assets.svg.contactIcons.compass, ), - ContactIconsModel( - text: digitalGuideResponseExtended.telephoneNumber, - icon: Assets.svg.contactIcons.phone, - // TODO(Bartosh): url not working, nothing happens - url: - "tel:+48${digitalGuideResponseExtended.telephoneNumber.replaceAll("

", "").replaceAll("

", "")}", + ...digitalGuideResponseExtended.phoneNumbers.map( + (phoneNumber) => ContactIconsModel( + text: "+48$phoneNumber", + icon: Assets.svg.contactIcons.phone, + url: "tel:+48$phoneNumber", + ), ), ContactIconsModel( text: context.localize diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart index 8951fa11..85fbe089 100644 --- a/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/accessibility_button.dart @@ -15,16 +15,16 @@ class AccessibilityButton extends StatelessWidget { borderRadius: BorderRadius.circular( DigitalGuideConfig.borderRadiusMedium, ), - side: BorderSide( - color: context.colorTheme.greyPigeon, - ), + ), + side: BorderSide( + color: context.colorTheme.greyPigeon, ), backgroundColor: context.colorTheme.greyLight, minimumSize: const Size(56, 32), ), - child: const Icon( + child: Icon( Icons.accessible, - color: Colors.black, + color: context.colorTheme.blackMirage, ), ), ); diff --git a/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart b/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart index 77452d0c..0c60e01c 100644 --- a/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart +++ b/lib/features/digital_guide_view/general_info/presentation/widgets/headlines_section.dart @@ -22,7 +22,7 @@ class HeadlinesSection extends StatelessWidget { name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), ), - Text(description), + if (description.isNotEmpty) Text(description), ], ), ); diff --git a/lib/features/home_view/widgets/science_clubs_section.dart b/lib/features/home_view/widgets/science_clubs_section.dart index b7b57859..1d00a258 100644 --- a/lib/features/home_view/widgets/science_clubs_section.dart +++ b/lib/features/home_view/widgets/science_clubs_section.dart @@ -28,7 +28,7 @@ class ScienceClubsSection extends ConsumerWidget { ), FilledButton( onPressed: () { - unawaited(ref.navigateDigitalGuide(101)); + unawaited(ref.navigateDigitalGuide(204)); }, child: const Text("Navigate to digital guide screen!"), ), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 04d424f1..ad1155df 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -180,7 +180,7 @@ "assistance_dog" : "Do budynku i wszystkich jego pomieszczeń można wejść z psem asystującym i psem przewodnikiem", "induction_loop": "W budynku jest/są pętle indukcyjne", "micronavigation_system": "W budynku zostały zainstalowane urządzenia systemu nawigacyjno-informacyjnego", - "guidance_paths": "W budynku zastosowane zostały ścieżki naprowadzające (dotykowe)", + "orientation_paths": "W budynku zastosowane zostały ścieżki naprowadzające (dotykowe)", "information_boards_with_braille_description": "W budynku znajdują się czytelne tablice informacyjne zawierające opisy w alfabecie Braille'a", "information_boards_with_large_font": "W budynku znajdują się czytelne tablice informacyjne zawierające napisy w dużej czcionce", "sign_language_interpreter": "W budynku zapewniona jest możliwość skorzystania z usług tłumacza języka migowego", diff --git a/pubspec.yaml b/pubspec.yaml index 19ddbfea..1215eccb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -147,6 +147,14 @@ flutter: - assets/svg/contact_icons/tiktok.svg - assets/svg/contact_icons/discord.svg - assets/svg/digital_guide/storey.svg + - assets/svg/digital_guide/assistance_dog.svg + - assets/svg/digital_guide/braille.svg + - assets/svg/digital_guide/emergency_chairs.svg + - assets/svg/digital_guide/induction_loop.svg + - assets/svg/digital_guide/large_font.svg + - assets/svg/digital_guide/micronavigation.svg + - assets/svg/digital_guide/orientation_paths.svg + - assets/svg/digital_guide/sign_language.svg - assets/animations/error.json - assets/animations/search.json - assets/animations/offline.json From 6643b58c617dd148f6cee7a244df539bbd16eb5a Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 10:54:32 +0100 Subject: [PATCH 48/83] feat(sks-chart): add new version of chart --- lib/config/ui_config.dart | 15 ++- .../data/repository/sks_chart_repository.dart | 5 - .../chart_elements/sks_chart.dart | 99 +++++++++++++++++++ .../sks_chart_bar_touch_data.dart | 28 ------ .../chart_elements/sks_chart_border_data.dart | 8 -- .../chart_elements/sks_chart_grid_data.dart | 11 ++- .../chart_elements/sks_chart_header.dart | 55 +++++++++++ .../chart_elements/sks_chart_labels.dart | 38 ++++--- ...legend_item.dart => sks_chart_legend.dart} | 47 ++++++--- .../sks_chart_line_touch_data.dart | 37 +++++++ .../sks_chart/presentation/sks_chart.dart | 90 ----------------- .../presentation/sks_chart_card.dart | 62 ++++++++++++ .../presentation/sks_chart_sheet.dart | 89 ++++++++++------- .../presentation/sks_menu_screen.dart | 5 +- .../widgets/sks_menu_data_source_link.dart | 16 +-- lib/l10n/app_pl.arb | 7 +- 16 files changed, 401 insertions(+), 211 deletions(-) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart.dart delete mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart delete mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart rename lib/features/sks_chart/presentation/chart_elements/{sks_chart_legend_item.dart => sks_chart_legend.dart} (53%) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart delete mode 100644 lib/features/sks_chart/presentation/sks_chart.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart_card.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index a3859e33..0e65fa4f 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -204,11 +204,24 @@ abstract class SksConfig { abstract class SksChartConfig { static const borderDashArray = 4.0; - static const borderRadius = 8.0; + static const borderRadius = 16.0; static const paddingLarge = 16.0; static const paddingMedium = 12.0; + static const paddingSmall = 8.0; static const paddingExtraSmall = 4.0; static const legendItemSize = 18.0; + static const heightSmall = 8.0; + static const heightMedium = 12.0; + static const heightLarge = 16.0; + static const paddingLargeLTR = EdgeInsets.only( + left: SksChartConfig.paddingLarge, + top: SksChartConfig.paddingLarge, + right: SksChartConfig.paddingLarge, + ); + static const sksChartDataUrl = "https://live.pwr.edu.pl/sks/"; + static const sksAddress = "Hoene-Wrońskiego 10"; + static const sksPostalCode = "50-370 Wrocław"; + static const buildingCode = "C-18"; } abstract class NavigationTabViewConfig { diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 40c743d8..9120c7b1 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -18,11 +18,6 @@ Future> getLatestChartData(Ref ref) async { .map( (entry) => SksChartData.fromJson(entry as Map), ) - .where( - (e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 15, - ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart new file mode 100644 index 00000000..70cd0005 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart @@ -0,0 +1,99 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../theme/colors.dart"; +import "../../../../widgets/chart_elements.dart"; +import "../../data/models/sks_chart_data.dart"; +import "sks_chart_grid_data.dart"; +import "sks_chart_labels.dart"; +import "sks_chart_line_touch_data.dart"; + +class SksChart extends StatelessWidget { + const SksChart({ + required this.maxNumberOfUsers, + required this.chartData, + }); + + final double maxNumberOfUsers; + final IList chartData; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: SksChartConfig.paddingLarge), + child: AspectRatio( + aspectRatio: 1.5, + child: LineChart( + duration: Duration.zero, + LineChartData( + clipData: const FlClipData.all(), + backgroundColor: context.colorTheme.whiteSoap, + gridData: SksChartGridData(context), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + lineBarsData: [ + LineChartBarData( + belowBarData: BarAreaData( + show: true, + gradient: ColorsConsts.toPwrGradient, + applyCutOffY: true, + ), + isCurved: true, + color: context.colorTheme.orangePomegranade, + dotData: FlDotData( + checkToShowDot: (FlSpot spot, LineChartBarData barData) { + return false; + }, + ), + spots: chartData.asMap().entries.map((e) { + if (e.value.externalTimestamp.isAfter(DateTime.now())) { + return FlSpot.nullSpot; + } else { + return FlSpot( + e.key.toDouble(), + e.value.activeUsers.toDouble(), + ); + } + }).toList(), + ), + LineChartBarData( + isCurved: true, + dashArray: [ + SksChartConfig.borderDashArray.toInt(), + SksChartConfig.borderDashArray.toInt(), + ], + dotData: FlDotData( + checkToShowDot: (FlSpot spot, LineChartBarData barData) { + return false; + }, + ), + color: context.colorTheme.blueAzure, + spots: chartData.asMap().entries.map((e) { + return FlSpot( + e.key.toDouble(), + e.value.movingAverage21.toDouble(), + ); + }).toList(), + ), + ], + titlesData: FlTitlesData( + topTitles: const HideLabels(), + rightTitles: const HideLabels(), + leftTitles: SksChartRightTiles(context), + bottomTitles: SksChartBottomTitles( + context, + chartData, + ), + ), + lineTouchData: SksChartLineTouchData( + context, + chartData.map((e) => e.externalTimestamp).toIList(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart deleted file mode 100644 index 587e1ab8..00000000 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:fl_chart/fl_chart.dart"; -import "package:flutter/cupertino.dart"; - -import "../../../../theme/app_theme.dart"; - -class SksChartBarTouchData extends BarTouchData { - SksChartBarTouchData(BuildContext context) - : super( - enabled: true, - touchTooltipData: _SksChartTooltipData(context), - ); -} - -class _SksChartTooltipData extends BarTouchTooltipData { - _SksChartTooltipData(BuildContext context) - : super( - getTooltipItem: (group, groupIndex, rod, rodIndex) { - return BarTooltipItem( - rod.toY.toStringAsFixed(0), - context.textTheme.title - .copyWith(color: context.colorTheme.whiteSoap), - ); - }, - getTooltipColor: (barChartGroup) { - return context.colorTheme.orangePomegranade; - }, - ); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart deleted file mode 100644 index f0c99c01..00000000 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:fl_chart/fl_chart.dart"; - -class SksChartBorderData extends FlBorderData { - SksChartBorderData() - : super( - show: false, - ); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart index c3c2cb6f..ce58ee5b 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart @@ -1,9 +1,14 @@ import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../parking_chart/chart_elements/chart_grid.dart"; class SksChartGridData extends FlGridData { - const SksChartGridData() + SksChartGridData(BuildContext context) : super( - drawHorizontalLine: false, - drawVerticalLine: false, + verticalInterval: 100, + horizontalInterval: 25, + getDrawingHorizontalLine: (value) => GridLine(context), + getDrawingVerticalLine: (value) => GridLine(context), ); } diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart new file mode 100644 index 00000000..fead65af --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart @@ -0,0 +1,55 @@ +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../sks_people_live/data/models/sks_user_data.dart"; +import "../../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; + +// TODO(mikolaj-jalocha): add navigation? maybe after click on building informations + +class SksChartHeader extends StatelessWidget { + const SksChartHeader({ + super.key, + required this.numberOfPeople, + required this.trend, + }); + + final String numberOfPeople; + final Trend? trend; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + SksChartConfig.buildingCode, + style: context.textTheme.headline, + ), + Text( + "${context.localize.street_abbreviation} ${SksChartConfig.sksAddress}", + style: context.textTheme.body, + ), + Text(SksChartConfig.sksPostalCode, style: context.textTheme.body), + ], + ), + Row( + children: [ + Text( + numberOfPeople, + style: context.textTheme.body.copyWith(fontSize: 18), + ), + const SizedBox( + width: SksChartConfig.heightSmall, + ), + trend?.icon ?? const SizedBox.shrink(), + ], + ), + ], + ); + } +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart index 02e7fec1..620f7d07 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart @@ -16,15 +16,17 @@ class SksChartRightTiles extends AxisTitles { style: context.textTheme.body .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - axisNameSize: 35, sideTitles: SideTitles( + maxIncluded: false, + reservedSize: 40, showTitles: true, - reservedSize: 25, getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + return Center( + child: Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 12, fontWeight: FontWeight.w400), + ), ); }, ), @@ -37,18 +39,30 @@ class SksChartBottomTitles extends AxisTitles { sideTitles: SideTitles( showTitles: true, reservedSize: 35, + interval: 5, getTitlesWidget: (double value, TitleMeta meta) { + final String text = (chartData.isNotEmpty) + ? chartData[value.toInt()] + .externalTimestamp + .toLocal() + .minute == + 0 + ? chartData[value.toInt()] + .externalTimestamp + .toLocal() + .toHourMinuteString() + : "" + : ""; + return Padding( padding: const EdgeInsets.only( - top: SksChartConfig.paddingExtraSmall * 2, + top: SksChartConfig.paddingSmall, + left: 50, ), child: Text( style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - chartData[value.toInt()] - .externalTimestamp - .toLocal() - .toHourMinuteString(), + .copyWith(fontSize: 12, fontWeight: FontWeight.w400), + text, ), ); }, diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart similarity index 53% rename from lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart rename to lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart index acaa6532..8c1dd77a 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart @@ -3,6 +3,29 @@ import "package:flutter/cupertino.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; + +class SksChartLegend extends StatelessWidget { + const SksChartLegend({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SksChartLegendItem( + text: context.localize.measured_number_of_users, + isPredicted: false, + ), + SksChartLegendItem( + text: context.localize.forecasted_number_of_users, + isPredicted: true, + ), + ], + ); + } +} class SksChartLegendItem extends StatelessWidget { const SksChartLegendItem({required this.text, required this.isPredicted}); @@ -18,31 +41,25 @@ class SksChartLegendItem extends StatelessWidget { DottedBorder( borderType: BorderType.RRect, dashPattern: const [SksChartConfig.borderDashArray], - radius: const Radius.circular(SksChartConfig.borderRadius), + color: context.colorTheme.blueAzure, padding: EdgeInsets.zero, // ignore: sized_box_for_whitespace child: Container( width: SksChartConfig.legendItemSize, - height: SksChartConfig.legendItemSize, ), ) else Container( width: SksChartConfig.legendItemSize, - height: SksChartConfig.legendItemSize, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all( - Radius.circular(SksChartConfig.borderRadius), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: SksChartConfig.paddingLarge), - child: Text( - text, - style: context.textTheme.body, + height: 2, + color: context.colorTheme.orangePomegranade, ), + const SizedBox( + width: SksChartConfig.heightMedium, + ), + Text( + text, + style: context.textTheme.body, ), ], ); diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart new file mode 100644 index 00000000..2f0afb06 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart @@ -0,0 +1,37 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../theme/app_theme.dart"; +import "../../../../utils/datetime_utils.dart"; + +// TODO(mikolaj-jalocha): Sometimes hour label is displayed at the bottom, sometimes between the 2 values. +// TODO(mikolaj-jalocha): Make hour label be always in black (now is red if values were measured, blue otherwise) + +class SksChartLineTouchData extends LineTouchData { + final IList dateTime; + + SksChartLineTouchData(BuildContext context, this.dateTime) + : super( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (LineBarSpot lineBarSpot) => + context.colorTheme.greyLight, + getTooltipItems: (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final hour = (touchedSpot.barIndex == 0 || + touchedSpots.length == 1) + ? "\n${dateTime.get(touchedSpot.x.toInt()).toHourMinuteString()}" + : ""; + final value = touchedSpot.y.toStringAsFixed(0) + hour; + final Color color = (touchedSpot.barIndex == 0) + ? context.colorTheme.orangePomegranade + : context.colorTheme.blueAzure; + return LineTooltipItem( + value, + TextStyle(color: color), + ); + }).toList(); + }, + ), + ); +} diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart deleted file mode 100644 index 225d6158..00000000 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ /dev/null @@ -1,90 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; -import "../../../widgets/chart_elements.dart"; -import "../data/models/sks_chart_data.dart"; -import "chart_elements/sks_chart_bar_touch_data.dart"; -import "chart_elements/sks_chart_border_data.dart"; -import "chart_elements/sks_chart_grid_data.dart"; -import "chart_elements/sks_chart_labels.dart"; - -class SksChart extends StatelessWidget { - const SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - duration: Duration.zero, - BarChartData( - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: entry.key, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - alignment: BarChartAlignment.spaceAround, - backgroundColor: context.colorTheme.whiteSoap, - titlesData: FlTitlesData( - topTitles: const HideLabels(), - leftTitles: const HideLabels(), - rightTitles: SksChartRightTiles(context), - bottomTitles: SksChartBottomTitles( - context, - asyncChartData.value ?? const IList.empty(), - ), - ), - barTouchData: SksChartBarTouchData(context), - gridData: const SksChartGridData(), - borderData: SksChartBorderData(), - ), - ), - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(SksChartConfig.borderRadius), - ), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: - isPredicted ? [SksChartConfig.borderDashArray.toInt()] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} diff --git a/lib/features/sks_chart/presentation/sks_chart_card.dart b/lib/features/sks_chart/presentation/sks_chart_card.dart new file mode 100644 index 00000000..223c8618 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_card.dart @@ -0,0 +1,62 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../sks_people_live/data/models/sks_user_data.dart"; +import "../data/models/sks_chart_data.dart"; +import "chart_elements/sks_chart.dart"; +import "chart_elements/sks_chart_header.dart"; +import "chart_elements/sks_chart_legend.dart"; + +class SksChartCard extends StatelessWidget { + const SksChartCard({ + super.key, + required this.currentNumberOfUsers, + required this.maxNumberOfUsers, + required this.chartData, + }); + + final SksUserData? currentNumberOfUsers; + final double maxNumberOfUsers; + final IList chartData; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.colorTheme.greyLight, + borderRadius: BorderRadius.circular(SksChartConfig.borderRadius), + ), + child: Padding( + padding: SksChartConfig.paddingLargeLTR, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SksChartHeader( + numberOfPeople: + currentNumberOfUsers?.activeUsers.toString() ?? "", + trend: currentNumberOfUsers?.trend, + ), + const SizedBox( + height: SksChartConfig.heightLarge, + ), + SksChart( + maxNumberOfUsers: maxNumberOfUsers, + chartData: chartData, + ), + const Padding( + padding: EdgeInsets.only( + left: SksChartConfig.paddingLarge, + bottom: SksChartConfig.paddingLarge, + top: SksChartConfig.paddingSmall, + ), + child: SksChartLegend(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index a2606ec7..6f31b8b9 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -1,14 +1,19 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; import "../../../utils/context_extensions.dart"; +import "../../../widgets/my_error_widget.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../../sks_menu/presentation/widgets/sks_menu_data_source_link.dart"; +import "../../sks_people_live/data/repository/latest_sks_user_data_repo.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; +import "sks_chart_card.dart"; + +// TODO(mikolaj-jalocha): create shimmer loading class SksChartSheet extends ConsumerWidget { const SksChartSheet({super.key}); @@ -16,50 +21,58 @@ class SksChartSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final asyncNumberOfUsers = ref.watch(getLatestSksUserDataProvider); + + final currentNumberOfUsers = asyncNumberOfUsers.value; final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(SksChartConfig.paddingLarge), - child: _SksSheetHeader(), + return switch (asyncChartData) { + AsyncError(:final error) => MyErrorWidget(error), + AsyncLoading() => const SizedBox.shrink(), + AsyncValue() => Padding( + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, ), - Expanded( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Column( + children: [ + Padding( + padding: SksChartConfig.paddingLargeLTR + .copyWith(bottom: SksChartConfig.paddingMedium), + child: const _SksSheetHeader(), + ), + Expanded( + child: ListView( + physics: const NeverScrollableScrollPhysics(), children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, + Padding( + padding: const EdgeInsets.fromLTRB( + SksChartConfig.paddingMedium, + SksChartConfig.paddingMedium, + SksChartConfig.paddingMedium, + 0, + ), + child: SksChartCard( + currentNumberOfUsers: currentNumberOfUsers, + maxNumberOfUsers: maxNumberOfUsers, + chartData: asyncChartData.value ?? const IList.empty(), + ), ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, + Padding( + padding: const EdgeInsets.all( + SksChartConfig.paddingSmall, + ), + child: SksMenuDataSourceLink( + SksChartConfig.sksChartDataUrl, + "${context.localize.data_come_from_website}: ", + ), ), ], ), - ], - ), + ), + ], ), - ], - ), - ); + ), + }; } } @@ -71,7 +84,7 @@ class _SksSheetHeader extends StatelessWidget { return Column( children: [ const LineHandle(), - const SizedBox(height: 8), + const SizedBox(height: SksChartConfig.heightSmall), Text( context.localize.sks_chart_title, style: context.textTheme.headline diff --git a/lib/features/sks_menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart index 049f0c02..a8c3103f 100644 --- a/lib/features/sks_menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks_menu/presentation/sks_menu_screen.dart @@ -111,7 +111,10 @@ class _SksMenuView extends ConsumerWidget { padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), child: SksMenuSection(sksMenuData.meals), ), - const SksMenuDataSourceLink(), + SksMenuDataSourceLink( + SksMenuConfig.sksDataSource, + context.localize.data_come_from_website, + ), const SizedBox( height: ScienceClubsViewConfig.mediumPadding, ), diff --git a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart index 1d7a770b..556604b3 100644 --- a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart @@ -2,37 +2,39 @@ import "package:flutter/cupertino.dart"; import "package:flutter/gestures.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../../utils/context_extensions.dart"; import "../../../../utils/launch_url_util.dart"; class SksMenuDataSourceLink extends ConsumerWidget { - const SksMenuDataSourceLink({ + const SksMenuDataSourceLink( + this.url, + this.text, { super.key, }); + final String url; + final String text; + @override Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.all(16), child: Text.rich( TextSpan( - text: "${context.localize.data_come_from_website}: ", + text: text, style: const TextStyle( fontWeight: FontWeight.bold, ), children: [ TextSpan( - text: - SksMenuConfig.sksDataSource.replaceFirst("https://", "www."), + text: url.replaceFirst("https://", "www"), style: context.textTheme.bodyOrange.copyWith( decoration: TextDecoration.underline, decorationColor: context.colorTheme.orangePomegranade, fontWeight: FontWeight.bold, ), recognizer: TapGestureRecognizer() - ..onTap = () async => ref.launch(SksMenuConfig.sksDataSource), + ..onTap = () async => ref.launch(url), ), ], ), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index c2af3777..a5ea3a04 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -153,8 +153,7 @@ "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", "sks_chart_legend_users" : "Zmierzona liczba osób", "sks_chart_legend_forecast" : "Prognozowana liczba osób", - "sks_chart_number_of_users" : "Liczba osób" - "other_view" : "Inne", + "sks_chart_number_of_users" : "Liczba osób", "map" : "Mapa", "report_change_title" : "Coś się zmieniło?", "report_change_button" : "Zgłoś zmianę", @@ -209,5 +208,7 @@ "example": "wydziałów" } } - } + }, + "measured_number_of_users": "Zmierzona liczba użytkowników", + "forecasted_number_of_users": "Prognozowana liczba użytkowników" } From 3cae6aedafb3c6fd195b1b8fae3c075c3ff43951 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 14:10:57 +0100 Subject: [PATCH 49/83] chore(sks-chart): cleanup and refactor --- .../sks_chart/presentation/sks_chart_sheet.dart | 4 ++-- .../sks_menu/presentation/sks_menu_screen.dart | 6 +++--- .../text_and_url_widget.dart} | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) rename lib/{features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart => widgets/text_and_url_widget.dart} (81%) diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index 6f31b8b9..3bfae61d 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -6,8 +6,8 @@ import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; import "../../../utils/context_extensions.dart"; import "../../../widgets/my_error_widget.dart"; +import "../../../widgets/text_and_url_widget.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../../sks_menu/presentation/widgets/sks_menu_data_source_link.dart"; import "../../sks_people_live/data/repository/latest_sks_user_data_repo.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; @@ -61,7 +61,7 @@ class SksChartSheet extends ConsumerWidget { padding: const EdgeInsets.all( SksChartConfig.paddingSmall, ), - child: SksMenuDataSourceLink( + child: TextAndUrl( SksChartConfig.sksChartDataUrl, "${context.localize.data_come_from_website}: ", ), diff --git a/lib/features/sks_menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart index a8c3103f..812c417d 100644 --- a/lib/features/sks_menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks_menu/presentation/sks_menu_screen.dart @@ -13,10 +13,10 @@ import "../../../utils/context_extensions.dart"; import "../../../widgets/detail_views/detail_view_app_bar.dart"; import "../../../widgets/my_error_widget.dart"; import "../../../widgets/my_text_button.dart"; +import "../../../widgets/text_and_url_widget.dart"; import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; import "../data/models/sks_menu_response.dart"; import "../data/repository/sks_menu_repository.dart"; -import "widgets/sks_menu_data_source_link.dart"; import "widgets/sks_menu_header.dart"; import "widgets/sks_menu_section.dart"; import "widgets/sks_menu_view_loading.dart"; @@ -111,9 +111,9 @@ class _SksMenuView extends ConsumerWidget { padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), child: SksMenuSection(sksMenuData.meals), ), - SksMenuDataSourceLink( + TextAndUrl( SksMenuConfig.sksDataSource, - context.localize.data_come_from_website, + "${context.localize.data_come_from_website}: ", ), const SizedBox( height: ScienceClubsViewConfig.mediumPadding, diff --git a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/widgets/text_and_url_widget.dart similarity index 81% rename from lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart rename to lib/widgets/text_and_url_widget.dart index 556604b3..babe388d 100644 --- a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/widgets/text_and_url_widget.dart @@ -2,11 +2,11 @@ import "package:flutter/cupertino.dart"; import "package:flutter/gestures.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../../theme/app_theme.dart"; -import "../../../../utils/launch_url_util.dart"; +import "../theme/app_theme.dart"; +import "../utils/launch_url_util.dart"; -class SksMenuDataSourceLink extends ConsumerWidget { - const SksMenuDataSourceLink( +class TextAndUrl extends ConsumerWidget { + const TextAndUrl( this.url, this.text, { super.key, @@ -27,7 +27,7 @@ class SksMenuDataSourceLink extends ConsumerWidget { ), children: [ TextSpan( - text: url.replaceFirst("https://", "www"), + text: url.replaceFirst("https://", " www."), style: context.textTheme.bodyOrange.copyWith( decoration: TextDecoration.underline, decorationColor: context.colorTheme.orangePomegranade, From c70b588ddbf5dd9f40ecc096abc9726ee1301507 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 14:14:57 +0100 Subject: [PATCH 50/83] chore(sks-chart): trend's icons have one color no matter the trend --- .../presentation/widgets/sks_user_data_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index ac5cd8d7..0e04af30 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -86,9 +86,9 @@ extension TrendIcon on Trend { Icon get icon { switch (this) { case Trend.increasing: - return const Icon(Icons.trending_up, color: Color(0xFF28a745)); + return const Icon(Icons.trending_up, color: Colors.grey); case Trend.decreasing: - return const Icon(Icons.trending_down, color: Color(0xFFdc3545)); + return const Icon(Icons.trending_down, color: Colors.grey); case Trend.stable: return const Icon(Icons.trending_flat, color: Colors.grey); } From fb393d33d3ec888bfff5294eae4b82a0ae1257e5 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Tue, 17 Dec 2024 14:14:32 +0100 Subject: [PATCH 51/83] feat(sks-chart): change sheet's color to black --- lib/features/sks_chart/presentation/sks_chart_sheet.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index 3bfae61d..ad477052 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -87,8 +87,7 @@ class _SksSheetHeader extends StatelessWidget { const SizedBox(height: SksChartConfig.heightSmall), Text( context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), + style: context.textTheme.headline, textAlign: TextAlign.center, ), ], From 7a7c87c3cfac0e36940e1a33e78f1be4b31b6d6b Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 52/83] feat: api-setup --- .../sks_chart/data/models/sks_chart_data.dart | 17 ++++++++++++ .../data/repository/sks_chart_repository.dart | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/features/sks_chart/data/models/sks_chart_data.dart create mode 100644 lib/features/sks_chart/data/repository/sks_chart_repository.dart diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart new file mode 100644 index 00000000..ae820b0b --- /dev/null +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -0,0 +1,17 @@ + +import "package:freezed_annotation/freezed_annotation.dart"; + +part "sks_chart_data.freezed.dart"; +part "sks_chart_data.g.dart"; + +@freezed +class SksChartData with _$SksChartData { + const factory SksChartData({ + required int activeUsers, + required int movingAverage21, + required DateTime externalTimestamp +}) = _SksChartData; + + factory SksChartData.fromJson(Map json) => + _$SksChartDataFromJson(json); +} \ No newline at end of file diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart new file mode 100644 index 00000000..63c1b47a --- /dev/null +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -0,0 +1,26 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../api_base_rest/client/dio_client.dart"; +import "../../../../config/env.dart"; +import "../models/sks_chart_data.dart"; + +part "sks_chart_repository.g.dart"; + +@riverpod +Future> getLatestChartData(Ref ref) async { + final dio = ref.watch(restClientProvider); + final latestChartDataUrl = "${Env.sksUrl}/sks-users/today/"; + final response = await dio.get(latestChartDataUrl); + final data = response.data as List; + final chartDataList = data + .map( + (entry) => SksChartData.fromJson(entry as Map),) + .where((e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) + .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + .toIList(); + + return chartDataList; +} From 58142f9aac9925d85df5aa8c63e8b47cb1b0f705 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 53/83] feat: temp navigation setup --- .../navigator/utils/navigation_commands.dart | 3 ++ .../presentation/sks_chart_screen.dart | 39 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 9 +++-- lib/utils/datetime_utils.dart | 11 ++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 5c492ab6..ccd8e3cf 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,4 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { + await _router.push(const SksChartRoute()); + } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart new file mode 100644 index 00000000..c9619a78 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -0,0 +1,39 @@ +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../utils/datetime_utils.dart"; +import "../data/repository/sks_chart_repository.dart"; + + +@RoutePage() +class SksChartView extends ConsumerWidget { + const SksChartView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + + return Scaffold( + body: asyncChartData.when( + data: (chartDataList) { + if (chartDataList.isEmpty) { + return const Center(child: Text("No data available")); + } + return ListView.builder( + itemCount: chartDataList.length, + itemBuilder: (context, index) { + final data = chartDataList[index]; + return ListTile( + title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + subtitle: Text("Value: ${data.activeUsers}"), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 6758ac7f..80ee7856 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../navigator/utils/navigation_commands.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -14,7 +15,8 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: _SksButton.new, + data: (sksUsersData) => + _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); @@ -22,8 +24,9 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key}); + const _SksButton(this.sksUserData, {super.key, required this.onTap}); + final VoidCallback onTap; final SksUserData sksUserData; @override @@ -31,7 +34,7 @@ class _SksButton extends StatelessWidget { return Padding( padding: SksConfig.outerPadding, child: GestureDetector( - onTap: () {}, + onTap: onTap, child: Row( children: [ Container( diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 88588e93..7e51ca37 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); From a45943819d4277c7770f6feecac5e62b80fa3c23 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 54/83] chore: apply linter rules --- .../navigator/utils/navigation_commands.dart | 1 + .../sks_chart/data/models/sks_chart_data.dart | 9 ++++----- .../data/repository/sks_chart_repository.dart | 16 ++++++++++++---- .../sks_chart/presentation/sks_chart_screen.dart | 5 +++-- .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index ccd8e3cf..6677921b 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,6 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae820b0b..ae0b3441 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,4 +1,3 @@ - import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -9,9 +8,9 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; - factory SksChartData.fromJson(Map json) => + factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); -} \ No newline at end of file +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 63c1b47a..cef81e3a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,10 +16,18 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map),) - .where((e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) - .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + (entry) => SksChartData.fromJson(entry as Map), + ) + .where( + (e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= + 25, + ) + .map( + (e) => e = e.copyWith( + externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), + ), + ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index c9619a78..45f3b9ef 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../utils/datetime_utils.dart"; import "../data/repository/sks_chart_repository.dart"; - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -25,7 +24,9 @@ class SksChartView extends ConsumerWidget { itemBuilder: (context, index) { final data = chartDataList[index]; return ListTile( - title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + title: Text( + "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + ), subtitle: Text("Value: ${data.activeUsers}"), ); }, diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 80ee7856..a1562974 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -24,7 +24,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key, required this.onTap}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 7e51ca37..cd1ef2e9 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From d1164925b28e39388a4b5cbde995a36f4ba52b14 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 23:18:19 +0100 Subject: [PATCH 55/83] feat: add sks chart --- .../presentation/sks_chart_screen.dart | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 45f3b9ef..e4b96293 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,10 +1,15 @@ import "package:auto_route/annotations.dart"; +import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../utils/datetime_utils.dart"; +import "../../../theme/app_theme.dart"; +import "../../about_us_view/utils/custom_license_dialog.dart"; import "../data/repository/sks_chart_repository.dart"; + +// TODO: after click the hover should be int instead of double + @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -14,26 +19,49 @@ class SksChartView extends ConsumerWidget { final asyncChartData = ref.watch(getLatestChartDataProvider); return Scaffold( - body: asyncChartData.when( - data: (chartDataList) { - if (chartDataList.isEmpty) { - return const Center(child: Text("No data available")); - } - return ListView.builder( - itemCount: chartDataList.length, - itemBuilder: (context, index) { - final data = chartDataList[index]; - return ListTile( - title: Text( - "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 250), + child: BarChart( + BarChartData( + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: 75, + barGroups: [ + BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), + BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), + BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), + BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), + BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) + ,],), + ], + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + leftTitles: const AxisTitles( + axisNameWidget: Text("Ilość osób"), + axisNameSize: 40, + ), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + style: context.textTheme.body.copyWith(fontSize: 12), + (value / 100) + .toStringAsFixed(2) + .replaceRange(2, 3, ":")); + }, + ), ), - subtitle: Text("Value: ${data.activeUsers}"), - ); - }, - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ), + ), + ), ), ); } From 2ad51162230789a245327ca633b16faf7a9e9eec Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:26:18 +0100 Subject: [PATCH 56/83] feat: add sks chart --- .../sks_chart/data/models/sks_chart_data.dart | 11 + .../data/repository/sks_chart_repository.dart | 7 +- .../presentation/sks_chart_screen.dart | 245 +++++++++++++++--- .../widgets/sks_user_data_button.dart | 19 +- lib/l10n/app_pl.arb | 5 + lib/utils/datetime_utils.dart | 5 + pubspec.yaml | 1 + 7 files changed, 242 insertions(+), 51 deletions(-) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index ae0b3441..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -14,3 +15,13 @@ class SksChartData with _$SksChartData { factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); } + +extension SksChartDataIListX on IList { + double get maxNumberOfUsers { + return map( + (data) => data.activeUsers > data.movingAverage21 + ? data.activeUsers + : data.movingAverage21, + ).reduce((a, b) => a > b ? a : b).toDouble(); + } +} diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index cef81e3a..40c743d8 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -21,12 +21,7 @@ Future> getLatestChartData(Ref ref) async { .where( (e) => e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 25, - ) - .map( - (e) => e = e.copyWith( - externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), - ), + 15, ) .toIList(); diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index e4b96293..922a8585 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,15 +1,18 @@ import "package:auto_route/annotations.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../theme/app_theme.dart"; -import "../../about_us_view/utils/custom_license_dialog.dart"; +import "../../../theme/colors.dart"; +import "../../../utils/context_extensions.dart"; +import "../../../utils/datetime_utils.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; - -// TODO: after click the hover should be int instead of double - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -17,47 +20,139 @@ class SksChartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ListView( + children: [ + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center(child: LineHandle()), + ), + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 12, + ), + child: Center( + child: Text( + context.localize.sks_chart_title, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: _SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _LegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + _LegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ); + } +} + +class _SksChart extends StatelessWidget { + const _SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 250), - child: BarChart( - BarChartData( - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + backgroundColor: context.colorTheme.whiteSoap, + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + borderData: FlBorderData( + show: false, + ), + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final int index = entry.key; + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: index, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + leftTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - maxY: 75, - barGroups: [ - BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), - BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), - BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), - BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), - BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) - ,],), - ], - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - leftTitles: const AxisTitles( - axisNameWidget: Text("Ilość osób"), - axisNameSize: 40, - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - style: context.textTheme.body.copyWith(fontSize: 12), - (value / 100) - .toStringAsFixed(2) - .replaceRange(2, 3, ":")); - }, - ), - ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + asyncChartData.value![value.toInt()].externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, ), ), ), @@ -66,3 +161,67 @@ class SksChartView extends ConsumerWidget { ); } } + +class _LegendItem extends StatelessWidget { + const _LegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [4], + radius: const Radius.circular(8), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: 18, + height: 18, + ), + ) + else + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: isPredicted ? [4] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index a1562974..0f8dfeeb 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../navigator/utils/navigation_commands.dart"; +import "../../../sks_chart/presentation/sks_chart_screen.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -16,7 +16,22 @@ class SksUserDataButton extends ConsumerWidget { return asyncSksUserData.when( data: (sksUsersData) => - _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), + _SksButton( + sksUsersData, + onTap: () async => showModalBottomSheet( + context: context, + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * + FilterConfig.bottomSheetHeightFactor, + ), + isScrollControlled: true, + builder: (BuildContext context) => UncontrolledProviderScope( + container: ProviderScope.containerOf(context), + child: const SksChartView(), + ), + ), + ), error: (error, stackTrace) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ); diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index ad1155df..ce38c0ef 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -150,6 +150,11 @@ "settings": "Ustawienia", "about_the_app": "O aplikacji", "other_view" : "Inne", + "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", + "sks_chart_legend_users" : "Zmierzona liczba osób", + "sks_chart_legend_forecast" : "Prognozowana liczba osób", + "sks_chart_number_of_users" : "Liczba osób" + "other_view" : "Inne", "map" : "Mapa", "report_change_title" : "Coś się zmieniło?", "report_change_button" : "Zgłoś zmianę", diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index cd1ef2e9..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,6 +30,11 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } + String toHourMinuteString() { + final DateFormat hourFormat = DateFormat("HH:mm"); + return hourFormat.format(this); + } + // Convert DateTime to Date (remove time) DateTime get date => DateTime(year, month, day); diff --git a/pubspec.yaml b/pubspec.yaml index 1215eccb..b5d383a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: in_app_review: ^2.0.9 flutter_map_animations: ^0.7.1 fluttertoast: ^8.2.8 + dotted_border: ^2.1.0 dev_dependencies: flutter_test: From a09f432324bddd189f5079e3176f5d6c8348135e Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 57/83] feat: temp navigation setup --- lib/utils/datetime_utils.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 18b1f035..9f062db5 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); From c9409eb1a99781f489f24177845b8272890103b4 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 58/83] chore: apply linter rules --- lib/utils/datetime_utils.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 9f062db5..a89258d3 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From 9a066a6340066d13f6a8dd47d8e28a585d1f5256 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:34:40 +0100 Subject: [PATCH 59/83] chore: code cleanup --- .../navigator/utils/navigation_commands.dart | 4 ---- lib/utils/datetime_utils.dart | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 6677921b..5c492ab6 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,8 +80,4 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } - - Future navigateToSksChart() async { - await _router.push(const SksChartRoute()); - } } diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index a89258d3..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,18 +30,6 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } - String toDayDateHourString() { - final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); - final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); - final DateFormat hourFormat = DateFormat("HH:mm"); - final String day = dayFormat.format(this); - final String capitalizedDay = - day[0].toUpperCase() + day.substring(1).toLowerCase(); - final String date = dateFormat.format(this); - final String hour = hourFormat.format(this); - return "$capitalizedDay, $date $hour"; - } - String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this); From 77d968ee0814a8856084f80e1d7ec7036ba42f7e Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:05:23 +0100 Subject: [PATCH 60/83] chore: code separation --- lib/config/ui_config.dart | 9 + .../parking_chart/widgets/chart_widget.dart | 9 +- .../sks_chart_bar_touch_data.dart | 28 +++ .../chart_elements/sks_chart_border_data.dart | 8 + .../chart_elements/sks_chart_grid_data.dart | 9 + .../chart_elements/sks_chart_labels.dart | 57 ++++++ .../chart_elements/sks_chart_legend_item.dart | 50 +++++ .../sks_chart/presentation/sks_chart.dart | 90 +++++++++ .../presentation/sks_chart_screen.dart | 182 ++---------------- lib/widgets/chart_elements.dart | 5 + 10 files changed, 274 insertions(+), 173 deletions(-) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart.dart create mode 100644 lib/widgets/chart_elements.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 434d1c1c..a3859e33 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -202,6 +202,15 @@ abstract class SksConfig { static const outerPadding = EdgeInsets.only(right: 12, bottom: 2); } +abstract class SksChartConfig { + static const borderDashArray = 4.0; + static const borderRadius = 8.0; + static const paddingLarge = 16.0; + static const paddingMedium = 12.0; + static const paddingExtraSmall = 4.0; + static const legendItemSize = 18.0; +} + abstract class NavigationTabViewConfig { static const universalPadding = 12.0; static const radius = 8.0; diff --git a/lib/features/parking_chart/widgets/chart_widget.dart b/lib/features/parking_chart/widgets/chart_widget.dart index 221ea8a8..5eea93f2 100644 --- a/lib/features/parking_chart/widgets/chart_widget.dart +++ b/lib/features/parking_chart/widgets/chart_widget.dart @@ -3,6 +3,7 @@ import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "../../../theme/app_theme.dart"; +import "../../../widgets/chart_elements.dart"; import "../../parkings_view/models/parking.dart"; import "../chart_elements/chart_border.dart"; import "../chart_elements/chart_grid.dart"; @@ -30,8 +31,8 @@ class ChartWidget extends StatelessWidget { borderData: ChartBorder(context), gridData: ChartGrid(context), titlesData: FlTitlesData( - rightTitles: const _HideLabels(), - topTitles: const _HideLabels(), + rightTitles: const HideLabels(), + topTitles: const HideLabels(), bottomTitles: BottomLabels(context), leftTitles: LeftLabels(context), ), @@ -64,7 +65,3 @@ class ChartWidget extends StatelessWidget { ); } } - -class _HideLabels extends AxisTitles { - const _HideLabels() : super(sideTitles: const SideTitles()); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart new file mode 100644 index 00000000..587e1ab8 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart @@ -0,0 +1,28 @@ +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../theme/app_theme.dart"; + +class SksChartBarTouchData extends BarTouchData { + SksChartBarTouchData(BuildContext context) + : super( + enabled: true, + touchTooltipData: _SksChartTooltipData(context), + ); +} + +class _SksChartTooltipData extends BarTouchTooltipData { + _SksChartTooltipData(BuildContext context) + : super( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + rod.toY.toStringAsFixed(0), + context.textTheme.title + .copyWith(color: context.colorTheme.whiteSoap), + ); + }, + getTooltipColor: (barChartGroup) { + return context.colorTheme.orangePomegranade; + }, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart new file mode 100644 index 00000000..f0c99c01 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart @@ -0,0 +1,8 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartBorderData extends FlBorderData { + SksChartBorderData() + : super( + show: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart new file mode 100644 index 00000000..c3c2cb6f --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart @@ -0,0 +1,9 @@ +import "package:fl_chart/fl_chart.dart"; + +class SksChartGridData extends FlGridData { + const SksChartGridData() + : super( + drawHorizontalLine: false, + drawVerticalLine: false, + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart new file mode 100644 index 00000000..02e7fec1 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart @@ -0,0 +1,57 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../../utils/datetime_utils.dart"; +import "../../data/models/sks_chart_data.dart"; + +class SksChartRightTiles extends AxisTitles { + SksChartRightTiles(BuildContext context) + : super( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ); +} + +class SksChartBottomTitles extends AxisTitles { + SksChartBottomTitles(BuildContext context, IList chartData) + : super( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only( + top: SksChartConfig.paddingExtraSmall * 2, + ), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + chartData[value.toInt()] + .externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, + ), + ); +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart new file mode 100644 index 00000000..acaa6532 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart @@ -0,0 +1,50 @@ +import "package:dotted_border/dotted_border.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; + +class SksChartLegendItem extends StatelessWidget { + const SksChartLegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [SksChartConfig.borderDashArray], + radius: const Radius.circular(SksChartConfig.borderRadius), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + ), + ) + else + Container( + width: SksChartConfig.legendItemSize, + height: SksChartConfig.legendItemSize, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all( + Radius.circular(SksChartConfig.borderRadius), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: SksChartConfig.paddingLarge), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart new file mode 100644 index 00000000..b43523be --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -0,0 +1,90 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../theme/colors.dart"; +import "../../../widgets/chart_elements.dart"; +import "../data/models/sks_chart_data.dart"; +import "chart_elements/sks_chart_bar_touch_data.dart"; +import "chart_elements/sks_chart_border_data.dart"; +import "chart_elements/sks_chart_grid_data.dart"; +import "chart_elements/sks_chart_labels.dart"; + +class SksChart extends StatelessWidget { + const SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: entry.key, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + alignment: BarChartAlignment.spaceAround, + backgroundColor: context.colorTheme.whiteSoap, + titlesData: FlTitlesData( + topTitles: const HideLabels(), + leftTitles: const HideLabels(), + rightTitles: SksChartRightTiles(context), + bottomTitles: SksChartBottomTitles( + context, + asyncChartData.value ?? const IList.empty(), + ), + ), + barTouchData: SksChartBarTouchData(context), + gridData: const SksChartGridData(), + borderData: SksChartBorderData(), + ), + ), + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(SksChartConfig.borderRadius), + ), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: + isPredicted ? [SksChartConfig.borderDashArray.toInt()] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 922a8585..88a61cd9 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,17 +1,15 @@ import "package:auto_route/annotations.dart"; -import "package:dotted_border/dotted_border.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; import "../../../utils/context_extensions.dart"; -import "../../../utils/datetime_utils.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; @RoutePage() class SksChartView extends ConsumerWidget { @@ -23,23 +21,26 @@ class SksChartView extends ConsumerWidget { final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), child: ListView( children: [ const Padding( - padding: EdgeInsets.only(top: 16), + padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), child: Center(child: LineHandle()), ), Padding( padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 12, + left: SksChartConfig.paddingMedium, + right: SksChartConfig.paddingMedium, + top: SksChartConfig.paddingMedium, ), child: Center( child: Text( context.localize.sks_chart_title, - style: context.textTheme.headline, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), textAlign: TextAlign.center, ), ), @@ -49,7 +50,7 @@ class SksChartView extends ConsumerWidget { top: 50, left: 25, ), - child: _SksChart( + child: SksChart( maxNumberOfUsers: maxNumberOfUsers, asyncChartData: asyncChartData, ), @@ -57,11 +58,11 @@ class SksChartView extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_users, isPredicted: false, ), - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_forecast, isPredicted: true, ), @@ -72,156 +73,3 @@ class SksChartView extends ConsumerWidget { ); } } - -class _SksChart extends StatelessWidget { - const _SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - swapAnimationDuration: Duration.zero, - BarChartData( - backgroundColor: context.colorTheme.whiteSoap, - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, - ), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - borderData: FlBorderData( - show: false, - ), - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final int index = entry.key; - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: index, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - topTitles: const AxisTitles(), - leftTitles: const AxisTitles(), - rightTitles: AxisTitles( - axisNameWidget: Text( - context.localize.sks_chart_number_of_users, - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ), - axisNameSize: 35, - sideTitles: SideTitles( - showTitles: true, - reservedSize: 25, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ); - }, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 35, - getTitlesWidget: (double value, TitleMeta meta) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - asyncChartData.value![value.toInt()].externalTimestamp - .toLocal() - .toHourMinuteString(), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LegendItem extends StatelessWidget { - const _LegendItem({required this.text, required this.isPredicted}); - - final String text; - final bool isPredicted; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - if (isPredicted) - DottedBorder( - borderType: BorderType.RRect, - dashPattern: const [4], - radius: const Radius.circular(8), - padding: EdgeInsets.zero, - // ignore: sized_box_for_whitespace - child: Container( - width: 18, - height: 18, - ), - ) - else - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - text, - style: context.textTheme.body, - ), - ), - ], - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: isPredicted ? [4] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} diff --git a/lib/widgets/chart_elements.dart b/lib/widgets/chart_elements.dart new file mode 100644 index 00000000..b4bd09af --- /dev/null +++ b/lib/widgets/chart_elements.dart @@ -0,0 +1,5 @@ +import "package:fl_chart/fl_chart.dart"; + +class HideLabels extends AxisTitles { + const HideLabels() : super(sideTitles: const SideTitles()); +} From 7f6c2d9fc1fa0936f5c37cfbe9b1ea10eb298fc0 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:08:22 +0100 Subject: [PATCH 61/83] chore: remove navigation --- lib/features/sks_chart/presentation/sks_chart_screen.dart | 2 -- .../presentation/widgets/sks_user_data_button.dart | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 88a61cd9..fc1c3660 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,4 +1,3 @@ -import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -11,7 +10,6 @@ import "../data/repository/sks_chart_repository.dart"; import "chart_elements/sks_chart_legend_item.dart"; import "sks_chart.dart"; -@RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 0f8dfeeb..9fda8812 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -15,9 +15,7 @@ class SksUserDataButton extends ConsumerWidget { final asyncSksUserData = ref.watch(getLatestSksUserDataProvider); return asyncSksUserData.when( - data: (sksUsersData) => - // _SksButton(sksUsersData, onTap: () async => ref.navigateToSksChart()), - _SksButton( + data: (sksUsersData) => _SksButton( sksUsersData, onTap: () async => showModalBottomSheet( context: context, From 3c4941c1e4bb0ff0f2f1ca81cbd1fecbb1bf8bac Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:33:56 +0100 Subject: [PATCH 62/83] chore : final formatting --- .../presentation/sks_chart_screen.dart | 73 ---------------- .../presentation/sks_chart_sheet.dart | 84 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 7 +- 3 files changed, 86 insertions(+), 78 deletions(-) delete mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart_sheet.dart diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart deleted file mode 100644 index fc1c3660..00000000 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../utils/context_extensions.dart"; -import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../data/models/sks_chart_data.dart"; -import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; - -class SksChartView extends ConsumerWidget { - const SksChartView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asyncChartData = ref.watch(getLatestChartDataProvider); - final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: ListView( - children: [ - const Padding( - padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), - child: Center(child: LineHandle()), - ), - Padding( - padding: const EdgeInsets.only( - left: SksChartConfig.paddingMedium, - right: SksChartConfig.paddingMedium, - top: SksChartConfig.paddingMedium, - ), - child: Center( - child: Text( - context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, - ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart new file mode 100644 index 00000000..a2606ec7 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -0,0 +1,84 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../../utils/context_extensions.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; +import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; + +class SksChartSheet extends ConsumerWidget { + const SksChartSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(SksChartConfig.paddingLarge), + child: _SksSheetHeader(), + ), + Expanded( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SksChartLegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + SksChartLegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SksSheetHeader extends StatelessWidget { + const _SksSheetHeader(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const LineHandle(), + const SizedBox(height: 8), + Text( + context.localize.sks_chart_title, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 9fda8812..ac5cd8d7 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../sks_chart/presentation/sks_chart_screen.dart"; +import "../../../sks_chart/presentation/sks_chart_sheet.dart"; import "../../data/models/sks_user_data.dart"; import "../../data/repository/latest_sks_user_data_repo.dart"; @@ -24,10 +24,7 @@ class SksUserDataButton extends ConsumerWidget { FilterConfig.bottomSheetHeightFactor, ), isScrollControlled: true, - builder: (BuildContext context) => UncontrolledProviderScope( - container: ProviderScope.containerOf(context), - child: const SksChartView(), - ), + builder: (BuildContext context) => const SksChartSheet(), ), ), error: (error, stackTrace) => const SizedBox.shrink(), From f8f6fe417979704236e9887eb2d71451b7dde9ee Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:38:09 +0100 Subject: [PATCH 63/83] chore : change sks menu package name to reflect convention --- .../{sks-menu => sks_menu}/data/models/dish_category_enum.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_data.dart | 0 .../{sks-menu => sks_menu}/data/models/sks_menu_response.dart | 0 .../data/repository/sks_menu_repository.dart | 0 .../{sks-menu => sks_menu}/presentation/sks_menu_screen.dart | 0 .../presentation/widgets/sks_menu_data_source_link.dart | 0 .../presentation/widgets/sks_menu_header.dart | 0 .../presentation/widgets/sks_menu_section.dart | 0 .../presentation/widgets/sks_menu_tiles.dart | 0 .../presentation/widgets/sks_menu_view_loading.dart | 0 .../presentation/widgets/technical_message.dart | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename lib/features/{sks-menu => sks_menu}/data/models/dish_category_enum.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_data.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/models/sks_menu_response.dart (100%) rename lib/features/{sks-menu => sks_menu}/data/repository/sks_menu_repository.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/sks_menu_screen.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_data_source_link.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_header.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_section.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_tiles.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/sks_menu_view_loading.dart (100%) rename lib/features/{sks-menu => sks_menu}/presentation/widgets/technical_message.dart (100%) diff --git a/lib/features/sks-menu/data/models/dish_category_enum.dart b/lib/features/sks_menu/data/models/dish_category_enum.dart similarity index 100% rename from lib/features/sks-menu/data/models/dish_category_enum.dart rename to lib/features/sks_menu/data/models/dish_category_enum.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_data.dart b/lib/features/sks_menu/data/models/sks_menu_data.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_data.dart rename to lib/features/sks_menu/data/models/sks_menu_data.dart diff --git a/lib/features/sks-menu/data/models/sks_menu_response.dart b/lib/features/sks_menu/data/models/sks_menu_response.dart similarity index 100% rename from lib/features/sks-menu/data/models/sks_menu_response.dart rename to lib/features/sks_menu/data/models/sks_menu_response.dart diff --git a/lib/features/sks-menu/data/repository/sks_menu_repository.dart b/lib/features/sks_menu/data/repository/sks_menu_repository.dart similarity index 100% rename from lib/features/sks-menu/data/repository/sks_menu_repository.dart rename to lib/features/sks_menu/data/repository/sks_menu_repository.dart diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart similarity index 100% rename from lib/features/sks-menu/presentation/sks_menu_screen.dart rename to lib/features/sks_menu/presentation/sks_menu_screen.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_data_source_link.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_header.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_header.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_header.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_header.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_section.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_section.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_section.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_section.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_tiles.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_tiles.dart diff --git a/lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_view_loading.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/sks_menu_view_loading.dart rename to lib/features/sks_menu/presentation/widgets/sks_menu_view_loading.dart diff --git a/lib/features/sks-menu/presentation/widgets/technical_message.dart b/lib/features/sks_menu/presentation/widgets/technical_message.dart similarity index 100% rename from lib/features/sks-menu/presentation/widgets/technical_message.dart rename to lib/features/sks_menu/presentation/widgets/technical_message.dart From 2e7ffb8c2ebfd529ed7cfe61582033fe8f8c3ba4 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:47:33 +0100 Subject: [PATCH 64/83] chore : change fl chart version & fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index b43523be..e117d58a 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - swapAnimationDuration: Duration.zero, + duration : Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() diff --git a/pubspec.yaml b/pubspec.yaml index b5d383a5..90bf3ec6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 collection: ^1.19.1 - fl_chart: ^0.69.0 + fl_chart: ^0.69.2 permission_handler: ^11.3.1 flutter_widget_from_html_core: ^0.15.2 html: ^0.15.4 From c3ba8c1e2e634f6857c903dad8ccde15876ec2bd Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:52:38 +0100 Subject: [PATCH 65/83] chore : fix linter complaints --- lib/features/sks_chart/presentation/sks_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart index e117d58a..225d6158 100644 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ b/lib/features/sks_chart/presentation/sks_chart.dart @@ -27,7 +27,7 @@ class SksChart extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: BarChart( - duration : Duration.zero, + duration: Duration.zero, BarChartData( barGroups: asyncChartData.value ?.asMap() From ede6cc3af1166e7395cd5b9b3fb4954029e6fc8c Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 66/83] feat: api-setup --- lib/features/sks_chart/data/models/sks_chart_data.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index dd378ea9..dcdf345c 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -9,8 +9,8 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp, - }) = _SksChartData; + required DateTime externalTimestamp +}) = _SksChartData; factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); From 9f4fdc99bbdf923d2b488cb0a5a48f294fb5d13b Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 67/83] feat: temp navigation setup --- lib/features/navigator/app_router.dart | 2 +- .../navigator/utils/navigation_commands.dart | 3 ++ .../presentation/sks_chart_screen.dart | 39 +++++++++++++++++++ .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 11 ++++++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index ab86a511..d5cbaba3 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -79,7 +79,7 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), - AutoRoute( + _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, ), diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 5c492ab6..ccd8e3cf 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,4 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { + await _router.push(const SksChartRoute()); + } } diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart new file mode 100644 index 00000000..c9619a78 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -0,0 +1,39 @@ +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../utils/datetime_utils.dart"; +import "../data/repository/sks_chart_repository.dart"; + + +@RoutePage() +class SksChartView extends ConsumerWidget { + const SksChartView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncChartData = ref.watch(getLatestChartDataProvider); + + return Scaffold( + body: asyncChartData.when( + data: (chartDataList) { + if (chartDataList.isEmpty) { + return const Center(child: Text("No data available")); + } + return ListView.builder( + itemCount: chartDataList.length, + itemBuilder: (context, index) { + final data = chartDataList[index]; + return ListTile( + title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + subtitle: Text("Value: ${data.activeUsers}"), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ); + } +} diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index ac5cd8d7..93148356 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -34,7 +34,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {required this.onTap}); + const _SksButton(this.sksUserData, {super.key}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 18b1f035..9f062db5 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); From 635af658585cf43da0c07dc1a97f61e46d72c9c5 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 68/83] chore: apply linter rules --- lib/features/navigator/utils/navigation_commands.dart | 1 + .../sks_chart/data/models/sks_chart_data.dart | 4 ++-- .../data/repository/sks_chart_repository.dart | 11 ++++------- .../sks_chart/presentation/sks_chart_screen.dart | 5 +++-- .../presentation/widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index ccd8e3cf..6677921b 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,6 +80,7 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } + Future navigateToSksChart() async { await _router.push(const SksChartRoute()); } diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index dcdf345c..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -9,8 +9,8 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 40c743d8..63c1b47a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,13 +16,10 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map), - ) - .where( - (e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 15, - ) + (entry) => SksChartData.fromJson(entry as Map),) + .where((e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) + .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index c9619a78..45f3b9ef 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../utils/datetime_utils.dart"; import "../data/repository/sks_chart_repository.dart"; - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -25,7 +24,9 @@ class SksChartView extends ConsumerWidget { itemBuilder: (context, index) { final data = chartDataList[index]; return ListTile( - title: Text("Timestamp: ${data.externalTimestamp.toDayDateHourString()}"), + title: Text( + "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + ), subtitle: Text("Value: ${data.activeUsers}"), ); }, diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index 93148356..ac5cd8d7 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -34,7 +34,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 9f062db5..a89258d3 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From bb68b532d9430c2d1b8096578a52c77963c08604 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 23:18:19 +0100 Subject: [PATCH 69/83] feat: add sks chart --- .../presentation/sks_chart_screen.dart | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 45f3b9ef..e4b96293 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,10 +1,15 @@ import "package:auto_route/annotations.dart"; +import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../utils/datetime_utils.dart"; +import "../../../theme/app_theme.dart"; +import "../../about_us_view/utils/custom_license_dialog.dart"; import "../data/repository/sks_chart_repository.dart"; + +// TODO: after click the hover should be int instead of double + @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -14,26 +19,49 @@ class SksChartView extends ConsumerWidget { final asyncChartData = ref.watch(getLatestChartDataProvider); return Scaffold( - body: asyncChartData.when( - data: (chartDataList) { - if (chartDataList.isEmpty) { - return const Center(child: Text("No data available")); - } - return ListView.builder( - itemCount: chartDataList.length, - itemBuilder: (context, index) { - final data = chartDataList[index]; - return ListTile( - title: Text( - "Timestamp: ${data.externalTimestamp.toDayDateHourString()}", + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 250), + child: BarChart( + BarChartData( + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: 75, + barGroups: [ + BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), + BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), + BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), + BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), + BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), + BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) + ,],), + ], + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + leftTitles: const AxisTitles( + axisNameWidget: Text("Ilość osób"), + axisNameSize: 40, + ), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + style: context.textTheme.body.copyWith(fontSize: 12), + (value / 100) + .toStringAsFixed(2) + .replaceRange(2, 3, ":")); + }, + ), ), - subtitle: Text("Value: ${data.activeUsers}"), - ); - }, - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text("Error: $error")), + ), + ), + ), + ), ), ); } From 278871f5ad7f9526b4c3b82f1fc481314f21afab Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:26:18 +0100 Subject: [PATCH 70/83] feat: add sks chart --- .../presentation/sks_chart_screen.dart | 245 +++++++++++++++--- lib/l10n/app_pl.arb | 11 +- lib/utils/datetime_utils.dart | 1 - 3 files changed, 207 insertions(+), 50 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index e4b96293..922a8585 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,15 +1,18 @@ import "package:auto_route/annotations.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../theme/app_theme.dart"; -import "../../about_us_view/utils/custom_license_dialog.dart"; +import "../../../theme/colors.dart"; +import "../../../utils/context_extensions.dart"; +import "../../../utils/datetime_utils.dart"; +import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; - -// TODO: after click the hover should be int instead of double - @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); @@ -17,47 +20,139 @@ class SksChartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ListView( + children: [ + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center(child: LineHandle()), + ), + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 12, + ), + child: Center( + child: Text( + context.localize.sks_chart_title, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + ), + child: _SksChart( + maxNumberOfUsers: maxNumberOfUsers, + asyncChartData: asyncChartData, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _LegendItem( + text: context.localize.sks_chart_legend_users, + isPredicted: false, + ), + _LegendItem( + text: context.localize.sks_chart_legend_forecast, + isPredicted: true, + ), + ], + ), + ], + ), + ); + } +} + +class _SksChart extends StatelessWidget { + const _SksChart({ + required this.maxNumberOfUsers, + required this.asyncChartData, + }); + + final double maxNumberOfUsers; + final AsyncValue> asyncChartData; - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 250), - child: BarChart( - BarChartData( - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: BarChart( + swapAnimationDuration: Duration.zero, + BarChartData( + backgroundColor: context.colorTheme.whiteSoap, + gridData: const FlGridData( + drawHorizontalLine: false, + drawVerticalLine: false, + ), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + borderData: FlBorderData( + show: false, + ), + barGroups: asyncChartData.value + ?.asMap() + .entries + .map((entry) { + final int index = entry.key; + final data = entry.value; + final isPredicted = + data.externalTimestamp.isAfter(DateTime.now()) || + (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && + data.activeUsers == 0); + return _makeSksChartBar( + x: index, + y: isPredicted ? data.movingAverage21 : data.activeUsers, + isPredicted: isPredicted, + ); + }).toList(), + alignment: BarChartAlignment.spaceAround, + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + leftTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: Text( + context.localize.sks_chart_number_of_users, + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - maxY: 75, - barGroups: [ - BarChartGroupData(x: 1105, barRods: [BarChartRodData(toY: 12),]), - BarChartGroupData(x: 1110, barRods: [BarChartRodData(toY: 23)]), - BarChartGroupData(x: 1115, barRods: [BarChartRodData(toY: 26)]), - BarChartGroupData(x: 1120, barRods: [BarChartRodData(toY: 29)]), - BarChartGroupData(x: 1125, barRods: [BarChartRodData(toY: 35, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1140, barRods: [BarChartRodData(toY: 50, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide())]), - BarChartGroupData(x: 1145, barRods: [BarChartRodData(toY: 45, color: Colors.transparent, borderDashArray: [4], borderSide: const BorderSide()) - ,],), - ], - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - leftTitles: const AxisTitles( - axisNameWidget: Text("Ilość osób"), - axisNameSize: 40, - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - style: context.textTheme.body.copyWith(fontSize: 12), - (value / 100) - .toStringAsFixed(2) - .replaceRange(2, 3, ":")); - }, - ), - ), + axisNameSize: 35, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 25, + getTitlesWidget: (double value, TitleMeta meta) { + return Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 35, + getTitlesWidget: (double value, TitleMeta meta) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + style: context.textTheme.body + .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + asyncChartData.value![value.toInt()].externalTimestamp + .toLocal() + .toHourMinuteString(), + ), + ); + }, ), ), ), @@ -66,3 +161,67 @@ class SksChartView extends ConsumerWidget { ); } } + +class _LegendItem extends StatelessWidget { + const _LegendItem({required this.text, required this.isPredicted}); + + final String text; + final bool isPredicted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isPredicted) + DottedBorder( + borderType: BorderType.RRect, + dashPattern: const [4], + radius: const Radius.circular(8), + padding: EdgeInsets.zero, + // ignore: sized_box_for_whitespace + child: Container( + width: 18, + height: 18, + ), + ) + else + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: context.colorTheme.orangePomegranade, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + text, + style: context.textTheme.body, + ), + ), + ], + ); + } +} + +BarChartGroupData _makeSksChartBar({ + required int x, + required int y, + bool isPredicted = false, +}) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + width: 15, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + toY: y.toDouble(), + color: + isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, + borderDashArray: isPredicted ? [4] : null, + borderSide: isPredicted ? const BorderSide() : null, + ), + ], + ); +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index ce38c0ef..b8ea3723 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -149,12 +149,6 @@ "rest_header": "Pozostałe", "settings": "Ustawienia", "about_the_app": "O aplikacji", - "other_view" : "Inne", - "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", - "sks_chart_legend_users" : "Zmierzona liczba osób", - "sks_chart_legend_forecast" : "Prognozowana liczba osób", - "sks_chart_number_of_users" : "Liczba osób" - "other_view" : "Inne", "map" : "Mapa", "report_change_title" : "Coś się zmieniło?", "report_change_button" : "Zgłoś zmianę", @@ -210,4 +204,9 @@ } } } + "other_view" : "Inne", + "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", + "sks_chart_legend_users" : "Zmierzona liczba osób", + "sks_chart_legend_forecast" : "Prognozowana liczba osób", + "sks_chart_number_of_users" : "Liczba osób" } diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index a89258d3..0fe36ca7 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -41,7 +41,6 @@ extension DateTimeUtilsX on DateTime { final String hour = hourFormat.format(this); return "$capitalizedDay, $date $hour"; } - String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this); From c7a16cfc950998bf3afa32a0110549392375fdfc Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:46:31 +0100 Subject: [PATCH 71/83] feat: api-setup --- lib/features/sks_chart/data/models/sks_chart_data.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index dd378ea9..97a81a36 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,4 +1,5 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; + import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; From 5a22843a04274f114e3c142229c246c47058ff7c Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:47:00 +0100 Subject: [PATCH 72/83] feat: temp navigation setup --- lib/features/navigator/app_router.dart | 5 +++++ lib/features/navigator/utils/navigation_commands.dart | 4 ---- .../presentation/widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 11 +++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index d5cbaba3..e2867dc5 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -16,6 +16,7 @@ import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; import "../sks-menu/presentation/sks_menu_screen.dart"; +import "../sks_chart/presentation/sks_chart_screen.dart"; import "root_view.dart"; part "app_router.g.dart"; @@ -79,6 +80,10 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), + AutoRoute( + path: "/sks-chart", + page: SksChartRoute.page, + ), _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 6677921b..5c492ab6 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -80,8 +80,4 @@ extension NavigationX on WidgetRef { Future navigateDigitalGuide(int id) async { await _router.push(DigitalGuideRoute(id: id)); } - - Future navigateToSksChart() async { - await _router.push(const SksChartRoute()); - } } diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index ac5cd8d7..db20d1bf 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -34,7 +34,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {required this.onTap}); + const _SksButton(this.sksUserData, {super.key, required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 0fe36ca7..e3534308 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,17 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { + final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); + final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); + final DateFormat hourFormat = DateFormat("HH:mm"); + final String day = dayFormat.format(this); + final String capitalizedDay = + day[0].toUpperCase() + day.substring(1).toLowerCase(); + final String date = dateFormat.format(this); + final String hour = hourFormat.format(this); + return "$capitalizedDay, $date $hour"; + } String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); From b6435158828fa8651c41b25a40f35e625e79bff9 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 25 Nov 2024 12:49:12 +0100 Subject: [PATCH 73/83] chore: apply linter rules --- .../sks_chart/data/models/sks_chart_data.dart | 4 ++-- .../data/repository/sks_chart_repository.dart | 16 ++++++++++++---- .../sks_chart/presentation/sks_chart_screen.dart | 1 + .../widgets/sks_user_data_button.dart | 2 +- lib/utils/datetime_utils.dart | 1 + 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index 97a81a36..70bbbbb9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -10,8 +10,8 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp, - }) = _SksChartData; + required DateTime externalTimestamp +}) = _SksChartData; factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index 63c1b47a..cef81e3a 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -16,10 +16,18 @@ Future> getLatestChartData(Ref ref) async { final data = response.data as List; final chartDataList = data .map( - (entry) => SksChartData.fromJson(entry as Map),) - .where((e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= 25,) - .map((e) => e = e.copyWith(externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)))) + (entry) => SksChartData.fromJson(entry as Map), + ) + .where( + (e) => + e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= + 25, + ) + .map( + (e) => e = e.copyWith( + externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), + ), + ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 922a8585..186c0793 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -13,6 +13,7 @@ import "../../bottom_scroll_sheet/drag_handle.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; + @RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index db20d1bf..ac5cd8d7 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -34,7 +34,7 @@ class SksUserDataButton extends ConsumerWidget { } class _SksButton extends StatelessWidget { - const _SksButton(this.sksUserData, {super.key, required this.onTap}); + const _SksButton(this.sksUserData, {required this.onTap}); final VoidCallback onTap; final SksUserData sksUserData; diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index e3534308..20edb2b3 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -17,6 +17,7 @@ extension DateTimeUtilsX on DateTime { final String date = dateFormat.format(this); return "$capitalizedDay, $date"; } + String toDayDateHourString() { final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); From b625520b3aca3e06e074850450ab09981d0c6dbc Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 17:34:40 +0100 Subject: [PATCH 74/83] chore: code cleanup --- lib/features/navigator/utils/navigation_commands.dart | 8 ++++++-- .../sks_chart/data/models/sks_chart_data.dart | 5 ++--- lib/utils/datetime_utils.dart | 11 ----------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 5c492ab6..7b2fd09e 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -73,8 +73,12 @@ extension NavigationX on WidgetRef { await _router.pushNamed(uri); } - Future navigateToSksMenu() async { - await _router.push(const SksMenuRoute()); + Future navigateToSksMenu({String? appBarPopTitle}) async { + if (appBarPopTitle != null) { + await _router.push(SksMenuRoute(appBarPopTitle: appBarPopTitle)); + } else { + await _router.push(SksMenuRoute()); + } } Future navigateDigitalGuide(int id) async { diff --git a/lib/features/sks_chart/data/models/sks_chart_data.dart b/lib/features/sks_chart/data/models/sks_chart_data.dart index 70bbbbb9..dd378ea9 100644 --- a/lib/features/sks_chart/data/models/sks_chart_data.dart +++ b/lib/features/sks_chart/data/models/sks_chart_data.dart @@ -1,5 +1,4 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; - import "package:freezed_annotation/freezed_annotation.dart"; part "sks_chart_data.freezed.dart"; @@ -10,8 +9,8 @@ class SksChartData with _$SksChartData { const factory SksChartData({ required int activeUsers, required int movingAverage21, - required DateTime externalTimestamp -}) = _SksChartData; + required DateTime externalTimestamp, + }) = _SksChartData; factory SksChartData.fromJson(Map json) => _$SksChartDataFromJson(json); diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index 20edb2b3..a89258d3 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -42,17 +42,6 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } - String toDayDateHourString() { - final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); - final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); - final DateFormat hourFormat = DateFormat("HH:mm"); - final String day = dayFormat.format(this); - final String capitalizedDay = - day[0].toUpperCase() + day.substring(1).toLowerCase(); - final String date = dateFormat.format(this); - final String hour = hourFormat.format(this); - return "$capitalizedDay, $date $hour"; - } String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this); From 86af3d9a742c73eb6aab6192aec6d12a27cdc6e3 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:05:23 +0100 Subject: [PATCH 75/83] chore: code separation --- .../presentation/sks_menu_screen.dart | 130 +++++++++++++ .../presentation/sks_chart_screen.dart | 182 ++---------------- 2 files changed, 145 insertions(+), 167 deletions(-) create mode 100644 lib/features/sks-menu/presentation/sks_menu_screen.dart diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart new file mode 100644 index 00000000..40c5d753 --- /dev/null +++ b/lib/features/sks-menu/presentation/sks_menu_screen.dart @@ -0,0 +1,130 @@ +import "dart:core"; + +import "package:auto_route/annotations.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:logger/logger.dart"; +import "package:lottie/lottie.dart"; + +import "../../../../theme/app_theme.dart"; +import "../../../config/ui_config.dart"; +import "../../../gen/assets.gen.dart"; +import "../../../utils/context_extensions.dart"; +import "../../../widgets/detail_views/detail_view_app_bar.dart"; +import "../../home_view/widgets/paddings.dart"; +import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; +import "../data/models/sks_menu_response.dart"; +import "../data/repository/sks_menu_repository.dart"; +import "widgets/sks_menu_data_source_link.dart"; +import "widgets/sks_menu_header.dart"; +import "widgets/sks_menu_section.dart"; + +@RoutePage() +class SksMenuView extends ConsumerWidget { + const SksMenuView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncSksMenuData = ref.watch(getSksMenuDataProvider); + + return asyncSksMenuData.when( + data: (sksMenuData) => _SksMenuView( + asyncSksMenuData.value ?? + SksMenuResponse( + isMenuOnline: false, + lastUpdate: DateTime.now(), + meals: List.empty(), + ), + ), + error: (error, stackTrace) => _SKSMenuLottieAnimation(error: error), + loading: () => const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } +} + +class _SksMenuView extends StatelessWidget { + const _SksMenuView(this.sksMenuData); + + final SksMenuResponse sksMenuData; + @override + Widget build(BuildContext context) { + if (!sksMenuData.isMenuOnline) { + return const _SKSMenuLottieAnimation(); + } + return Scaffold( + appBar: DetailViewAppBar( + actions: const [ + SksUserDataButton(), + ], + ), + body: ListView( + children: [ + SksMenuHeader( + dateTimeOfLastUpdate: sksMenuData.lastUpdate.toIso8601String(), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: HomeViewConfig.paddingMedium, + ), + child: MediumHorizontalPadding( + child: SksMenuSection(sksMenuData.meals), + ), + ), + const SksMenuDataSourceLink(), + const SizedBox( + height: ScienceClubsViewConfig.mediumPadding, + ), + ], + ), + ); + } +} + +class _SKSMenuLottieAnimation extends StatelessWidget { + const _SKSMenuLottieAnimation({ + this.error, + }); + + final Object? error; + @override + Widget build(BuildContext context) { + Logger().e(error.toString()); + return Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.square( + dimension: 200, + child: Lottie.asset( + Assets.animations.sksClosed, + fit: BoxFit.cover, + repeat: false, + frameRate: const FrameRate(LottieAnimationConfig.frameRate), + renderCache: RenderCache.drawingCommands, + ), + ), + Align( + child: Text( + context.localize.sks_menu_closed, + style: context.textTheme.headline, + textAlign: TextAlign.center, + ), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + error.toString(), + style: context.textTheme.titleGrey, + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 186c0793..2a312179 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,17 +1,15 @@ import "package:auto_route/annotations.dart"; -import "package:dotted_border/dotted_border.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; import "../../../utils/context_extensions.dart"; -import "../../../utils/datetime_utils.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; +import "chart_elements/sks_chart_legend_item.dart"; +import "sks_chart.dart"; @RoutePage() @@ -24,23 +22,26 @@ class SksChartView extends ConsumerWidget { final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, + ), child: ListView( children: [ const Padding( - padding: EdgeInsets.only(top: 16), + padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), child: Center(child: LineHandle()), ), Padding( padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 12, + left: SksChartConfig.paddingMedium, + right: SksChartConfig.paddingMedium, + top: SksChartConfig.paddingMedium, ), child: Center( child: Text( context.localize.sks_chart_title, - style: context.textTheme.headline, + style: context.textTheme.headline + .copyWith(color: context.colorTheme.blueAzure), textAlign: TextAlign.center, ), ), @@ -50,7 +51,7 @@ class SksChartView extends ConsumerWidget { top: 50, left: 25, ), - child: _SksChart( + child: SksChart( maxNumberOfUsers: maxNumberOfUsers, asyncChartData: asyncChartData, ), @@ -58,11 +59,11 @@ class SksChartView extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_users, isPredicted: false, ), - _LegendItem( + SksChartLegendItem( text: context.localize.sks_chart_legend_forecast, isPredicted: true, ), @@ -73,156 +74,3 @@ class SksChartView extends ConsumerWidget { ); } } - -class _SksChart extends StatelessWidget { - const _SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - swapAnimationDuration: Duration.zero, - BarChartData( - backgroundColor: context.colorTheme.whiteSoap, - gridData: const FlGridData( - drawHorizontalLine: false, - drawVerticalLine: false, - ), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - borderData: FlBorderData( - show: false, - ), - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final int index = entry.key; - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: index, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - alignment: BarChartAlignment.spaceAround, - titlesData: FlTitlesData( - topTitles: const AxisTitles(), - leftTitles: const AxisTitles(), - rightTitles: AxisTitles( - axisNameWidget: Text( - context.localize.sks_chart_number_of_users, - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ), - axisNameSize: 35, - sideTitles: SideTitles( - showTitles: true, - reservedSize: 25, - getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - ); - }, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 35, - getTitlesWidget: (double value, TitleMeta meta) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - asyncChartData.value![value.toInt()].externalTimestamp - .toLocal() - .toHourMinuteString(), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LegendItem extends StatelessWidget { - const _LegendItem({required this.text, required this.isPredicted}); - - final String text; - final bool isPredicted; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - if (isPredicted) - DottedBorder( - borderType: BorderType.RRect, - dashPattern: const [4], - radius: const Radius.circular(8), - padding: EdgeInsets.zero, - // ignore: sized_box_for_whitespace - child: Container( - width: 18, - height: 18, - ), - ) - else - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - text, - style: context.textTheme.body, - ), - ), - ], - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: isPredicted ? [4] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} From 43f90386986e9c4057fd98725e2e57fce486a4d2 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:08:22 +0100 Subject: [PATCH 76/83] chore: remove navigation --- lib/features/navigator/app_router.dart | 4 ---- lib/features/sks_chart/presentation/sks_chart_screen.dart | 3 --- 2 files changed, 7 deletions(-) diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index e2867dc5..9b8988c8 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -80,10 +80,6 @@ class AppRouter extends RootStackRouter { path: "/sks-menu", page: SksMenuRoute.page, ), - AutoRoute( - path: "/sks-chart", - page: SksChartRoute.page, - ), _NoTransitionRoute( path: "/departments", page: DepartmentsRoute.page, diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart index 2a312179..fc1c3660 100644 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ b/lib/features/sks_chart/presentation/sks_chart_screen.dart @@ -1,4 +1,3 @@ -import "package:auto_route/annotations.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -11,8 +10,6 @@ import "../data/repository/sks_chart_repository.dart"; import "chart_elements/sks_chart_legend_item.dart"; import "sks_chart.dart"; - -@RoutePage() class SksChartView extends ConsumerWidget { const SksChartView({super.key}); From f012a63d2f660515e56d3b173fdb813eb9519f78 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:33:56 +0100 Subject: [PATCH 77/83] chore : final formatting --- lib/features/navigator/app_router.dart | 2 +- .../presentation/sks_chart_screen.dart | 73 ------------------- 2 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 lib/features/sks_chart/presentation/sks_chart_screen.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index 9b8988c8..091fb776 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -16,7 +16,7 @@ import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; import "../sks-menu/presentation/sks_menu_screen.dart"; -import "../sks_chart/presentation/sks_chart_screen.dart"; +import "../sks_chart/presentation/sks_chart_sheet.dart"; import "root_view.dart"; part "app_router.g.dart"; diff --git a/lib/features/sks_chart/presentation/sks_chart_screen.dart b/lib/features/sks_chart/presentation/sks_chart_screen.dart deleted file mode 100644 index fc1c3660..00000000 --- a/lib/features/sks_chart/presentation/sks_chart_screen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../utils/context_extensions.dart"; -import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../data/models/sks_chart_data.dart"; -import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; - -class SksChartView extends ConsumerWidget { - const SksChartView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asyncChartData = ref.watch(getLatestChartDataProvider); - final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: ListView( - children: [ - const Padding( - padding: EdgeInsets.only(top: SksChartConfig.paddingLarge), - child: Center(child: LineHandle()), - ), - Padding( - padding: const EdgeInsets.only( - left: SksChartConfig.paddingMedium, - right: SksChartConfig.paddingMedium, - top: SksChartConfig.paddingMedium, - ), - child: Center( - child: Text( - context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, - ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, - ), - ], - ), - ], - ), - ); - } -} From c21b6b15092c2609be1702f065d28ef59c94f518 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Sat, 30 Nov 2024 21:38:09 +0100 Subject: [PATCH 78/83] chore : change sks menu package name to reflect convention --- lib/features/navigator/app_router.dart | 3 +- .../presentation/sks_menu_screen.dart | 130 ------------------ 2 files changed, 1 insertion(+), 132 deletions(-) delete mode 100644 lib/features/sks-menu/presentation/sks_menu_screen.dart diff --git a/lib/features/navigator/app_router.dart b/lib/features/navigator/app_router.dart index 091fb776..041dc15e 100644 --- a/lib/features/navigator/app_router.dart +++ b/lib/features/navigator/app_router.dart @@ -15,8 +15,7 @@ import "../navigation_tab_view/navigation_tab_view.dart"; import "../parkings_view/parkings_view.dart"; import "../science_club_detail_view/science_club_detail_view.dart"; import "../science_clubs_view/science_clubs_view.dart"; -import "../sks-menu/presentation/sks_menu_screen.dart"; -import "../sks_chart/presentation/sks_chart_sheet.dart"; +import "../sks_menu/presentation/sks_menu_screen.dart"; import "root_view.dart"; part "app_router.g.dart"; diff --git a/lib/features/sks-menu/presentation/sks_menu_screen.dart b/lib/features/sks-menu/presentation/sks_menu_screen.dart deleted file mode 100644 index 40c5d753..00000000 --- a/lib/features/sks-menu/presentation/sks_menu_screen.dart +++ /dev/null @@ -1,130 +0,0 @@ -import "dart:core"; - -import "package:auto_route/annotations.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:logger/logger.dart"; -import "package:lottie/lottie.dart"; - -import "../../../../theme/app_theme.dart"; -import "../../../config/ui_config.dart"; -import "../../../gen/assets.gen.dart"; -import "../../../utils/context_extensions.dart"; -import "../../../widgets/detail_views/detail_view_app_bar.dart"; -import "../../home_view/widgets/paddings.dart"; -import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; -import "../data/models/sks_menu_response.dart"; -import "../data/repository/sks_menu_repository.dart"; -import "widgets/sks_menu_data_source_link.dart"; -import "widgets/sks_menu_header.dart"; -import "widgets/sks_menu_section.dart"; - -@RoutePage() -class SksMenuView extends ConsumerWidget { - const SksMenuView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asyncSksMenuData = ref.watch(getSksMenuDataProvider); - - return asyncSksMenuData.when( - data: (sksMenuData) => _SksMenuView( - asyncSksMenuData.value ?? - SksMenuResponse( - isMenuOnline: false, - lastUpdate: DateTime.now(), - meals: List.empty(), - ), - ), - error: (error, stackTrace) => _SKSMenuLottieAnimation(error: error), - loading: () => const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ), - ); - } -} - -class _SksMenuView extends StatelessWidget { - const _SksMenuView(this.sksMenuData); - - final SksMenuResponse sksMenuData; - @override - Widget build(BuildContext context) { - if (!sksMenuData.isMenuOnline) { - return const _SKSMenuLottieAnimation(); - } - return Scaffold( - appBar: DetailViewAppBar( - actions: const [ - SksUserDataButton(), - ], - ), - body: ListView( - children: [ - SksMenuHeader( - dateTimeOfLastUpdate: sksMenuData.lastUpdate.toIso8601String(), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: HomeViewConfig.paddingMedium, - ), - child: MediumHorizontalPadding( - child: SksMenuSection(sksMenuData.meals), - ), - ), - const SksMenuDataSourceLink(), - const SizedBox( - height: ScienceClubsViewConfig.mediumPadding, - ), - ], - ), - ); - } -} - -class _SKSMenuLottieAnimation extends StatelessWidget { - const _SKSMenuLottieAnimation({ - this.error, - }); - - final Object? error; - @override - Widget build(BuildContext context) { - Logger().e(error.toString()); - return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.square( - dimension: 200, - child: Lottie.asset( - Assets.animations.sksClosed, - fit: BoxFit.cover, - repeat: false, - frameRate: const FrameRate(LottieAnimationConfig.frameRate), - renderCache: RenderCache.drawingCommands, - ), - ), - Align( - child: Text( - context.localize.sks_menu_closed, - style: context.textTheme.headline, - textAlign: TextAlign.center, - ), - ), - if (error != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - error.toString(), - style: context.textTheme.titleGrey, - textAlign: TextAlign.center, - ), - ), - ], - ), - ); - } -} From 56a4c4adaa0e81cded6abc4f2989319fb38b0d37 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 10:54:32 +0100 Subject: [PATCH 79/83] feat(sks-chart): add new version of chart --- lib/config/ui_config.dart | 15 ++- .../data/repository/sks_chart_repository.dart | 10 -- .../chart_elements/sks_chart.dart | 99 +++++++++++++++++++ .../sks_chart_bar_touch_data.dart | 28 ------ .../chart_elements/sks_chart_border_data.dart | 8 -- .../chart_elements/sks_chart_grid_data.dart | 11 ++- .../chart_elements/sks_chart_header.dart | 55 +++++++++++ .../chart_elements/sks_chart_labels.dart | 38 ++++--- ...legend_item.dart => sks_chart_legend.dart} | 47 ++++++--- .../sks_chart_line_touch_data.dart | 37 +++++++ .../sks_chart/presentation/sks_chart.dart | 90 ----------------- .../presentation/sks_chart_card.dart | 62 ++++++++++++ .../presentation/sks_chart_sheet.dart | 89 ++++++++++------- .../presentation/sks_menu_screen.dart | 5 +- .../widgets/sks_menu_data_source_link.dart | 16 +-- lib/l10n/app_pl.arb | 11 ++- 16 files changed, 406 insertions(+), 215 deletions(-) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart.dart delete mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart delete mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart rename lib/features/sks_chart/presentation/chart_elements/{sks_chart_legend_item.dart => sks_chart_legend.dart} (53%) create mode 100644 lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart delete mode 100644 lib/features/sks_chart/presentation/sks_chart.dart create mode 100644 lib/features/sks_chart/presentation/sks_chart_card.dart diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index a3859e33..0e65fa4f 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -204,11 +204,24 @@ abstract class SksConfig { abstract class SksChartConfig { static const borderDashArray = 4.0; - static const borderRadius = 8.0; + static const borderRadius = 16.0; static const paddingLarge = 16.0; static const paddingMedium = 12.0; + static const paddingSmall = 8.0; static const paddingExtraSmall = 4.0; static const legendItemSize = 18.0; + static const heightSmall = 8.0; + static const heightMedium = 12.0; + static const heightLarge = 16.0; + static const paddingLargeLTR = EdgeInsets.only( + left: SksChartConfig.paddingLarge, + top: SksChartConfig.paddingLarge, + right: SksChartConfig.paddingLarge, + ); + static const sksChartDataUrl = "https://live.pwr.edu.pl/sks/"; + static const sksAddress = "Hoene-Wrońskiego 10"; + static const sksPostalCode = "50-370 Wrocław"; + static const buildingCode = "C-18"; } abstract class NavigationTabViewConfig { diff --git a/lib/features/sks_chart/data/repository/sks_chart_repository.dart b/lib/features/sks_chart/data/repository/sks_chart_repository.dart index cef81e3a..9120c7b1 100644 --- a/lib/features/sks_chart/data/repository/sks_chart_repository.dart +++ b/lib/features/sks_chart/data/repository/sks_chart_repository.dart @@ -18,16 +18,6 @@ Future> getLatestChartData(Ref ref) async { .map( (entry) => SksChartData.fromJson(entry as Map), ) - .where( - (e) => - e.externalTimestamp.difference(DateTime.now()).inMinutes.abs() <= - 25, - ) - .map( - (e) => e = e.copyWith( - externalTimestamp: e.externalTimestamp.add(const Duration(hours: 1)), - ), - ) .toIList(); return chartDataList; diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart new file mode 100644 index 00000000..70cd0005 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart.dart @@ -0,0 +1,99 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../theme/colors.dart"; +import "../../../../widgets/chart_elements.dart"; +import "../../data/models/sks_chart_data.dart"; +import "sks_chart_grid_data.dart"; +import "sks_chart_labels.dart"; +import "sks_chart_line_touch_data.dart"; + +class SksChart extends StatelessWidget { + const SksChart({ + required this.maxNumberOfUsers, + required this.chartData, + }); + + final double maxNumberOfUsers; + final IList chartData; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: SksChartConfig.paddingLarge), + child: AspectRatio( + aspectRatio: 1.5, + child: LineChart( + duration: Duration.zero, + LineChartData( + clipData: const FlClipData.all(), + backgroundColor: context.colorTheme.whiteSoap, + gridData: SksChartGridData(context), + maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), + lineBarsData: [ + LineChartBarData( + belowBarData: BarAreaData( + show: true, + gradient: ColorsConsts.toPwrGradient, + applyCutOffY: true, + ), + isCurved: true, + color: context.colorTheme.orangePomegranade, + dotData: FlDotData( + checkToShowDot: (FlSpot spot, LineChartBarData barData) { + return false; + }, + ), + spots: chartData.asMap().entries.map((e) { + if (e.value.externalTimestamp.isAfter(DateTime.now())) { + return FlSpot.nullSpot; + } else { + return FlSpot( + e.key.toDouble(), + e.value.activeUsers.toDouble(), + ); + } + }).toList(), + ), + LineChartBarData( + isCurved: true, + dashArray: [ + SksChartConfig.borderDashArray.toInt(), + SksChartConfig.borderDashArray.toInt(), + ], + dotData: FlDotData( + checkToShowDot: (FlSpot spot, LineChartBarData barData) { + return false; + }, + ), + color: context.colorTheme.blueAzure, + spots: chartData.asMap().entries.map((e) { + return FlSpot( + e.key.toDouble(), + e.value.movingAverage21.toDouble(), + ); + }).toList(), + ), + ], + titlesData: FlTitlesData( + topTitles: const HideLabels(), + rightTitles: const HideLabels(), + leftTitles: SksChartRightTiles(context), + bottomTitles: SksChartBottomTitles( + context, + chartData, + ), + ), + lineTouchData: SksChartLineTouchData( + context, + chartData.map((e) => e.externalTimestamp).toIList(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart deleted file mode 100644 index 587e1ab8..00000000 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_bar_touch_data.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:fl_chart/fl_chart.dart"; -import "package:flutter/cupertino.dart"; - -import "../../../../theme/app_theme.dart"; - -class SksChartBarTouchData extends BarTouchData { - SksChartBarTouchData(BuildContext context) - : super( - enabled: true, - touchTooltipData: _SksChartTooltipData(context), - ); -} - -class _SksChartTooltipData extends BarTouchTooltipData { - _SksChartTooltipData(BuildContext context) - : super( - getTooltipItem: (group, groupIndex, rod, rodIndex) { - return BarTooltipItem( - rod.toY.toStringAsFixed(0), - context.textTheme.title - .copyWith(color: context.colorTheme.whiteSoap), - ); - }, - getTooltipColor: (barChartGroup) { - return context.colorTheme.orangePomegranade; - }, - ); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart deleted file mode 100644 index f0c99c01..00000000 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_border_data.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:fl_chart/fl_chart.dart"; - -class SksChartBorderData extends FlBorderData { - SksChartBorderData() - : super( - show: false, - ); -} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart index c3c2cb6f..ce58ee5b 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_grid_data.dart @@ -1,9 +1,14 @@ import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../parking_chart/chart_elements/chart_grid.dart"; class SksChartGridData extends FlGridData { - const SksChartGridData() + SksChartGridData(BuildContext context) : super( - drawHorizontalLine: false, - drawVerticalLine: false, + verticalInterval: 100, + horizontalInterval: 25, + getDrawingHorizontalLine: (value) => GridLine(context), + getDrawingVerticalLine: (value) => GridLine(context), ); } diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart new file mode 100644 index 00000000..fead65af --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_header.dart @@ -0,0 +1,55 @@ +import "package:flutter/cupertino.dart"; + +import "../../../../config/ui_config.dart"; +import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; +import "../../../sks_people_live/data/models/sks_user_data.dart"; +import "../../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; + +// TODO(mikolaj-jalocha): add navigation? maybe after click on building informations + +class SksChartHeader extends StatelessWidget { + const SksChartHeader({ + super.key, + required this.numberOfPeople, + required this.trend, + }); + + final String numberOfPeople; + final Trend? trend; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + SksChartConfig.buildingCode, + style: context.textTheme.headline, + ), + Text( + "${context.localize.street_abbreviation} ${SksChartConfig.sksAddress}", + style: context.textTheme.body, + ), + Text(SksChartConfig.sksPostalCode, style: context.textTheme.body), + ], + ), + Row( + children: [ + Text( + numberOfPeople, + style: context.textTheme.body.copyWith(fontSize: 18), + ), + const SizedBox( + width: SksChartConfig.heightSmall, + ), + trend?.icon ?? const SizedBox.shrink(), + ], + ), + ], + ); + } +} diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart index 02e7fec1..620f7d07 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_labels.dart @@ -16,15 +16,17 @@ class SksChartRightTiles extends AxisTitles { style: context.textTheme.body .copyWith(fontSize: 14, fontWeight: FontWeight.w400), ), - axisNameSize: 35, sideTitles: SideTitles( + maxIncluded: false, + reservedSize: 40, showTitles: true, - reservedSize: 25, getTitlesWidget: (double value, TitleMeta meta) { - return Text( - "${value.toInt()}", - style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), + return Center( + child: Text( + "${value.toInt()}", + style: context.textTheme.body + .copyWith(fontSize: 12, fontWeight: FontWeight.w400), + ), ); }, ), @@ -37,18 +39,30 @@ class SksChartBottomTitles extends AxisTitles { sideTitles: SideTitles( showTitles: true, reservedSize: 35, + interval: 5, getTitlesWidget: (double value, TitleMeta meta) { + final String text = (chartData.isNotEmpty) + ? chartData[value.toInt()] + .externalTimestamp + .toLocal() + .minute == + 0 + ? chartData[value.toInt()] + .externalTimestamp + .toLocal() + .toHourMinuteString() + : "" + : ""; + return Padding( padding: const EdgeInsets.only( - top: SksChartConfig.paddingExtraSmall * 2, + top: SksChartConfig.paddingSmall, + left: 50, ), child: Text( style: context.textTheme.body - .copyWith(fontSize: 14, fontWeight: FontWeight.w400), - chartData[value.toInt()] - .externalTimestamp - .toLocal() - .toHourMinuteString(), + .copyWith(fontSize: 12, fontWeight: FontWeight.w400), + text, ), ); }, diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart similarity index 53% rename from lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart rename to lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart index acaa6532..8c1dd77a 100644 --- a/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend_item.dart +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_legend.dart @@ -3,6 +3,29 @@ import "package:flutter/cupertino.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../../../utils/context_extensions.dart"; + +class SksChartLegend extends StatelessWidget { + const SksChartLegend({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SksChartLegendItem( + text: context.localize.measured_number_of_users, + isPredicted: false, + ), + SksChartLegendItem( + text: context.localize.forecasted_number_of_users, + isPredicted: true, + ), + ], + ); + } +} class SksChartLegendItem extends StatelessWidget { const SksChartLegendItem({required this.text, required this.isPredicted}); @@ -18,31 +41,25 @@ class SksChartLegendItem extends StatelessWidget { DottedBorder( borderType: BorderType.RRect, dashPattern: const [SksChartConfig.borderDashArray], - radius: const Radius.circular(SksChartConfig.borderRadius), + color: context.colorTheme.blueAzure, padding: EdgeInsets.zero, // ignore: sized_box_for_whitespace child: Container( width: SksChartConfig.legendItemSize, - height: SksChartConfig.legendItemSize, ), ) else Container( width: SksChartConfig.legendItemSize, - height: SksChartConfig.legendItemSize, - decoration: BoxDecoration( - color: context.colorTheme.orangePomegranade, - borderRadius: const BorderRadius.all( - Radius.circular(SksChartConfig.borderRadius), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: SksChartConfig.paddingLarge), - child: Text( - text, - style: context.textTheme.body, + height: 2, + color: context.colorTheme.orangePomegranade, ), + const SizedBox( + width: SksChartConfig.heightMedium, + ), + Text( + text, + style: context.textTheme.body, ), ], ); diff --git a/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart b/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart new file mode 100644 index 00000000..2f0afb06 --- /dev/null +++ b/lib/features/sks_chart/presentation/chart_elements/sks_chart_line_touch_data.dart @@ -0,0 +1,37 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../../theme/app_theme.dart"; +import "../../../../utils/datetime_utils.dart"; + +// TODO(mikolaj-jalocha): Sometimes hour label is displayed at the bottom, sometimes between the 2 values. +// TODO(mikolaj-jalocha): Make hour label be always in black (now is red if values were measured, blue otherwise) + +class SksChartLineTouchData extends LineTouchData { + final IList dateTime; + + SksChartLineTouchData(BuildContext context, this.dateTime) + : super( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (LineBarSpot lineBarSpot) => + context.colorTheme.greyLight, + getTooltipItems: (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final hour = (touchedSpot.barIndex == 0 || + touchedSpots.length == 1) + ? "\n${dateTime.get(touchedSpot.x.toInt()).toHourMinuteString()}" + : ""; + final value = touchedSpot.y.toStringAsFixed(0) + hour; + final Color color = (touchedSpot.barIndex == 0) + ? context.colorTheme.orangePomegranade + : context.colorTheme.blueAzure; + return LineTooltipItem( + value, + TextStyle(color: color), + ); + }).toList(); + }, + ), + ); +} diff --git a/lib/features/sks_chart/presentation/sks_chart.dart b/lib/features/sks_chart/presentation/sks_chart.dart deleted file mode 100644 index 225d6158..00000000 --- a/lib/features/sks_chart/presentation/sks_chart.dart +++ /dev/null @@ -1,90 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:fl_chart/fl_chart.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -import "../../../config/ui_config.dart"; -import "../../../theme/app_theme.dart"; -import "../../../theme/colors.dart"; -import "../../../widgets/chart_elements.dart"; -import "../data/models/sks_chart_data.dart"; -import "chart_elements/sks_chart_bar_touch_data.dart"; -import "chart_elements/sks_chart_border_data.dart"; -import "chart_elements/sks_chart_grid_data.dart"; -import "chart_elements/sks_chart_labels.dart"; - -class SksChart extends StatelessWidget { - const SksChart({ - required this.maxNumberOfUsers, - required this.asyncChartData, - }); - - final double maxNumberOfUsers; - final AsyncValue> asyncChartData; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: BarChart( - duration: Duration.zero, - BarChartData( - barGroups: asyncChartData.value - ?.asMap() - .entries - .map((entry) { - final data = entry.value; - final isPredicted = - data.externalTimestamp.isAfter(DateTime.now()) || - (data.externalTimestamp.isAtSameMomentAs(DateTime.now()) && - data.activeUsers == 0); - return _makeSksChartBar( - x: entry.key, - y: isPredicted ? data.movingAverage21 : data.activeUsers, - isPredicted: isPredicted, - ); - }).toList(), - maxY: maxNumberOfUsers + (maxNumberOfUsers / 10).toInt(), - alignment: BarChartAlignment.spaceAround, - backgroundColor: context.colorTheme.whiteSoap, - titlesData: FlTitlesData( - topTitles: const HideLabels(), - leftTitles: const HideLabels(), - rightTitles: SksChartRightTiles(context), - bottomTitles: SksChartBottomTitles( - context, - asyncChartData.value ?? const IList.empty(), - ), - ), - barTouchData: SksChartBarTouchData(context), - gridData: const SksChartGridData(), - borderData: SksChartBorderData(), - ), - ), - ); - } -} - -BarChartGroupData _makeSksChartBar({ - required int x, - required int y, - bool isPredicted = false, -}) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - width: 15, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(SksChartConfig.borderRadius), - ), - toY: y.toDouble(), - color: - isPredicted ? Colors.transparent : ColorsConsts.orangePomegranade, - borderDashArray: - isPredicted ? [SksChartConfig.borderDashArray.toInt()] : null, - borderSide: isPredicted ? const BorderSide() : null, - ), - ], - ); -} diff --git a/lib/features/sks_chart/presentation/sks_chart_card.dart b/lib/features/sks_chart/presentation/sks_chart_card.dart new file mode 100644 index 00000000..223c8618 --- /dev/null +++ b/lib/features/sks_chart/presentation/sks_chart_card.dart @@ -0,0 +1,62 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/cupertino.dart"; + +import "../../../config/ui_config.dart"; +import "../../../theme/app_theme.dart"; +import "../../sks_people_live/data/models/sks_user_data.dart"; +import "../data/models/sks_chart_data.dart"; +import "chart_elements/sks_chart.dart"; +import "chart_elements/sks_chart_header.dart"; +import "chart_elements/sks_chart_legend.dart"; + +class SksChartCard extends StatelessWidget { + const SksChartCard({ + super.key, + required this.currentNumberOfUsers, + required this.maxNumberOfUsers, + required this.chartData, + }); + + final SksUserData? currentNumberOfUsers; + final double maxNumberOfUsers; + final IList chartData; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.colorTheme.greyLight, + borderRadius: BorderRadius.circular(SksChartConfig.borderRadius), + ), + child: Padding( + padding: SksChartConfig.paddingLargeLTR, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SksChartHeader( + numberOfPeople: + currentNumberOfUsers?.activeUsers.toString() ?? "", + trend: currentNumberOfUsers?.trend, + ), + const SizedBox( + height: SksChartConfig.heightLarge, + ), + SksChart( + maxNumberOfUsers: maxNumberOfUsers, + chartData: chartData, + ), + const Padding( + padding: EdgeInsets.only( + left: SksChartConfig.paddingLarge, + bottom: SksChartConfig.paddingLarge, + top: SksChartConfig.paddingSmall, + ), + child: SksChartLegend(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index a2606ec7..6f31b8b9 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -1,14 +1,19 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; import "../../../utils/context_extensions.dart"; +import "../../../widgets/my_error_widget.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; +import "../../sks_menu/presentation/widgets/sks_menu_data_source_link.dart"; +import "../../sks_people_live/data/repository/latest_sks_user_data_repo.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; -import "chart_elements/sks_chart_legend_item.dart"; -import "sks_chart.dart"; +import "sks_chart_card.dart"; + +// TODO(mikolaj-jalocha): create shimmer loading class SksChartSheet extends ConsumerWidget { const SksChartSheet({super.key}); @@ -16,50 +21,58 @@ class SksChartSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asyncChartData = ref.watch(getLatestChartDataProvider); + final asyncNumberOfUsers = ref.watch(getLatestSksUserDataProvider); + + final currentNumberOfUsers = asyncNumberOfUsers.value; final maxNumberOfUsers = asyncChartData.value?.maxNumberOfUsers ?? 0; - return Padding( - padding: const EdgeInsets.symmetric( - vertical: SksChartConfig.paddingExtraSmall, - ), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(SksChartConfig.paddingLarge), - child: _SksSheetHeader(), + return switch (asyncChartData) { + AsyncError(:final error) => MyErrorWidget(error), + AsyncLoading() => const SizedBox.shrink(), + AsyncValue() => Padding( + padding: const EdgeInsets.symmetric( + vertical: SksChartConfig.paddingExtraSmall, ), - Expanded( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - ), - child: SksChart( - maxNumberOfUsers: maxNumberOfUsers, - asyncChartData: asyncChartData, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Column( + children: [ + Padding( + padding: SksChartConfig.paddingLargeLTR + .copyWith(bottom: SksChartConfig.paddingMedium), + child: const _SksSheetHeader(), + ), + Expanded( + child: ListView( + physics: const NeverScrollableScrollPhysics(), children: [ - SksChartLegendItem( - text: context.localize.sks_chart_legend_users, - isPredicted: false, + Padding( + padding: const EdgeInsets.fromLTRB( + SksChartConfig.paddingMedium, + SksChartConfig.paddingMedium, + SksChartConfig.paddingMedium, + 0, + ), + child: SksChartCard( + currentNumberOfUsers: currentNumberOfUsers, + maxNumberOfUsers: maxNumberOfUsers, + chartData: asyncChartData.value ?? const IList.empty(), + ), ), - SksChartLegendItem( - text: context.localize.sks_chart_legend_forecast, - isPredicted: true, + Padding( + padding: const EdgeInsets.all( + SksChartConfig.paddingSmall, + ), + child: SksMenuDataSourceLink( + SksChartConfig.sksChartDataUrl, + "${context.localize.data_come_from_website}: ", + ), ), ], ), - ], - ), + ), + ], ), - ], - ), - ); + ), + }; } } @@ -71,7 +84,7 @@ class _SksSheetHeader extends StatelessWidget { return Column( children: [ const LineHandle(), - const SizedBox(height: 8), + const SizedBox(height: SksChartConfig.heightSmall), Text( context.localize.sks_chart_title, style: context.textTheme.headline diff --git a/lib/features/sks_menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart index 049f0c02..a8c3103f 100644 --- a/lib/features/sks_menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks_menu/presentation/sks_menu_screen.dart @@ -111,7 +111,10 @@ class _SksMenuView extends ConsumerWidget { padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), child: SksMenuSection(sksMenuData.meals), ), - const SksMenuDataSourceLink(), + SksMenuDataSourceLink( + SksMenuConfig.sksDataSource, + context.localize.data_come_from_website, + ), const SizedBox( height: ScienceClubsViewConfig.mediumPadding, ), diff --git a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart index 1d7a770b..556604b3 100644 --- a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart @@ -2,37 +2,39 @@ import "package:flutter/cupertino.dart"; import "package:flutter/gestures.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; -import "../../../../utils/context_extensions.dart"; import "../../../../utils/launch_url_util.dart"; class SksMenuDataSourceLink extends ConsumerWidget { - const SksMenuDataSourceLink({ + const SksMenuDataSourceLink( + this.url, + this.text, { super.key, }); + final String url; + final String text; + @override Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.all(16), child: Text.rich( TextSpan( - text: "${context.localize.data_come_from_website}: ", + text: text, style: const TextStyle( fontWeight: FontWeight.bold, ), children: [ TextSpan( - text: - SksMenuConfig.sksDataSource.replaceFirst("https://", "www."), + text: url.replaceFirst("https://", "www"), style: context.textTheme.bodyOrange.copyWith( decoration: TextDecoration.underline, decorationColor: context.colorTheme.orangePomegranade, fontWeight: FontWeight.bold, ), recognizer: TapGestureRecognizer() - ..onTap = () async => ref.launch(SksMenuConfig.sksDataSource), + ..onTap = () async => ref.launch(url), ), ], ), diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index b8ea3723..e50c646c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -149,6 +149,11 @@ "rest_header": "Pozostałe", "settings": "Ustawienia", "about_the_app": "O aplikacji", + "other_view" : "Inne", + "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", + "sks_chart_legend_users" : "Zmierzona liczba osób", + "sks_chart_legend_forecast" : "Prognozowana liczba osób", + "sks_chart_number_of_users" : "Liczba osób", "map" : "Mapa", "report_change_title" : "Coś się zmieniło?", "report_change_button" : "Zgłoś zmianę", @@ -203,10 +208,12 @@ "example": "wydziałów" } } - } + }, "other_view" : "Inne", "sks_chart_title" : "Przybliżona liczba osób w Strefie Kultury Studenckiej", "sks_chart_legend_users" : "Zmierzona liczba osób", "sks_chart_legend_forecast" : "Prognozowana liczba osób", - "sks_chart_number_of_users" : "Liczba osób" + "sks_chart_number_of_users" : "Liczba osób", + "measured_number_of_users": "Zmierzona liczba użytkowników", + "forecasted_number_of_users": "Prognozowana liczba użytkowników" } From 581f9c120d4844e82f1d2f093cabac30b038351e Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 14:10:57 +0100 Subject: [PATCH 80/83] chore(sks-chart): cleanup and refactor --- .../sks_chart/presentation/sks_chart_sheet.dart | 4 ++-- .../sks_menu/presentation/sks_menu_screen.dart | 6 +++--- .../text_and_url_widget.dart} | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) rename lib/{features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart => widgets/text_and_url_widget.dart} (81%) diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index 6f31b8b9..3bfae61d 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -6,8 +6,8 @@ import "../../../config/ui_config.dart"; import "../../../theme/app_theme.dart"; import "../../../utils/context_extensions.dart"; import "../../../widgets/my_error_widget.dart"; +import "../../../widgets/text_and_url_widget.dart"; import "../../bottom_scroll_sheet/drag_handle.dart"; -import "../../sks_menu/presentation/widgets/sks_menu_data_source_link.dart"; import "../../sks_people_live/data/repository/latest_sks_user_data_repo.dart"; import "../data/models/sks_chart_data.dart"; import "../data/repository/sks_chart_repository.dart"; @@ -61,7 +61,7 @@ class SksChartSheet extends ConsumerWidget { padding: const EdgeInsets.all( SksChartConfig.paddingSmall, ), - child: SksMenuDataSourceLink( + child: TextAndUrl( SksChartConfig.sksChartDataUrl, "${context.localize.data_come_from_website}: ", ), diff --git a/lib/features/sks_menu/presentation/sks_menu_screen.dart b/lib/features/sks_menu/presentation/sks_menu_screen.dart index a8c3103f..812c417d 100644 --- a/lib/features/sks_menu/presentation/sks_menu_screen.dart +++ b/lib/features/sks_menu/presentation/sks_menu_screen.dart @@ -13,10 +13,10 @@ import "../../../utils/context_extensions.dart"; import "../../../widgets/detail_views/detail_view_app_bar.dart"; import "../../../widgets/my_error_widget.dart"; import "../../../widgets/my_text_button.dart"; +import "../../../widgets/text_and_url_widget.dart"; import "../../sks_people_live/presentation/widgets/sks_user_data_button.dart"; import "../data/models/sks_menu_response.dart"; import "../data/repository/sks_menu_repository.dart"; -import "widgets/sks_menu_data_source_link.dart"; import "widgets/sks_menu_header.dart"; import "widgets/sks_menu_section.dart"; import "widgets/sks_menu_view_loading.dart"; @@ -111,9 +111,9 @@ class _SksMenuView extends ConsumerWidget { padding: const EdgeInsets.all(HomeViewConfig.paddingMedium), child: SksMenuSection(sksMenuData.meals), ), - SksMenuDataSourceLink( + TextAndUrl( SksMenuConfig.sksDataSource, - context.localize.data_come_from_website, + "${context.localize.data_come_from_website}: ", ), const SizedBox( height: ScienceClubsViewConfig.mediumPadding, diff --git a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart b/lib/widgets/text_and_url_widget.dart similarity index 81% rename from lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart rename to lib/widgets/text_and_url_widget.dart index 556604b3..babe388d 100644 --- a/lib/features/sks_menu/presentation/widgets/sks_menu_data_source_link.dart +++ b/lib/widgets/text_and_url_widget.dart @@ -2,11 +2,11 @@ import "package:flutter/cupertino.dart"; import "package:flutter/gestures.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "../../../../theme/app_theme.dart"; -import "../../../../utils/launch_url_util.dart"; +import "../theme/app_theme.dart"; +import "../utils/launch_url_util.dart"; -class SksMenuDataSourceLink extends ConsumerWidget { - const SksMenuDataSourceLink( +class TextAndUrl extends ConsumerWidget { + const TextAndUrl( this.url, this.text, { super.key, @@ -27,7 +27,7 @@ class SksMenuDataSourceLink extends ConsumerWidget { ), children: [ TextSpan( - text: url.replaceFirst("https://", "www"), + text: url.replaceFirst("https://", " www."), style: context.textTheme.bodyOrange.copyWith( decoration: TextDecoration.underline, decorationColor: context.colorTheme.orangePomegranade, From cbb743602593ffb15d3316f47434a838b06aa4b2 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Mon, 16 Dec 2024 14:14:57 +0100 Subject: [PATCH 81/83] chore(sks-chart): trend's icons have one color no matter the trend --- .../presentation/widgets/sks_user_data_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart index ac5cd8d7..0e04af30 100644 --- a/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart +++ b/lib/features/sks_people_live/presentation/widgets/sks_user_data_button.dart @@ -86,9 +86,9 @@ extension TrendIcon on Trend { Icon get icon { switch (this) { case Trend.increasing: - return const Icon(Icons.trending_up, color: Color(0xFF28a745)); + return const Icon(Icons.trending_up, color: Colors.grey); case Trend.decreasing: - return const Icon(Icons.trending_down, color: Color(0xFFdc3545)); + return const Icon(Icons.trending_down, color: Colors.grey); case Trend.stable: return const Icon(Icons.trending_flat, color: Colors.grey); } From f17f212d41e69a2d356c11fd1241219524336cad Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Tue, 17 Dec 2024 14:14:32 +0100 Subject: [PATCH 82/83] feat(sks-chart): change sheet's color to black --- lib/features/sks_chart/presentation/sks_chart_sheet.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/features/sks_chart/presentation/sks_chart_sheet.dart b/lib/features/sks_chart/presentation/sks_chart_sheet.dart index 3bfae61d..ad477052 100644 --- a/lib/features/sks_chart/presentation/sks_chart_sheet.dart +++ b/lib/features/sks_chart/presentation/sks_chart_sheet.dart @@ -87,8 +87,7 @@ class _SksSheetHeader extends StatelessWidget { const SizedBox(height: SksChartConfig.heightSmall), Text( context.localize.sks_chart_title, - style: context.textTheme.headline - .copyWith(color: context.colorTheme.blueAzure), + style: context.textTheme.headline, textAlign: TextAlign.center, ), ], From 2bdd620bd974e0d0c6b75f7be7cbf3a78e691607 Mon Sep 17 00:00:00 2001 From: mikolaj-jalocha Date: Tue, 17 Dec 2024 16:11:32 +0100 Subject: [PATCH 83/83] chore(sks-chart): rebase with main --- .../navigator/utils/navigation_commands.dart | 8 ++------ lib/utils/datetime_utils.dart | 12 ------------ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/lib/features/navigator/utils/navigation_commands.dart b/lib/features/navigator/utils/navigation_commands.dart index 7b2fd09e..5c492ab6 100644 --- a/lib/features/navigator/utils/navigation_commands.dart +++ b/lib/features/navigator/utils/navigation_commands.dart @@ -73,12 +73,8 @@ extension NavigationX on WidgetRef { await _router.pushNamed(uri); } - Future navigateToSksMenu({String? appBarPopTitle}) async { - if (appBarPopTitle != null) { - await _router.push(SksMenuRoute(appBarPopTitle: appBarPopTitle)); - } else { - await _router.push(SksMenuRoute()); - } + Future navigateToSksMenu() async { + await _router.push(const SksMenuRoute()); } Future navigateDigitalGuide(int id) async { diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart index a89258d3..18b1f035 100644 --- a/lib/utils/datetime_utils.dart +++ b/lib/utils/datetime_utils.dart @@ -30,18 +30,6 @@ extension DateTimeUtilsX on DateTime { return "$capitalizedDay, $date $hour"; } - String toDayDateHourString() { - final DateFormat dayFormat = DateFormat("EEEE", "pl_PL"); - final DateFormat dateFormat = DateFormat("dd.MM.yyyy"); - final DateFormat hourFormat = DateFormat("HH:mm"); - final String day = dayFormat.format(this); - final String capitalizedDay = - day[0].toUpperCase() + day.substring(1).toLowerCase(); - final String date = dateFormat.format(this); - final String hour = hourFormat.format(this); - return "$capitalizedDay, $date $hour"; - } - String toHourMinuteString() { final DateFormat hourFormat = DateFormat("HH:mm"); return hourFormat.format(this);