diff --git a/.github/workflows/fastlane.action.yaml b/.github/workflows/fastlane.action.yaml index 3d0b6cf6..93accf78 100644 --- a/.github/workflows/fastlane.action.yaml +++ b/.github/workflows/fastlane.action.yaml @@ -34,7 +34,7 @@ jobs: - name: 'Generate changelog' run: ./.github/scripts/generate_changelog.sh - name: 'Setup Ruby' - uses: ruby/setup-ruby@v1.146.0 + uses: ruby/setup-ruby@v1.148.0 with: ruby-version: '3.0' bundler-cache: true diff --git a/.github/workflows/flutter.analyze-test.action.yaml b/.github/workflows/flutter.analyze-test.action.yaml index f644b952..1832900e 100644 --- a/.github/workflows/flutter.analyze-test.action.yaml +++ b/.github/workflows/flutter.analyze-test.action.yaml @@ -25,7 +25,7 @@ jobs: with: flutter-version: ${{ inputs.flutter_version }} - name: 'Flutter analyze' - run: flutter analyze + run: flutter analyze lib format: name: 'Format' runs-on: ubuntu-latest @@ -38,6 +38,24 @@ jobs: flutter-version: ${{ inputs.flutter_version }} - name: 'Flutter format' run: dart format lib --set-exit-if-changed + code-metrics: + name: 'Code metrics' + runs-on: ubuntu-latest + steps: + - name: 'Checkout source code' + uses: actions/checkout@v3 + - name: 'Setup flutter action' + uses: subosito/flutter-action@v2.10.0 + with: + flutter-version: ${{ inputs.flutter_version }} + - name: 'Get dependencies' + run: flutter pub get + - name: '[Global] Code metrics' + run: flutter pub run dart_code_metrics:metrics analyze --fatal-style --fatal-warnings --fatal-performance --reporter=github lib + - name: '[Unused files] Code metrics' + run: flutter pub run dart_code_metrics:metrics check-unused-files lib + - name: '[Unused code] Code metrics' + run: flutter pub run dart_code_metrics:metrics check-unused-code lib test: name: 'Test' runs-on: ubuntu-latest diff --git a/.github/workflows/flutter.build.action.yaml b/.github/workflows/flutter.build.action.yaml index 7bbda5a6..e1f4be2f 100644 --- a/.github/workflows/flutter.build.action.yaml +++ b/.github/workflows/flutter.build.action.yaml @@ -1,99 +1,99 @@ -name: Flutter - Build - -on: - workflow_call: - inputs: - flutter_version: - description: 'The Flutter used (ex: 2.5.1)' - required: true - type: string - android_output: - description: 'Android build file type output (apk or abb)' - required: true - type: string - secrets: - passphrase: - description: 'The passphrase to decrypt the configuration' - required: true - - -jobs: - build_android: - name: 'Android (${{ inputs.android_output }})' - runs-on: ubuntu-latest - steps: - - name: 'Checkout source code' - uses: actions/checkout@v3.5.2 - with: - fetch-depth: 0 - - name: 'Decrypt secret configuration' - run: ./.github/scripts/decrypt_secret.sh - env: - PASSPHRASE: ${{ secrets.passphrase }} - - name: 'Check secret configuration' - run: ./.github/scripts/check_secrets_decryption.sh - - name: 'Set up JAVA' - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '11.x' - - name: 'Setup Flutter' - uses: subosito/flutter-action@v2.10.0 - with: - flutter-version: ${{ inputs.flutter_version }} - - name: 'Build Android APK' - if: ${{ inputs.android_output == 'apk' }} - # Build APK version of the app - run: flutter build apk --split-per-abi - - name: 'Save APK' - if: ${{ inputs.android_output == 'apk' }} - uses: actions/upload-artifact@v3 - with: - name: 'apk-build' - path: build/app/outputs/apk/release/app-arm64-v8a-release.apk - - name: 'Generate build number' - if: ${{ inputs.android_output == 'aab' }} - # Build App Bundle version of the app - run: | - BUILD_NUMBER=$(git rev-list --all --count) - echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV - echo "This build is tagged as $BUILD_NUMBER on $GITHUB_REF" - - name: 'Build Android App Bundle' - if: ${{ inputs.android_output == 'aab' }} - run: flutter build appbundle --dart-define=FLAVOR=prod --build-number="$BUILD_NUMBER" - env: - BUILD_NUMBER: ${{ env.BUILD_NUMBER }} - - name: 'Save AAB' - if: ${{ inputs.android_output == 'aab' }} - uses: actions/upload-artifact@v3 - with: - name: 'aab-build' - path: build/app/outputs/bundle/release/app-release.aab - build_ios: - name: 'iOS' - runs-on: ubuntu-latest - steps: - - name: 'Checkout source code' - uses: actions/checkout@v3 - - name: 'Decrypt secret configuration' - run: ./.github/scripts/decrypt_secret.sh - env: - PASSPHRASE: ${{ secrets.passphrase }} - - name: 'Check secret configuration' - run: ./.github/scripts/check_secrets_decryption.sh - - name: '🥺' - run: echo 'WIP' - build_web: - name: 'WEB' - runs-on: ubuntu-latest - steps: - - name: 'Checkout source code' - uses: actions/checkout@v3 - - name: 'Decrypt secret configuration' - run: ./.github/scripts/decrypt_secret.sh - env: - PASSPHRASE: ${{ secrets.passphrase }} - - name: 'Check secret configuration' - run: ./.github/scripts/check_secrets_decryption.sh - - name: '🥺' +name: Flutter - Build + +on: + workflow_call: + inputs: + flutter_version: + description: 'The Flutter used (ex: 2.5.1)' + required: true + type: string + android_output: + description: 'Android build file type output (apk or abb)' + required: true + type: string + secrets: + passphrase: + description: 'The passphrase to decrypt the configuration' + required: true + + +jobs: + build_android: + name: 'Android (${{ inputs.android_output }})' + runs-on: ubuntu-latest + steps: + - name: 'Checkout source code' + uses: actions/checkout@v3.5.2 + with: + fetch-depth: 0 + - name: 'Decrypt secret configuration' + run: ./.github/scripts/decrypt_secret.sh + env: + PASSPHRASE: ${{ secrets.passphrase }} + - name: 'Check secret configuration' + run: ./.github/scripts/check_secrets_decryption.sh + - name: 'Set up JAVA' + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11.x' + - name: 'Setup Flutter' + uses: subosito/flutter-action@v2.10.0 + with: + flutter-version: ${{ inputs.flutter_version }} + - name: 'Build Android APK' + if: ${{ inputs.android_output == 'apk' }} + # Build APK version of the app + run: flutter build apk --split-per-abi + - name: 'Save APK' + if: ${{ inputs.android_output == 'apk' }} + uses: actions/upload-artifact@v3 + with: + name: 'apk-build' + path: build/app/outputs/apk/release/app-arm64-v8a-release.apk + - name: 'Generate build number' + if: ${{ inputs.android_output == 'aab' }} + # Build App Bundle version of the app + run: | + BUILD_NUMBER=$(git rev-list --all --count) + echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV + echo "This build is tagged as $BUILD_NUMBER on $GITHUB_REF" + - name: 'Build Android App Bundle' + if: ${{ inputs.android_output == 'aab' }} + run: flutter build appbundle --build-number="$BUILD_NUMBER" + env: + BUILD_NUMBER: ${{ env.BUILD_NUMBER }} + - name: 'Save AAB' + if: ${{ inputs.android_output == 'aab' }} + uses: actions/upload-artifact@v3 + with: + name: 'aab-build' + path: build/app/outputs/bundle/release/app-release.aab + build_ios: + name: 'iOS' + runs-on: ubuntu-latest + steps: + - name: 'Checkout source code' + uses: actions/checkout@v3 + - name: 'Decrypt secret configuration' + run: ./.github/scripts/decrypt_secret.sh + env: + PASSPHRASE: ${{ secrets.passphrase }} + - name: 'Check secret configuration' + run: ./.github/scripts/check_secrets_decryption.sh + - name: '🥺' + run: echo 'WIP' + build_web: + name: 'WEB' + runs-on: ubuntu-latest + steps: + - name: 'Checkout source code' + uses: actions/checkout@v3 + - name: 'Decrypt secret configuration' + run: ./.github/scripts/decrypt_secret.sh + env: + PASSPHRASE: ${{ secrets.passphrase }} + - name: 'Check secret configuration' + run: ./.github/scripts/check_secrets_decryption.sh + - name: '🥺' run: echo 'WIP' \ No newline at end of file diff --git a/CHANGELOG_en.md b/CHANGELOG_en.md index e280649e..c22a47a6 100644 --- a/CHANGELOG_en.md +++ b/CHANGELOG_en.md @@ -1,3 +1,20 @@ +# **v1.6.1** : + +- *Fix*: + - The SnackBar warning of activation of notifications on slots is now correctly in place +- *Interface*: + - Adding a padding to display the last item in the closures list + - Only one ad is now displayed +*** +# **v1.6.0** : + +- *Features*: + - It is now possible to add two favorite slots to be notified only of impacting events +- *Interface*: + - Revised list of closures to make it easier to read. + - Highlighting impacting events with an orange border. + - Adding a message if notifications are disabled +*** # **v1.4.0** : - *Features*: diff --git a/CHANGELOG_es.md b/CHANGELOG_es.md index a1c663b6..fe6d1750 100644 --- a/CHANGELOG_es.md +++ b/CHANGELOG_es.md @@ -1,3 +1,20 @@ +# **v1.6.1** : + +- *Fix*: + - El snackbar de advertencia de la activación de las notificaciones en las ranuras está ahora correctamente en su lugar +- *Interfaz*: + - Añadir un relleno para mostrar el último elemento de la lista de cierres + - Ahora solo se muestra un anuncio +*** +# **v1.6.0** : + +- *Funcionalidades*: + - Ahora es posible añadir dos franjas horarias favoritas para ser notificado solo de los eventos impactantes +- *Interfaz*: + - Reordenar la lista de cierres para que sea más fácil de leer. + - Resalta eventos impactantes con un borde naranja. + - Añadido un mensaje si las notificaciones están desactivadas +*** # **v1.4.0** : - *Características*: diff --git a/CHANGELOG_fr.md b/CHANGELOG_fr.md index f07c095f..2fd68345 100644 --- a/CHANGELOG_fr.md +++ b/CHANGELOG_fr.md @@ -1,3 +1,20 @@ +# **v1.6.1** : + +- *Fix*: + - La SnackBar d'avertissement de l'activation des notifications sur les créneaux est maintenant correctement en place +- *Interface*: + - Ajout d'un padding pour afficher le dernier élément de la liste des fermetures + - Seulement une seule publicité est maintenant affichée +*** +# **v1.6.0** : + +- *Fonctionnalités*: + - Il est maintenant possible d'ajouter deux créneaux favoris pour n'être averti que des évènements impactant +- *Interface*: + - Remaniement de la liste des fermetures afin de la rendre plus facile à lire. + - Mise en valeur d'événements impactants grâce à une bordure orange. + - Ajout d'un message si les notifications sont désactivées +*** # **v1.4.0** : - *Fonctionnalités*: diff --git a/analysis_options.yaml b/analysis_options.yaml index bac0015c..6847264e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,7 +3,34 @@ include: package:flutter_lints/flutter.yaml linter: rules: prefer_single_quotes: true - use_build_context_synchronously: false -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + plugins: + - dart_code_metrics + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-parameters: 5 + maximum-nesting-level: 5 + metrics-exclude: + - test/** + rules: + - avoid-dynamic + - avoid-passing-async-when-sync-expected + - avoid-redundant-async + - avoid-unnecessary-type-assertions + - avoid-unnecessary-type-casts + - avoid-unrelated-type-assertions + - avoid-nested-conditional-expressions: + acceptable-level: 2 + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions: + ignore-nested: true + - no-equal-then-else + - prefer-moving-to-variable: + allowed-duplicated-chains: 3 + - prefer-match-file-name diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c71fca2b..d060281a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/android/build.gradle b/android/build.gradle index 6b10a38f..13b3050e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '1.8.21' repositories { google() mavenCentral() diff --git a/lib/app_theme.dart b/lib/app_theme.dart index e9d67c61..48c61cc9 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -class AppThemes { +class AppTheme { static final lightTheme = ThemeData.light( useMaterial3: true, ).copyWith( diff --git a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart b/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart deleted file mode 100644 index 4570e5ad..00000000 --- a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart +++ /dev/null @@ -1,166 +0,0 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - -import 'dart:async'; -import 'dart:convert'; - -import 'package:chabo/bloc/chabo_event.dart'; -import 'package:chabo/const.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; -import 'package:chabo/models/chaban_bridge_boat_forecast.dart'; -import 'package:chabo/models/chaban_bridge_maintenance_forecast.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:http/http.dart' as http; - -part 'chaban_bridge_forecast_event.dart'; -part 'chaban_bridge_forecast_state.dart'; - -class ChabanBridgeForecastBloc - extends Bloc { - final http.Client httpClient; - - ChabanBridgeForecastBloc({required this.httpClient}) - : super(const ChabanBridgeForecastState()) { - Timer.periodic(const Duration(seconds: 1), _onRefreshCurrentStatus); - on( - _onChabanBridgeForecastFetched, - ); - } - - void _onRefreshCurrentStatus(Timer timer) { - try { - if (state.status == ChabanBridgeForecastStatus.success) { - final currentStatus = _getCurrentStatus(state.chabanBridgeForecasts); - final previousStatus = - _getPreviousStatus(state.chabanBridgeForecasts, currentStatus); - if (currentStatus != state.currentChabanBridgeForecast && - currentStatus != previousStatus) { - emit( - state.copyWith( - currentChabanBridgeForecast: currentStatus, - previousChabanBridgeForecast: previousStatus, - ), - ); - } - } - } catch (_) { - emit(state.copyWith( - status: ChabanBridgeForecastStatus.failure, message: _.toString())); - } - } - - Future> _fetchChabanBridgeForecasts( - int offset) async { - var uri = Uri.https( - 'opendata.bordeaux-metropole.fr', - '/api/records/1.0/search', - { - 'dataset': 'previsions_pont_chaban', - 'rows': '${Const.chabanBridgeForecastLimit}', - 'sort': '-date_passage', - 'start': '$offset', - 'timezone': 'Europe/Paris' - }, - ); - final response = await httpClient.get(uri); - if (response.statusCode == 200) { - final body = json.decode(response.body); - return (body['records'] as List).map((dynamic json) { - if (json['fields']['bateau'].toString().toLowerCase() == - 'maintenance') { - final maintenanceForecast = - ChabanBridgeMaintenanceForecast.fromJSON(json); - return maintenanceForecast; - } - final boatForecast = ChabanBridgeBoatForecast.fromJSON(json); - return boatForecast; - }).toList() - ..sort((a, b) => - a.circulationClosingDate.compareTo(b.circulationClosingDate)); - } - return []; - } - - AbstractChabanBridgeForecast _getCurrentStatus( - List chabanBridgeForecast) { - int middle = chabanBridgeForecast.length ~/ 2; - if ((chabanBridgeForecast[middle] - .circulationClosingDate - .isBefore(DateTime.now()) && - chabanBridgeForecast[middle] - .circulationReOpeningDate - .isAfter(DateTime.now()))) { - return chabanBridgeForecast[middle]; - } - if (chabanBridgeForecast.length == 2) { - return chabanBridgeForecast[1] - .circulationClosingDate - .isAfter(DateTime.now()) && - chabanBridgeForecast[0] - .circulationReOpeningDate - .isBefore(DateTime.now()) - ? chabanBridgeForecast[1] - : chabanBridgeForecast[0]; - } else if (chabanBridgeForecast[middle] - .circulationClosingDate - .isAfter(DateTime.now())) { - return _getCurrentStatus(chabanBridgeForecast.sublist(0, middle + 1)); - } else { - return _getCurrentStatus(chabanBridgeForecast.sublist(middle)); - } - } - - AbstractChabanBridgeForecast? _getPreviousStatus( - List chabanBridgeForecasts, - AbstractChabanBridgeForecast currentStatus) { - if (chabanBridgeForecasts.indexOf(currentStatus) == 0) { - return null; - } else { - return chabanBridgeForecasts - .elementAt(chabanBridgeForecasts.indexOf(currentStatus) - 1); - } - } - - Future _onChabanBridgeForecastFetched(ChabanBridgeForecastFetched event, - Emitter emit) async { - if (state.hasReachedMax) return; - try { - if (state.status == ChabanBridgeForecastStatus.initial) { - final chabanBridgeForecasts = - await _fetchChabanBridgeForecasts(state.offset); - final currentStatus = _getCurrentStatus(chabanBridgeForecasts); - emit(state.copyWith( - status: ChabanBridgeForecastStatus.success, - chabanBridgeForecasts: chabanBridgeForecasts, - currentChabanBridgeForecast: currentStatus, - previousChabanBridgeForecast: - _getPreviousStatus(chabanBridgeForecasts, currentStatus), - hasReachedMax: false, - offset: state.offset + Const.chabanBridgeForecastLimit)); - } - final chabanBridgeForecasts = - await _fetchChabanBridgeForecasts(state.chabanBridgeForecasts.length); - emit( - chabanBridgeForecasts.isEmpty - ? state.copyWith(hasReachedMax: true) - : state.copyWith( - currentChabanBridgeForecast: - state.currentChabanBridgeForecast ?? - _getCurrentStatus(chabanBridgeForecasts), - previousChabanBridgeForecast: - state.previousChabanBridgeForecast ?? - _getPreviousStatus(chabanBridgeForecasts, - _getCurrentStatus(chabanBridgeForecasts)), - status: ChabanBridgeForecastStatus.success, - chabanBridgeForecasts: List.of(state.chabanBridgeForecasts) - ..addAll(chabanBridgeForecasts), - hasReachedMax: false, - offset: state.offset + Const.chabanBridgeForecastLimit, - ), - ); - } catch (_) { - emit(state.copyWith( - status: ChabanBridgeForecastStatus.failure, message: _.toString())); - } - } -} diff --git a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_event.dart b/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_event.dart deleted file mode 100644 index 020cc3d3..00000000 --- a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_event.dart +++ /dev/null @@ -1,8 +0,0 @@ -part of 'chaban_bridge_forecast_bloc.dart'; - -abstract class ChabanBridgeForecastEvent extends ChaboEvent {} - -class ChabanBridgeForecastFetched extends ChabanBridgeForecastEvent {} - -class ChabanBridgeForecastRefreshCurrentStatus - extends ChabanBridgeForecastEvent {} diff --git a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_state.dart b/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_state.dart deleted file mode 100644 index 67feb542..00000000 --- a/lib/bloc/chaban_bridge_forecast/chaban_bridge_forecast_state.dart +++ /dev/null @@ -1,59 +0,0 @@ -part of 'chaban_bridge_forecast_bloc.dart'; - -enum ChabanBridgeForecastStatus { initial, success, failure } - -class ChabanBridgeForecastState extends Equatable { - final ChabanBridgeForecastStatus status; - final List chabanBridgeForecasts; - final AbstractChabanBridgeForecast? currentChabanBridgeForecast; - final AbstractChabanBridgeForecast? previousChabanBridgeForecast; - final bool hasReachedMax; - final int offset; - final String message; - - const ChabanBridgeForecastState( - {this.status = ChabanBridgeForecastStatus.initial, - this.chabanBridgeForecasts = const [], - this.currentChabanBridgeForecast, - this.previousChabanBridgeForecast, - this.hasReachedMax = false, - this.offset = 0, - this.message = 'OK'}); - - ChabanBridgeForecastState copyWith( - {ChabanBridgeForecastStatus? status, - List? chabanBridgeForecasts, - AbstractChabanBridgeForecast? currentChabanBridgeForecast, - AbstractChabanBridgeForecast? previousChabanBridgeForecast, - bool? hasReachedMax, - int? offset, - String? message}) { - return ChabanBridgeForecastState( - status: status ?? this.status, - chabanBridgeForecasts: - chabanBridgeForecasts ?? this.chabanBridgeForecasts, - currentChabanBridgeForecast: - currentChabanBridgeForecast ?? this.currentChabanBridgeForecast, - previousChabanBridgeForecast: - previousChabanBridgeForecast ?? this.previousChabanBridgeForecast, - hasReachedMax: hasReachedMax ?? this.hasReachedMax, - offset: offset ?? this.offset, - message: message ?? this.message); - } - - @override - String toString() { - return 'ChabanBridgeForecastState{status: $status, chabanBridgeForecasts: $chabanBridgeForecasts, currentChabanBridgeForecast: $currentChabanBridgeForecast, hasReachedMax: $hasReachedMax, offset: $offset, message: $message}'; - } - - @override - List get props => [ - status, - chabanBridgeForecasts, - hasReachedMax, - offset, - message, - currentChabanBridgeForecast, - previousChabanBridgeForecast, - ]; -} diff --git a/lib/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart b/lib/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart deleted file mode 100644 index 5c069866..00000000 --- a/lib/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:chabo/bloc/chabo_event.dart'; -import 'package:chabo/const.dart'; -import 'package:chabo/extensions/color_scheme_extension.dart'; -import 'package:chabo/extensions/string_extension.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:intl/intl.dart'; - -part 'chaban_bridge_status_event.dart'; - -part 'chaban_bridge_status_state.dart'; - -class ChabanBridgeStatusBloc - extends Bloc { - ChabanBridgeStatusBloc() : super(const ChabanBridgeStatusStateInitial()) { - on( - _onChabanBridgeStatusChanged, - ); - on( - _onRefresh, - ); - on( - _onDurationChanged, - ); - } - - void _onDurationChanged(ChabanBridgeStatusDurationChanged event, - Emitter emit) { - emit( - state.copyWith( - durationForCloseClosing: event.duration, - ), - ); - } - - void _onChabanBridgeStatusChanged( - ChabanBridgeStatusChanged event, Emitter emit) { - emit( - state.copyWith( - currentChabanBridgeForecast: event.currentChabanBridgeForecast, - previousChabanBridgeForecast: event.previousChabanBridgeForecast), - ); - } - - void _onRefresh( - ChabanBridgeStatusRefresh event, Emitter emit) { - final Duration? durationUntilNextEvent = _getDurationUntilNextEvent(); - final Duration? durationBetweenPreviousAndNextEvent = - _getDurationBetweenPreviousAndNextEvent(); - final double completionPercentage = _getDiffPercentage( - durationBetweenPreviousAndNextEvent, durationUntilNextEvent); - final String mainMessageStatus = _getMainStatus(event.context); - final String timeMessagePrefix = _getTimeMessagePrefix(event.context); - final Color foregroundColor = _getForegroundColor(event.context); - final Color backgroundColor = _getBackgroundColor(event.context); - - emit( - state.copyWith( - durationUntilNextEvent: durationUntilNextEvent, - durationBetweenPreviousAndNextEvent: - durationBetweenPreviousAndNextEvent, - completionPercentage: completionPercentage, - mainMessageStatus: mainMessageStatus, - timeMessagePrefix: timeMessagePrefix, - foregroundColor: foregroundColor, - chabanBridgeStatusLifecycle: state.durationUntilNextEvent != - Duration.zero // Prevents from displaying the wrong status color - ? ChabanBridgeStatusLifecycle.populated - : ChabanBridgeStatusLifecycle.empty, - backgroundColor: backgroundColor, - ), - ); - } - - Color _getBackgroundColor(BuildContext context) { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - if (currentChabanBridgeForecast != null) { - final isOpen = !currentChabanBridgeForecast.isCurrentlyClosed(); - if (isOpen && - state.durationUntilNextEvent.inMinutes < - state.durationForCloseClosing.inMinutes) { - return Theme.of(context).colorScheme.warningColor; - } else if (isOpen) { - return Colors.green; - } else { - return Theme.of(context).colorScheme.error; - } - } else { - return state.backgroundColor; - } - } - - Color _getForegroundColor(BuildContext context) { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - if (currentChabanBridgeForecast != null) { - final isOpen = !currentChabanBridgeForecast.isCurrentlyClosed(); - if (isOpen || - state.durationUntilNextEvent.inMinutes < - state.durationForCloseClosing.inMinutes) { - return Theme.of(context).colorScheme.background; - } else { - return Theme.of(context).colorScheme.onError; - } - } else { - return state.foregroundColor; - } - } - - String _getTimeMessagePrefix(BuildContext context) { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - if (currentChabanBridgeForecast != null) { - if (currentChabanBridgeForecast.isCurrentlyClosed()) { - return '${AppLocalizations.of(context)!.scheduledToOpen.capitalize()} '; - } else { - return '${AppLocalizations.of(context)!.nextClosingScheduled.capitalize()} '; - } - } else { - return 'NO_TIME'; - } - } - - String _getMainStatus(BuildContext context) { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - if (currentChabanBridgeForecast != null && - !currentChabanBridgeForecast.isCurrentlyClosed() && - state.durationUntilNextEvent.inMinutes >= - state.durationForCloseClosing.inMinutes) { - return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.open}'; - } else if (currentChabanBridgeForecast != null && - !currentChabanBridgeForecast.isCurrentlyClosed() && - state.durationUntilNextEvent.inMinutes < - state.durationForCloseClosing.inMinutes) { - return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.aboutToClose}'; - } else { - return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.closed}'; - } - } - - Duration? _getDurationUntilNextEvent() { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - final DateTime now = DateTime.now(); - if (currentChabanBridgeForecast != null) { - if (currentChabanBridgeForecast.isCurrentlyClosed()) { - return currentChabanBridgeForecast.circulationReOpeningDate - .difference(now); - } else { - return currentChabanBridgeForecast.circulationClosingDate - .difference(now); - } - } else { - return null; - } - } - - Duration? _getDurationBetweenPreviousAndNextEvent() { - final currentChabanBridgeForecast = state.currentChabanBridgeForecast; - final previousChabanBridgeForecast = state.previousChabanBridgeForecast; - if (currentChabanBridgeForecast != null && - previousChabanBridgeForecast != null) { - if (currentChabanBridgeForecast.isCurrentlyClosed()) { - return currentChabanBridgeForecast.closedDuration; - } else { - return currentChabanBridgeForecast.circulationClosingDate - .difference(previousChabanBridgeForecast.circulationReOpeningDate); - } - } else { - return null; - } - } - - double _getDiffPercentage(Duration? durationBetweenPreviousAndNextEvent, - Duration? durationUntilNextEvent) { - if (durationBetweenPreviousAndNextEvent != null && - durationUntilNextEvent != null) { - return 1 - - (durationUntilNextEvent.inSeconds / - durationBetweenPreviousAndNextEvent.inSeconds); - } else { - return -1; - } - } - - String _getGreetings(BuildContext context) { - int hours = int.parse(DateFormat('HH').format(DateTime.now())); - if (hours >= 6 && hours <= 12) { - return AppLocalizations.of(context)!.goodMorning.capitalize(); - } else if (hours > 12 && hours <= 18) { - return AppLocalizations.of(context)!.goodAfternoon.capitalize(); - } else { - return AppLocalizations.of(context)!.goodEvening.capitalize(); - } - } -} diff --git a/lib/bloc/chaban_bridge_status/chaban_bridge_status_event.dart b/lib/bloc/chaban_bridge_status/chaban_bridge_status_event.dart deleted file mode 100644 index 3f0ab772..00000000 --- a/lib/bloc/chaban_bridge_status/chaban_bridge_status_event.dart +++ /dev/null @@ -1,29 +0,0 @@ -part of 'chaban_bridge_status_bloc.dart'; - -class ChabanBridgeStatusEvent extends ChaboEvent {} - -class ChabanBridgeStatusChanged extends ChabanBridgeStatusEvent { - final AbstractChabanBridgeForecast? currentChabanBridgeForecast; - final AbstractChabanBridgeForecast? previousChabanBridgeForecast; - - ChabanBridgeStatusChanged( - {required this.currentChabanBridgeForecast, - required this.previousChabanBridgeForecast}) - : super(); -} - -class ChabanBridgeStatusRefresh extends ChabanBridgeStatusEvent { - final BuildContext context; - - ChabanBridgeStatusRefresh({ - required this.context, - }) : super(); -} - -class ChabanBridgeStatusDurationChanged extends ChabanBridgeStatusEvent { - final Duration duration; - - ChabanBridgeStatusDurationChanged({ - required this.duration, - }) : super(); -} diff --git a/lib/bloc/chaban_bridge_status/chaban_bridge_status_state.dart b/lib/bloc/chaban_bridge_status/chaban_bridge_status_state.dart deleted file mode 100644 index 74c48b3f..00000000 --- a/lib/bloc/chaban_bridge_status/chaban_bridge_status_state.dart +++ /dev/null @@ -1,95 +0,0 @@ -part of 'chaban_bridge_status_bloc.dart'; - -enum ChabanBridgeStatusLifecycle { empty, populated } - -class ChabanBridgeStatusState extends Equatable { - final ChabanBridgeStatusLifecycle chabanBridgeStatusLifecycle; - final AbstractChabanBridgeForecast? currentChabanBridgeForecast; - final AbstractChabanBridgeForecast? previousChabanBridgeForecast; - final Duration durationUntilNextEvent; - final Duration durationForCloseClosing; - final Duration? durationBetweenPreviousAndNextEvent; - final double completionPercentage; - final String mainMessageStatus; - final String timeMessagePrefix; - final Color foregroundColor; - final Color backgroundColor; - - const ChabanBridgeStatusState( - {required this.chabanBridgeStatusLifecycle, - required this.currentChabanBridgeForecast, - required this.previousChabanBridgeForecast, - required this.durationUntilNextEvent, - required this.durationForCloseClosing, - required this.durationBetweenPreviousAndNextEvent, - required this.completionPercentage, - required this.mainMessageStatus, - required this.timeMessagePrefix, - required this.foregroundColor, - required this.backgroundColor}); - - ChabanBridgeStatusState copyWith( - {ChabanBridgeStatusLifecycle? chabanBridgeStatusLifecycle, - AbstractChabanBridgeForecast? currentChabanBridgeForecast, - AbstractChabanBridgeForecast? previousChabanBridgeForecast, - Duration? durationUntilNextEvent, - Duration? durationForCloseClosing, - Duration? durationBetweenPreviousAndNextEvent, - double? completionPercentage, - String? mainMessageStatus, - String? timeMessagePrefix, - Color? foregroundColor, - Color? backgroundColor}) { - return ChabanBridgeStatusState( - chabanBridgeStatusLifecycle: - chabanBridgeStatusLifecycle ?? this.chabanBridgeStatusLifecycle, - currentChabanBridgeForecast: - currentChabanBridgeForecast ?? this.currentChabanBridgeForecast, - previousChabanBridgeForecast: - previousChabanBridgeForecast ?? this.previousChabanBridgeForecast, - durationUntilNextEvent: - durationUntilNextEvent ?? this.durationUntilNextEvent, - durationForCloseClosing: - durationForCloseClosing ?? this.durationForCloseClosing, - durationBetweenPreviousAndNextEvent: - durationBetweenPreviousAndNextEvent ?? - this.durationBetweenPreviousAndNextEvent, - completionPercentage: completionPercentage ?? this.completionPercentage, - mainMessageStatus: mainMessageStatus ?? this.mainMessageStatus, - timeMessagePrefix: timeMessagePrefix ?? this.timeMessagePrefix, - foregroundColor: foregroundColor ?? this.foregroundColor, - backgroundColor: backgroundColor ?? this.backgroundColor); - } - - @override - List get props => [ - chabanBridgeStatusLifecycle, - currentChabanBridgeForecast, - previousChabanBridgeForecast, - durationUntilNextEvent, - durationForCloseClosing, - durationBetweenPreviousAndNextEvent, - completionPercentage, - mainMessageStatus, - timeMessagePrefix, - foregroundColor, - backgroundColor - ]; -} - -class ChabanBridgeStatusStateInitial extends ChabanBridgeStatusState { - const ChabanBridgeStatusStateInitial() - : super( - previousChabanBridgeForecast: null, - currentChabanBridgeForecast: null, - durationUntilNextEvent: Duration.zero, - durationBetweenPreviousAndNextEvent: null, - durationForCloseClosing: - Const.notificationDurationValueDefaultValue, - chabanBridgeStatusLifecycle: ChabanBridgeStatusLifecycle.empty, - completionPercentage: 0, - mainMessageStatus: '', - timeMessagePrefix: '', - foregroundColor: Colors.white, - backgroundColor: Colors.white); -} diff --git a/lib/bloc/forecast/forecast_bloc.dart b/lib/bloc/forecast/forecast_bloc.dart new file mode 100644 index 00000000..efb9bd39 --- /dev/null +++ b/lib/bloc/forecast/forecast_bloc.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chabo/bloc/chabo_event.dart'; +import 'package:chabo/const.dart'; +import 'package:chabo/models/abstract_forecast.dart'; +import 'package:chabo/models/boat_forecast.dart'; +import 'package:chabo/models/maintenance_forecast.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; + +part 'forecast_event.dart'; +part 'forecast_state.dart'; + +class ForecastBloc extends Bloc { + final http.Client httpClient; + + ForecastBloc({required this.httpClient}) : super(const ForecastState()) { + Timer.periodic(const Duration(seconds: 1), _onRefreshCurrentStatus); + on( + _onForecastFetched, + ); + } + + void _onRefreshCurrentStatus(Timer timer) { + try { + if (state.status == ForecastStatus.success) { + final currentStatus = _getCurrentStatus(state.forecasts); + final previousStatus = + _getPreviousStatus(state.forecasts, currentStatus); + if (currentStatus != state.currentForecast && + currentStatus != previousStatus) { + // ignore: invalid_use_of_visible_for_testing_member + emit( + state.copyWith( + currentForecast: currentStatus, + previousForecast: previousStatus, + ), + ); + } + } + } catch (_) { + // ignore: invalid_use_of_visible_for_testing_member + emit(state.copyWith( + status: ForecastStatus.failure, + message: _.toString(), + )); + } + } + + Future> _fetchForecasts( + int offset, + ) async { + var uri = Uri.https( + 'opendata.bordeaux-metropole.fr', + '/api/records/1.0/search', + { + 'dataset': 'previsions_pont_chaban', + 'rows': '${Const.forecastLimit}', + 'sort': '-date_passage', + 'start': '$offset', + 'timezone': 'Europe/Paris', + }, + ); + final response = await httpClient.get(uri); + if (response.statusCode == 200) { + final body = json.decode(response.body); + + return (body['records'] as List).map((json) { + if (json['fields']['bateau'].toString().toLowerCase() == + 'maintenance') { + final maintenanceForecast = MaintenanceForecast.fromJSON(json); + + return maintenanceForecast; + } + final boatForecast = BoatForecast.fromJSON(json); + + return boatForecast; + }).toList() + ..sort((a, b) => + a.circulationClosingDate.compareTo(b.circulationClosingDate)); + } + + return []; + } + + AbstractForecast _getCurrentStatus( + List forecast, + ) { + int middle = forecast.length ~/ 2; + if ((forecast[middle].circulationClosingDate.isBefore(DateTime.now()) && + forecast[middle].circulationReOpeningDate.isAfter(DateTime.now()))) { + return forecast[middle]; + } + if (forecast.length == 2) { + return forecast[1].circulationClosingDate.isAfter(DateTime.now()) && + forecast[0].circulationReOpeningDate.isBefore(DateTime.now()) + ? forecast[1] + : forecast[0]; + } else if (forecast[middle] + .circulationClosingDate + .isAfter(DateTime.now())) { + return _getCurrentStatus(forecast.sublist(0, middle + 1)); + } else { + return _getCurrentStatus(forecast.sublist(middle)); + } + } + + AbstractForecast? _getPreviousStatus( + List forecasts, + AbstractForecast currentStatus, + ) { + return forecasts.indexOf(currentStatus) == 0 + ? null + : forecasts.elementAt(forecasts.indexOf(currentStatus) - 1); + } + + Future _onForecastFetched( + ForecastFetched event, + Emitter emit, + ) async { + if (state.hasReachedMax) return; + try { + if (state.status == ForecastStatus.initial) { + final forecasts = await _fetchForecasts(state.offset); + final currentStatus = _getCurrentStatus(forecasts); + emit(state.copyWith( + status: ForecastStatus.success, + forecasts: forecasts, + currentForecast: currentStatus, + previousForecast: _getPreviousStatus(forecasts, currentStatus), + hasReachedMax: false, + offset: state.offset + Const.forecastLimit, + )); + } + final forecasts = await _fetchForecasts(state.forecasts.length); + emit( + forecasts.isEmpty + ? state.copyWith(hasReachedMax: true) + : state.copyWith( + currentForecast: + state.currentForecast ?? _getCurrentStatus(forecasts), + previousForecast: state.previousForecast ?? + _getPreviousStatus( + forecasts, + _getCurrentStatus(forecasts), + ), + status: ForecastStatus.success, + forecasts: List.of(state.forecasts)..addAll(forecasts), + hasReachedMax: false, + offset: state.offset + Const.forecastLimit, + ), + ); + } catch (_) { + emit(state.copyWith( + status: ForecastStatus.failure, + message: _.toString(), + )); + } + } +} diff --git a/lib/bloc/forecast/forecast_event.dart b/lib/bloc/forecast/forecast_event.dart new file mode 100644 index 00000000..5bbf454d --- /dev/null +++ b/lib/bloc/forecast/forecast_event.dart @@ -0,0 +1,5 @@ +part of 'forecast_bloc.dart'; + +abstract class ForecastEvent extends ChaboEvent {} + +class ForecastFetched extends ForecastEvent {} diff --git a/lib/bloc/forecast/forecast_state.dart b/lib/bloc/forecast/forecast_state.dart new file mode 100644 index 00000000..3cac3b30 --- /dev/null +++ b/lib/bloc/forecast/forecast_state.dart @@ -0,0 +1,54 @@ +part of 'forecast_bloc.dart'; + +class ForecastState extends Equatable { + final ForecastStatus status; + final List forecasts; + final AbstractForecast? currentForecast; + final AbstractForecast? previousForecast; + final bool hasReachedMax; + final int offset; + final String message; + + const ForecastState({ + this.status = ForecastStatus.initial, + this.forecasts = const [], + this.currentForecast, + this.previousForecast, + this.hasReachedMax = false, + this.offset = 0, + this.message = 'OK', + }); + + ForecastState copyWith({ + ForecastStatus? status, + List? forecasts, + AbstractForecast? currentForecast, + AbstractForecast? previousForecast, + bool? hasReachedMax, + int? offset, + String? message, + }) { + return ForecastState( + status: status ?? this.status, + forecasts: forecasts ?? this.forecasts, + currentForecast: currentForecast ?? this.currentForecast, + previousForecast: previousForecast ?? this.previousForecast, + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + offset: offset ?? this.offset, + message: message ?? this.message, + ); + } + + @override + List get props => [ + status, + forecasts, + hasReachedMax, + offset, + message, + currentForecast, + previousForecast, + ]; +} + +enum ForecastStatus { initial, success, failure } diff --git a/lib/bloc/notification/notification_bloc.dart b/lib/bloc/notification/notification_bloc.dart index 8c2639a7..fdd724ba 100644 --- a/lib/bloc/notification/notification_bloc.dart +++ b/lib/bloc/notification/notification_bloc.dart @@ -1,6 +1,12 @@ +import 'dart:async'; + +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:chabo/bloc/chabo_event.dart'; import 'package:chabo/const.dart'; +import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/models/enums/day.dart'; +import 'package:chabo/models/time_slot.dart'; +import 'package:chabo/service/notification_service.dart'; import 'package:chabo/service/storage_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,154 +15,287 @@ import 'package:flutter_bloc/flutter_bloc.dart'; part 'notification_event.dart'; part 'notification_state.dart'; -class NotificationBloc extends Bloc { +class NotificationBloc extends Bloc { final StorageService storageService; + final NotificationService notificationService; - NotificationBloc({required this.storageService}) - : super( - NotificationSate( - durationNotificationEnabled: - Const.notificationDurationEnabledDefaultValue, - durationNotificationValue: - Const.notificationDurationValueDefaultValue, - timeNotificationEnabled: - Const.notificationTimeEnabledDefaultValue, - timeNotificationValue: Const.notificationTimeValueDefaultValue, - dayNotificationEnabled: Const.notificationDayEnabledDefaultValue, - dayNotificationValue: Const.notificationDayValueDefaultValue, - dayNotificationTimeValue: - Const.notificationDayValueDefaultTimeValue, - openingNotificationEnabled: - Const.notificationOpeningEnabledDefaultValue, - closingNotificationEnabled: - Const.notificationClosingEnabledDefaultValue), + NotificationBloc({ + required this.storageService, + required this.notificationService, + }) : super( + NotificationState( + durationNotificationEnabled: + Const.notificationDurationEnabledDefaultValue, + durationNotificationValue: + Const.notificationDurationValueDefaultValue, + timeNotificationEnabled: Const.notificationTimeEnabledDefaultValue, + timeNotificationValue: Const.notificationTimeValueDefaultValue, + dayNotificationEnabled: Const.notificationDayEnabledDefaultValue, + dayNotificationValue: Const.notificationDayValueDefaultValue, + dayNotificationTimeValue: + Const.notificationDayValueDefaultTimeValue, + openingNotificationEnabled: + Const.notificationOpeningEnabledDefaultValue, + closingNotificationEnabled: + Const.notificationClosingEnabledDefaultValue, + timeSlotsEnabledForNotifications: + Const.notificationFavoriteSlotsEnabledDefaultValue, + timeSlotsValue: Const.notificationFavoriteSlotsDefaultValue, + notificationEnabled: false, + ), ) { on( _onOpeningNotificationStateEvent, + transformer: sequential(), ); on( _onClosingNotificationStateEvent, + transformer: sequential(), ); on( _onDayNotificationStateEvent, + transformer: sequential(), ); on( _onDayNotificationValueEvent, + transformer: sequential(), ); on( _onDayNotificationTimeValueEvent, + transformer: sequential(), ); on( _onTimeNotificationStateEvent, + transformer: sequential(), ); on( _onTimeNotificationValueEvent, + transformer: sequential(), ); on( _onDurationNotificationStateEvent, + transformer: sequential(), ); on( _onDurationNotificationValueEvent, + transformer: sequential(), + ); + on( + _onEnabledTimeSlotEvent, + transformer: sequential(), + ); + on( + _onTimeSlotsEventValue, + transformer: sequential(), + ); + on( + _onComputeNotificationEvent, + transformer: sequential(), ); on( _onAppEvent, + transformer: sequential(), ); } Future _onOpeningNotificationStateEvent( - OpeningNotificationStateEvent event, - Emitter emit) async { + OpeningNotificationStateEvent event, + Emitter emit, + ) async { await storageService.saveBool( - Const.notificationOpeningEnabledKey, event.enabled); + Const.notificationOpeningEnabledKey, + event.enabled, + ); HapticFeedback.lightImpact(); emit( - state.copyWith(openingNotificationEnabled: event.enabled), + state.copyWith( + openingNotificationEnabled: event.enabled, + ), ); } Future _onClosingNotificationStateEvent( - ClosingNotificationStateEvent event, - Emitter emit) async { + ClosingNotificationStateEvent event, + Emitter emit, + ) async { await storageService.saveBool( - Const.notificationClosingEnabledKey, event.enabled); + Const.notificationClosingEnabledKey, + event.enabled, + ); + HapticFeedback.lightImpact(); + final enabled = await notificationService.areNotificationsEnabled(); emit( - state.copyWith(closingNotificationEnabled: event.enabled), + state.copyWith( + closingNotificationEnabled: event.enabled, + notificationEnabled: enabled, + ), ); } Future _onDayNotificationStateEvent( - DayNotificationStateEvent event, Emitter emit) async { + DayNotificationStateEvent event, + Emitter emit, + ) async { await storageService.saveBool( - Const.notificationDayEnabledKey, event.enabled); + Const.notificationDayEnabledKey, + event.enabled, + ); HapticFeedback.lightImpact(); + final enabled = await notificationService.areNotificationsEnabled(); emit( - state.copyWith(dayNotificationEnabled: event.enabled), + state.copyWith( + dayNotificationEnabled: event.enabled, + notificationEnabled: enabled, + ), ); } Future _onDayNotificationValueEvent( - DayNotificationValueEvent event, Emitter emit) async { - await storageService.saveDay(Const.notificationDayValueKey, event.day); + DayNotificationValueEvent event, + Emitter emit, + ) async { + await storageService.saveDay( + Const.notificationDayValueKey, + event.day, + ); HapticFeedback.lightImpact(); emit( - state.copyWith(dayNotificationValue: event.day), + state.copyWith( + dayNotificationValue: event.day, + ), ); } Future _onDayNotificationTimeValueEvent( - DayNotificationTimeValueEvent event, - Emitter emit) async { + DayNotificationTimeValueEvent event, + Emitter emit, + ) async { await storageService.saveTimeOfDay( - Const.notificationDayTimeValueKey, event.time); + Const.notificationDayTimeValueKey, + event.time, + ); HapticFeedback.lightImpact(); emit( - state.copyWith(dayNotificationTimeValue: event.time), + state.copyWith( + dayNotificationTimeValue: event.time, + ), ); } Future _onTimeNotificationStateEvent( - TimeNotificationStateEvent event, Emitter emit) async { + TimeNotificationStateEvent event, + Emitter emit, + ) async { await storageService.saveBool( - Const.notificationTimeEnabledKey, event.enabled); + Const.notificationTimeEnabledKey, + event.enabled, + ); HapticFeedback.lightImpact(); + final enabled = await notificationService.areNotificationsEnabled(); emit( - state.copyWith(timeNotificationEnabled: event.enabled), + state.copyWith( + timeNotificationEnabled: event.enabled, + notificationEnabled: enabled, + ), ); } Future _onTimeNotificationValueEvent( - TimeNotificationValueEvent event, Emitter emit) async { + TimeNotificationValueEvent event, + Emitter emit, + ) async { await storageService.saveTimeOfDay( - Const.notificationTimeValueKey, event.time); + Const.notificationTimeValueKey, + event.time, + ); emit( - state.copyWith(timeNotificationValue: event.time), + state.copyWith( + timeNotificationValue: event.time, + ), ); } Future _onDurationNotificationStateEvent( - DurationNotificationStateEvent event, - Emitter emit) async { + DurationNotificationStateEvent event, + Emitter emit, + ) async { await storageService.saveBool( - Const.notificationDurationEnabledKey, event.enabled); + Const.notificationDurationEnabledKey, + event.enabled, + ); HapticFeedback.lightImpact(); + final enabled = await notificationService.areNotificationsEnabled(); emit( - state.copyWith(durationNotificationEnabled: event.enabled), + state.copyWith( + durationNotificationEnabled: event.enabled, + notificationEnabled: enabled, + ), ); } Future _onDurationNotificationValueEvent( - DurationNotificationValueEvent event, - Emitter emit) async { + DurationNotificationValueEvent event, + Emitter emit, + ) async { await storageService.saveDuration( - Const.notificationDurationValueKey, event.duration); + Const.notificationDurationValueKey, + event.duration, + ); emit( state.copyWith(durationNotificationValue: event.duration), ); } - Future _onAppEvent( - AppEvent event, Emitter emit) async { + Future _onEnabledTimeSlotEvent( + EnabledTimeSlotEvent event, + Emitter emit, + ) async { + await storageService.saveBool( + Const.notificationFavoriteSlotsEnabledKey, + event.enabled, + ); + HapticFeedback.lightImpact(); + + emit(state.copyWith( + timeSlotsEnabledForNotifications: event.enabled, + )); + } + + Future _onTimeSlotsEventValue( + ValueTimeSlotEvent event, + Emitter emit, + ) async { + final timeSlots = List.from(state.timeSlotsValue); + timeSlots[event.index] = event.timeSlot; + await storageService.saveTimeSlots( + Const.notificationFavoriteSlotsValueKey, + timeSlots, + ); + HapticFeedback.lightImpact(); + + emit( + state.copyWith( + timeSlotsValue: timeSlots, + ), + ); + } + + Future _onComputeNotificationEvent( + ComputeNotificationEvent event, + Emitter emit, + ) async { + await notificationService.computeNotifications( + event.forecasts, + state, + event.context, + ); + } + + void _onAppEvent( + AppEvent event, + Emitter emit, + ) async { final durationNotificationEnabled = storageService.readBool(Const.notificationDurationEnabledKey) ?? Const.notificationDurationEnabledDefaultValue; @@ -193,17 +332,31 @@ class NotificationBloc extends Bloc { storageService.readBool(Const.notificationClosingEnabledKey) ?? Const.notificationClosingEnabledDefaultValue; + final timeSlots = + storageService.readTimeSlots(Const.notificationFavoriteSlotsValueKey) ?? + Const.notificationFavoriteSlotsDefaultValue; + + final enabledForNotifications = + storageService.readBool(Const.notificationFavoriteSlotsEnabledKey) ?? + Const.notificationFavoriteSlotsEnabledDefaultValue; + + final enabled = await notificationService.areNotificationsEnabled(); + emit( state.copyWith( - durationNotificationEnabled: durationNotificationEnabled, - durationNotificationValue: durationNotificationValue, - timeNotificationEnabled: timeNotificationEnabled, - timeNotificationValue: timeNotificationValue, - dayNotificationEnabled: dayNotificationEnabled, - dayNotificationValue: dayNotificationValue, - dayNotificationTimeValue: dayNotificationTimeValue, - openingNotificationEnabled: openingNotificationEnabled, - closingNotificationEnabled: closingNotificationEnabled), + durationNotificationEnabled: durationNotificationEnabled, + durationNotificationValue: durationNotificationValue, + timeNotificationEnabled: timeNotificationEnabled, + timeNotificationValue: timeNotificationValue, + dayNotificationEnabled: dayNotificationEnabled, + dayNotificationValue: dayNotificationValue, + dayNotificationTimeValue: dayNotificationTimeValue, + openingNotificationEnabled: openingNotificationEnabled, + closingNotificationEnabled: closingNotificationEnabled, + timeSlotsValue: timeSlots, + timeSlotsEnabledForNotifications: enabledForNotifications, + notificationEnabled: enabled, + ), ); } } diff --git a/lib/bloc/notification/notification_event.dart b/lib/bloc/notification/notification_event.dart index 6952ab3a..706b5458 100644 --- a/lib/bloc/notification/notification_event.dart +++ b/lib/bloc/notification/notification_event.dart @@ -56,6 +56,27 @@ class TimeNotificationValueEvent extends NotificationEvent { TimeNotificationValueEvent({required this.time}) : super(); } +class EnabledTimeSlotEvent extends NotificationEvent { + final bool enabled; + + EnabledTimeSlotEvent({required this.enabled}) : super(); +} + +class ValueTimeSlotEvent extends NotificationEvent { + final TimeSlot timeSlot; + final int index; + + ValueTimeSlotEvent({required this.timeSlot, required this.index}) : super(); +} + +class ComputeNotificationEvent extends NotificationEvent { + final List forecasts; + final BuildContext context; + + ComputeNotificationEvent({required this.forecasts, required this.context}) + : super(); +} + class AppEvent extends NotificationEvent { AppEvent() : super(); } diff --git a/lib/bloc/notification/notification_state.dart b/lib/bloc/notification/notification_state.dart index 3ff54583..47269678 100644 --- a/lib/bloc/notification/notification_state.dart +++ b/lib/bloc/notification/notification_state.dart @@ -1,6 +1,6 @@ part of 'notification_bloc.dart'; -class NotificationSate { +class NotificationState { final bool durationNotificationEnabled; final Duration durationNotificationValue; final bool timeNotificationEnabled; @@ -10,45 +10,61 @@ class NotificationSate { final TimeOfDay dayNotificationTimeValue; final bool openingNotificationEnabled; final bool closingNotificationEnabled; + final bool timeSlotsEnabledForNotifications; + final List timeSlotsValue; + final bool notificationEnabled; - NotificationSate( - {required this.durationNotificationEnabled, - required this.durationNotificationValue, - required this.timeNotificationEnabled, - required this.timeNotificationValue, - required this.dayNotificationEnabled, - required this.dayNotificationValue, - required this.dayNotificationTimeValue, - required this.openingNotificationEnabled, - required this.closingNotificationEnabled}); + NotificationState({ + required this.notificationEnabled, + required this.durationNotificationEnabled, + required this.durationNotificationValue, + required this.timeNotificationEnabled, + required this.timeNotificationValue, + required this.dayNotificationEnabled, + required this.dayNotificationValue, + required this.dayNotificationTimeValue, + required this.openingNotificationEnabled, + required this.closingNotificationEnabled, + required this.timeSlotsEnabledForNotifications, + required this.timeSlotsValue, + }); - NotificationSate copyWith( - {bool? durationNotificationEnabled, - Duration? durationNotificationValue, - bool? timeNotificationEnabled, - TimeOfDay? timeNotificationValue, - bool? dayNotificationEnabled, - Day? dayNotificationValue, - TimeOfDay? dayNotificationTimeValue, - bool? openingNotificationEnabled, - bool? closingNotificationEnabled}) { - return NotificationSate( - durationNotificationEnabled: - durationNotificationEnabled ?? this.durationNotificationEnabled, - durationNotificationValue: - durationNotificationValue ?? this.durationNotificationValue, - timeNotificationEnabled: - timeNotificationEnabled ?? this.timeNotificationEnabled, - timeNotificationValue: - timeNotificationValue ?? this.timeNotificationValue, - dayNotificationEnabled: - dayNotificationEnabled ?? this.dayNotificationEnabled, - dayNotificationValue: dayNotificationValue ?? this.dayNotificationValue, - dayNotificationTimeValue: - dayNotificationTimeValue ?? this.dayNotificationTimeValue, - openingNotificationEnabled: - openingNotificationEnabled ?? this.openingNotificationEnabled, - closingNotificationEnabled: - closingNotificationEnabled ?? this.closingNotificationEnabled); + NotificationState copyWith({ + bool? durationNotificationEnabled, + Duration? durationNotificationValue, + bool? timeNotificationEnabled, + TimeOfDay? timeNotificationValue, + bool? dayNotificationEnabled, + Day? dayNotificationValue, + TimeOfDay? dayNotificationTimeValue, + bool? openingNotificationEnabled, + bool? closingNotificationEnabled, + bool? timeSlotsEnabledForNotifications, + List? timeSlotsValue, + bool? notificationEnabled, + }) { + return NotificationState( + durationNotificationEnabled: + durationNotificationEnabled ?? this.durationNotificationEnabled, + durationNotificationValue: + durationNotificationValue ?? this.durationNotificationValue, + timeNotificationEnabled: + timeNotificationEnabled ?? this.timeNotificationEnabled, + timeNotificationValue: + timeNotificationValue ?? this.timeNotificationValue, + dayNotificationEnabled: + dayNotificationEnabled ?? this.dayNotificationEnabled, + dayNotificationValue: dayNotificationValue ?? this.dayNotificationValue, + dayNotificationTimeValue: + dayNotificationTimeValue ?? this.dayNotificationTimeValue, + openingNotificationEnabled: + openingNotificationEnabled ?? this.openingNotificationEnabled, + closingNotificationEnabled: + closingNotificationEnabled ?? this.closingNotificationEnabled, + timeSlotsEnabledForNotifications: timeSlotsEnabledForNotifications ?? + this.timeSlotsEnabledForNotifications, + timeSlotsValue: timeSlotsValue ?? this.timeSlotsValue, + notificationEnabled: notificationEnabled ?? this.notificationEnabled, + ); } } diff --git a/lib/bloc/scroll_status/scroll_status_bloc.dart b/lib/bloc/scroll_status/scroll_status_bloc.dart index 035a871b..f56c9c17 100644 --- a/lib/bloc/scroll_status/scroll_status_bloc.dart +++ b/lib/bloc/scroll_status/scroll_status_bloc.dart @@ -1,6 +1,6 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:chabo/bloc/chabo_event.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; +import 'package:chabo/models/abstract_forecast.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,9 +12,10 @@ class ScrollStatusBloc extends Bloc { ScrollStatusBloc({required this.scrollController}) : super(ScrollStatusState( - showCurrentStatus: true, - status: ScrollStatus.ok, - currentTarget: null)) { + showCurrentStatus: true, + status: ScrollStatus.ok, + currentTarget: null, + )) { on( _onScrollChanged, transformer: droppable(), @@ -25,13 +26,16 @@ class ScrollStatusBloc extends Bloc { ); } - Future _onScrollChanged( - ScrollStatusChanged event, Emitter emit) async { + void _onScrollChanged( + ScrollStatusChanged event, + Emitter emit, + ) { emit( state.copyWith( - showCurrentStatus: true, - status: ScrollStatus.ok, - currentTarget: state.currentTarget), + showCurrentStatus: true, + status: ScrollStatus.ok, + currentTarget: state.currentTarget, + ), ); } @@ -44,8 +48,11 @@ class ScrollStatusBloc extends Bloc { ? scrollController.position.pixels - offset : scrollController.position.pixels + offset; - await scrollController.animateTo(pixel, - duration: const Duration(milliseconds: 300), curve: Curves.linear); + await scrollController.animateTo( + pixel, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); targetContext = GlobalObjectKey(event.goTo.hashCode).currentContext; offset += 100; emit( @@ -55,13 +62,18 @@ class ScrollStatusBloc extends Bloc { ); } - Scrollable.ensureVisible(targetContext, - duration: const Duration(seconds: 1), curve: Curves.fastOutSlowIn); + // ignore: use_build_context_synchronously + Scrollable.ensureVisible( + targetContext, + duration: const Duration(seconds: 1), + curve: Curves.fastOutSlowIn, + ); emit( state.copyWith( - showCurrentStatus: false, - status: ScrollStatus.ok, - currentTarget: event.goTo), + showCurrentStatus: false, + status: ScrollStatus.ok, + currentTarget: event.goTo, + ), ); } } diff --git a/lib/bloc/scroll_status/scroll_status_event.dart b/lib/bloc/scroll_status/scroll_status_event.dart index fa6789c2..e6fe331c 100644 --- a/lib/bloc/scroll_status/scroll_status_event.dart +++ b/lib/bloc/scroll_status/scroll_status_event.dart @@ -7,7 +7,7 @@ class ScrollStatusChanged extends ScrollStatusEvent { } class GoTo extends ScrollStatusEvent { - final AbstractChabanBridgeForecast? goTo; + final AbstractForecast? goTo; GoTo({this.goTo}) : super(); } diff --git a/lib/bloc/scroll_status/scroll_status_state.dart b/lib/bloc/scroll_status/scroll_status_state.dart index 9cdc1c2e..df7344ab 100644 --- a/lib/bloc/scroll_status/scroll_status_state.dart +++ b/lib/bloc/scroll_status/scroll_status_state.dart @@ -1,24 +1,27 @@ part of 'scroll_status_bloc.dart'; -enum ScrollStatus { ok, error } - class ScrollStatusState { - final AbstractChabanBridgeForecast? currentTarget; + final AbstractForecast? currentTarget; final bool showCurrentStatus; final ScrollStatus status; - ScrollStatusState( - {required this.status, - required this.showCurrentStatus, - required this.currentTarget}); + ScrollStatusState({ + required this.status, + required this.showCurrentStatus, + required this.currentTarget, + }); - ScrollStatusState copyWith( - {bool? showCurrentStatus, - ScrollStatus? status, - AbstractChabanBridgeForecast? currentTarget}) { + ScrollStatusState copyWith({ + bool? showCurrentStatus, + ScrollStatus? status, + AbstractForecast? currentTarget, + }) { return ScrollStatusState( - status: status ?? this.status, - showCurrentStatus: showCurrentStatus ?? this.showCurrentStatus, - currentTarget: currentTarget ?? this.currentTarget); + status: status ?? this.status, + showCurrentStatus: showCurrentStatus ?? this.showCurrentStatus, + currentTarget: currentTarget ?? this.currentTarget, + ); } } + +enum ScrollStatus { ok, error } diff --git a/lib/bloc/simple_bloc_observer.dart b/lib/bloc/simple_bloc_observer.dart deleted file mode 100644 index fe8198a1..00000000 --- a/lib/bloc/simple_bloc_observer.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:developer' as developer; - -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SimpleBlocObserver extends BlocObserver { - @override - void onTransition(Bloc bloc, Transition transition) { - super.onTransition(bloc, transition); - developer.log(transition.toString(), name: 'bloc.on.transition'); - } - - @override - void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - super.onError(bloc, error, stackTrace); - developer.log(stackTrace.toString(), name: 'bloc.on.error'); - } -} diff --git a/lib/bloc/status/status_bloc.dart b/lib/bloc/status/status_bloc.dart new file mode 100644 index 00000000..99111b4b --- /dev/null +++ b/lib/bloc/status/status_bloc.dart @@ -0,0 +1,195 @@ +import 'package:chabo/bloc/chabo_event.dart'; +import 'package:chabo/const.dart'; +import 'package:chabo/extensions/color_scheme_extension.dart'; +import 'package:chabo/extensions/string_extension.dart'; +import 'package:chabo/models/abstract_forecast.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +part 'status_event.dart'; +part 'status_state.dart'; + +class StatusBloc extends Bloc { + StatusBloc() : super(const StatusStateInitial()) { + on( + _onStatusChanged, + ); + on( + _onRefresh, + ); + on( + _onDurationChanged, + ); + } + + void _onDurationChanged( + StatusDurationChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + durationForCloseClosing: event.duration, + ), + ); + } + + void _onStatusChanged( + StatusChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + currentForecast: event.currentForecast, + previousForecast: event.previousForecast, + ), + ); + } + + void _onRefresh( + StatusRefresh event, + Emitter emit, + ) { + final Duration? durationUntilNextEvent = _getDurationUntilNextEvent(); + final Duration? durationBetweenPreviousAndNextEvent = + _getDurationBetweenPreviousAndNextEvent(); + final double completionPercentage = _getDiffPercentage( + durationBetweenPreviousAndNextEvent, + durationUntilNextEvent, + ); + final String mainMessageStatus = _getMainStatus(event.context); + final String timeMessagePrefix = _getTimeMessagePrefix(event.context); + final Color foregroundColor = _getForegroundColor(event.context); + final Color backgroundColor = _getBackgroundColor(event.context); + + emit( + state.copyWith( + durationUntilNextEvent: durationUntilNextEvent, + durationBetweenPreviousAndNextEvent: + durationBetweenPreviousAndNextEvent, + completionPercentage: completionPercentage, + mainMessageStatus: mainMessageStatus, + timeMessagePrefix: timeMessagePrefix, + foregroundColor: foregroundColor, + statusLifecycle: state.durationUntilNextEvent != + Duration.zero // Prevents from displaying the wrong status color + ? StatusLifecycle.populated + : StatusLifecycle.empty, + backgroundColor: backgroundColor, + ), + ); + } + + Color _getBackgroundColor(BuildContext context) { + final currentForecast = state.currentForecast; + if (currentForecast != null) { + final isOpen = !currentForecast.isCurrentlyClosed(); + if (isOpen && + state.durationUntilNextEvent.inMinutes < + state.durationForCloseClosing.inMinutes) { + return Theme.of(context).colorScheme.warningColor; + } else if (isOpen) { + return Colors.green; + } else { + return Theme.of(context).colorScheme.error; + } + } else { + return state.backgroundColor; + } + } + + Color _getForegroundColor(BuildContext context) { + final currentForecast = state.currentForecast; + if (currentForecast != null) { + final isOpen = !currentForecast.isCurrentlyClosed(); + final colorScheme = Theme.of(context).colorScheme; + + return isOpen || + state.durationUntilNextEvent.inMinutes < + state.durationForCloseClosing.inMinutes + ? colorScheme.background + : colorScheme.onError; + } else { + return state.foregroundColor; + } + } + + String _getTimeMessagePrefix(BuildContext context) { + final currentForecast = state.currentForecast; + if (currentForecast != null) { + return currentForecast.isCurrentlyClosed() + ? '${AppLocalizations.of(context)!.scheduledToOpen.capitalize()} ' + : '${AppLocalizations.of(context)!.nextClosingScheduled.capitalize()} '; + } else { + return 'NO_TIME'; + } + } + + String _getMainStatus(BuildContext context) { + final currentForecast = state.currentForecast; + if (currentForecast != null && + !currentForecast.isCurrentlyClosed() && + state.durationUntilNextEvent.inMinutes >= + state.durationForCloseClosing.inMinutes) { + return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.open}'; + } else if (currentForecast != null && + !currentForecast.isCurrentlyClosed() && + state.durationUntilNextEvent.inMinutes < + state.durationForCloseClosing.inMinutes) { + return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.aboutToClose}'; + } else { + return '${_getGreetings(context)}, ${AppLocalizations.of(context)!.theBridgeIsCurrently} ${AppLocalizations.of(context)!.closed}'; + } + } + + Duration? _getDurationUntilNextEvent() { + final currentForecast = state.currentForecast; + final DateTime now = DateTime.now(); + if (currentForecast != null) { + return currentForecast.isCurrentlyClosed() + ? currentForecast.circulationReOpeningDate.difference(now) + : currentForecast.circulationClosingDate.difference(now); + } else { + return null; + } + } + + Duration? _getDurationBetweenPreviousAndNextEvent() { + final currentForecast = state.currentForecast; + final previousForecast = state.previousForecast; + if (currentForecast != null && previousForecast != null) { + return currentForecast.isCurrentlyClosed() + ? currentForecast.closedDuration + : currentForecast.circulationClosingDate.difference( + previousForecast.circulationReOpeningDate, + ); + } else { + return null; + } + } + + double _getDiffPercentage( + Duration? durationBetweenPreviousAndNextEvent, + Duration? durationUntilNextEvent, + ) { + return durationBetweenPreviousAndNextEvent != null && + durationUntilNextEvent != null + ? 1 - + (durationUntilNextEvent.inSeconds / + durationBetweenPreviousAndNextEvent.inSeconds) + : -1; + } + + String _getGreetings(BuildContext context) { + int hours = int.parse(DateFormat('HH').format(DateTime.now())); + if (hours >= 6 && hours <= 12) { + return AppLocalizations.of(context)!.goodMorning.capitalize(); + } else if (hours > 12 && hours <= 18) { + return AppLocalizations.of(context)!.goodAfternoon.capitalize(); + } else { + return AppLocalizations.of(context)!.goodEvening.capitalize(); + } + } +} diff --git a/lib/bloc/status/status_event.dart b/lib/bloc/status/status_event.dart new file mode 100644 index 00000000..fbb1f278 --- /dev/null +++ b/lib/bloc/status/status_event.dart @@ -0,0 +1,29 @@ +part of 'status_bloc.dart'; + +class StatusEvent extends ChaboEvent {} + +class StatusChanged extends StatusEvent { + final AbstractForecast? currentForecast; + final AbstractForecast? previousForecast; + + StatusChanged({ + required this.currentForecast, + required this.previousForecast, + }) : super(); +} + +class StatusRefresh extends StatusEvent { + final BuildContext context; + + StatusRefresh({ + required this.context, + }) : super(); +} + +class StatusDurationChanged extends StatusEvent { + final Duration duration; + + StatusDurationChanged({ + required this.duration, + }) : super(); +} diff --git a/lib/bloc/status/status_state.dart b/lib/bloc/status/status_state.dart new file mode 100644 index 00000000..318f6b35 --- /dev/null +++ b/lib/bloc/status/status_state.dart @@ -0,0 +1,95 @@ +part of 'status_bloc.dart'; + +class StatusState extends Equatable { + final StatusLifecycle statusLifecycle; + final AbstractForecast? currentForecast; + final AbstractForecast? previousForecast; + final Duration durationUntilNextEvent; + final Duration durationForCloseClosing; + final Duration? durationBetweenPreviousAndNextEvent; + final double completionPercentage; + final String mainMessageStatus; + final String timeMessagePrefix; + final Color foregroundColor; + final Color backgroundColor; + + const StatusState({ + required this.statusLifecycle, + required this.currentForecast, + required this.previousForecast, + required this.durationUntilNextEvent, + required this.durationForCloseClosing, + required this.durationBetweenPreviousAndNextEvent, + required this.completionPercentage, + required this.mainMessageStatus, + required this.timeMessagePrefix, + required this.foregroundColor, + required this.backgroundColor, + }); + + StatusState copyWith({ + StatusLifecycle? statusLifecycle, + AbstractForecast? currentForecast, + AbstractForecast? previousForecast, + Duration? durationUntilNextEvent, + Duration? durationForCloseClosing, + Duration? durationBetweenPreviousAndNextEvent, + double? completionPercentage, + String? mainMessageStatus, + String? timeMessagePrefix, + Color? foregroundColor, + Color? backgroundColor, + }) { + return StatusState( + statusLifecycle: statusLifecycle ?? this.statusLifecycle, + currentForecast: currentForecast ?? this.currentForecast, + previousForecast: previousForecast ?? this.previousForecast, + durationUntilNextEvent: + durationUntilNextEvent ?? this.durationUntilNextEvent, + durationForCloseClosing: + durationForCloseClosing ?? this.durationForCloseClosing, + durationBetweenPreviousAndNextEvent: + durationBetweenPreviousAndNextEvent ?? + this.durationBetweenPreviousAndNextEvent, + completionPercentage: completionPercentage ?? this.completionPercentage, + mainMessageStatus: mainMessageStatus ?? this.mainMessageStatus, + timeMessagePrefix: timeMessagePrefix ?? this.timeMessagePrefix, + foregroundColor: foregroundColor ?? this.foregroundColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + } + + @override + List get props => [ + statusLifecycle, + currentForecast, + previousForecast, + durationUntilNextEvent, + durationForCloseClosing, + durationBetweenPreviousAndNextEvent, + completionPercentage, + mainMessageStatus, + timeMessagePrefix, + foregroundColor, + backgroundColor, + ]; +} + +class StatusStateInitial extends StatusState { + const StatusStateInitial() + : super( + previousForecast: null, + currentForecast: null, + durationUntilNextEvent: Duration.zero, + durationBetweenPreviousAndNextEvent: null, + durationForCloseClosing: Const.notificationDurationValueDefaultValue, + statusLifecycle: StatusLifecycle.empty, + completionPercentage: 0, + mainMessageStatus: '', + timeMessagePrefix: '', + foregroundColor: Colors.white, + backgroundColor: Colors.white, + ); +} + +enum StatusLifecycle { empty, populated } diff --git a/lib/bloc/theme/theme_bloc.dart b/lib/bloc/theme/theme_bloc.dart index 525d2667..6845096d 100644 --- a/lib/bloc/theme/theme_bloc.dart +++ b/lib/bloc/theme/theme_bloc.dart @@ -14,7 +14,7 @@ class ThemeBloc extends Bloc { final StorageService storageService; ThemeBloc({required this.storageService}) - : super(ThemeState(themeData: AppThemes.lightTheme)) { + : super(ThemeState(themeData: AppTheme.lightTheme)) { on( _onThemeChanged, ); @@ -27,11 +27,14 @@ class ThemeBloc extends Bloc { var brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; bool isDarkMode = brightness == Brightness.dark; - return isDarkMode ? AppThemes.darkTheme : AppThemes.lightTheme; + + return isDarkMode ? AppTheme.darkTheme : AppTheme.lightTheme; } - Future _onAppStateChanged( - AppStateChanged event, Emitter emit) async { + void _onAppStateChanged( + AppStateChanged event, + Emitter emit, + ) { var savedStatus = storageService.readTheme(Const.storageThemeKey); if (savedStatus == null) { emit( @@ -45,14 +48,14 @@ class ThemeBloc extends Bloc { emit( state.copyWith( status: ThemeStateStatus.light, - themeData: AppThemes.lightTheme, + themeData: AppTheme.lightTheme, ), ); } else if (savedStatus == ThemeStateStatus.dark) { emit( state.copyWith( status: ThemeStateStatus.dark, - themeData: AppThemes.darkTheme, + themeData: AppTheme.darkTheme, ), ); } else if (savedStatus == ThemeStateStatus.system) { @@ -67,7 +70,9 @@ class ThemeBloc extends Bloc { } Future _onThemeChanged( - ThemeChanged event, Emitter emit) async { + ThemeChanged event, + Emitter emit, + ) async { await storageService.saveTheme( Const.storageThemeKey, event.status, @@ -76,14 +81,14 @@ class ThemeBloc extends Bloc { emit( state.copyWith( status: ThemeStateStatus.light, - themeData: AppThemes.lightTheme, + themeData: AppTheme.lightTheme, ), ); } else if (event.status == ThemeStateStatus.dark) { emit( state.copyWith( status: ThemeStateStatus.dark, - themeData: AppThemes.darkTheme, + themeData: AppTheme.darkTheme, ), ); } else if (event.status == ThemeStateStatus.system) { diff --git a/lib/bloc/theme/theme_state.dart b/lib/bloc/theme/theme_state.dart index 00bce4e4..c5442111 100644 --- a/lib/bloc/theme/theme_state.dart +++ b/lib/bloc/theme/theme_state.dart @@ -8,14 +8,14 @@ class ThemeState { ThemeState copyWith({ThemeStateStatus? status, ThemeData? themeData}) { return ThemeState( - status: status ?? this.status, themeData: themeData ?? this.themeData); + status: status ?? this.status, + themeData: themeData ?? this.themeData, + ); } IconData getIconData() { - if (themeData == AppThemes.lightTheme) { - return Icons.brightness_low; - } else { - return Icons.dark_mode_outlined; - } + return themeData == AppTheme.lightTheme + ? Icons.brightness_low + : Icons.dark_mode_outlined; } } diff --git a/lib/chabo.dart b/lib/chabo.dart index 4f568409..a09ebe5d 100644 --- a/lib/chabo.dart +++ b/lib/chabo.dart @@ -1,11 +1,10 @@ -import 'package:chabo/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart'; -import 'package:chabo/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart'; -import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:chabo/bloc/forecast/forecast_bloc.dart'; import 'package:chabo/bloc/notification/notification_bloc.dart'; -import 'package:chabo/cubits/notification_service_cubit.dart'; import 'package:chabo/bloc/scroll_status/scroll_status_bloc.dart'; +import 'package:chabo/bloc/status/status_bloc.dart'; import 'package:chabo/bloc/theme/theme_bloc.dart'; -import 'package:chabo/screens/chaban_bridge_forecast_screen.dart'; +import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:chabo/screens/forecast_screen.dart'; import 'package:chabo/service/notification_service.dart'; import 'package:chabo/service/storage_service.dart'; import 'package:flutter/material.dart'; @@ -37,13 +36,6 @@ class Chabo extends StatelessWidget { ), ), - /// Bloc intended to manage the Notifications service - BlocProvider( - create: (_) => NotificationServiceCubit( - notificationService, - ), - ), - /// Bloc intended to manage the FloatingActions BlocProvider( create: (_) => FloatingActionsCubit( @@ -54,16 +46,16 @@ class Chabo extends StatelessWidget { /// Bloc intended to manage the forecast displayed BlocProvider( - create: (_) => ChabanBridgeForecastBloc( + create: (_) => ForecastBloc( httpClient: http.Client(), )..add( - ChabanBridgeForecastFetched(), + ForecastFetched(), ), ), /// Bloc intended to manage the status BlocProvider( - create: (_) => ChabanBridgeStatusBloc(), + create: (_) => StatusBloc(), ), /// Bloc intended to manage scroll to status to display (or not) the current status @@ -77,6 +69,7 @@ class Chabo extends StatelessWidget { BlocProvider( create: (_) => NotificationBloc( storageService: storageService, + notificationService: notificationService, )..add( AppEvent(), ), @@ -87,7 +80,7 @@ class Chabo extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: state.themeData, - home: const ChabanBridgeForecastScreen(), + home: const ForecastScreen(), localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, @@ -105,6 +98,7 @@ class Chabo extends StatelessWidget { return deviceLocale; } } + return const Locale('en', ''); }, ); diff --git a/lib/const.dart b/lib/const.dart index bb5a34c8..91cf4b1b 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -1,5 +1,6 @@ import 'package:chabo/models/enums/day.dart'; -import 'package:chabo/models/link_icon.dart'; +import 'package:chabo/models/time_slot.dart'; +import 'package:chabo/models/web_link_icon.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -9,7 +10,7 @@ class Const { static String legalLease = '© ${DateTime.now().year} - Valentin REVERSAT'; /// List - static const int chabanBridgeForecastLimit = 1000; + static const int forecastLimit = 1000; /// Paths static const String changelogPlaceholder = ':lang:'; @@ -24,16 +25,31 @@ class Const { static const String privacyInfoLink = 'https://chabo.vareversat.fr/privacy'; static List usefulLinks = [ - WebLinkIcon('https://www.instagram.com/_yuhliet_/', - FontAwesomeIcons.instagram, 'yuhliet_instagram'), - WebLinkIcon('https://bordeaux-metropole.fr/', Icons.location_city_rounded, - 'city_of_bordeaux'), - WebLinkIcon('https://opendata.bordeaux-metropole.fr/', - Icons.data_thresholding_rounded, 'bordeaux_open_data'), - WebLinkIcon('https://github.com/vareversat/chabo', FontAwesomeIcons.github, - 'source_code'), - WebLinkIcon('https://chabo.vareversat.fr/privacy', - Icons.privacy_tip_rounded, 'privacy_policy'), + WebLinkIcon( + 'https://www.instagram.com/_yuhliet_/', + FontAwesomeIcons.instagram, + 'yuhliet_instagram', + ), + WebLinkIcon( + 'https://bordeaux-metropole.fr/', + Icons.location_city_rounded, + 'city_of_bordeaux', + ), + WebLinkIcon( + 'https://opendata.bordeaux-metropole.fr/', + Icons.data_thresholding_rounded, + 'bordeaux_open_data', + ), + WebLinkIcon( + 'https://github.com/vareversat/chabo', + FontAwesomeIcons.github, + 'source_code', + ), + WebLinkIcon( + 'https://chabo.vareversat.fr/privacy', + Icons.privacy_tip_rounded, + 'privacy_policy', + ), ]; /// Local storage @@ -57,13 +73,17 @@ class Const { static const String notificationClosingEnabledKey = 'NOTIFICATION_CLOSING_SETTINGS_ENABLED'; static const String isRightHandedKey = 'RIGHT_HANDED'; + static const String notificationFavoriteSlotsEnabledKey = + 'NOTIFICATION_FAVORITE_SLOTS_SETTINGS_ENABLED'; + static const String notificationFavoriteSlotsValueKey = + 'NOTIFICATION_FAVORITE_SLOTS_SETTINGS_VALUE'; /// Notifications static const String androidAppLogoPath = '@mipmap/ic_slice_launcher_adaptive_fore'; static const Duration notificationDurationValueDefaultValue = Duration(minutes: 60); - static const bool notificationDurationEnabledDefaultValue = true; + static const bool notificationDurationEnabledDefaultValue = false; static TimeOfDay notificationTimeValueDefaultValue = const TimeOfDay(hour: 6, minute: 0); static const bool notificationTimeEnabledDefaultValue = false; @@ -73,6 +93,25 @@ class Const { static const bool notificationDayEnabledDefaultValue = false; static const bool notificationOpeningEnabledDefaultValue = false; static const bool notificationClosingEnabledDefaultValue = false; + static const bool notificationFavoriteSlotsEnabledDefaultValue = false; + static List notificationFavoriteSlotsDefaultValue = [ + const TimeSlot( + name: '', + from: TimeOfDay(hour: 7, minute: 0), + to: TimeOfDay( + hour: 9, + minute: 30, + ), + ), + const TimeSlot( + name: '', + from: TimeOfDay(hour: 17, minute: 0), + to: TimeOfDay( + hour: 19, + minute: 30, + ), + ), + ]; /// UI static const bool isRightHandedDefaultValue = true; diff --git a/lib/cubits/floating_actions_cubit.dart b/lib/cubits/floating_actions_cubit.dart index f3c24c73..711b78a5 100644 --- a/lib/cubits/floating_actions_cubit.dart +++ b/lib/cubits/floating_actions_cubit.dart @@ -25,7 +25,7 @@ class FloatingActionsCubit extends Cubit { ); } - void init() async { + void init() { final isRightHanded = storageService.readBool(Const.isRightHandedKey) ?? Const.isRightHandedDefaultValue; emit( @@ -40,13 +40,16 @@ class FloatingActionsState extends Equatable { final bool isMenuOpen; final bool isRightHanded; - const FloatingActionsState( - {required this.isMenuOpen, required this.isRightHanded}); + const FloatingActionsState({ + required this.isMenuOpen, + required this.isRightHanded, + }); FloatingActionsState copyWith({bool? isMenuOpen, bool? isRightHanded}) { return FloatingActionsState( - isMenuOpen: isMenuOpen ?? this.isMenuOpen, - isRightHanded: isRightHanded ?? this.isRightHanded); + isMenuOpen: isMenuOpen ?? this.isMenuOpen, + isRightHanded: isRightHanded ?? this.isRightHanded, + ); } @override diff --git a/lib/cubits/notification_service_cubit.dart b/lib/cubits/notification_service_cubit.dart deleted file mode 100644 index 43df0e87..00000000 --- a/lib/cubits/notification_service_cubit.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:chabo/service/notification_service.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class NotificationServiceCubit extends Cubit { - NotificationServiceCubit(super.initialState); -} diff --git a/lib/custom_properties.dart b/lib/custom_properties.dart index 8d7e1182..f4a70526 100644 --- a/lib/custom_properties.dart +++ b/lib/custom_properties.dart @@ -1,5 +1,5 @@ class CustomProperties { - static const double borderRadius = 12; + static const double borderRadius = 17; static const int animationDurationMs = 200; static const double blurSigmaX = 4; diff --git a/lib/custom_widgets_state.dart b/lib/custom_widget_state.dart similarity index 100% rename from lib/custom_widgets_state.dart rename to lib/custom_widget_state.dart diff --git a/lib/dialogs/chaban_bridge_forecast_information_dialog.dart b/lib/dialogs/chaban_bridge_forecast_information_dialog.dart deleted file mode 100644 index 21f75ea5..00000000 --- a/lib/dialogs/chaban_bridge_forecast_information_dialog.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ChabanBridgeForecastInformationDialog extends StatelessWidget { - final AbstractChabanBridgeForecast chabanBridgeForecast; - - const ChabanBridgeForecastInformationDialog( - {super.key, required this.chabanBridgeForecast}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - insetPadding: const EdgeInsets.symmetric( - horizontal: 20, - ), - titlePadding: const EdgeInsets.all(0), - contentPadding: const EdgeInsets.all(20), - actionsPadding: const EdgeInsets.fromLTRB( - 0, - 0, - 20, - 10, - ), - title: Container( - decoration: BoxDecoration( - color: chabanBridgeForecast.getColor(context, false), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular( - 15.0, - ), - topRight: Radius.circular( - 15.0, - ), - ), - ), - padding: const EdgeInsets.fromLTRB( - 20, - 20, - 0, - 15, - ), - child: Row( - children: [ - chabanBridgeForecast.getIconWidget(context, true), - const SizedBox(width: 20), - Text( - AppLocalizations.of(context)!.information, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).cardColor, - ), - ), - ], - ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 15, - ), - ), - content: chabanBridgeForecast.getInformationWidget(context), - ); - } -} diff --git a/lib/dialogs/chabo_about_dialog.dart b/lib/dialogs/chabo_about_dialog.dart index ac2fac02..a3b4c502 100644 --- a/lib/dialogs/chabo_about_dialog.dart +++ b/lib/dialogs/chabo_about_dialog.dart @@ -20,6 +20,9 @@ class ChaboAboutDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return FutureBuilder( builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -29,6 +32,7 @@ class ChaboAboutDialog extends StatelessWidget { snapshot.data == null) { return Text(AppLocalizations.of(context)!.unableAppInfo); } + return AlertDialog( insetPadding: const EdgeInsets.symmetric(horizontal: 20), titlePadding: const EdgeInsets.all(20), @@ -70,14 +74,14 @@ class ChaboAboutDialog extends StatelessWidget { ), ), Text( - ' | v${snapshot.data!.version} (${snapshot.data!.buildNumber})', - style: Theme.of(context).textTheme.bodyMedium), + ' | v${snapshot.data!.version} (${snapshot.data!.buildNumber})', + style: textTheme.bodyMedium, + ), ], ), Text( Const.legalLease, - style: - Theme.of(context).textTheme.bodySmall!.copyWith(), + style: textTheme.bodySmall!.copyWith(), ), ], ), @@ -94,7 +98,7 @@ class ChaboAboutDialog extends StatelessWidget { children: [ Text( AppLocalizations.of(context)!.appDescription, - style: Theme.of(context).textTheme.bodyLarge, + style: textTheme.bodyLarge, ), const SizedBox( height: 15, @@ -116,7 +120,7 @@ class ChaboAboutDialog extends StatelessWidget { children: Const.usefulLinks .map( (link) => ElevatedButton( - onPressed: () async => link.launchURL(), + onPressed: () => link.launchURL(), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -156,9 +160,11 @@ class ChaboAboutDialog extends StatelessWidget { ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondaryContainer), + colorScheme.secondaryContainer, + ), foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.onSecondaryContainer), + colorScheme.onSecondaryContainer, + ), ), onPressed: () => Navigator.push( context, @@ -190,9 +196,11 @@ class ChaboAboutDialog extends StatelessWidget { ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondaryContainer), + colorScheme.secondaryContainer, + ), foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.onSecondaryContainer), + colorScheme.onSecondaryContainer, + ), ), onPressed: () { showLicensePage( @@ -249,7 +257,8 @@ class ChaboAboutDialog extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.white), foregroundColor: MaterialStateProperty.all( - Theme.of(context).primaryColor), + Theme.of(context).primaryColor, + ), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -263,7 +272,7 @@ class ChaboAboutDialog extends StatelessWidget { label: Text( MaterialLocalizations.of(context).closeButtonLabel, ), - ) + ), ], scrollable: true, ); diff --git a/lib/dialogs/days_of_the_week_dialog.dart b/lib/dialogs/days_of_the_week_dialog.dart index 8298ccad..cf550f2d 100644 --- a/lib/dialogs/days_of_the_week_dialog.dart +++ b/lib/dialogs/days_of_the_week_dialog.dart @@ -5,10 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class DaysOfTheWeekDialog extends StatelessWidget { - final Day selectedDay; - - const DaysOfTheWeekDialog({Key? key, required this.selectedDay}) - : super(key: key); + const DaysOfTheWeekDialog({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -19,7 +16,7 @@ class DaysOfTheWeekDialog extends StatelessWidget { 15, ), ), - content: BlocBuilder( + content: BlocBuilder( builder: (context, state) { return Wrap( alignment: WrapAlignment.center, @@ -28,7 +25,7 @@ class DaysOfTheWeekDialog extends StatelessWidget { runSpacing: 10, children: [ ElevatedButton( - onPressed: () {}, + onPressed: () {}, // ignore: no-empty-block child: DropdownButtonHideUnderline( child: DropdownButton( borderRadius: BorderRadius.circular(12.0), @@ -66,8 +63,8 @@ class DaysOfTheWeekDialog extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), ElevatedButton( - onPressed: () async { - var time = await showTimePicker( + onPressed: () { + showTimePicker( initialEntryMode: TimePickerEntryMode.dialOnly, context: context, initialTime: state.dayNotificationTimeValue, @@ -77,15 +74,18 @@ class DaysOfTheWeekDialog extends StatelessWidget { child: child!, ); }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + DayNotificationTimeValueEvent( + time: value, + ), + ), + }, + }, ); - if (time != null) { - // ignore: use_build_context_synchronously - BlocProvider.of(context).add( - DayNotificationTimeValueEvent( - time: time, - ), - ); - } }, child: Text( state.dayNotificationTimeValue.format(context), diff --git a/lib/dialogs/forecast_information_dialog.dart b/lib/dialogs/forecast_information_dialog.dart new file mode 100644 index 00000000..982deb62 --- /dev/null +++ b/lib/dialogs/forecast_information_dialog.dart @@ -0,0 +1,106 @@ +import 'package:chabo/extensions/color_scheme_extension.dart'; +import 'package:chabo/models/abstract_forecast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ForecastInformationDialog extends StatelessWidget { + final AbstractForecast forecast; + + const ForecastInformationDialog({ + super.key, + required this.forecast, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + insetPadding: const EdgeInsets.symmetric( + horizontal: 20, + ), + titlePadding: const EdgeInsets.all(0), + contentPadding: const EdgeInsets.all(0), + actionsPadding: const EdgeInsets.fromLTRB( + 0, + 0, + 20, + 10, + ), + title: Container( + decoration: BoxDecoration( + color: forecast.getColor(context, false), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular( + 15.0, + ), + topRight: Radius.circular( + 15.0, + ), + ), + ), + padding: const EdgeInsets.fromLTRB( + 20, + 20, + 0, + 15, + ), + child: Row( + children: [ + forecast.getIconWidget(context, true), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.information, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).cardColor, + ), + ), + ], + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 15, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: forecast.getInformationWidget(context), + ), + if (forecast.interferingTimeSlots.isNotEmpty) + Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.warningColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular( + 15.0, + ), + bottomRight: Radius.circular( + 15.0, + ), + ), + ), + child: Row( + children: [ + const SizedBox(width: 10), + Flexible( + child: Text( + AppLocalizations.of(context)! + .favoriteSlotsInterferenceWarning, + overflow: TextOverflow.clip, + style: TextStyle( + color: Theme.of(context).cardColor, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/dialogs/theme_picker_dialog.dart b/lib/dialogs/theme_picker_dialog.dart deleted file mode 100644 index 3e4c5805..00000000 --- a/lib/dialogs/theme_picker_dialog.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:chabo/bloc/theme/theme_bloc.dart'; -import 'package:chabo/custom_properties.dart'; -import 'package:chabo/models/enums/theme_state_status.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ThemePickerDialog extends StatelessWidget { - const ThemePickerDialog({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.lightTheme, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox( - width: 10, - ), - AnimatedRotation( - duration: const Duration( - milliseconds: CustomProperties.animationDurationMs), - curve: Curves.easeOut, - turns: state.status == ThemeStateStatus.light ? 1 : 0, - child: Icon( - Icons.brightness_low, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - value: ThemeStateStatus.light, - groupValue: state.status, - onChanged: (ThemeStateStatus? value) { - if (value != null) { - BlocProvider.of(context).add( - ThemeChanged(status: value), - ); - } - }, - ), - RadioListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.darkTheme, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - width: 10, - ), - AnimatedRotation( - duration: const Duration( - milliseconds: CustomProperties.animationDurationMs), - curve: Curves.easeOut, - turns: state.status == ThemeStateStatus.light ? 0 : 1, - child: Icon( - Icons.dark_mode_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - value: ThemeStateStatus.dark, - groupValue: state.status, - onChanged: (ThemeStateStatus? value) { - if (value != null) { - BlocProvider.of(context).add( - ThemeChanged(status: value), - ); - } - }, - ), - RadioListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.systemTheme, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox( - width: 10, - ), - const Icon( - Icons.settings, - ), - ], - ), - value: ThemeStateStatus.system, - groupValue: state.status, - onChanged: (ThemeStateStatus? value) { - if (value != null) { - BlocProvider.of(context).add( - ThemeChanged( - status: value, - ), - ); - } - }, - ), - ], - ); - }, - ), - ); - } -} diff --git a/lib/dialogs/time_slot_dialog.dart b/lib/dialogs/time_slot_dialog.dart new file mode 100644 index 00000000..dbfe8c6d --- /dev/null +++ b/lib/dialogs/time_slot_dialog.dart @@ -0,0 +1,111 @@ +import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/models/time_slot.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class TimeSlotDialog extends StatelessWidget { + final int index; + + const TimeSlotDialog({Key? key, required this.index}) : super(key: key); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return AlertDialog( + contentPadding: const EdgeInsets.all(15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 15, + ), + ), + content: BlocBuilder( + builder: (context, state) { + return Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5, + runSpacing: 10, + children: [ + Text( + ' ${AppLocalizations.of(context)!.favoriteSlotsFrom} ', + style: textTheme.titleMedium, + ), + ElevatedButton( + onPressed: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dialOnly, + context: context, + initialTime: state.timeSlotsValue[index].from, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context), + child: child!, + ); + }, + ).then((value) => { + if (value != null) + { + BlocProvider.of(context).add( + ValueTimeSlotEvent( + timeSlot: TimeSlot( + name: state.timeSlotsValue[index].name, + from: value, + to: state.timeSlotsValue[index].to, + ), + index: index, + ), + ), + }, + }); + }, + child: Text( + state.timeSlotsValue[index].from.format(context), + style: textTheme.titleMedium, + ), + ), + Text( + ' ${AppLocalizations.of(context)!.favoriteSlotsTo} ', + style: textTheme.titleMedium, + ), + ElevatedButton( + onPressed: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dialOnly, + context: context, + initialTime: state.timeSlotsValue[index].to, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context), + child: child!, + ); + }, + ).then((value) => { + if (value != null) + { + BlocProvider.of(context).add( + ValueTimeSlotEvent( + timeSlot: TimeSlot( + name: state.timeSlotsValue[index].name, + from: state.timeSlotsValue[index].from, + to: value, + ), + index: index, + ), + ), + }, + }); + }, + child: Text( + state.timeSlotsValue[index].to.format(context), + style: textTheme.titleMedium, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/extensions/boats_extensions.dart b/lib/extensions/boats_extension.dart similarity index 93% rename from lib/extensions/boats_extensions.dart rename to lib/extensions/boats_extension.dart index d4026946..86e64361 100644 --- a/lib/extensions/boats_extensions.dart +++ b/lib/extensions/boats_extension.dart @@ -15,7 +15,10 @@ extension BoatsExtension on List { .add(TextSpan(text: ' ${AppLocalizations.of(context)!.and} ')); } } - return TextSpan(children: finalTextSpan); + + return TextSpan( + children: finalTextSpan, + ); } } @@ -30,6 +33,7 @@ extension BoatsExtension on List { finalString += ' ${AppLocalizations.of(context)!.and} '; } } + return finalString; } } diff --git a/lib/extensions/date_time_extension.dart b/lib/extensions/date_time_extension.dart index 0c12f24e..70f19b77 100644 --- a/lib/extensions/date_time_extension.dart +++ b/lib/extensions/date_time_extension.dart @@ -1,14 +1,19 @@ +import 'package:flutter/material.dart'; + extension DateTimeExtension on DateTime { DateTime previous(int day) { - if (day == weekday) { - return subtract(const Duration(days: 7)); - } else { - return subtract( - Duration( - days: (weekday - day) % DateTime.daysPerWeek, - hours: hour, - minutes: minute), - ); - } + return day == weekday + ? subtract(const Duration(days: 7)) + : subtract( + Duration( + days: (weekday - day) % DateTime.daysPerWeek, + hours: hour, + minutes: minute, + ), + ); + } + + DateTime applied(TimeOfDay time) { + return DateTime(year, month, day, time.hour, time.minute); } } diff --git a/lib/extensions/duration_extension.dart b/lib/extensions/duration_extension.dart index 35bb91b2..4dc7defe 100644 --- a/lib/extensions/duration_extension.dart +++ b/lib/extensions/duration_extension.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -extension DurationExtention on Duration { +extension DurationExtension on Duration { String durationToString(BuildContext context) { final dayToken = inDays == 0 ? '' @@ -15,6 +15,7 @@ extension DurationExtention on Duration { final secsToken = inSeconds.remainder(60) == 0 ? '' : '${inSeconds.remainder(60).toString()}s '; + return '$dayToken$hourToken$minToken$secsToken'; } diff --git a/lib/extensions/string_extension.dart b/lib/extensions/string_extension.dart index 860aff14..d8444c08 100644 --- a/lib/extensions/string_extension.dart +++ b/lib/extensions/string_extension.dart @@ -1,9 +1,7 @@ extension StringExtension on String { String capitalize() { - if (isEmpty) { - return this; - } else { - return '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; - } + return isEmpty + ? this + : '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; } } diff --git a/lib/helpers/ad_helper.dart b/lib/helpers/ad_helper.dart index 9c143e24..3c6ff4af 100644 --- a/lib/helpers/ad_helper.dart +++ b/lib/helpers/ad_helper.dart @@ -8,7 +8,8 @@ class AdHelper { return Const.androidInlineBanner; } else { throw UnsupportedError( - 'Unsupported platform to determine the banner unit ID'); + 'Unsupported platform to determine the banner unit ID', + ); } } @@ -17,7 +18,8 @@ class AdHelper { return Const.androidNativeBanner; } else { throw UnsupportedError( - 'Unsupported platform to determine the banner unit ID'); + 'Unsupported platform to determine the banner unit ID', + ); } } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1b8fc6f0..cc14bdac 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -152,8 +152,6 @@ } }, "day": "Day", - "refreshingNotifications": "Refreshing your notifications", - "refreshingNotificationsDone": "Done !", "notificationDayTitle": "\uD83D\uDD2E Closing scheduled", "notificationDayMessage": "{count, plural, =0{No closures scheduled for next week} =1{Next week, the Chaban Delmas bridge will only close once} other{Next week, the Chaban Delmas bridge will close {count} times}}", "@notificationDayMessage": { @@ -166,5 +164,19 @@ "rightHanded": "Right handed", "statusLoadMessage": "Loading of the bridge's current status", "loading": "Loading...", - "dayNotificationAt": "at" + "dayNotificationAt": "at", + "favoriteSlotsFrom": "From", + "favoriteSlotsTo": "to", + "favoriteSlots": "My favorite time slots", + "favoriteSlotsDescription": "You can fill in two time slots during which the events of the Chaban bridge are likely to impact you", + "favoriteTimeSlotDefaultName": "Time slot n°{index}", + "@favoriteTimeSlotDefaultName": { + "placeholders": { + "index": {} + } + }, + "favoriteSlotsInterferenceWarning": "This schedule interferes with one or more time slots", + "favoriteTimeSlotEnabledWarning": "Attention, by activating this parameter you will only receive notifications when an event occurs during one of your time slots", + "notificationNotEnabledMessage": "Please note that notifications are not allowed. Please go to the application settings to authorize the sending of notifications", + "notificationNotEnabledOpenSettings": "Open notification settings" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 32eae21e..cbe7c664 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -152,8 +152,6 @@ } }, "day": "Día", - "refreshingNotifications": "Refrescando tus notificaciones", - "refreshingNotificationsDone": "Terminado !", "notificationDayTitle": "\uD83D\uDD2E Cierre programado", "notificationDayMessage": "{count, plural, =0{No hay cierres programados para la próxima semana} =1{La próxima semana, el puente Chaban Delmas solo cerrará una vez} other{La próxima semana, el puente Chaban Delmas cerrará {count} veces}}", "@notificationDayMessage": { @@ -166,5 +164,19 @@ "rightHanded": "Diestro.a", "statusLoadMessage": "Carga del estado actual del puente", "loading": "Cargando...", - "dayNotificationAt": "en las" + "dayNotificationAt": "en las", + "favoriteSlotsFrom": "De", + "favoriteSlotsTo": "a", + "favoriteSlots": "Mis franjas horarias favoritas", + "favoriteSlotsDescription": "Puede completar dos intervalos de tiempo durante los cuales es probable que los eventos del puente Chaban lo afecten", + "favoriteTimeSlotDefaultName": "Franja horaria n°{index}", + "@favoriteTimeSlotDefaultName": { + "placeholders": { + "index": {} + } + }, + "favoriteSlotsInterferenceWarning": "Este horario interfiere con uno o más intervalos de tiempo", + "favoriteTimeSlotEnabledWarning": "Atención, al activar este parámetro solo recibirás notificaciones cuando ocurra un evento durante una de tus franjas horarias", + "notificationNotEnabledMessage": "Tenga en cuenta que las notificaciones no están permitidas. Vaya a la configuración de la aplicación para autorizar el envío de notificaciones.", + "notificationNotEnabledOpenSettings": "Abrir configuración de notificaciones" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e72edff6..7312e65d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -152,8 +152,6 @@ } }, "day": "Jour", - "refreshingNotifications": "Actualisation de vos notifications", - "refreshingNotificationsDone": "Terminé !", "notificationDayTitle": "\uD83D\uDD2E Fermetures prévues", "notificationDayMessage": "{count, plural, =0{Aucune fermetures de prévue pour la semaine prochaine} =1{La semaine prochaine, le pont Chaban Delmas ne fermera qu'une seul fois} other{La semaine prochaine, le pont Chaban Delmas fermera a {count} reprises}}", "@notificationDayMessage": { @@ -166,5 +164,19 @@ "rightHanded": "Droitier.ère", "statusLoadMessage": "Chargement de l'état actuel du pont", "loading": "Chargement...", - "dayNotificationAt": "à" + "dayNotificationAt": "à", + "favoriteSlotsFrom": "De", + "favoriteSlotsTo": "à", + "favoriteSlots": "Mes créneaux horaires favoris", + "favoriteSlotsDescription": "Vous pouvez renseigner deux créneaux durant lesquels les évènements du pont Chaban risques de vous impacter", + "favoriteTimeSlotDefaultName": "Créneau n°{index}", + "@favoriteTimeSlotDefaultName": { + "placeholders": { + "index": {} + } + }, + "favoriteSlotsInterferenceWarning": "Cette prévision interfère avec un ou plusieurs créneaux", + "favoriteTimeSlotEnabledWarning": "Attention, en activant ce paramètres vous allez uniquement recevoir les notifications lorsqu'un évèmenent se produit durant l'un de vos créneaux", + "notificationNotEnabledMessage": "Attention, les notifications ne sont pas autorisées. Veuillez vous rendre dans les paramètres de l'application pour autoriser l'envoi de notification", + "notificationNotEnabledOpenSettings": "Ouvrir les paramètres de notification" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2596d76e..c555330f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,8 @@ void main() async { runApp( Chabo( - storageService: storageService, - notificationService: notificationService), + storageService: storageService, + notificationService: notificationService, + ), ); } diff --git a/lib/misc/no_scaling_animation.dart b/lib/misc/no_scaling_animation.dart index 4b9cf364..3e107ae5 100644 --- a/lib/misc/no_scaling_animation.dart +++ b/lib/misc/no_scaling_animation.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; class NoScalingAnimation extends FloatingActionButtonAnimator { @override - Offset getOffset( - {required Offset begin, required Offset end, required double progress}) { + Offset getOffset({ + required Offset begin, + required Offset end, + required double progress, + }) { return end; } diff --git a/lib/models/abstract_chaban_bridge_forecast.dart b/lib/models/abstract_chaban_bridge_forecast.dart deleted file mode 100644 index 5b7d36fc..00000000 --- a/lib/models/abstract_chaban_bridge_forecast.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_reason.dart'; -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_type.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -abstract class AbstractChabanBridgeForecast extends Equatable { - final bool totalClosing; - final ChabanBridgeForecastClosingReason closingReason; - late final Duration closedDuration; - late final DateTime _circulationClosingDate; - late final DateTime _circulationReOpeningDate; - final ChabanBridgeForecastClosingType closingType; - - AbstractChabanBridgeForecast( - {required this.totalClosing, - required this.closingReason, - required DateTime circulationClosingDate, - required DateTime circulationReOpeningDate, - required this.closingType}) { - _circulationClosingDate = circulationClosingDate; - - var tmpCirculationReOpeningDate = circulationReOpeningDate; - var tmpDuration = - tmpCirculationReOpeningDate.difference(_circulationClosingDate); - - if (tmpDuration.isNegative) { - tmpCirculationReOpeningDate = - tmpCirculationReOpeningDate.add(const Duration(days: 1)); - tmpDuration = - tmpCirculationReOpeningDate.difference(_circulationClosingDate); - } - _circulationReOpeningDate = tmpCirculationReOpeningDate; - closedDuration = tmpDuration; - } - - DateTime get circulationReOpeningDate => _circulationReOpeningDate.toLocal(); - - DateTime get circulationReOpeningDateUTC => circulationClosingDate; - - set circulationReOpeningDate(DateTime value) { - _circulationReOpeningDate = value; - } - - DateTime get circulationClosingDate => _circulationClosingDate.toLocal(); - - DateTime get circulationClosingDateUTC => _circulationClosingDate; - - set circulationClosingDate(DateTime value) { - _circulationClosingDate = value; - } - - Widget getInformationWidget(BuildContext context); - - Widget getIconWidget(BuildContext context, bool reversed); - - String getNotificationDurationMessage( - BuildContext context, String pickedDuration); - - String getNotificationTimeMessage(BuildContext context); - - String getNotificationClosingMessage(BuildContext context); - - Color getColor(BuildContext context, bool reversed); - - String circulationClosingDateString(BuildContext context) { - return DateFormat.jm(Localizations.localeOf(context).languageCode) - .format(circulationClosingDate); - } - - String circulationReOpeningDateString(BuildContext context) { - return DateFormat.jm(Localizations.localeOf(context).languageCode) - .format(circulationReOpeningDate); - } - - bool isCurrentlyClosed() { - var now = DateTime.now(); - return now.isAfter(circulationClosingDate) && - now.isBefore(circulationReOpeningDate); - } - - static bool getBooleanTotalClosingValue(String stringValue) { - if (stringValue == 'oui') { - return true; - } else { - return false; - } - } - - static String getApiTimeZone(String recordTimestamp) { - return recordTimestamp.substring( - recordTimestamp.indexOf('+'), recordTimestamp.length); - } - - static DateTime parseFieldDate( - Map json, String fieldName, String timezone) { - return DateTime.parse( - "${json['fields']['date_passage']}T${json['fields'][fieldName]}:00$timezone"); - } - - @override - List get props => [ - totalClosing, - closingReason, - closedDuration, - circulationClosingDate, - circulationReOpeningDate, - closingType, - ]; -} diff --git a/lib/models/abstract_forecast.dart b/lib/models/abstract_forecast.dart new file mode 100644 index 00000000..f9acb8d8 --- /dev/null +++ b/lib/models/abstract_forecast.dart @@ -0,0 +1,167 @@ +import 'package:chabo/extensions/date_time_extension.dart'; +import 'package:chabo/models/enums/forecast_closing_reason.dart'; +import 'package:chabo/models/enums/forecast_closing_type.dart'; +import 'package:chabo/models/time_slot.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +abstract class AbstractForecast extends Equatable { + final bool totalClosing; + late final bool isDuringTwoDays; + final ForecastClosingReason closingReason; + late final Duration closedDuration; + late final DateTime _circulationClosingDate; + late final DateTime _circulationReOpeningDate; + final ForecastClosingType closingType; + final List interferingTimeSlots = []; + + AbstractForecast({ + required this.totalClosing, + required this.closingReason, + required DateTime circulationClosingDate, + required DateTime circulationReOpeningDate, + required this.closingType, + }) { + _circulationClosingDate = circulationClosingDate; + + var tmpCirculationReOpeningDate = circulationReOpeningDate.toLocal(); + var tmpDuration = tmpCirculationReOpeningDate + .difference(_circulationClosingDate.toLocal()); + var tmpIsDuringTwoDays = false; + + if (tmpDuration.isNegative) { + tmpIsDuringTwoDays = true; + tmpCirculationReOpeningDate = + tmpCirculationReOpeningDate.add(const Duration(days: 1)); + tmpDuration = + tmpCirculationReOpeningDate.difference(_circulationClosingDate); + } + isDuringTwoDays = tmpIsDuringTwoDays; + _circulationReOpeningDate = tmpCirculationReOpeningDate; + closedDuration = tmpDuration; + } + + DateTime get circulationReOpeningDate => _circulationReOpeningDate.toLocal(); + + set circulationReOpeningDate(DateTime value) { + _circulationReOpeningDate = value; + } + + DateTime get circulationClosingDate => _circulationClosingDate.toLocal(); + + set circulationClosingDate(DateTime value) { + _circulationClosingDate = value; + } + + Widget getInformationWidget(BuildContext context); + + Widget getIconWidget(BuildContext context, bool reversed); + + String getNotificationDurationMessage( + BuildContext context, + String pickedDuration, + ); + + String getNotificationTimeMessage(BuildContext context); + + String getNotificationClosingMessage(BuildContext context); + + Color getColor(BuildContext context, bool reversed); + + String circulationClosingDateString(BuildContext context) { + return DateFormat.jm(Localizations.localeOf(context).languageCode) + .format(circulationClosingDate); + } + + String circulationReOpeningDateString(BuildContext context) { + return DateFormat.jm(Localizations.localeOf(context).languageCode) + .format(circulationReOpeningDate); + } + + void computeSlotInterference(List timeSlots) { + interferingTimeSlots.clear(); + for (var timeSlot in timeSlots) { + if (isOverlappingWithPeriod(timeSlot.from, timeSlot.to)) { + interferingTimeSlots.add(timeSlot); + } + } + } + + bool isCurrentlyClosed() { + return isOverlappingWith(DateTime.now()); + } + + bool isOverlappingWith(DateTime dateTime) { + return dateTime.isAfter(circulationClosingDate) && + dateTime.isBefore(circulationReOpeningDate); + } + + bool isOverlappingWithPeriod(TimeOfDay start, TimeOfDay end) { + final startDateTime = circulationClosingDate.applied(start); + final endDateTime = circulationClosingDate.applied(end); + + final startIsBeforeClosing = startDateTime.isBefore( + circulationClosingDate, + ); + + final endIsBeforeClosing = endDateTime.isBefore( + circulationClosingDate, + ); + + final startIsBeforeReopening = startDateTime.isBefore( + circulationReOpeningDate, + ); + final endIsBeforeReopening = endDateTime.isBefore( + circulationReOpeningDate, + ); + + return (startIsBeforeClosing && + startIsBeforeReopening && + !endIsBeforeClosing && + endIsBeforeReopening) || + (!startIsBeforeClosing && + startIsBeforeReopening && + endIsBeforeClosing && + !endIsBeforeClosing) || + (!startIsBeforeClosing && + startIsBeforeReopening && + !endIsBeforeClosing && + !endIsBeforeReopening) || + (startIsBeforeClosing && + startIsBeforeReopening && + !endIsBeforeClosing && + !endIsBeforeReopening); + } + + static bool getBooleanTotalClosingValue(String stringValue) { + return stringValue == 'oui'; + } + + static String getApiTimeZone(String recordTimestamp) { + return recordTimestamp.substring( + recordTimestamp.indexOf('+'), + recordTimestamp.length, + ); + } + + static DateTime parseFieldDate( + Map json, + String fieldName, + String timezone, + ) { + return DateTime.parse( + "${json['fields']['date_passage']}T${json['fields'][fieldName]}:00$timezone", + ); + } + + @override + List get props => [ + totalClosing, + closingReason, + closedDuration, + circulationClosingDate, + circulationReOpeningDate, + closingType, + ]; +} diff --git a/lib/models/boat.dart b/lib/models/boat.dart index 6e4dd6d0..141733fd 100644 --- a/lib/models/boat.dart +++ b/lib/models/boat.dart @@ -31,20 +31,25 @@ class Boat { decoration: TextDecoration.underline, ), ); - if (isLeaving) { - return TextSpan(children: [ - TextSpan( - text: - '${AppLocalizations.of(context)!.dialogInformationContentBridgeDeparture} '), - textSpanLink, - ]); - } else { - return TextSpan(children: [ - TextSpan( - text: - '${AppLocalizations.of(context)!.dialogInformationContentBridgeArrival} '), - textSpanLink, - ]); - } + + return isLeaving + ? TextSpan( + children: [ + TextSpan( + text: + '${AppLocalizations.of(context)!.dialogInformationContentBridgeDeparture} ', + ), + textSpanLink, + ], + ) + : TextSpan( + children: [ + TextSpan( + text: + '${AppLocalizations.of(context)!.dialogInformationContentBridgeArrival} ', + ), + textSpanLink, + ], + ); } } diff --git a/lib/models/chaban_bridge_boat_forecast.dart b/lib/models/boat_forecast.dart similarity index 72% rename from lib/models/chaban_bridge_boat_forecast.dart rename to lib/models/boat_forecast.dart index 02b95c5e..a810cd93 100644 --- a/lib/models/chaban_bridge_boat_forecast.dart +++ b/lib/models/boat_forecast.dart @@ -1,48 +1,55 @@ -import 'package:chabo/extensions/boats_extensions.dart'; +import 'package:chabo/extensions/boats_extension.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/extensions/string_extension.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; +import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/models/boat.dart'; -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_reason.dart'; -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_type.dart'; +import 'package:chabo/models/enums/forecast_closing_reason.dart'; +import 'package:chabo/models/enums/forecast_closing_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; -class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { +class BoatForecast extends AbstractForecast { final List boats; static final List allBoatNames = []; - ChabanBridgeBoatForecast( - {required bool totalClosing, - required DateTime circulationClosingDate, - required DateTime circulationReOpeningDate, - required this.boats, - required ChabanBridgeForecastClosingType closingType}) - : assert(boats.isNotEmpty), + BoatForecast({ + required bool totalClosing, + required DateTime circulationClosingDate, + required DateTime circulationReOpeningDate, + required this.boats, + required ForecastClosingType closingType, + }) : assert(boats.isNotEmpty), super( - circulationClosingDate: circulationClosingDate, - circulationReOpeningDate: circulationReOpeningDate, - closingReason: ChabanBridgeForecastClosingReason.boat, - closingType: closingType, - totalClosing: totalClosing); + circulationClosingDate: circulationClosingDate, + circulationReOpeningDate: circulationReOpeningDate, + closingReason: ForecastClosingReason.boat, + closingType: closingType, + totalClosing: totalClosing, + ); - factory ChabanBridgeBoatForecast.fromJSON(Map json) { - var apiTimezone = - AbstractChabanBridgeForecast.getApiTimeZone(json['record_timestamp']); - var closingDate = AbstractChabanBridgeForecast.parseFieldDate( - json, 'fermeture_a_la_circulation', apiTimezone); - var reopeningDate = AbstractChabanBridgeForecast.parseFieldDate( - json, 're_ouverture_a_la_circulation', apiTimezone); + factory BoatForecast.fromJSON(Map json) { + var apiTimezone = AbstractForecast.getApiTimeZone(json['record_timestamp']); + var closingDate = AbstractForecast.parseFieldDate( + json, + 'fermeture_a_la_circulation', + apiTimezone, + ); + var reopeningDate = AbstractForecast.parseFieldDate( + json, + 're_ouverture_a_la_circulation', + apiTimezone, + ); var closingType = (json['fields']['type_de_fermeture'] as String).toLowerCase() == 'totale' - ? ChabanBridgeForecastClosingType.complete - : ChabanBridgeForecastClosingType.partial; - var totalClosing = AbstractChabanBridgeForecast.getBooleanTotalClosingValue( - json['fields']['fermeture_totale']); + ? ForecastClosingType.complete + : ForecastClosingType.partial; + var totalClosing = AbstractForecast.getBooleanTotalClosingValue( + json['fields']['fermeture_totale'], + ); List boats = []; bool isLeaving = false; @@ -58,12 +65,13 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { } } - return ChabanBridgeBoatForecast( - boats: boats, - totalClosing: totalClosing, - circulationReOpeningDate: reopeningDate, - circulationClosingDate: closingDate, - closingType: closingType); + return BoatForecast( + boats: boats, + totalClosing: totalClosing, + circulationReOpeningDate: reopeningDate, + circulationClosingDate: closingDate, + closingType: closingType, + ); } @override @@ -74,7 +82,7 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { boats, circulationClosingDate, circulationReOpeningDate, - closingType + closingType, ]; @override @@ -135,12 +143,14 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { ), ), TextSpan( - text: - ', ${AppLocalizations.of(context)!.dialogInformationContentBridge_closed} '), + text: + ', ${AppLocalizations.of(context)!.dialogInformationContentBridge_closed} ', + ), boats.toLocalizedTextSpan(context), TextSpan( - text: - '\n\n${AppLocalizations.of(context)!.dialogInformationContentClosing_time.capitalize()} : '), + text: + '\n\n${AppLocalizations.of(context)!.dialogInformationContentClosing_time.capitalize()} : ', + ), TextSpan( text: '${closedDuration.durationToString(context)}\n', style: TextStyle( @@ -149,8 +159,9 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { ), ), TextSpan( - text: - '${AppLocalizations.of(context)!.dialogInformationContentTime_of_crossing.capitalize()} : '), + text: + '${AppLocalizations.of(context)!.dialogInformationContentTime_of_crossing.capitalize()} : ', + ), TextSpan( text: scheduleString, style: TextStyle( @@ -165,7 +176,9 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { @override String getNotificationDurationMessage( - BuildContext context, String pickedDuration) { + BuildContext context, + String pickedDuration, + ) { return AppLocalizations.of(context)!.notificationDurationBoatMessage( boats.toLocalizedString(context), pickedDuration, @@ -199,31 +212,31 @@ class ChabanBridgeBoatForecast extends AbstractChabanBridgeForecast { child: Icon( Icons.directions_boat_rounded, color: getColor(context, reversed), - size: 30, + size: 25, ), ), Positioned( right: 0, - top: 0, + top: -3, child: RotatedBox( quarterTurns: boats[0].isLeaving ? 0 : 2, child: Icon( Icons.double_arrow_rounded, color: getColor(context, reversed), - size: 18, + size: 15, ), ), ), boats.length == 2 ? Positioned( right: 0, - top: 14, + top: 10, child: RotatedBox( quarterTurns: boats[1].isLeaving ? 0 : 2, child: Icon( Icons.double_arrow_rounded, color: getColor(context, reversed), - size: 18, + size: 15, ), ), ) diff --git a/lib/models/enums/chaban_bridge_forecast_closing_reason.dart b/lib/models/enums/chaban_bridge_forecast_closing_reason.dart deleted file mode 100644 index bb522c46..00000000 --- a/lib/models/enums/chaban_bridge_forecast_closing_reason.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum ChabanBridgeForecastClosingReason { - boat, - maintenance; -} diff --git a/lib/models/enums/chaban_bridge_forecast_closing_type.dart b/lib/models/enums/chaban_bridge_forecast_closing_type.dart deleted file mode 100644 index 6ddffd9c..00000000 --- a/lib/models/enums/chaban_bridge_forecast_closing_type.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum ChabanBridgeForecastClosingType { - partial, - complete; -} diff --git a/lib/models/enums/forecast_closing_reason.dart b/lib/models/enums/forecast_closing_reason.dart new file mode 100644 index 00000000..65173c27 --- /dev/null +++ b/lib/models/enums/forecast_closing_reason.dart @@ -0,0 +1,4 @@ +enum ForecastClosingReason { + boat, + maintenance; +} diff --git a/lib/models/enums/forecast_closing_type.dart b/lib/models/enums/forecast_closing_type.dart new file mode 100644 index 00000000..58f7e2cb --- /dev/null +++ b/lib/models/enums/forecast_closing_type.dart @@ -0,0 +1,4 @@ +enum ForecastClosingType { + partial, + complete; +} diff --git a/lib/models/chaban_bridge_maintenance_forecast.dart b/lib/models/maintenance_forecast.dart similarity index 71% rename from lib/models/chaban_bridge_maintenance_forecast.dart rename to lib/models/maintenance_forecast.dart index d4f5ae5e..0b784551 100644 --- a/lib/models/chaban_bridge_maintenance_forecast.dart +++ b/lib/models/maintenance_forecast.dart @@ -1,46 +1,54 @@ import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/extensions/string_extension.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_reason.dart'; -import 'package:chabo/models/enums/chaban_bridge_forecast_closing_type.dart'; +import 'package:chabo/models/abstract_forecast.dart'; +import 'package:chabo/models/enums/forecast_closing_reason.dart'; +import 'package:chabo/models/enums/forecast_closing_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; -class ChabanBridgeMaintenanceForecast extends AbstractChabanBridgeForecast { - ChabanBridgeMaintenanceForecast( - {required bool totalClosing, - required DateTime circulationClosingDate, - required DateTime circulationReOpeningDate, - required ChabanBridgeForecastClosingType closingType}) - : super( - circulationClosingDate: circulationClosingDate, - circulationReOpeningDate: circulationReOpeningDate, - closingReason: ChabanBridgeForecastClosingReason.maintenance, - closingType: closingType, - totalClosing: totalClosing); +class MaintenanceForecast extends AbstractForecast { + MaintenanceForecast({ + required bool totalClosing, + required DateTime circulationClosingDate, + required DateTime circulationReOpeningDate, + required ForecastClosingType closingType, + }) : super( + circulationClosingDate: circulationClosingDate, + circulationReOpeningDate: circulationReOpeningDate, + closingReason: ForecastClosingReason.maintenance, + closingType: closingType, + totalClosing: totalClosing, + ); - factory ChabanBridgeMaintenanceForecast.fromJSON(Map json) { - var apiTimezone = - AbstractChabanBridgeForecast.getApiTimeZone(json['record_timestamp']); - var closingDate = AbstractChabanBridgeForecast.parseFieldDate( - json, 'fermeture_a_la_circulation', apiTimezone); - var reopeningDate = AbstractChabanBridgeForecast.parseFieldDate( - json, 're_ouverture_a_la_circulation', apiTimezone); + factory MaintenanceForecast.fromJSON(Map json) { + var apiTimezone = AbstractForecast.getApiTimeZone(json['record_timestamp']); + var closingDate = AbstractForecast.parseFieldDate( + json, + 'fermeture_a_la_circulation', + apiTimezone, + ); + var reopeningDate = AbstractForecast.parseFieldDate( + json, + 're_ouverture_a_la_circulation', + apiTimezone, + ); var closingType = (json['fields']['type_de_fermeture'] as String).toLowerCase() == 'totale' - ? ChabanBridgeForecastClosingType.complete - : ChabanBridgeForecastClosingType.partial; - var totalClosing = AbstractChabanBridgeForecast.getBooleanTotalClosingValue( - json['fields']['fermeture_totale']); + ? ForecastClosingType.complete + : ForecastClosingType.partial; + var totalClosing = AbstractForecast.getBooleanTotalClosingValue( + json['fields']['fermeture_totale'], + ); - return ChabanBridgeMaintenanceForecast( - totalClosing: totalClosing, - circulationReOpeningDate: reopeningDate, - circulationClosingDate: closingDate, - closingType: closingType); + return MaintenanceForecast( + totalClosing: totalClosing, + circulationReOpeningDate: reopeningDate, + circulationClosingDate: closingDate, + closingType: closingType, + ); } @override @@ -50,12 +58,14 @@ class ChabanBridgeMaintenanceForecast extends AbstractChabanBridgeForecast { closedDuration, circulationClosingDate, circulationReOpeningDate, - closingType + closingType, ]; @override String getNotificationDurationMessage( - BuildContext context, String pickedDuration) { + BuildContext context, + String pickedDuration, + ) { return AppLocalizations.of(context)!.notificationDurationMaintenanceMessage( pickedDuration, closedDuration.durationToString(context), @@ -73,7 +83,10 @@ class ChabanBridgeMaintenanceForecast extends AbstractChabanBridgeForecast { @override String getNotificationClosingMessage(BuildContext context) { return AppLocalizations.of(context)!.notificationClosingMaintenanceMessage( - closedDuration.durationToString(context)); + closedDuration.durationToString( + context, + ), + ); } @override @@ -99,6 +112,7 @@ class ChabanBridgeMaintenanceForecast extends AbstractChabanBridgeForecast { '${MaterialLocalizations.of(context).formatFullDate(circulationReOpeningDate)}, ' '${DateFormat.jm(Localizations.localeOf(context).languageCode).format(circulationReOpeningDate)}'; } + return Text.rich( TextSpan( style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6), @@ -160,7 +174,7 @@ class ChabanBridgeMaintenanceForecast extends AbstractChabanBridgeForecast { return Icon( Icons.construction_rounded, color: getColor(context, reversed), - size: 30, + size: 25, ); } diff --git a/lib/models/time_slot.dart b/lib/models/time_slot.dart new file mode 100644 index 00000000..61c86218 --- /dev/null +++ b/lib/models/time_slot.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class TimeSlot extends Equatable { + final String name; + final TimeOfDay from; + final TimeOfDay to; + + const TimeSlot({required this.name, required this.from, required this.to}); + + Map toJson() { + return { + 'name': name, + 'from': '${from.hour}:${from.minute}', + 'to': '${to.hour}:${to.minute}', + }; + } + + factory TimeSlot.fromJSON(Map json) { + final format = DateFormat.Hm(); + + return TimeSlot( + name: json['name'] ?? '', + from: TimeOfDay.fromDateTime( + format.parse( + json['from'], + ), + ), + to: TimeOfDay.fromDateTime( + format.parse( + json['to'], + ), + ), + ); + } + + @override + List get props => [name, from, to]; +} diff --git a/lib/models/link_icon.dart b/lib/models/web_link_icon.dart similarity index 100% rename from lib/models/link_icon.dart rename to lib/models/web_link_icon.dart diff --git a/lib/screens/chaban_bridge_forecast_screen.dart b/lib/screens/chaban_bridge_forecast_screen.dart deleted file mode 100644 index 7963bf93..00000000 --- a/lib/screens/chaban_bridge_forecast_screen.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:chabo/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart'; -import 'package:chabo/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart'; -import 'package:chabo/cubits/floating_actions_cubit.dart'; -import 'package:chabo/bloc/notification/notification_bloc.dart'; -import 'package:chabo/cubits/notification_service_cubit.dart'; -import 'package:chabo/bloc/scroll_status/scroll_status_bloc.dart'; -import 'package:chabo/custom_widgets_state.dart'; -import 'package:chabo/misc/no_scaling_animation.dart'; -import 'package:chabo/screens/error_screen.dart'; -import 'package:chabo/widgets/forecast/forecast_list_widget.dart'; -import 'package:chabo/widgets/forecast/status_widget.dart'; -import 'package:chabo/widgets/progress_indicator/custom_circular_progress_indicator.dart'; -import 'package:chabo/widgets/floating_actions/floating_actions_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ChabanBridgeForecastScreen extends StatefulWidget { - const ChabanBridgeForecastScreen({Key? key}) : super(key: key); - - @override - State createState() { - return _ChabanBridgeForecastScreenState(); - } -} - -class _ChabanBridgeForecastScreenState - extends CustomWidgetState { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Scaffold( - floatingActionButton: const FloatingActionsWidget(), - floatingActionButtonLocation: state.isRightHanded - ? FloatingActionButtonLocation.endFloat - : FloatingActionButtonLocation.startFloat, - floatingActionButtonAnimator: NoScalingAnimation(), - body: SafeArea( - child: BlocBuilder( - buildWhen: (previous, current) => - previous.status == ChabanBridgeForecastStatus.initial && - current.status == ChabanBridgeForecastStatus.success, - builder: (context, state) { - switch (state.status) { - case ChabanBridgeForecastStatus.failure: - return ErrorScreen(errorMessage: state.message); - case ChabanBridgeForecastStatus.success: - if (state.chabanBridgeForecasts.isEmpty) { - return const ErrorScreen(errorMessage: 'Empty return'); - } - - return MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) async { - BlocProvider.of(context) - .add( - ChabanBridgeStatusChanged( - currentChabanBridgeForecast: - state.currentChabanBridgeForecast, - previousChabanBridgeForecast: - state.previousChabanBridgeForecast, - ), - ); - BlocProvider.of(context).add( - GoTo(goTo: state.currentChabanBridgeForecast), - ); - }, - ), - BlocListener( - listener: (context, state) async { - BlocProvider.of(context) - .add( - ChabanBridgeStatusDurationChanged( - duration: state.durationNotificationValue, - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: CircularProgressIndicator( - color: Theme.of(context) - .colorScheme - .inversePrimary, - strokeWidth: 5, - ), - ), - Expanded( - flex: 5, - child: Text( - AppLocalizations.of(context)! - .refreshingNotifications, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ); - await context - .read() - .state - .computeNotifications( - BlocProvider.of( - context) - .state - .chabanBridgeForecasts, - state, - context); - ScaffoldMessenger.of(context).removeCurrentSnackBar( - reason: SnackBarClosedReason.remove, - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration( - milliseconds: 1000, - ), - content: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Icon( - Icons.check, - color: Theme.of(context) - .colorScheme - .inversePrimary, - )), - Expanded( - flex: 8, - child: Text( - AppLocalizations.of(context)! - .refreshingNotificationsDone, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ); - }, - ) - ], - child: Column( - children: const [ - StatusWidget(), - Expanded( - flex: 11, - child: ForecastListWidget(), - ), - ], - ), - ); - default: - return CustomCircularProgressIndicator( - message: AppLocalizations.of(context)!.loading, - ); - } - }, - ), - ), - ); - }, - ); - } -} diff --git a/lib/screens/changelog_screen.dart b/lib/screens/changelog_screen.dart index 1c1e60b8..c16647b1 100644 --- a/lib/screens/changelog_screen.dart +++ b/lib/screens/changelog_screen.dart @@ -1,5 +1,5 @@ import 'package:chabo/const.dart'; -import 'package:chabo/custom_widgets_state.dart'; +import 'package:chabo/custom_widget_state.dart'; import 'package:chabo/screens/error_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,8 +17,10 @@ class ChangeLogScreen extends StatefulWidget { class _ChangeLogScreenState extends CustomWidgetState { String _getChangelogPath(BuildContext context) { - return Const.changelogPath.replaceAll(Const.changelogPlaceholder, - Localizations.localeOf(context).languageCode); + return Const.changelogPath.replaceAll( + Const.changelogPlaceholder, + Localizations.localeOf(context).languageCode, + ); } @override @@ -41,6 +43,7 @@ class _ChangeLogScreenState extends CustomWidgetState { errorMessage: snapshot.error.toString(), ); } + return const Center( child: CircularProgressIndicator(), ); diff --git a/lib/screens/error_screen.dart b/lib/screens/error_screen.dart index d5494cc7..2d581e83 100644 --- a/lib/screens/error_screen.dart +++ b/lib/screens/error_screen.dart @@ -1,4 +1,4 @@ -import 'package:chabo/custom_widgets_state.dart'; +import 'package:chabo/custom_widget_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -44,15 +44,16 @@ class _ErrorScreenState extends CustomWidgetState { ), ), Flexible( - flex: 1, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${AppLocalizations.of(context)!.errorScreenContentTechnical_Info} : ${widget.errorMessage}', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.grey), - ), - )) + flex: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '${AppLocalizations.of(context)!.errorScreenContentTechnical_Info} : ${widget.errorMessage}', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.grey), + ), + ), + ), ], ), ), diff --git a/lib/screens/forecast_screen.dart b/lib/screens/forecast_screen.dart new file mode 100644 index 00000000..e4ddb0da --- /dev/null +++ b/lib/screens/forecast_screen.dart @@ -0,0 +1,122 @@ +import 'package:chabo/bloc/forecast/forecast_bloc.dart'; +import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/bloc/scroll_status/scroll_status_bloc.dart'; +import 'package:chabo/bloc/status/status_bloc.dart'; +import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:chabo/custom_widget_state.dart'; +import 'package:chabo/misc/no_scaling_animation.dart'; +import 'package:chabo/screens/error_screen.dart'; +import 'package:chabo/widgets/ad_banner_widget.dart'; +import 'package:chabo/widgets/floating_actions/floating_actions_widget.dart'; +import 'package:chabo/widgets/forecast/forecast_list_widget.dart'; +import 'package:chabo/widgets/forecast/status_widget.dart'; +import 'package:chabo/widgets/progress_indicator/custom_circular_progress_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ForecastScreen extends StatefulWidget { + const ForecastScreen({Key? key}) : super(key: key); + + @override + State createState() { + return _ForecastScreenState(); + } +} + +class _ForecastScreenState extends CustomWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + floatingActionButton: Column( + crossAxisAlignment: state.isRightHanded + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + const Expanded(child: FloatingActionsWidget()), + Padding( + padding: EdgeInsets.only( + left: state.isRightHanded ? 32 : 0, + right: state.isRightHanded ? 0 : 32, + top: 15, + ), + child: const AdBannerWidget(), + ), + ], + ), + floatingActionButtonLocation: state.isRightHanded + ? FloatingActionButtonLocation.endFloat + : FloatingActionButtonLocation.startFloat, + floatingActionButtonAnimator: NoScalingAnimation(), + body: SafeArea( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.status == ForecastStatus.initial && + current.status == ForecastStatus.success, + builder: (context, state) { + switch (state.status) { + case ForecastStatus.failure: + return ErrorScreen(errorMessage: state.message); + case ForecastStatus.success: + if (state.forecasts.isEmpty) { + return const ErrorScreen(errorMessage: 'Empty return'); + } + + return MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + BlocProvider.of(context).add( + StatusChanged( + currentForecast: state.currentForecast, + previousForecast: state.previousForecast, + ), + ); + BlocProvider.of(context).add( + GoTo(goTo: state.currentForecast), + ); + }, + ), + BlocListener( + listener: (context, state) { + BlocProvider.of(context).add( + StatusDurationChanged( + duration: state.durationNotificationValue, + ), + ); + BlocProvider.of(context).add( + ComputeNotificationEvent( + forecasts: BlocProvider.of( + context, + ).state.forecasts, + context: context, + ), + ); + }, + ), + ], + child: Column( + children: const [ + StatusWidget(), + Expanded( + flex: 11, + child: ForecastListWidget(), + ), + ], + ), + ); + default: + return CustomCircularProgressIndicator( + message: AppLocalizations.of(context)!.loading, + ); + } + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screens/notification_screen.dart b/lib/screens/notification_screen.dart index 2d31bdf0..4f39bfc2 100644 --- a/lib/screens/notification_screen.dart +++ b/lib/screens/notification_screen.dart @@ -1,13 +1,16 @@ import 'dart:ui'; -import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:app_settings/app_settings.dart'; import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/cubits/floating_actions_cubit.dart'; import 'package:chabo/custom_properties.dart'; -import 'package:chabo/custom_widgets_state.dart'; +import 'package:chabo/custom_widget_state.dart'; import 'package:chabo/dialogs/days_of_the_week_dialog.dart'; +import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/misc/no_scaling_animation.dart'; import 'package:chabo/models/enums/day.dart'; +import 'package:chabo/widgets/time_slot_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -58,10 +61,140 @@ class _NotificationScreenState extends CustomWidgetState { padding: const EdgeInsets.only( top: 20, ), - child: BlocBuilder( - builder: (context, state) { + child: BlocBuilder( + builder: (context, notificationState) { return Column( children: [ + Builder( + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (!notificationState.notificationEnabled) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 10), + showCloseIcon: true, + backgroundColor: + Theme.of(context).colorScheme.error, + content: Column( + children: [ + Text( + AppLocalizations.of(context)! + .notificationNotEnabledMessage, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onError, + ), + overflow: TextOverflow.visible, + ), + const SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () => AppSettings + .openNotificationSettings(), + child: Text( + AppLocalizations.of(context)! + .notificationNotEnabledOpenSettings, + ), + ), + ], + ), + ), + ); + } + }, + ); + + return const SizedBox.shrink(); + }, + ), + Column( + children: [ + _CustomListTile( + onChanged: (bool value) => { + BlocProvider.of(context).add( + EnabledTimeSlotEvent( + enabled: value, + ), + ), + if (value) + { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 7), + showCloseIcon: true, + backgroundColor: Theme.of(context) + .colorScheme + .warningColor, + content: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)! + .favoriteTimeSlotEnabledWarning, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + } + else + { + ScaffoldMessenger.of(context) + .hideCurrentSnackBar( + reason: SnackBarClosedReason.action, + ), + }, + }, + enabled: notificationState + .timeSlotsEnabledForNotifications, + title: AppLocalizations.of(context)!.favoriteSlots, + subtitle: AppLocalizations.of(context)! + .favoriteSlotsDescription, + leadingIcon: Icons.warning_rounded, + iconColor: Theme.of(context).colorScheme.warningColor, + constrainedBySlots: false, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (var i = 0; + i < notificationState.timeSlotsValue.length; + i++) ...[ + TimeSlotWidget( + timeSlot: notificationState.timeSlotsValue[i], + index: i, + ), + ], + ], + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + const Divider( + height: 5, + indent: 25, + endIndent: 25, + ), + const SizedBox( + height: 10, + ), _CustomListTile( onChanged: (bool value) => BlocProvider.of(context).add( @@ -69,13 +202,15 @@ class _NotificationScreenState extends CustomWidgetState { enabled: value, ), ), - enabled: state.openingNotificationEnabled, + enabled: notificationState.openingNotificationEnabled, title: AppLocalizations.of(context)! .openingNotificationTitle, subtitle: AppLocalizations.of(context)! .openingNotificationExplanation, leadingIcon: Icons.check_circle, iconColor: Colors.green, + constrainedBySlots: + notificationState.timeSlotsEnabledForNotifications, ), _CustomListTile( onChanged: (bool value) => @@ -84,23 +219,26 @@ class _NotificationScreenState extends CustomWidgetState { enabled: value, ), ), - enabled: state.closingNotificationEnabled, + enabled: notificationState.closingNotificationEnabled, title: AppLocalizations.of(context)! .closingNotificationTitle, subtitle: AppLocalizations.of(context)! .closingNotificationExplanation, leadingIcon: Icons.block_rounded, iconColor: Colors.red, + constrainedBySlots: + notificationState.timeSlotsEnabledForNotifications, ), const SizedBox( height: 20, ), _CustomListTile( - onTap: () async { - var time = await showTimePicker( + onTap: () { + showTimePicker( initialEntryMode: TimePickerEntryMode.dialOnly, context: context, - initialTime: state.durationNotificationValue + initialTime: notificationState + .durationNotificationValue .durationToTimeOfDay(), builder: (BuildContext context, Widget? child) { return MediaQuery( @@ -110,18 +248,21 @@ class _NotificationScreenState extends CustomWidgetState { child: child!, ); }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + DurationNotificationValueEvent( + duration: Duration( + hours: value.hour, + minutes: value.minute, + ), + ), + ), + }, + }, ); - if (time != null) { - // ignore: use_build_context_synchronously - BlocProvider.of(context).add( - DurationNotificationValueEvent( - duration: Duration( - hours: time.hour, - minutes: time.minute, - ), - ), - ); - } }, onChanged: (bool value) => BlocProvider.of(context).add( @@ -129,25 +270,27 @@ class _NotificationScreenState extends CustomWidgetState { enabled: value, ), ), - enabled: state.durationNotificationEnabled, + enabled: notificationState.durationNotificationEnabled, title: AppLocalizations.of(context)! .durationNotificationTitle( - state.durationNotificationValue + notificationState.durationNotificationValue .durationToString(context), ), subtitle: AppLocalizations.of(context)! .durationNotificationExplanation( - state.durationNotificationValue + notificationState.durationNotificationValue .durationToString(context), ), leadingIcon: Icons.timer_outlined, + constrainedBySlots: + notificationState.timeSlotsEnabledForNotifications, ), _CustomListTile( - onTap: () async { - var time = await showTimePicker( + onTap: () { + showTimePicker( initialEntryMode: TimePickerEntryMode.dialOnly, context: context, - initialTime: state.timeNotificationValue, + initialTime: notificationState.timeNotificationValue, builder: (BuildContext context, Widget? child) { return MediaQuery( data: MediaQuery.of(context).copyWith( @@ -156,18 +299,21 @@ class _NotificationScreenState extends CustomWidgetState { child: child!, ); }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + TimeNotificationValueEvent( + time: TimeOfDay( + hour: value.hour, + minute: value.minute, + ), + ), + ), + }, + }, ); - if (time != null) { - // ignore: use_build_context_synchronously - BlocProvider.of(context).add( - TimeNotificationValueEvent( - time: TimeOfDay( - hour: time.hour, - minute: time.minute, - ), - ), - ); - } }, onChanged: (bool value) => BlocProvider.of(context).add( @@ -175,47 +321,60 @@ class _NotificationScreenState extends CustomWidgetState { enabled: value, ), ), - enabled: state.timeNotificationEnabled, + enabled: notificationState.timeNotificationEnabled, title: AppLocalizations.of(context)!.timeNotificationTitle( - state.timeNotificationValue.format(context), + notificationState.timeNotificationValue.format(context), ), subtitle: AppLocalizations.of(context)! .timeNotificationExplanation( - state.timeNotificationValue.format(context), + notificationState.timeNotificationValue.format(context), ), leadingIcon: Icons.plus_one_outlined, + constrainedBySlots: + notificationState.timeSlotsEnabledForNotifications, ), _CustomListTile( - onTap: () async { - final day = await showDialog( + onTap: () { + showDialog( context: context, builder: ( BuildContext context, ) { return BackdropFilter( filter: ImageFilter.blur( - sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY), - child: DaysOfTheWeekDialog( - selectedDay: state.dayNotificationValue), + sigmaX: CustomProperties.blurSigmaX, + sigmaY: CustomProperties.blurSigmaY, + ), + child: const DaysOfTheWeekDialog(), ); }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + DayNotificationValueEvent( + day: value, + ), + ), + }, + }, ); - if (day != null) { - BlocProvider.of(context).add( - DayNotificationValueEvent(day: day), - ); - } }, - enabled: state.dayNotificationEnabled, + enabled: notificationState.dayNotificationEnabled, title: AppLocalizations.of(context)!.dayNotificationTitle( - state.dayNotificationValue.localizedName(context), + notificationState.dayNotificationValue + .localizedName(context), ), subtitle: AppLocalizations.of(context)! .dayNotificationExplanation( - state.dayNotificationValue.localizedName(context), - state.dayNotificationTimeValue.format(context)), + notificationState.dayNotificationValue + .localizedName(context), + notificationState.dayNotificationTimeValue.format( + context, + ), + ), leadingIcon: Icons.calendar_month_outlined, onChanged: (bool value) => BlocProvider.of(context).add( @@ -223,7 +382,9 @@ class _NotificationScreenState extends CustomWidgetState { enabled: value, ), ), - ) + constrainedBySlots: + notificationState.timeSlotsEnabledForNotifications, + ), ], ); }, @@ -237,6 +398,7 @@ class _NotificationScreenState extends CustomWidgetState { class _CustomListTile extends StatelessWidget { final bool enabled; + final bool constrainedBySlots; final Function()? onTap; final Function(bool) onChanged; final String title; @@ -244,23 +406,62 @@ class _CustomListTile extends StatelessWidget { final IconData leadingIcon; final Color? iconColor; - const _CustomListTile( - {Key? key, - required this.enabled, - this.onTap, - this.iconColor, - required this.title, - required this.subtitle, - required this.leadingIcon, - required this.onChanged}) - : super(key: key); + const _CustomListTile({ + Key? key, + required this.enabled, + this.onTap, + this.iconColor, + required this.title, + required this.subtitle, + required this.leadingIcon, + required this.onChanged, + required this.constrainedBySlots, + }) : super(key: key); @override Widget build(BuildContext context) { return ListTile( - title: Text( - title, - style: Theme.of(context).textTheme.titleLarge, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + flex: 3, + child: Text( + title, + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.clip, + ), + ), + const SizedBox( + width: 10, + ), + Flexible( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: + CurvedAnimation(parent: animation, curve: Curves.easeIn), + child: SlideTransition( + position: Tween( + begin: const Offset(-1.0, 0.0), + end: const Offset(0.0, 0.0), + ).animate(animation), + child: child, + ), + ); + }, + child: constrainedBySlots && enabled + ? CircleAvatar( + radius: 5, + backgroundColor: + Theme.of(context).colorScheme.warningColor, + child: Container(), + ) + : const SizedBox(), + ), + ), + ], ), horizontalTitleGap: 0, subtitle: Text(subtitle), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart deleted file mode 100644 index 95edf0a3..00000000 --- a/lib/screens/settings_screen.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'dart:ui'; - -import 'package:chabo/bloc/theme/theme_bloc.dart'; -import 'package:chabo/custom_properties.dart'; -import 'package:chabo/custom_widgets_state.dart'; -import 'package:chabo/dialogs/chabo_about_dialog.dart'; -import 'package:chabo/dialogs/theme_picker_dialog.dart'; -import 'package:chabo/models/enums/theme_state_status.dart'; -import 'package:chabo/widgets/notification_tile_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class SettingsScreen extends StatefulWidget { - const SettingsScreen({Key? key}) : super(key: key); - - @override - State createState() { - return _SettingsScreenState(); - } -} - -class _SettingsScreenState extends CustomWidgetState { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.primaryContainer, - leading: const Icon( - Icons.settings, - ), - title: Text( - AppLocalizations.of(context)!.settingsTitle, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.only( - top: 20, - ), - child: Column( - children: [ - BlocBuilder( - builder: (context, state) { - return ListTile( - key: const ValueKey('themeDialog'), - title: Text( - AppLocalizations.of(context)!.themeSetting, - style: const TextStyle(fontSize: 25), - ), - subtitle: Text( - AppLocalizations.of(context)!.themeSettingSubtitle, - style: const TextStyle( - fontSize: 15, - ), - ), - selected: true, - leading: AnimatedRotation( - duration: const Duration( - milliseconds: CustomProperties.animationDurationMs), - turns: state.status == ThemeStateStatus.light ? 0 : 1, - child: Icon( - state.getIconData(), - size: 30, - ), - ), - onTap: () async => await showDialog( - context: context, - builder: ( - BuildContext context, - ) { - return BackdropFilter( - filter: ImageFilter.blur( - sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY), - child: const ThemePickerDialog(), - ); - }, - ), - ); - }, - ), - const NotificationTileWidget(), - ListTile( - key: const ValueKey('aboutButton'), - title: Text( - AppLocalizations.of(context)!.about, - style: const TextStyle(fontSize: 25), - ), - subtitle: Text( - AppLocalizations.of(context)!.informationAboutTheApp, - style: const TextStyle( - fontSize: 15, - ), - ), - selected: true, - leading: const Icon( - Icons.info_outline, - size: 30, - ), - onTap: () async => await showDialog( - context: context, - builder: (BuildContext context) { - return BackdropFilter( - filter: ImageFilter.blur( - sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY, - ), - child: ChaboAboutDialog(), - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart index 4ee75364..d05019cd 100644 --- a/lib/service/notification_service.dart +++ b/lib/service/notification_service.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:developer' as developer; import 'dart:io'; @@ -5,7 +7,7 @@ import 'package:chabo/bloc/notification/notification_bloc.dart'; import 'package:chabo/const.dart'; import 'package:chabo/extensions/date_time_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; +import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/models/enums/day.dart'; import 'package:chabo/service/storage_service.dart'; import 'package:flutter/foundation.dart'; @@ -23,8 +25,9 @@ class NotificationService { NotificationService._({required this.storageService}); - static Future create( - {required StorageService storageService}) async { + static Future create({ + required StorageService storageService, + }) async { var notificationService = NotificationService._(storageService: storageService); @@ -37,31 +40,38 @@ class NotificationService { ); /// Initialize the notification plugin - await localNotifications.initialize(initializationSettings, - onDidReceiveNotificationResponse: _onDidReceiveLocalNotification, - onDidReceiveBackgroundNotificationResponse: - _onDidReceiveBackgroundNotificationResponse); - developer.log('Notification plugin initialized', - name: 'notification-service.on.ctor'); + await localNotifications.initialize( + initializationSettings, + onDidReceiveNotificationResponse: _onDidReceiveLocalNotification, + onDidReceiveBackgroundNotificationResponse: + _onDidReceiveBackgroundNotificationResponse, + ); + developer.log( + 'Notification plugin initialized', + name: 'notification-service.on.ctor', + ); /// Wip out all existing notifications if (!kIsWeb) { await localNotifications.cancelAll(); - developer.log('Previous existing notifications cleaned', - name: 'notification-service.on.ctor'); + developer.log( + 'Previous existing notifications cleaned', + name: 'notification-service.on.ctor', + ); } + return notificationService; } static _onDidReceiveBackgroundNotificationResponse( - NotificationResponse notificationResponse) { - // WIP - } + NotificationResponse notificationResponse, + // ignore: avoid-unused-parameters + ) {} // ignore: no-empty-block static _onDidReceiveLocalNotification( - NotificationResponse notificationResponse) { - // WIP - } + NotificationResponse notificationResponse, + // ignore: avoid-unused-parameters + ) {} // ignore: no-empty-block Future _requestPermissions() async { if (Platform.isAndroid) { @@ -71,105 +81,160 @@ class NotificationService { return await androidImplementation?.requestPermission() ?? false; } + + return false; + } + + Future areNotificationsEnabled() async { + if (Platform.isAndroid) { + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + localNotifications.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + + return await androidImplementation?.areNotificationsEnabled() ?? false; + } + return false; } Future computeNotifications( - List chabanBridgeForecasts, - NotificationSate notificationSate, - BuildContext context) async { + List forecasts, + NotificationState notificationSate, + BuildContext context, + ) async { tz.initializeTimeZones(); int index = 0; - await localNotifications.cancelAll(); - List weekSeparatedChabanBridgeForecast = []; - for (final chabanBridgeForecast in chabanBridgeForecasts) { - if (notificationSate.openingNotificationEnabled) { - index += 1; - await _createOpeningScheduledNotifications( - index, chabanBridgeForecast, context); - } - if (notificationSate.closingNotificationEnabled) { - index += 1; - await _createClosingScheduledNotifications( - index, chabanBridgeForecast, context); - } - if (notificationSate.timeNotificationEnabled) { - index += 1; - await _createTimeScheduledNotifications(index, chabanBridgeForecast, - context, notificationSate.timeNotificationValue); - } - if (notificationSate.dayNotificationEnabled) { - var last = chabanBridgeForecast.circulationClosingDate - .previous(notificationSate.dayNotificationValue.weekPosition); - if (weekSeparatedChabanBridgeForecast.isEmpty || - weekSeparatedChabanBridgeForecast.last == last) { - weekSeparatedChabanBridgeForecast.add(last); - } else { + localNotifications.cancelAll(); + List weekSeparatedForecast = []; + if (await _requestPermissions()) { + for (final forecast in forecasts) { + /// Compute the slot time linked to a forecast before starting the notification computation + forecast.computeSlotInterference(notificationSate.timeSlotsValue); + final hasTimeSlots = forecast.interferingTimeSlots.isNotEmpty; + if ((notificationSate.openingNotificationEnabled && + !notificationSate.timeSlotsEnabledForNotifications) || + (notificationSate.openingNotificationEnabled && + notificationSate.timeSlotsEnabledForNotifications && + hasTimeSlots)) { index += 1; - await _createDayScheduledNotifications( + await _createOpeningScheduledNotifications( index, - weekSeparatedChabanBridgeForecast.length, - weekSeparatedChabanBridgeForecast.last, - notificationSate.dayNotificationTimeValue, + forecast, context, ); - weekSeparatedChabanBridgeForecast.clear(); - weekSeparatedChabanBridgeForecast.add(last); } - } - if (notificationSate.durationNotificationEnabled) { - index += 1; - await _createDurationScheduledNotifications( - index, - chabanBridgeForecast, - context, - notificationSate.durationNotificationValue, - notificationSate.durationNotificationValue.durationToString(context), - ); + if ((notificationSate.closingNotificationEnabled && + !notificationSate.timeSlotsEnabledForNotifications) || + (notificationSate.closingNotificationEnabled && + notificationSate.timeSlotsEnabledForNotifications && + hasTimeSlots)) { + index += 1; + await _createClosingScheduledNotifications( + index, + forecast, + context, + ); + } + if ((notificationSate.timeNotificationEnabled && + !notificationSate.timeSlotsEnabledForNotifications) || + (notificationSate.timeNotificationEnabled && + notificationSate.timeSlotsEnabledForNotifications && + hasTimeSlots)) { + index += 1; + await _createTimeScheduledNotifications( + index, + forecast, + context, + notificationSate.timeNotificationValue, + ); + } + if ((notificationSate.dayNotificationEnabled && + !notificationSate.timeSlotsEnabledForNotifications) || + (notificationSate.dayNotificationEnabled && + notificationSate.timeSlotsEnabledForNotifications && + hasTimeSlots)) { + var last = forecast.circulationClosingDate + .previous(notificationSate.dayNotificationValue.weekPosition); + if (weekSeparatedForecast.isEmpty || + weekSeparatedForecast.last == last) { + weekSeparatedForecast.add(last); + } else { + index += 1; + await _createDayScheduledNotifications( + index, + weekSeparatedForecast.length, + weekSeparatedForecast.last, + notificationSate.dayNotificationTimeValue, + context, + ); + weekSeparatedForecast.clear(); + weekSeparatedForecast.add(last); + } + } + if ((notificationSate.durationNotificationEnabled && + !notificationSate.timeSlotsEnabledForNotifications) || + (notificationSate.durationNotificationEnabled && + notificationSate.timeSlotsEnabledForNotifications && + hasTimeSlots)) { + index += 1; + await _createDurationScheduledNotifications( + index, + forecast, + context, + notificationSate.durationNotificationValue, + notificationSate.durationNotificationValue + .durationToString(context), + ); + } } } } Future _createOpeningScheduledNotifications( - int index, - AbstractChabanBridgeForecast chabanBridgeForecast, - BuildContext context) async { - final notificationScheduleTime = - chabanBridgeForecast.circulationReOpeningDate; + int index, + AbstractForecast forecast, + BuildContext context, + ) async { + final notificationScheduleTime = forecast.circulationReOpeningDate; NotificationDetails notificationDetails = _notificationDetails( - Const.notificationOpeningChannelId, - AppLocalizations.of(context)!.notificationOpeningChannelName); + Const.notificationOpeningChannelId, + AppLocalizations.of(context)!.notificationOpeningChannelName, + ); await _scheduleNotification( - index, - AppLocalizations.of(context)!.notificationOpeningTitle, - AppLocalizations.of(context)!.notificationOpeningMessage, - notificationScheduleTime, - notificationDetails); + index, + AppLocalizations.of(context)!.notificationOpeningTitle, + AppLocalizations.of(context)!.notificationOpeningMessage, + notificationScheduleTime, + notificationDetails, + ); } Future _createClosingScheduledNotifications( - int index, - AbstractChabanBridgeForecast chabanBridgeForecast, - BuildContext context) async { - final notificationScheduleTime = - chabanBridgeForecast.circulationClosingDate; + int index, + AbstractForecast forecast, + BuildContext context, + ) async { + final notificationScheduleTime = forecast.circulationClosingDate; NotificationDetails notificationDetails = _notificationDetails( - Const.notificationClosingChannelId, - AppLocalizations.of(context)!.notificationClosingChannelName); + Const.notificationClosingChannelId, + AppLocalizations.of(context)!.notificationClosingChannelName, + ); await _scheduleNotification( - index, - AppLocalizations.of(context)!.notificationClosingTitle, - chabanBridgeForecast.getNotificationClosingMessage(context), - notificationScheduleTime, - notificationDetails); + index, + AppLocalizations.of(context)!.notificationClosingTitle, + forecast.getNotificationClosingMessage(context), + notificationScheduleTime, + notificationDetails, + ); } Future _createTimeScheduledNotifications( - int index, - AbstractChabanBridgeForecast chabanBridgeForecast, - BuildContext context, - TimeOfDay value) async { - final notificationScheduleTime = chabanBridgeForecast.circulationClosingDate + int index, + AbstractForecast forecast, + BuildContext context, + TimeOfDay value, + ) async { + final notificationScheduleTime = forecast.circulationClosingDate .subtract( const Duration( days: 1, @@ -177,89 +242,110 @@ class NotificationService { ) .copyWith(hour: value.hour, minute: value.minute); NotificationDetails notificationDetails = _notificationDetails( - Const.notificationTimeChannelId, - AppLocalizations.of(context)!.notificationTimeChannelName); + Const.notificationTimeChannelId, + AppLocalizations.of(context)!.notificationTimeChannelName, + ); await _scheduleNotification( - index, - AppLocalizations.of(context)!.notificationTimeTitle, - chabanBridgeForecast.getNotificationTimeMessage(context), - notificationScheduleTime, - notificationDetails); + index, + AppLocalizations.of(context)!.notificationTimeTitle, + forecast.getNotificationTimeMessage(context), + notificationScheduleTime, + notificationDetails, + ); } Future _createDurationScheduledNotifications( - int index, - AbstractChabanBridgeForecast chabanBridgeForecast, - BuildContext context, - Duration durationValue, - String durationString) async { + int index, + AbstractForecast forecast, + BuildContext context, + Duration durationValue, + String durationString, + ) async { final notificationScheduleTime = - chabanBridgeForecast.circulationClosingDate.subtract(durationValue); + forecast.circulationClosingDate.subtract(durationValue); NotificationDetails notificationDetails = _notificationDetails( - Const.notificationDurationChannelId, - AppLocalizations.of(context)!.notificationDurationChannelName); + Const.notificationDurationChannelId, + AppLocalizations.of(context)!.notificationDurationChannelName, + ); await _scheduleNotification( - index, - AppLocalizations.of(context)!.notificationDurationTitle, - chabanBridgeForecast.getNotificationDurationMessage( - context, durationString), - notificationScheduleTime, - notificationDetails); + index, + AppLocalizations.of(context)!.notificationDurationTitle, + forecast.getNotificationDurationMessage( + context, + durationString, + ), + notificationScheduleTime, + notificationDetails, + ); } - Future _createDayScheduledNotifications(int index, int closingCount, - DateTime day, TimeOfDay timeOfDay, BuildContext context) async { + Future _createDayScheduledNotifications( + int index, + int closingCount, + DateTime day, + TimeOfDay timeOfDay, + BuildContext context, + ) async { final notificationScheduleTime = day.copyWith(hour: timeOfDay.hour, minute: timeOfDay.minute, second: 0); NotificationDetails notificationDetails = _notificationDetails( - Const.notificationDayChannelId, - AppLocalizations.of(context)!.notificationDayChannelName); + Const.notificationDayChannelId, + AppLocalizations.of(context)!.notificationDayChannelName, + ); await _scheduleNotification( - index, - AppLocalizations.of(context)!.notificationDayTitle, - AppLocalizations.of(context)!.notificationDayMessage(closingCount), - notificationScheduleTime, - notificationDetails); + index, + AppLocalizations.of(context)!.notificationDayTitle, + AppLocalizations.of(context)!.notificationDayMessage(closingCount), + notificationScheduleTime, + notificationDetails, + ); } NotificationDetails _notificationDetails( - String notificationChannelId, String notificationChannelName) { + String notificationChannelId, + String notificationChannelName, + ) { final AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( - notificationChannelId, notificationChannelName, - importance: Importance.high, - priority: Priority.max, - ongoing: true, - fullScreenIntent: true, - styleInformation: const BigTextStyleInformation(''), - ticker: Const.androidTicket); + notificationChannelId, + notificationChannelName, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + ongoing: false, + fullScreenIntent: true, + styleInformation: const BigTextStyleInformation(''), + ticker: Const.androidTicket, + ); + return NotificationDetails(android: androidNotificationDetails); } Future _scheduleNotification( - int notificationId, - String notificationTitle, - String notificationMessage, - DateTime notificationScheduleTime, - NotificationDetails notificationDetails) async { - /// Prevent from creating notification in the past - if (notificationScheduleTime.isAfter(DateTime.now()) && - await _requestPermissions()) { + int notificationId, + String notificationTitle, + String notificationMessage, + DateTime notificationScheduleTime, + NotificationDetails notificationDetails, + ) async { + /// Prevent from creating notification in the past AND make sure that the user enable the notification + if (notificationScheduleTime.isAfter(DateTime.now())) { developer.log( - 'Creating a notification on channel ${notificationDetails.android!.channelId} with ID $notificationId scheduled at $notificationScheduleTime', - name: 'notification-service.on.scheduleNotification'); + 'Creating a notification on channel ${notificationDetails.android!.channelId} with ID $notificationId scheduled at $notificationScheduleTime', + name: 'notification-service.on.scheduleNotification', + ); await localNotifications.zonedSchedule( - notificationId, - notificationTitle, - notificationMessage, - tz.TZDateTime.from( - notificationScheduleTime, - tz.local, - ), - notificationDetails, - androidAllowWhileIdle: true, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime); + notificationId, + notificationTitle, + notificationMessage, + tz.TZDateTime.from( + notificationScheduleTime, + tz.local, + ), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + ); } } } diff --git a/lib/service/storage_service.dart b/lib/service/storage_service.dart index 223d8610..8e5e0214 100644 --- a/lib/service/storage_service.dart +++ b/lib/service/storage_service.dart @@ -1,7 +1,9 @@ +import 'dart:convert'; import 'dart:developer' as developer; import 'package:chabo/models/enums/day.dart'; import 'package:chabo/models/enums/theme_state_status.dart'; +import 'package:chabo/models/time_slot.dart'; import 'package:enum_to_string/enum_to_string.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -14,61 +16,70 @@ class StorageService { Future saveString(String key, String message) async { developer.log('{$key: $message}', name: 'storage-service.on.saveString'); + return await sharedPreferences.setString(key, message); } Future saveBool(String key, bool value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveBool'); + return await sharedPreferences.setBool(key, value); } Future saveDuration(String key, Duration value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveDuration'); + return await sharedPreferences.setString(key, value.inMinutes.toString()); } Future saveDateTime(String key, DateTime value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveDuration'); + return await sharedPreferences.setString(key, value.toString()); } Future saveTimeOfDay(String key, TimeOfDay value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveTimeOfDay'); + return await sharedPreferences.setString( - key, '${value.hour.toString()}:${value.minute.toString()}'); + key, + '${value.hour.toString()}:${value.minute.toString()}', + ); } Future saveDay(String key, Day value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveDay'); + return await sharedPreferences.setString(key, value.name); } Future saveTheme(String key, ThemeStateStatus value) async { developer.log('{$key: $value}', name: 'storage-service.on.saveTheme'); + return await sharedPreferences.setString(key, value.name); } + Future saveTimeSlots(String key, List timeSlots) async { + developer.log( + '{$key: $timeSlots}', + name: 'storage-service.on.saveTimeSlots', + ); + + return await sharedPreferences.setString(key, jsonEncode(timeSlots)); + } + String? readString(String key) { final value = sharedPreferences.getString(key); developer.log('{$key: $value}', name: 'storage-service.on.readString'); + return value; } bool? readBool(String key) { final value = sharedPreferences.getBool(key); developer.log('{$key: $value}', name: 'storage-service.on.readBool'); - return value; - } - DateTime? reaDateTime(String key) { - final stringValue = sharedPreferences.getString(key); - if (stringValue == null) { - return null; - } else { - final value = DateTime.parse(sharedPreferences.getString(key)!); - developer.log('{$key: $value}', name: 'storage-service.on.readDuration'); - return value; - } + return value; } Duration? readDuration(String key) { @@ -79,6 +90,7 @@ class StorageService { final value = Duration(minutes: int.parse(sharedPreferences.getString(key)!)); developer.log('{$key: $value}', name: 'storage-service.on.readDuration'); + return value; } } @@ -89,8 +101,10 @@ class StorageService { return null; } else { final value = TimeOfDay.fromDateTime( - DateFormat('hh:mm').parse(sharedPreferences.getString(key)!)); + DateFormat('hh:mm').parse(sharedPreferences.getString(key)!), + ); developer.log('{$key: $value}', name: 'storage-service.on.readTimeOfDay'); + return value; } } @@ -102,6 +116,7 @@ class StorageService { } else { final value = EnumToString.fromString(Day.values, stringValue); developer.log('{$key: $value}', name: 'storage-service.on.readDay'); + return value; } } @@ -114,7 +129,25 @@ class StorageService { final value = EnumToString.fromString(ThemeStateStatus.values, stringValue); developer.log('{$key: $value}', name: 'storage-service.on.readTheme'); + return value; } } + + List? readTimeSlots(String key) { + final stringValue = sharedPreferences.getString(key); + if (stringValue == null) { + return null; + } else { + final list = json.decode(stringValue); + final List timeSlotList = + list.map((item) => TimeSlot.fromJSON(item)).toList(); + developer.log( + '{$key: $timeSlotList', + name: 'storage-service.on.readTimeSlots', + ); + + return timeSlotList; + } + } } diff --git a/lib/widgets/ad_banner_widget.dart b/lib/widgets/ad_banner_widget.dart index b6253ac9..3822e6da 100644 --- a/lib/widgets/ad_banner_widget.dart +++ b/lib/widgets/ad_banner_widget.dart @@ -31,8 +31,10 @@ class _AdBannerWidgetState extends State { ); }, onAdFailedToLoad: (ad, error) { - developer.log('Enable to load the ad : ${error.message}', - name: 'banner-widget'); + developer.log( + 'Enable to load the ad : ${error.message}', + name: 'banner-widget', + ); _ad?.dispose(); }, ), diff --git a/lib/widgets/floating_actions/floating_action_item.dart b/lib/widgets/floating_actions/floating_actions_item.dart similarity index 68% rename from lib/widgets/floating_actions/floating_action_item.dart rename to lib/widgets/floating_actions/floating_actions_item.dart index d1b2cddf..b8dba8b5 100644 --- a/lib/widgets/floating_actions/floating_action_item.dart +++ b/lib/widgets/floating_actions/floating_actions_item.dart @@ -4,15 +4,15 @@ class FloatingActionsItem extends StatelessWidget { final bool isSpaced; final List content; final bool isRightHanded; - final Function()? onPressed; + final Function() onPressed; - const FloatingActionsItem( - {Key? key, - required this.isRightHanded, - this.onPressed, - required this.content, - required this.isSpaced}) - : super(key: key); + const FloatingActionsItem({ + Key? key, + required this.isRightHanded, + required this.onPressed, + required this.content, + required this.isSpaced, + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/widgets/floating_actions/floating_actions_widget.dart b/lib/widgets/floating_actions/floating_actions_widget.dart index cbb84889..a89555e4 100644 --- a/lib/widgets/floating_actions/floating_actions_widget.dart +++ b/lib/widgets/floating_actions/floating_actions_widget.dart @@ -4,7 +4,7 @@ import 'package:chabo/cubits/floating_actions_cubit.dart'; import 'package:chabo/custom_properties.dart'; import 'package:chabo/dialogs/chabo_about_dialog.dart'; import 'package:chabo/screens/notification_screen.dart'; -import 'package:chabo/widgets/floating_actions/floating_action_item.dart'; +import 'package:chabo/widgets/floating_actions/floating_actions_item.dart'; import 'package:chabo/widgets/theme_switcher_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,8 +41,10 @@ class _FloatingActionsWidgetState extends State ), transitionBuilder: (Widget child, Animation animation) { return FadeTransition( - opacity: - CurvedAnimation(parent: animation, curve: Curves.easeIn), + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeIn, + ), child: SlideTransition( position: Tween( begin: const Offset(0.0, 1.0), @@ -95,7 +97,7 @@ class _FloatingActionsWidgetState extends State ), ), builder: (context) { - return const TheSwitcherWidget(); + return const ThemeSwitcherWidget(); }, ); context @@ -104,7 +106,9 @@ class _FloatingActionsWidgetState extends State }, content: [ Text(AppLocalizations.of(context)!.themeSetting), - const Icon(Icons.format_paint_rounded) + const Icon( + Icons.format_paint_rounded, + ), ], isRightHanded: state.isRightHanded, isSpaced: true, @@ -116,8 +120,12 @@ class _FloatingActionsWidgetState extends State pageBuilder: (context, animation1, animation2) => const NotificationScreen(), - transitionsBuilder: (context, animation, - secondaryAnimation, child) { + transitionsBuilder: ( + context, + animation, + secondaryAnimation, + child, + ) { const begin = Offset(0.0, 1.0); const end = Offset.zero; const curve = Curves.ease; @@ -217,11 +225,12 @@ class _FloatingActionsWidgetState extends State end: const Offset(0.0, 0.0), ).animate(animation), child: FadeTransition( - opacity: CurvedAnimation( - parent: animation, - curve: Curves.easeIn, - ), - child: child), + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeIn, + ), + child: child, + ), ); }, child: state.isMenuOpen diff --git a/lib/widgets/forecast/forecast_list_item_widget.dart b/lib/widgets/forecast/forecast_list_item_widget.dart index d5bf5c8c..7643782c 100644 --- a/lib/widgets/forecast/forecast_list_item_widget.dart +++ b/lib/widgets/forecast/forecast_list_item_widget.dart @@ -1,57 +1,63 @@ import 'dart:ui'; import 'package:chabo/custom_properties.dart'; -import 'package:chabo/dialogs/chaban_bridge_forecast_information_dialog.dart'; +import 'package:chabo/dialogs/forecast_information_dialog.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; +import 'package:chabo/models/abstract_forecast.dart'; +import 'package:chabo/models/time_slot.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class ForecastListItemWidget extends StatelessWidget { - final AbstractChabanBridgeForecast chabanBridgeForecast; + final AbstractForecast forecast; final Function()? onTap; final bool hasPassed; + final List timeSlots; final bool isCurrent; final int index; - const ForecastListItemWidget( - {Key? key, - required this.chabanBridgeForecast, - required this.index, - required this.hasPassed, - required this.isCurrent, - this.onTap}) - : super(key: key); + const ForecastListItemWidget({ + Key? key, + required this.forecast, + required this.index, + required this.hasPassed, + required this.isCurrent, + this.onTap, + required this.timeSlots, + }) : super(key: key); @override Widget build(BuildContext context) { - return Stack( - children: [ - Card( - shape: isCurrent - ? RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular( - 12, - ), - ), - side: BorderSide( - // border color - color: chabanBridgeForecast.getColor(context, false), - // border thickness - width: 2, - ), - ) - : null, - child: InkWell( - borderRadius: const BorderRadius.all( - Radius.circular( - 12, + final textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.all(5), + child: Stack( + children: [ + ElevatedButton( + style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(CustomProperties.borderRadius), + side: isCurrent + ? BorderSide( + width: 2, + color: forecast.getColor(context, false), + ) + : BorderSide.none, + ), + ), + padding: MaterialStateProperty.all( + const EdgeInsets.only( + right: 0, + ), ), ), - onTap: onTap ?? + onPressed: onTap ?? () async => { await showGeneralDialog( context: context, @@ -65,10 +71,11 @@ class ForecastListItemWidget extends StatelessWidget { Tween(begin: 0.0, end: 1.0).animate(a1), child: BackdropFilter( filter: ImageFilter.blur( - sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY), - child: ChabanBridgeForecastInformationDialog( - chabanBridgeForecast: chabanBridgeForecast, + sigmaX: CustomProperties.blurSigmaX, + sigmaY: CustomProperties.blurSigmaY, + ), + child: ForecastInformationDialog( + forecast: forecast, ), ), ); @@ -78,36 +85,65 @@ class ForecastListItemWidget extends StatelessWidget { transitionDuration: const Duration( milliseconds: 300, ), - ) + ), }, - child: ListTile( - horizontalTitleGap: 0, - leading: chabanBridgeForecast.getIconWidget(context, false), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Container( + width: 55, + height: 60, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular( + CustomProperties.borderRadius, + ), + bottomLeft: Radius.circular( + CustomProperties.borderRadius, + ), + ), + color: forecast.getColor( + context, + false, + ), + ), + child: Center( + child: forecast.getIconWidget( + context, + true, + ), + ), + ), Flexible( + flex: 2, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const Icon(Icons.block_rounded, - size: 20, color: Colors.red), + const Icon( + Icons.block_rounded, + size: 18, + color: Colors.red, + ), Text( - chabanBridgeForecast.circulationClosingDateString( - context, + MaterialLocalizations.of(context) + .formatMediumDate( + forecast.circulationClosingDate, ), + style: textTheme.bodySmall, ), ], ), Text( - MaterialLocalizations.of(context).formatMediumDate( - chabanBridgeForecast.circulationClosingDate, + forecast.circulationClosingDateString( + context, ), - style: Theme.of(context).textTheme.bodyMedium, - ) + style: textTheme.headlineSmall, + ), ], ), ), @@ -117,8 +153,7 @@ class ForecastListItemWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - chabanBridgeForecast.closedDuration - .durationToString(context), + forecast.closedDuration.durationToString(context), style: TextStyle( color: Theme.of(context).colorScheme.timeColor, fontWeight: FontWeight.bold, @@ -133,53 +168,85 @@ class ForecastListItemWidget extends StatelessWidget { ), ), Flexible( + flex: 2, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const Icon(Icons.check_circle, - size: 20, color: Colors.green), + const Icon( + Icons.check_circle, + size: 18, + color: Colors.green, + ), Text( - chabanBridgeForecast - .circulationReOpeningDateString( - context, + MaterialLocalizations.of(context) + .formatMediumDate( + forecast.circulationReOpeningDate, ), + style: textTheme.bodySmall, ), ], ), Text( - MaterialLocalizations.of(context).formatMediumDate( - chabanBridgeForecast.circulationReOpeningDate, + forecast.circulationReOpeningDateString( + context, ), - style: Theme.of(context).textTheme.bodyMedium, - ) + style: textTheme.headlineSmall, + ), ], ), ), + timeSlots.isNotEmpty + ? Container( + width: 30, + height: 60, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular( + CustomProperties.borderRadius, + ), + bottomRight: Radius.circular( + CustomProperties.borderRadius, + ), + ), + color: Theme.of(context).colorScheme.warningColor, + ), + child: Icon( + Icons.warning_rounded, + size: 20, + color: Theme.of(context).cardColor, + ), + ) + : Container( + width: 15, + ), ], ), ), ), - ), - if (hasPassed) - Positioned.fill( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur( + if (hasPassed) + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY), - child: Center( - child: Text( - AppLocalizations.of(context)!.passedClosure, - style: const TextStyle(fontWeight: FontWeight.bold), + sigmaY: CustomProperties.blurSigmaY, + ), + child: Center( + child: Text( + AppLocalizations.of(context)!.passedClosure, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), ), ), ), ), - ), - ], + ], + ), ); } } diff --git a/lib/widgets/forecast/forecast_list_widget.dart b/lib/widgets/forecast/forecast_list_widget.dart index 2577fa42..d825ec22 100644 --- a/lib/widgets/forecast/forecast_list_widget.dart +++ b/lib/widgets/forecast/forecast_list_widget.dart @@ -1,9 +1,8 @@ -import 'package:chabo/bloc/chaban_bridge_forecast/chaban_bridge_forecast_bloc.dart'; +import 'package:chabo/bloc/forecast/forecast_bloc.dart'; +import 'package:chabo/bloc/notification/notification_bloc.dart'; import 'package:chabo/bloc/scroll_status/scroll_status_bloc.dart'; -import 'package:chabo/models/abstract_chaban_bridge_forecast.dart'; -import 'package:chabo/widgets/ad_banner_widget.dart'; +import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/widgets/forecast/forecast_list_item_widget.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; @@ -27,47 +26,59 @@ class _ForecastListWidgetState extends State { ScrollStatusChanged(), ); } + return true; }, - child: BlocBuilder( - builder: (context, state) { - return ListView.separated( - cacheExtent: 5000, - padding: const EdgeInsets.all(0), - itemBuilder: (BuildContext context, int index) { - return ForecastListItemWidget( - key: GlobalObjectKey( - state.chabanBridgeForecasts[index].hashCode), - isCurrent: state.chabanBridgeForecasts[index] == - state.currentChabanBridgeForecast, - hasPassed: state - .chabanBridgeForecasts[index].circulationReOpeningDate - .isBefore(DateTime.now()), - chabanBridgeForecast: state.chabanBridgeForecasts[index], - index: index); - }, - itemCount: state.chabanBridgeForecasts.length, - controller: - BlocProvider.of(context).scrollController, - separatorBuilder: (BuildContext context, int index) { - if ((index + 1 <= state.chabanBridgeForecasts.length && - state.chabanBridgeForecasts[index].circulationClosingDate - .month != - state.chabanBridgeForecasts[index + 1] - .circulationClosingDate.month)) { - return _MonthWidget( - chabanBridgeForecast: state.chabanBridgeForecasts[index + 1], - ); - } - if (((index % 10 == 0 || - index == - state.chabanBridgeForecasts.indexOf( - state.currentChabanBridgeForecast!)) && - index != 0) && - !kIsWeb) { - return const AdBannerWidget(); - } - return const SizedBox.shrink(); + child: BlocBuilder( + builder: (context, forecastState) { + return BlocBuilder( + buildWhen: (previous, next) => + previous.timeSlotsValue != next.timeSlotsValue, + builder: (context, timeSlotState) { + return ListView.separated( + cacheExtent: 5000, + padding: const EdgeInsets.symmetric(horizontal: 5).copyWith( + bottom: 200, + ), + itemBuilder: ( + BuildContext context, + int index, + ) { + forecastState.forecasts[index] + .computeSlotInterference(timeSlotState.timeSlotsValue); + + return ForecastListItemWidget( + key: GlobalObjectKey( + forecastState.forecasts[index].hashCode, + ), + isCurrent: forecastState.forecasts[index] == + forecastState.currentForecast, + hasPassed: forecastState + .forecasts[index].circulationReOpeningDate + .isBefore(DateTime.now()), + forecast: forecastState.forecasts[index], + index: index, + timeSlots: + forecastState.forecasts[index].interferingTimeSlots, + ); + }, + itemCount: forecastState.forecasts.length, + controller: + BlocProvider.of(context).scrollController, + separatorBuilder: (BuildContext context, int index) { + if ((index + 1 <= forecastState.forecasts.length && + forecastState + .forecasts[index].circulationClosingDate.month != + forecastState.forecasts[index + 1] + .circulationClosingDate.month)) { + return _MonthWidget( + forecast: forecastState.forecasts[index + 1], + ); + } + + return const SizedBox.shrink(); + }, + ); }, ); }, @@ -77,10 +88,9 @@ class _ForecastListWidgetState extends State { } class _MonthWidget extends StatelessWidget { - final AbstractChabanBridgeForecast chabanBridgeForecast; + final AbstractForecast forecast; - const _MonthWidget({Key? key, required this.chabanBridgeForecast}) - : super(key: key); + const _MonthWidget({Key? key, required this.forecast}) : super(key: key); @override Widget build(BuildContext context) { @@ -95,7 +105,7 @@ class _MonthWidget extends StatelessWidget { child: Text( DateFormat.MMMM(Localizations.localeOf(context).languageCode) .format( - chabanBridgeForecast.circulationClosingDate, + forecast.circulationClosingDate, ), style: const TextStyle(fontWeight: FontWeight.bold), ), diff --git a/lib/widgets/forecast/status_widget.dart b/lib/widgets/forecast/status_widget.dart index ad741d06..a58451c7 100644 --- a/lib/widgets/forecast/status_widget.dart +++ b/lib/widgets/forecast/status_widget.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:chabo/bloc/chaban_bridge_status/chaban_bridge_status_bloc.dart'; import 'package:chabo/bloc/scroll_status/scroll_status_bloc.dart'; +import 'package:chabo/bloc/status/status_bloc.dart'; import 'package:chabo/custom_properties.dart'; -import 'package:chabo/custom_widgets_state.dart'; +import 'package:chabo/custom_widget_state.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/widgets/forecast/forecast_list_item_widget.dart'; import 'package:chabo/widgets/progress_indicator/custom_circular_progress_indicator.dart'; @@ -29,8 +29,8 @@ class StatusWidgetState extends CustomWidgetState { (_) { Timer.periodic( const Duration(seconds: 1), - (Timer t) => BlocProvider.of(context).add( - ChabanBridgeStatusRefresh( + (Timer t) => BlocProvider.of(context).add( + StatusRefresh( context: context, ), ), @@ -42,7 +42,7 @@ class StatusWidgetState extends CustomWidgetState { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return AnimatedSize( curve: Curves.ease, @@ -56,11 +56,11 @@ class StatusWidgetState extends CustomWidgetState { child: child, ); }, - child: state.chabanBridgeStatusLifecycle == - ChabanBridgeStatusLifecycle.empty + child: state.statusLifecycle == StatusLifecycle.empty ? Padding( padding: EdgeInsets.symmetric( - vertical: MediaQuery.of(context).size.height / 5), + vertical: MediaQuery.of(context).size.height / 5, + ), child: CustomCircularProgressIndicator( message: AppLocalizations.of(context)!.statusLoadMessage, ), @@ -90,7 +90,9 @@ class StatusWidgetState extends CustomWidgetState { transitionBuilder: (Widget child, Animation animation) { return FadeTransition( - opacity: animation, child: child); + opacity: animation, + child: child, + ); }, child: Text( state.mainMessageStatus, @@ -127,7 +129,9 @@ class StatusWidgetState extends CustomWidgetState { state.completionPercentage != -1 ? Padding( padding: const EdgeInsets.symmetric( - horizontal: 20.0, vertical: 5), + horizontal: 20.0, + vertical: 5, + ), child: SizedBox( height: 10, child: ClipRRect( @@ -175,17 +179,17 @@ class StatusWidgetState extends CustomWidgetState { child: ForecastListItemWidget( onTap: () => BlocProvider.of( - context) - .add( + context, + ).add( GoTo( goTo: state.currentTarget, ), ), hasPassed: false, isCurrent: true, - chabanBridgeForecast: - state.currentTarget!, + forecast: state.currentTarget!, index: -1, + timeSlots: const [], ), ) : const SizedBox.shrink(), diff --git a/lib/widgets/notification_tile_widget.dart b/lib/widgets/notification_tile_widget.dart deleted file mode 100644 index c1904f0c..00000000 --- a/lib/widgets/notification_tile_widget.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:chabo/screens/notification_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class NotificationTileWidget extends StatelessWidget { - const NotificationTileWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - key: const ValueKey('notificationButton'), - title: Text( - AppLocalizations.of(context)!.notifications, - style: const TextStyle(fontSize: 25), - ), - subtitle: Text( - AppLocalizations.of(context)!.notificationsSubtitle, - style: const TextStyle( - fontSize: 15, - ), - ), - selected: true, - leading: const Icon( - Icons.notifications_active_outlined, - size: 30, - ), - onTap: () async => Navigator.of(context).push( - PageRouteBuilder( - pageBuilder: (context, animation1, animation2) => - const NotificationScreen(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(1.0, 0.0); - const end = Offset.zero; - const curve = Curves.ease; - - var tween = Tween(begin: begin, end: end).chain( - CurveTween( - curve: curve, - ), - ); - - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - ), - ), - ); - } -} diff --git a/lib/widgets/progress_indicator/custom_progress_bar_indicator.dart b/lib/widgets/progress_indicator/custom_progress_bar_indicator.dart index 18060336..4bd6ede0 100644 --- a/lib/widgets/progress_indicator/custom_progress_bar_indicator.dart +++ b/lib/widgets/progress_indicator/custom_progress_bar_indicator.dart @@ -5,9 +5,12 @@ class CustomProgressBarIndicator extends StatelessWidget { final double current; final Color color; - const CustomProgressBarIndicator( - {Key? key, required this.max, required this.current, required this.color}) - : super(key: key); + const CustomProgressBarIndicator({ + Key? key, + required this.max, + required this.current, + required this.color, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -15,6 +18,7 @@ class CustomProgressBarIndicator extends StatelessWidget { builder: (_, boxConstraints) { var x = boxConstraints.maxWidth; var percent = (current / max) * x; + return Stack( alignment: Alignment.centerLeft, children: [ diff --git a/lib/widgets/theme_switcher_widget.dart b/lib/widgets/theme_switcher_widget.dart index f904b1bf..c6aee3b1 100644 --- a/lib/widgets/theme_switcher_widget.dart +++ b/lib/widgets/theme_switcher_widget.dart @@ -5,11 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class TheSwitcherWidget extends StatelessWidget { - const TheSwitcherWidget({Key? key}) : super(key: key); +class ThemeSwitcherWidget extends StatelessWidget { + const ThemeSwitcherWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return BlocBuilder( builder: (context, state) { return Padding( @@ -31,18 +33,18 @@ class TheSwitcherWidget extends StatelessWidget { values: const [ ThemeStateStatus.light, ThemeStateStatus.dark, - ThemeStateStatus.system + ThemeStateStatus.system, ], - indicatorColor: Theme.of(context).colorScheme.tertiary, - innerColor: Theme.of(context).colorScheme.primaryContainer, - borderColor: Theme.of(context).colorScheme.primary, + indicatorColor: colorScheme.tertiary, + innerColor: colorScheme.primaryContainer, + borderColor: colorScheme.primary, indicatorSize: const Size.fromWidth(65), iconBuilder: (value, size) { return Icon( value.icon, color: state.status == value - ? Theme.of(context).colorScheme.onTertiary - : Theme.of(context).colorScheme.onPrimaryContainer, + ? colorScheme.onTertiary + : colorScheme.onPrimaryContainer, ); }, onChanged: (value) => BlocProvider.of(context).add( @@ -65,11 +67,12 @@ class TheSwitcherWidget extends StatelessWidget { end: const Offset(0.0, 0.0), ).animate(animation), child: FadeTransition( - opacity: CurvedAnimation( - parent: animation, - curve: Curves.easeIn, - ), - child: child), + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeIn, + ), + child: child, + ), ); }, child: Text( diff --git a/lib/widgets/time_slot_widget.dart b/lib/widgets/time_slot_widget.dart new file mode 100644 index 00000000..dbaf5a53 --- /dev/null +++ b/lib/widgets/time_slot_widget.dart @@ -0,0 +1,68 @@ +import 'dart:ui'; + +import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/custom_properties.dart'; +import 'package:chabo/dialogs/time_slot_dialog.dart'; +import 'package:chabo/models/time_slot.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class TimeSlotWidget extends StatelessWidget { + final TimeSlot timeSlot; + final int index; + + const TimeSlotWidget({Key? key, required this.timeSlot, required this.index}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: ( + BuildContext context, + ) { + return BackdropFilter( + filter: ImageFilter.blur( + sigmaX: CustomProperties.blurSigmaX, + sigmaY: CustomProperties.blurSigmaY, + ), + child: TimeSlotDialog( + index: index, + ), + ); + }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + DayNotificationValueEvent(day: value), + ), + }, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + timeSlot.name != '' + ? timeSlot.name + : AppLocalizations.of(context)! + .favoriteTimeSlotDefaultName(index + 1), + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '${timeSlot.from.format(context)} - ${timeSlot.to.format(context)}', + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ), + ); + } +} diff --git a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 987dc6a7..57995472 100644 --- a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -1,10 +1,10 @@ // This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=C:\src\flutter -FLUTTER_APPLICATION_PATH=C:\Users\Valentin\Documents\Projets\chabo +FLUTTER_ROOT=D:\src\Flutter +FLUTTER_APPLICATION_PATH=D:\Code\chabo COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.1.0 -FLUTTER_BUILD_NUMBER=1.1.0 +FLUTTER_BUILD_NAME=1.6.0 +FLUTTER_BUILD_NUMBER=1.6.0 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false diff --git a/macos/Flutter/ephemeral/flutter_export_environment.sh b/macos/Flutter/ephemeral/flutter_export_environment.sh index fb71f946..eb328ef2 100644 --- a/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -1,11 +1,11 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=C:\src\flutter" -export "FLUTTER_APPLICATION_PATH=C:\Users\Valentin\Documents\Projets\chabo" +export "FLUTTER_ROOT=D:\src\Flutter" +export "FLUTTER_APPLICATION_PATH=D:\Code\chabo" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.1.0" -export "FLUTTER_BUILD_NUMBER=1.1.0" +export "FLUTTER_BUILD_NAME=1.6.0" +export "FLUTTER_BUILD_NUMBER=1.6.0" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/pubspec.lock b/pubspec.lock index d07d37c3..bca39eb5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" + url: "https://pub.dev" + source: hosted + version: "59.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 + url: "https://pub.dev" + source: hosted + version: "5.11.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" animated_toggle_switch: dependency: "direct main" description: @@ -9,6 +33,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.2" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + app_settings: + dependency: "direct main" + description: + name: app_settings + sha256: "66715a323ac36d6c8201035ba678777c0d2ea869e4d7064300d95af10c3bb8cb" + url: "https://pub.dev" + source: hosted + version: "4.2.0" archive: dependency: transitive description: @@ -113,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -121,6 +169,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics + sha256: "162c81dbd0a2ba182f38ca615335f3e8878f212ec7beea83d6bfad4e99eb541a" + url: "https://pub.dev" + source: hosted + version: "5.7.3" + dart_code_metrics_presets: + dependency: transitive + description: + name: dart_code_metrics_presets + sha256: "22e27f98e8c7d8b11cca43d2656a822935280747050ae65e8cd03c52d09c0d1c" + url: "https://pub.dev" + source: hosted + version: "1.7.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" dbus: dependency: transitive description: @@ -202,26 +274,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.0.0+1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7" url: "https://pub.dev" source: hosted - version: "3.0.0+1" + version: "4.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -253,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.4.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" google_fonts: dependency: "direct main" description: @@ -265,10 +345,18 @@ packages: dependency: "direct main" description: name: google_mobile_ads - sha256: "7a39fe63007764115395550fc3eaf0d469de685298208040138c373d5444ab48" + sha256: "24ee4e9546866cc15ebe565dabf6a6c485ce5fbec0645fde22799800532000f0" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "3.0.0" + html: + dependency: transitive + description: + name: html + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" + url: "https://pub.dev" + source: hosted + version: "0.15.3" http: dependency: "direct main" description: @@ -365,6 +453,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -485,6 +581,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c" + url: "https://pub.dev" + source: hosted + version: "0.3.0" shared_preferences: dependency: "direct main" description: @@ -682,6 +794,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -698,6 +818,46 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "1a37bdbaaf5fbe09ad8579ab09ecfd473284ce482f900b5aea27cf834386a567" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: d6cf18cd6c809c5a9294cd99707a21986aac4e08c87e1916ce2590315fb55d3a + url: "https://pub.dev" + source: hosted + version: "3.6.2" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "78715dc442b7849dbde74e92bb67de1cecf5addf95531c5fb474e72f5fe9a507" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: c94d242d8cbe1012c06ba7ac790c46d6e6b68723b7d34f8c74ed19f68d166f49 + url: "https://pub.dev" + source: hosted + version: "3.4.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f05347ba..6dc9baa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Mobile app to get the closing and opening schedules of the Chaban D publish_to: 'none' -version: 1.4.0 +version: 1.6.1 environment: sdk: '>=2.17.6 <3.0.0' @@ -21,11 +21,11 @@ dependencies: flutter_bloc: ^8.0.0 flutter_localizations: sdk: flutter - flutter_local_notifications: ^13.0.0 + flutter_local_notifications: ^14.0.0 flutter_markdown: ^0.6.14 font_awesome_flutter: ^10.1.0 google_fonts: ^4.0.0 - google_mobile_ads: ^2.3.0 + google_mobile_ads: ^3.0.0 http: ^0.13.0 intl: ^0.17.0 package_info_plus: ^3.0.3 @@ -33,6 +33,7 @@ dependencies: stream_transform: ^2.0.0 timezone: ^0.9.1 url_launcher: ^6.1.7 + app_settings: ^4.2.0 dev_dependencies: @@ -40,6 +41,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.0 flutter_launcher_icons: ^0.13.0 + dart_code_metrics: ^5.7.3 flutter: generate: true