From 8f57fdae8e2fad51779421204cbe5bcb9c80ede1 Mon Sep 17 00:00:00 2001 From: Paul Pickhardt <33689888+PaulPickhardt@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:16:47 +0100 Subject: [PATCH] Removes the gamification (#360) * Remove gamification * set canUseGamification false * remove unused packages and set enable gamification default false --- .../challenges/models/challenges_profile.dart | 47 - lib/gamification/challenges/models/level.dart | 32 - .../challenges/models/profile_upgrade.dart | 80 - .../services/challenge_service.dart | 237 --- .../services/challenges_profile_service.dart | 142 -- .../challenges/utils/challenge_generator.dart | 250 --- .../challenges/utils/challenge_validator.dart | 152 -- .../challenges/views/challenges_card.dart | 111 -- .../challenges/views/challenges_tutorial.dart | 32 - .../views/profile/lvl_up_dialog.dart | 91 -- .../multiple_upgrades_lvl_up.dart.dart | 113 -- .../views/profile/profile_view.dart | 341 ---- .../views/profile/single_upgrade_lvl_up.dart | 45 - .../progress_bar/challenge_reward_dialog.dart | 91 -- .../challenge_selection_dialog.dart | 159 -- .../views/progress_bar/progress_bar.dart | 291 ---- .../progress_bar/single_challenge_dialog.dart | 82 - lib/gamification/common/colors.dart | 17 - .../common/custom_game_icons.dart | 45 - .../common/database/database.dart | 36 - .../common/database/database.g.dart | 1390 ----------------- .../common/database/database_dao.dart | 63 - .../achieved_location/achieved_location.dart | 47 - .../achieved_location.g.dart | 8 - .../database/model/challenges/challenge.dart | 61 - .../model/challenges/challenge.g.dart | 8 - .../model/event_badge/event_badge.dart | 42 - .../model/event_badge/event_badge.g.dart | 8 - .../model/ride_summary/ride_summary.dart | 121 -- .../model/ride_summary/ride_summary.g.dart | 8 - .../common/models/evaluation_data.dart | 22 - .../common/models/user_profile.dart | 44 - .../services/evaluation_data_service.dart | 78 - .../common/services/user_service.dart | 188 --- lib/gamification/common/utils.dart | 153 -- .../common/views/blink_animation.dart | 64 - .../common/views/confirm_button.dart | 43 - .../common/views/countdown_timer.dart | 62 - .../common/views/custom_dialog.dart | 78 - .../common/views/dialog_button.dart | 44 - .../common/views/feature_card.dart | 384 ----- .../common/views/map_background.dart | 65 - .../common/views/on_tap_animation.dart | 87 -- .../common/views/progress_ring.dart | 161 -- .../common/views/tutorial_page.dart | 148 -- .../community_event/model/event.dart | 30 - .../community_event/model/location.dart | 29 - .../model/shortcut_event_location.dart | 122 -- .../service/event_service.dart | 251 --- .../community_event/views/badge.dart | 63 - .../views/badge_collection.dart | 85 - .../community_event/views/event_card.dart | 302 ---- .../community_event/views/event_page.dart | 474 ------ .../goals/models/daily_goals.dart | 29 - .../goals/models/route_goals.dart | 27 - .../goals/services/goals_service.dart | 95 -- .../goals/views/edit_daily_goals.dart | 250 --- .../goals/views/edit_route_goals.dart | 188 --- lib/gamification/goals/views/goals_view.dart | 99 -- .../goals/views/weekday_button.dart | 44 - lib/gamification/intro/intro_card.dart | 117 -- lib/gamification/intro/intro_page.dart | 90 -- lib/gamification/main.dart | 135 -- .../statistics/models/ride_stats.dart | 231 --- .../statistics/models/stat_type.dart | 21 - .../services/statistics_service.dart | 51 - .../statistics/services/stats_view_model.dart | 132 -- .../statistics/views/card/daily_overview.dart | 162 -- .../statistics/views/card/route_stats.dart | 143 -- .../statistics/views/card/stats_card.dart | 213 --- .../statistics/views/graphs/month_graph.dart | 34 - .../views/graphs/multiple_weeks_graph.dart | 35 - .../views/graphs/ride_stats_graph.dart | 171 -- .../statistics/views/graphs/week_graph.dart | 34 - .../statistics/views/overall_stats.dart | 104 -- .../views/page/ride_graphs_page_view.dart | 205 --- .../views/page/route_goals_history.dart | 160 -- .../statistics/views/page/stats_page.dart | 254 --- .../statistics/views/route_goals_in_week.dart | 71 - .../statistics/views/stats_tutorial.dart | 30 - lib/home/views/main.dart | 8 - lib/loader.dart | 4 - lib/main.dart | 16 - lib/ride/views/finish_button.dart | 4 - lib/settings/services/settings.dart | 2 +- lib/settings/views/internal.dart | 23 - lib/statistics/services/statistics.dart | 10 - lib/tracking/models/track.dart | 2 +- pubspec.lock | 28 +- pubspec.yaml | 7 - 90 files changed, 4 insertions(+), 10352 deletions(-) delete mode 100644 lib/gamification/challenges/models/challenges_profile.dart delete mode 100644 lib/gamification/challenges/models/level.dart delete mode 100644 lib/gamification/challenges/models/profile_upgrade.dart delete mode 100644 lib/gamification/challenges/services/challenge_service.dart delete mode 100644 lib/gamification/challenges/services/challenges_profile_service.dart delete mode 100644 lib/gamification/challenges/utils/challenge_generator.dart delete mode 100644 lib/gamification/challenges/utils/challenge_validator.dart delete mode 100644 lib/gamification/challenges/views/challenges_card.dart delete mode 100644 lib/gamification/challenges/views/challenges_tutorial.dart delete mode 100644 lib/gamification/challenges/views/profile/lvl_up_dialog.dart delete mode 100644 lib/gamification/challenges/views/profile/multiple_upgrades_lvl_up.dart.dart delete mode 100644 lib/gamification/challenges/views/profile/profile_view.dart delete mode 100644 lib/gamification/challenges/views/profile/single_upgrade_lvl_up.dart delete mode 100644 lib/gamification/challenges/views/progress_bar/challenge_reward_dialog.dart delete mode 100644 lib/gamification/challenges/views/progress_bar/challenge_selection_dialog.dart delete mode 100644 lib/gamification/challenges/views/progress_bar/progress_bar.dart delete mode 100644 lib/gamification/challenges/views/progress_bar/single_challenge_dialog.dart delete mode 100644 lib/gamification/common/colors.dart delete mode 100644 lib/gamification/common/custom_game_icons.dart delete mode 100644 lib/gamification/common/database/database.dart delete mode 100644 lib/gamification/common/database/database.g.dart delete mode 100644 lib/gamification/common/database/database_dao.dart delete mode 100644 lib/gamification/common/database/model/achieved_location/achieved_location.dart delete mode 100644 lib/gamification/common/database/model/achieved_location/achieved_location.g.dart delete mode 100644 lib/gamification/common/database/model/challenges/challenge.dart delete mode 100644 lib/gamification/common/database/model/challenges/challenge.g.dart delete mode 100644 lib/gamification/common/database/model/event_badge/event_badge.dart delete mode 100644 lib/gamification/common/database/model/event_badge/event_badge.g.dart delete mode 100644 lib/gamification/common/database/model/ride_summary/ride_summary.dart delete mode 100644 lib/gamification/common/database/model/ride_summary/ride_summary.g.dart delete mode 100644 lib/gamification/common/models/evaluation_data.dart delete mode 100644 lib/gamification/common/models/user_profile.dart delete mode 100644 lib/gamification/common/services/evaluation_data_service.dart delete mode 100644 lib/gamification/common/services/user_service.dart delete mode 100644 lib/gamification/common/utils.dart delete mode 100644 lib/gamification/common/views/blink_animation.dart delete mode 100644 lib/gamification/common/views/confirm_button.dart delete mode 100644 lib/gamification/common/views/countdown_timer.dart delete mode 100644 lib/gamification/common/views/custom_dialog.dart delete mode 100644 lib/gamification/common/views/dialog_button.dart delete mode 100644 lib/gamification/common/views/feature_card.dart delete mode 100644 lib/gamification/common/views/map_background.dart delete mode 100644 lib/gamification/common/views/on_tap_animation.dart delete mode 100644 lib/gamification/common/views/progress_ring.dart delete mode 100644 lib/gamification/common/views/tutorial_page.dart delete mode 100644 lib/gamification/community_event/model/event.dart delete mode 100644 lib/gamification/community_event/model/location.dart delete mode 100644 lib/gamification/community_event/model/shortcut_event_location.dart delete mode 100644 lib/gamification/community_event/service/event_service.dart delete mode 100644 lib/gamification/community_event/views/badge.dart delete mode 100644 lib/gamification/community_event/views/badge_collection.dart delete mode 100644 lib/gamification/community_event/views/event_card.dart delete mode 100644 lib/gamification/community_event/views/event_page.dart delete mode 100644 lib/gamification/goals/models/daily_goals.dart delete mode 100644 lib/gamification/goals/models/route_goals.dart delete mode 100644 lib/gamification/goals/services/goals_service.dart delete mode 100644 lib/gamification/goals/views/edit_daily_goals.dart delete mode 100644 lib/gamification/goals/views/edit_route_goals.dart delete mode 100644 lib/gamification/goals/views/goals_view.dart delete mode 100644 lib/gamification/goals/views/weekday_button.dart delete mode 100644 lib/gamification/intro/intro_card.dart delete mode 100644 lib/gamification/intro/intro_page.dart delete mode 100644 lib/gamification/main.dart delete mode 100644 lib/gamification/statistics/models/ride_stats.dart delete mode 100644 lib/gamification/statistics/models/stat_type.dart delete mode 100644 lib/gamification/statistics/services/statistics_service.dart delete mode 100644 lib/gamification/statistics/services/stats_view_model.dart delete mode 100644 lib/gamification/statistics/views/card/daily_overview.dart delete mode 100644 lib/gamification/statistics/views/card/route_stats.dart delete mode 100644 lib/gamification/statistics/views/card/stats_card.dart delete mode 100644 lib/gamification/statistics/views/graphs/month_graph.dart delete mode 100644 lib/gamification/statistics/views/graphs/multiple_weeks_graph.dart delete mode 100644 lib/gamification/statistics/views/graphs/ride_stats_graph.dart delete mode 100644 lib/gamification/statistics/views/graphs/week_graph.dart delete mode 100644 lib/gamification/statistics/views/overall_stats.dart delete mode 100644 lib/gamification/statistics/views/page/ride_graphs_page_view.dart delete mode 100644 lib/gamification/statistics/views/page/route_goals_history.dart delete mode 100644 lib/gamification/statistics/views/page/stats_page.dart delete mode 100644 lib/gamification/statistics/views/route_goals_in_week.dart delete mode 100644 lib/gamification/statistics/views/stats_tutorial.dart diff --git a/lib/gamification/challenges/models/challenges_profile.dart b/lib/gamification/challenges/models/challenges_profile.dart deleted file mode 100644 index 37de0c548..000000000 --- a/lib/gamification/challenges/models/challenges_profile.dart +++ /dev/null @@ -1,47 +0,0 @@ -/// This profile holds the users' game state for the challenges feature . -class ChallengesProfile { - /// The xp of the user. - int xp; - - /// The level of the user. - int level; - - /// The number of medals the user has. - int medals; - - /// The number of trophies the user has. - int trophies; - - /// The number of challenges the user can chose for their daily challenge. - int dailyChallengeChoices; - - /// The number of challenges the user can chose for their weekly challenge. - int weeklyChallengeChoices; - - ChallengesProfile({ - this.xp = 0, - this.level = 0, - this.medals = 0, - this.trophies = 0, - this.dailyChallengeChoices = 1, - this.weeklyChallengeChoices = 1, - }); - - Map toJson() => { - 'xp': xp, - 'level': level, - 'medals': medals, - 'trophies': trophies, - 'dailyChallengeChoices': dailyChallengeChoices, - 'weeklyChallengeChoices': weeklyChallengeChoices, - }; - - factory ChallengesProfile.fromJson(Map json) => ChallengesProfile( - xp: json['xp'], - level: json['level'], - medals: json['medals'], - trophies: json['trophies'], - dailyChallengeChoices: json['dailyChallengeChoices'], - weeklyChallengeChoices: json['weeklyChallengeChoices'], - ); -} diff --git a/lib/gamification/challenges/models/level.dart b/lib/gamification/challenges/models/level.dart deleted file mode 100644 index 3f78a8040..000000000 --- a/lib/gamification/challenges/models/level.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/colors.dart'; - -/// A level, which can be reached by the user if they reach a certain xp value. -class Level { - /// The value of the level. - final int value; - - /// A short title for the level. - final String title; - - /// The color of the level. - final Color color; - - const Level({ - required this.value, - required this.title, - required this.color, - }); -} - -/// These are the levels that are possible to achieve by the user vie their xp. -List levels = [ - Level(value: 0, title: 'Novize', color: LevelColors.grey), - const Level(value: 10, title: 'Rad-Rookie', color: LevelColors.pink), - const Level(value: 25, title: 'Freizeitradler', color: LevelColors.green), - const Level(value: 100, title: 'Sattel-Routinier', color: LevelColors.bronze), - const Level(value: 250, title: 'Stadtsprinter', color: LevelColors.silver), - const Level(value: 500, title: 'Pedal-Profi', color: LevelColors.gold), - const Level(value: 1000, title: 'Fahrrad-Flüsterer', color: LevelColors.diamond), - const Level(value: 2500, title: 'Radsport-Legende', color: LevelColors.priobike), -]; diff --git a/lib/gamification/challenges/models/profile_upgrade.dart b/lib/gamification/challenges/models/profile_upgrade.dart deleted file mode 100644 index e50821a4b..000000000 --- a/lib/gamification/challenges/models/profile_upgrade.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:priobike/gamification/challenges/models/level.dart'; - -/// The possible upgrade types, which determine what to do, if an update is activated by the user. -enum ProfileUpgradeType { - moreChallengeTypes, - addWeeklyChallenge, - addDailyChoice, - addWeeklyChoice, - highestLevel, -} - -/// An upgrade which can be activated when the users reaches a certain level to enhance certain profile values. -class ProfileUpgrade { - /// Textual description of the upgrade. - final String description; - - /// Type of upgrade. - final ProfileUpgradeType type; - - ProfileUpgrade(this.description, this.type); - - Map toJson() => { - 'description': description, - 'type': type.index, - }; - - ProfileUpgrade.fromJson(Map json) - : description = json['description'], - type = ProfileUpgradeType.values.elementAt(json['type']); -} - -List getUpgradesForLevel(int level) { - if (level == 1) { - return [ - ProfileUpgrade( - 'Du hast 3 neue Arten von Tageschallenges freigeschaltet! Setze Dir persönliche Tages- und Routenziele, um sie auszuprobieren.', - ProfileUpgradeType.moreChallengeTypes, - ) - ]; - } else if (level == 2) { - return [ - ProfileUpgrade( - 'Du kannst ab jetzt an den Wochenchallenges teilnehmen!', - ProfileUpgradeType.addWeeklyChallenge, - ) - ]; - } else if (level == 3) { - return [ - ProfileUpgrade( - 'Ab jetzt kannst Du Deine Tageschallenge aus 2 Vorschlägen auswählen.', - ProfileUpgradeType.addDailyChoice, - ) - ]; - } else if (level == 4) { - return [ - ProfileUpgrade( - 'Ab jetzt kannst Du Deine Wochenchallenge aus 2 Vorschlägen auswählen.', - ProfileUpgradeType.addWeeklyChoice, - ) - ]; - } else if (level == levels.length - 1) { - return [ - ProfileUpgrade( - 'Herzlichen Glückwunsch, Du hast das höchste Level erreich!', - ProfileUpgradeType.highestLevel, - ), - ]; - } else { - return [ - ProfileUpgrade( - 'Erhalte eine extra Auswahlmöglichkeit für die täglichen Challenges.', - ProfileUpgradeType.addDailyChoice, - ), - ProfileUpgrade( - 'Erhalte eine extra Auswahlmöglichkeit für die wöchentlichen Challenges.', - ProfileUpgradeType.addWeeklyChoice, - ), - ]; - } -} diff --git a/lib/gamification/challenges/services/challenge_service.dart b/lib/gamification/challenges/services/challenge_service.dart deleted file mode 100644 index 66eaf265d..000000000 --- a/lib/gamification/challenges/services/challenge_service.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/model/challenges/challenge.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_validator.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/logging/logger.dart'; -import 'package:priobike/main.dart'; - -/// This class is to be extended by a service, which manages only challenges in a certain timeframe, such as -/// weekly or daily challenges. -abstract class ChallengeService with ChangeNotifier { - final log = Logger('ChallengeService'); - - /// DAO to access the challenges in the database. - final ChallengeDao _dao = AppDatabase.instance.challengeDao; - - /// Generator to generate new challenges according to the user goals. - ChallengeGenerator get _generator; - - /// Validator to continuously check the progress of the current open challenge corresponding to this service. - ChallengeValidator? _validator; - - /// The stream subscription for the db updates of the current challenge. Required to cancel the stream later. - StreamSubscription? _currentChallengeStream; - - /// The currently open challenge. This challenge can still be active, which means the user has still time to complete - /// it, or it can be inactive and completed, such that the user can collect their rewards. - Challenge? _currentChallenge; - Challenge? get currentChallenge => _currentChallenge; - - /// This bool determines wether it should be allowed to generate a new challenge. This is only allowed, if there is - /// no current challenge, or if the user hasn't started a challenge in the current timeframe yet. - bool _allowNew = false; - bool get allowNew => _allowNew; - - /// The length of the timeframe the user has to complete the challenges corresponding to this service. - int get _intervalLengthInDays; - - /// The start day of the current timeframe the user has to complete the challenges corresponding to this service. - DateTime get _intervalStartDay; - - /// Returns a list of challenges corresponding to the timeframe of this service, which are still open. - Future> get _openChallenges; - - /// Whether only weekly challenges should be taken into consideration. - bool get _isWeekly; - - List _challengeChoices = []; - List get challengeChoices => _challengeChoices; - - int get _numberOfChoices; - - ChallengeService() { - _setUpService(); - } - - /// Load relevant data for challenges in the services timeframge. - Future _setUpService() async { - await loadOpenChallenges(); - // Set the allowNew variable to true, if no challenge has exists for the services challenge timeframe. - _allowNew = (await _dao.getChallengesInInterval(_intervalStartDay, _intervalLengthInDays)) - .where((challenge) => challenge.isWeekly == _isWeekly) - .isEmpty; - notifyListeners(); - // Start timer to call set up method again, after the current challenge interval ended. - final intervalEnd = DateTime(_intervalStartDay.year, _intervalStartDay.month, _intervalStartDay.day) - .add(Duration(days: _intervalLengthInDays)); - final timeTillChallengeEnd = intervalEnd.difference(DateTime.now()); - Timer(timeTillChallengeEnd, () { - _currentChallengeStream?.cancel(); - _validator?.dispose(); - _currentChallenge == null; - _setUpService(); - }); - } - - /// Close a given challenge and send its data to the backend. - Future closeChallenge(Challenge challenge) async { - var closedChallenge = challenge.copyWith(isOpen: false); - var result = await _dao.updateObject(closedChallenge); - if (!result) throw Exception("Couldn't save challenge in database!"); - sendChallengeDataToBackend(closedChallenge); - } - - /// If the current challenge has been completed by the user and this method is called, the challenge is closed. - void completeChallenge() async { - if (_currentChallenge == null) return; - // Do nothing, if the challenge wasn't completed yet. - if (_currentChallenge!.progress < _currentChallenge!.target) return; - - // If the challenge has been completed, cancel the challenge stream, dispose the validator, and close it. - _currentChallengeStream?.cancel(); - _validator?.dispose(); - closeChallenge(_currentChallenge!); - _currentChallenge = null; - } - - /// This function starts a stream which listens for changes in the current challenge. - void startChallengeStream() { - if (_currentChallenge == null) return; - _currentChallengeStream?.cancel(); - _validator?.dispose(); - _validator = ChallengeValidator(challenge: _currentChallenge!); - _currentChallengeStream = _dao.streamObjectByPrimaryKey(_currentChallenge!.id).listen((update) { - _currentChallenge = update; - notifyListeners(); - // Cancel the stream, if the challenge has been deleted for some reason. - if (update == null) _currentChallengeStream?.cancel(); - }); - } - - /// This function checks if there are open challenges and either closes them or updates the current challenge. - Future loadOpenChallenges() async { - var openChallenges = await _openChallenges; - // If multiple challenges are open, those are already generated challenge choices for the user. - if (openChallenges.length > 1) { - _challengeChoices = openChallenges; - notifyListeners(); - } - // If only one challenge is open, validate its progress with the current rides and determine whether it has been completed. - else if (openChallenges.length == 1) { - var challenge = openChallenges.first; - var rides = await AppDatabase.instance.rideSummaryDao.getRidesInInterval( - challenge.startTime, - challenge.closingTime, - ); - await ChallengeValidator(challenge: challenge, startStream: false).validate(rides); - try { - challenge = (await AppDatabase.instance.challengeDao.getObjectByPrimaryKey(challenge.id))!; - } catch (e) { - log.e('Failed to validate open challenge'); - } - var isCompleted = challenge.progress / challenge.target >= 1; - - // If an open challenge was not completed and the time did run out, close the challenge and send it to the backend. - if (!isCompleted && DateTime.now().isAfter(challenge.closingTime)) { - return closeChallenge(challenge); - } - - // If a challenge has been completed, or it still can be completed select it as the current challenge. - _currentChallenge = challenge; - startChallengeStream(); - } - } - - /// If the current challenge is null, generate new challenges. - Future?> generateChallengeChoices() async { - if (_currentChallenge != null) return null; - _challengeChoices.clear(); - // Block the user from generating new challenges. - _allowNew = false; - // Generate as many challenges, as choices are allowed for the user. - var newChallenges = _generator.generateChallenges(_numberOfChoices); - for (var c in newChallenges) { - var challenge = await _dao.createObject(c); - _challengeChoices.add(challenge!); - } - // Return challenge choices to user. - return _challengeChoices; - } - - /// Select a challenge out of the available choices and start it. Delete the other choices. - void selectAndStartChallenge(int choiceIndex) { - if (_currentChallenge != null || _challengeChoices.length < choiceIndex + 1) return; - // Save selected challenge as current challenge. - _currentChallenge = _challengeChoices.elementAt(choiceIndex); - _challengeChoices.remove(_currentChallenge); - // Delete other open challenge choices. - for (var challenge in _challengeChoices) { - _dao.deleteObject(challenge); - } - _challengeChoices.clear(); - // Start the validator and observe changes in the challenge. - startChallengeStream(); - } - - /// Send a given completed challenge to the backend. - Future sendChallengeDataToBackend(Challenge challenge) async { - Map challengeData = { - 'challengeType': challenge.type, - 'isWeekly': challenge.isWeekly, - 'reachedValue': challenge.progress, - 'targetValue': challenge.target, - 'xp': challenge.xp, - 'completed': challenge.progress >= challenge.target, - 'startingTime': challenge.startTime.millisecondsSinceEpoch, - 'closingTime': challenge.closingTime.millisecondsSinceEpoch, - }; - getIt().sendJsonToAddress('challenges/send-challenge/', challengeData); - } -} - -/// This service implements the challenge service and manages daily challenges. -class DailyChallengeService extends ChallengeService { - @override - int get _intervalLengthInDays => 1; - - @override - DateTime get _intervalStartDay => DateTime.now(); - - @override - Future> get _openChallenges => _dao.getOpenDailyChallenges(); - - @override - ChallengeGenerator get _generator => DailyChallengeGenerator(); - - @override - bool get _isWeekly => false; - - @override - int get _numberOfChoices => getIt().profile!.dailyChallengeChoices; -} - -/// This service implements the challenge service and manages weekly challenges. -class WeeklyChallengeService extends ChallengeService { - @override - int get _intervalLengthInDays => DateTime.daysPerWeek; - - @override - DateTime get _intervalStartDay => DateTime.now().subtract(Duration(days: DateTime.now().weekday - 1)); - - @override - Future> get _openChallenges => _dao.getOpenWeeklyChallenges(); - - @override - ChallengeGenerator get _generator => WeeklyChallengeGenerator(); - - @override - bool get _isWeekly => true; - - @override - int get _numberOfChoices => getIt().profile!.weeklyChallengeChoices; -} diff --git a/lib/gamification/challenges/services/challenges_profile_service.dart b/lib/gamification/challenges/services/challenges_profile_service.dart deleted file mode 100644 index 03ff8ab86..000000000 --- a/lib/gamification/challenges/services/challenges_profile_service.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/challenges/models/challenges_profile.dart'; -import 'package:priobike/gamification/challenges/models/profile_upgrade.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/model/challenges/challenge.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/main.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// This service updates the challenges profile according to the user interaction. -class ChallengesProfileService with ChangeNotifier { - /// Key to store the challenges profile in the shared prefs. - static const profileKey = 'priobike.gamification.challenges.profile'; - - /// Instance of the shared preferences. - SharedPreferences? _prefs; - - /// Object which holds all the user profile values. If it is null, there is no user profile yet. - ChallengesProfile? _profile; - - /// This bool describes, whether the profiles medal value has been changed. - bool medalsChanged = false; - - /// This bool describes, whether the profiles trophy value has been changed. - bool trophiesChanged = false; - - /// Dao to access the challenges completed by the user. - ChallengeDao get _challengeDao => AppDatabase.instance.challengeDao; - - /// The current state of the users challenge profile. - ChallengesProfile? get profile => _profile; - - /// Returns list of upgrades allowed for the user according to their current level. - List get upgradesForNextLevel => getUpgradesForLevel(_profile!.level + 1); - - ChallengesProfileService() { - _loadData(); - } - - /// Load profile data from shared preferences. - Future _loadData() async { - _prefs ??= await SharedPreferences.getInstance(); - var parsedProfile = _prefs?.getString(profileKey); - if (parsedProfile == null) return; - _profile = ChallengesProfile.fromJson(jsonDecode(parsedProfile)); - - // If a profile was loaded, start the database stream of rides, to update the profile according to the challenges. - startDatabaseStreams(); - } - - /// Create a challenge profile for the user, store it an prefs and start streams to observe completed challenges. - Future createProfile() async { - if (_profile != null) return false; - _profile = ChallengesProfile(); - if (!(await _prefs?.setString(profileKey, jsonEncode(_profile!.toJson().toString())) ?? false)) { - return false; - } - sendProfileDataToBackend(); - startDatabaseStreams(); - return true; - } - - /// Start challenges database stream to update user profile accordingly. - void startDatabaseStreams() { - // Only challenges which are closed and completed, since open challenges are not regarded for the rewards yet. - _challengeDao.streamClosedCompletedChallenges().listen((update) => updateRewards(update)); - } - - /// Update profile data stored in shared prefs and notify listeners. - Future storeProfile() async { - _prefs ??= await SharedPreferences.getInstance(); - _prefs?.setString(profileKey, jsonEncode(_profile!.toJson())); - notifyListeners(); - } - - /// This function updates the profiles rewards according to a given list of completed challenges. - Future updateRewards(List challenges) async { - // If for some reason there is no user profile, return. - if (_profile == null) return; - // Save the old state of the trophies and medals to determine if they change. - var oldMedals = _profile!.medals; - var oldTrophies = _profile!.trophies; - // Update rewards according to the completed challenges. - _profile!.xp = ListUtils.getListSum(challenges.map((c) => c.xp.toDouble()).toList()).toInt(); - _profile!.medals = challenges.where((c) => !c.isWeekly).length; - _profile!.trophies = challenges.where((c) => c.isWeekly).length; - // If the medals or trophies changed, update the bools accordingly. - medalsChanged = oldMedals < _profile!.medals; - trophiesChanged = oldTrophies < _profile!.trophies; - storeProfile(); - } - - /// Perform a level up on the user profile with a given profile upgrade. - void levelUp(ProfileUpgrade? upgrade) { - var newUpgrade = upgrade ?? upgradesForNextLevel.firstOrNull; - // If there is an upgrade to apply, do that according to the upgrade type. - if (newUpgrade != null) { - if (newUpgrade.type == ProfileUpgradeType.addDailyChoice) { - profile!.dailyChallengeChoices += 1; - } else if (newUpgrade.type == ProfileUpgradeType.addWeeklyChoice) { - profile!.weeklyChallengeChoices += 1; - } - } - _profile!.level = min(_profile!.level + 1, levels.length - 1); - // Save changed profile in shared prefs. - storeProfile(); - sendProfileDataToBackend(); - } - - /// Reset all challenges and the their influence on the challenges profile. - Future resetChallenges() async { - _prefs ??= await SharedPreferences.getInstance(); - if (_profile == null) return; - _profile = ChallengesProfile(); - storeProfile(); - _challengeDao.clearObjects(); - notifyListeners(); - } - - /// Reset everything connected to the challenge feature. - Future reset() async { - _prefs ??= await SharedPreferences.getInstance(); - _profile = null; - _prefs!.remove(profileKey); - _challengeDao.clearObjects(); - notifyListeners(); - } - - Future sendProfileDataToBackend() async { - if (_profile == null) return; - Map data = { - 'level': _profile!.level, - }; - getIt().sendJsonToAddress('challenges/profile-update/', data); - } -} diff --git a/lib/gamification/challenges/utils/challenge_generator.dart b/lib/gamification/challenges/utils/challenge_generator.dart deleted file mode 100644 index 38c88bcb1..000000000 --- a/lib/gamification/challenges/utils/challenge_generator.dart +++ /dev/null @@ -1,250 +0,0 @@ -import 'dart:math' as math; - -import 'package:drift/drift.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/main.dart'; - -/// This enum describes the different kind of daily challenges. -enum DailyChallengeType { - distance, - duration, - dailyGoals, - routeGoal, -} - -/// This enum describes the different kind of weekly challenges. -enum WeeklyChallengeType { - overallDistance, - daysWithGoalsCompleted, - routeRidesPerWeek, - routeStreakInWeek, -} - -/// Helper object which describes a possible range of values for a challenge target. -class ValueRange { - final int min; - final int max; - final int stepsize; - - ValueRange.fromMax(int maxVal, int minVal, this.stepsize) - : max = math.max(maxVal, 1), - min = math.max(minVal, 1); - - int getRandomValue() { - var random = math.Random().nextInt(max - min + 1); - return random + min; - } - - int getXpForValue(int minXp, int xpStepSize, int maxSteps, int value) { - if (max == min) return minXp + maxSteps * xpStepSize; - if (value <= min) return minXp; - return minXp + ((value - min) / (max - min) * maxSteps).round() * xpStepSize; - } -} - -/// Return matching icon for a given challenge. -IconData getChallengeIcon(Challenge challenge) { - if (challenge.isWeekly) { - var type = WeeklyChallengeType.values.elementAt(challenge.type); - if (type == WeeklyChallengeType.overallDistance) return CustomGameIcons.distance_trophy; - if (type == WeeklyChallengeType.daysWithGoalsCompleted) return CustomGameIcons.explore_trophy; - if (type == WeeklyChallengeType.routeRidesPerWeek) return CustomGameIcons.map_trophy; - if (type == WeeklyChallengeType.routeStreakInWeek) return CustomGameIcons.map_trophy; - - return CustomGameIcons.blank_trophy; - } else { - var type = DailyChallengeType.values.elementAt(challenge.type); - if (type == DailyChallengeType.distance) return CustomGameIcons.distance_medal; - if (type == DailyChallengeType.duration) return CustomGameIcons.duration_medal; - if (type == DailyChallengeType.dailyGoals) return CustomGameIcons.explore_trophy; - if (type == DailyChallengeType.routeGoal) return CustomGameIcons.map_trophy; - return CustomGameIcons.blank_medal; - } -} - -/// A class that implements this class can be used, to generate a new challenge according to the user goals. -abstract class ChallengeGenerator { - /// Generate a list of challenges, which are different. - List generateChallenges(int length) { - List challenges = []; - while (challenges.length < length) { - var newChallenge = generate(); - var similar = challenges.where((c) => c.type == newChallenge.type && c.xp == newChallenge.xp); - if (similar.isEmpty) challenges.add(newChallenge); - } - return challenges; - } - - /// Route goals of the user, pulled from the goals service. - RouteGoals? get _routeGoals => getIt().routeGoals; - - /// Daily distance and duration goals of the user, pulled from the goals service. - DailyGoals get _dailyGoals => getIt().dailyGoals ?? DailyGoals.defaultGoals; - - double get levelFactor => getIt().profile!.level / (levels.length - 1) * 0.75 + 0.75; - - /// Generate a single new challenge. - ChallengesCompanion generate(); -} - -/// This class can be used to generate new daily challenges. -class DailyChallengeGenerator extends ChallengeGenerator { - final int _minXP = 10; - final int _xpMaxSteps = 8; - final int _xpStepSize = 5; - - List _getAllowedChallengeTypes() { - var level = getIt().profile!.level; - if (level == 0) return [DailyChallengeType.distance]; - - var types = List.from(DailyChallengeType.values); - var weekday = DateTime.now().weekday - 1; - - var dailyGoals = getIt().dailyGoals; - if (dailyGoals == null || !dailyGoals.weekdays.elementAt(weekday)) { - types.remove(DailyChallengeType.dailyGoals); - } - if (_routeGoals == null || !_routeGoals!.weekdays.elementAt(weekday)) { - types.remove(DailyChallengeType.routeGoal); - } - - return types; - } - - /// This function returns a fitting value range for a given challenge type. - ValueRange _getDailyChallengeValueRange(DailyChallengeType type) { - if (type == DailyChallengeType.distance) { - var max = _dailyGoals.distanceMetres ~/ 500; - return ValueRange.fromMax((max * 1.25).round(), (max * 2 / 3).round(), 500); - } else if (type == DailyChallengeType.duration) { - var max = _dailyGoals.durationMinutes ~/ 10; - return ValueRange.fromMax((max * 1.25).round(), (max * 2 / 3).round(), 10); - } else if (type == DailyChallengeType.dailyGoals) { - return ValueRange.fromMax(1, 1, 1); - } else if (type == DailyChallengeType.routeGoal) { - return ValueRange.fromMax(1, 1, 1); - } - return ValueRange.fromMax(1, 1, 1); - } - - /// This function returns a fitting challenge description for a given challenge type and the target value. - String _buildDescriptionDaily(DailyChallengeType type, int value) { - if (type == DailyChallengeType.distance) { - return 'Fahre eine Strecke von ${value / 1000} Kilometern!'; - } else if (type == DailyChallengeType.duration) { - return 'Verbringe mindestens $value Minuten auf Deinem Sattel!'; - } else if (type == DailyChallengeType.dailyGoals) { - return 'Erreiche Deine Tagesziele!'; - } else if (type == DailyChallengeType.routeGoal) { - return 'Fahre mindestens einmal Deine Route "${_routeGoals!.routeName}"'; - } - return ''; - } - - @override - ChallengesCompanion generate() { - var now = DateTime.now(); - var allowedTypes = _getAllowedChallengeTypes(); - var type = allowedTypes.elementAt(math.Random().nextInt(allowedTypes.length)); - var range = _getDailyChallengeValueRange(type); - var targetValue = range.getRandomValue(); - if ([DailyChallengeType.distance, DailyChallengeType.duration].contains(type)) { - targetValue = (targetValue * levelFactor).round(); - } - return ChallengesCompanion.insert( - xp: range.getXpForValue(_minXP, _xpStepSize, _xpMaxSteps, targetValue), - startTime: now, - closingTime: DateTime(now.year, now.month, now.day).add(const Duration(days: 1)), - description: _buildDescriptionDaily(type, targetValue * range.stepsize), - target: targetValue * range.stepsize, - progress: 0, - isWeekly: false, - isOpen: true, - type: DailyChallengeType.values.indexOf(type), - routeId: Value(_routeGoals?.routeID), - ); - } -} - -class WeeklyChallengeGenerator extends ChallengeGenerator { - final int _minXP = 100; - final int _xpMaxSteps = 10; - final int _xpStepSize = 10; - - List _getAllowedChallengeTypes() { - var types = List.from(WeeklyChallengeType.values); - var dailyGoals = getIt().dailyGoals; - if (dailyGoals == null || dailyGoals.numOfDays == 0) { - types.remove(WeeklyChallengeType.daysWithGoalsCompleted); - } - if (_routeGoals == null || _routeGoals?.numOfDays == 0) { - types.remove(WeeklyChallengeType.routeRidesPerWeek); - types.remove(WeeklyChallengeType.routeStreakInWeek); - } - return types; - } - - /// This function returns a fitting value range for a given challenge type. - ValueRange _getWeeklyChallengeValueRange(WeeklyChallengeType type) { - if (type == WeeklyChallengeType.overallDistance) { - var max = _dailyGoals.distanceMetres ~/ 500 * 5; - return ValueRange.fromMax(max, (max * 2 / 3).round(), 500); - } else if (type == WeeklyChallengeType.routeRidesPerWeek) { - var max = _routeGoals!.numOfDays - 1; - return ValueRange.fromMax(max, (max * 2 / 3).round(), 1); - } else if (type == WeeklyChallengeType.routeStreakInWeek) { - var max = _routeGoals!.numOfDays - 2; - return ValueRange.fromMax(max, (max * 2 / 3).round(), 1); - } else if (type == WeeklyChallengeType.daysWithGoalsCompleted) { - var max = _dailyGoals.numOfDays; - return ValueRange.fromMax(max, (max * 2 / 3).round(), 1); - } - return ValueRange.fromMax(1, 1, 1); - } - - /// This function returns a fitting challenge description for a given challenge type, target value and route label. - String _buildDescriptionWeekly(WeeklyChallengeType type, int value, String? routeLabel) { - if (type == WeeklyChallengeType.overallDistance) { - return 'Bringe diese Woche eine Strecke von ${value / 1000} Kilometern hinter Dich!'; - } else if (type == WeeklyChallengeType.routeRidesPerWeek) { - return 'Fahre diese Woche $value mal die Route "$routeLabel"!'; - } else if (type == WeeklyChallengeType.routeStreakInWeek) { - return 'Fahre diese Woche an $value Tagen hintereinander die Route "$routeLabel!"'; - } else if (type == WeeklyChallengeType.daysWithGoalsCompleted) { - return 'Erreiche diese Woche $value mal Deine Tagesziele'; - } - return ''; - } - - @override - ChallengesCompanion generate() { - var now = DateTime.now(); - var end = DateTime(now.year, now.month, now.day).add(Duration(days: 8 - now.weekday)); - var allowedTypes = _getAllowedChallengeTypes(); - var type = allowedTypes.elementAt(math.Random().nextInt(allowedTypes.length)); - var range = _getWeeklyChallengeValueRange(type); - var targetValue = range.getRandomValue(); - if ([WeeklyChallengeType.overallDistance].contains(type)) { - targetValue = (targetValue * levelFactor).round(); - } - return ChallengesCompanion.insert( - xp: range.getXpForValue(_minXP, _xpStepSize, _xpMaxSteps, targetValue), - startTime: now, - closingTime: end, - description: _buildDescriptionWeekly(type, targetValue * range.stepsize, _routeGoals?.routeName), - target: targetValue * range.stepsize, - progress: 0, - isWeekly: true, - isOpen: true, - type: WeeklyChallengeType.values.indexOf(type), - routeId: Value(_routeGoals?.routeID), - ); - } -} diff --git a/lib/gamification/challenges/utils/challenge_validator.dart b/lib/gamification/challenges/utils/challenge_validator.dart deleted file mode 100644 index eefa41c39..000000000 --- a/lib/gamification/challenges/utils/challenge_validator.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/main.dart'; - -/// This class continously checks the progress of a specific challenge, by listening to the finished rides in the db. -class ChallengeValidator { - /// The challenge that needs to be validated. - final Challenge _challenge; - - /// Stream sub of the db stream, to cancel it if not needed anymore. - late StreamSubscription _streamSub; - - /// This bool determines, whether to start the stream of rides automatically. - final bool _startStream; - - ChallengeValidator({required Challenge challenge, bool startStream = true}) - : _startStream = startStream, - _challenge = challenge { - if (_startStream) { - // Listen to rides in the challenge interval and call validate if needed. - _streamSub = AppDatabase.instance.rideSummaryDao - .streamRidesInInterval(_challenge.startTime, _challenge.closingTime) - .listen((rides) => validate(rides)); - } - } - - /// Call this function when the validator is not needed anymore and the ride stream can be cancelt. - void dispose() => _streamSub.cancel(); - - /// Updates the progress of the validators challenge according to a given list of rides. - Future validate(List rides) async { - // Handle rides according to the challenge type. - if (_challenge.isWeekly) { - var type = WeeklyChallengeType.values.elementAt(_challenge.type); - if (type == WeeklyChallengeType.overallDistance) await _handleDistanceChallenge(rides); - if (type == WeeklyChallengeType.daysWithGoalsCompleted) await _handleDailyGoalsWeeklyChallenge(rides); - if (type == WeeklyChallengeType.routeRidesPerWeek) await _handleRidesOnRouteChallenge(rides); - if (type == WeeklyChallengeType.routeStreakInWeek) await _handleStreakChallenge(rides); - } else { - var type = DailyChallengeType.values.elementAt(_challenge.type); - if (type == DailyChallengeType.distance) await _handleDistanceChallenge(rides); - if (type == DailyChallengeType.duration) await _handleDurationChallenge(rides); - if (type == DailyChallengeType.dailyGoals) await _handleDailyGoalsChallenge(rides); - if (type == DailyChallengeType.routeGoal) await _handleRidesOnRouteChallenge(rides); - } - } - - /// Update challenge progress according to the overall ride distances. - Future _handleDailyGoalsChallenge(List rides) async { - var goals = getIt().dailyGoals; - if (goals == null) return; - - var stats = RideStats.fromSummaries(rides); - - if (stats.distanceKilometres >= goals.distanceMetres / 1000 && stats.durationMinutes >= goals.durationMinutes) { - _updateChallenge(1); - } - - _updateChallenge(0); - } - - /// Update challenge progress according to the overall ride distances. - Future _handleDistanceChallenge(List rides) async { - var totalDistance = ListUtils.getListSum(rides.map((ride) => ride.distanceMetres).toList()).toInt(); - return _updateChallenge(totalDistance); - } - - /// Update challenge progress according to the overall ride durations. - Future _handleDurationChallenge(List rides) async { - var totalDuration = ListUtils.getListSum(rides.map((ride) => ride.durationSeconds).toList()); - var totalDurationMinutes = totalDuration ~/ 60; - return _updateChallenge(totalDurationMinutes); - } - - /// Update challenge progress according to the number of rides on the challenge route. - Future _handleRidesOnRouteChallenge(List rides) async { - if (_challenge.routeId == null) return; - var ridesWithShortcut = rides.where((ride) => ride.shortcutId == _challenge.routeId); - _updateChallenge(ridesWithShortcut.length); - } - - /// Update the challenge progress according to the number of rides on the challenge route on days in a row. - Future _handleStreakChallenge(List rides) async { - if (_challenge.routeId == null) return; - var ridesWithShortcut = rides.where((ride) => ride.shortcutId == _challenge.routeId); - - // Get start and endtime of the first day of the challenge interval. - var start = DateTime(_challenge.startTime.year, _challenge.startTime.month, _challenge.startTime.day); - var end = start.add(const Duration(days: 1)); - - var streak = 0; - final today = DateTime.now(); - // Iterate through all the days of the challenge interval, till the end of the interval or the current time - // and increase the streak value accordingly. - while (end.isBefore(_challenge.closingTime)) { - var ridesInInterval = ridesWithShortcut.where( - (ride) => ride.startTime.isAfter(start) && ride.startTime.isBefore(end), - ); - // Update streak, if user drove the route on the checked day. - if (ridesInInterval.isNotEmpty) streak++; - // End while loop, if the user reached the target value. - if (streak >= _challenge.target) break; - // End while loop, if the current day is checked, since future dates do not need to be checked. - if (today.isAfter(start) && today.isBefore(end)) break; - // Reset streak, if the checked date is not the current date and there are no rides on the route. - if (ridesInInterval.isEmpty) streak = 0; - // Increase the checked date. - start = start.add(const Duration(days: 1)); - end = end.add(const Duration(days: 1)); - } - - _updateChallenge(streak); - } - - /// Update challenge progress according to the daily goals of the user and the corresponding ride values. - Future _handleDailyGoalsWeeklyChallenge(List rides) async { - // Get start and endtime of the first day of the challenge interval. - var start = DateTime(_challenge.startTime.year, _challenge.startTime.month, _challenge.startTime.day); - var end = start.add(const Duration(days: 1)); - - // Iterate through all the days of the challenge interval, till the end of the interval or the current time - // and count the days where the daily goals were completed. - var goals = getIt().dailyGoals ?? DailyGoals.defaultGoals; - var daysWithGoalsCompleted = 0; - while (!(end.isAfter(_challenge.closingTime) || start.isAfter(DateTime.now()))) { - var ridesOnDay = rides.where((ride) => ride.startTime.isAfter(start) && ride.startTime.isBefore(end)).toList(); - var stats = RideStats.fromSummaries(ridesOnDay); - if (stats.distanceKilometres >= goals.distanceMetres / 1000 && stats.durationMinutes >= goals.durationMinutes) { - daysWithGoalsCompleted++; - } - start = start.add(const Duration(days: 1)); - end = end.add(const Duration(days: 1)); - } - - _updateChallenge(daysWithGoalsCompleted); - } - - /// Update the progress value of a challenge and store in database. - Future _updateChallenge(int newProgress) async { - if (_challenge.progress != newProgress) { - await AppDatabase.instance.challengeDao.updateObject( - _challenge.copyWith(progress: newProgress), - ); - } - } -} diff --git a/lib/gamification/challenges/views/challenges_card.dart b/lib/gamification/challenges/views/challenges_card.dart deleted file mode 100644 index 8d15d5e8a..000000000 --- a/lib/gamification/challenges/views/challenges_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/challenges/views/profile/profile_view.dart'; -import 'package:priobike/gamification/challenges/views/progress_bar/progress_bar.dart'; -import 'package:priobike/gamification/common/colors.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/views/feature_card.dart'; -import 'package:priobike/main.dart'; - -/// This card is displayed on the home view and holds all information about the users -/// game state regarding the challenges feature. -class ChallengesCard extends StatelessWidget { - const ChallengesCard({super.key}); - - @override - Widget build(BuildContext context) { - return GamificationFeatureCard( - featureKey: GamificationUserService.challengesFeatureKey, - // If the feature is enabled, show progress bars of the users challenges and the profile view. - onEnabled: () async { - await getIt().createProfile(); - await getIt().enableFeature(GamificationUserService.challengesFeatureKey); - }, - featureEnabledContent: const Column( - children: [ - GameProfileView(), - SmallVSpace(), - SmallVSpace(), - ChallengeProgressBar(isWeekly: true), - ChallengeProgressBar(isWeekly: false), - ], - ), - featureDisabledContent: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: BoldSubHeader( - text: 'PrioBike Challenges', - context: context, - textAlign: TextAlign.center, - ), - ), - const SmallHSpace(), - SizedBox( - width: 96, - height: 80, - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: Container( - width: 0, - height: 0, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: CI.radkulturRed.withOpacity(0.05), - blurRadius: 24, - spreadRadius: 24, - ), - BoxShadow( - color: Colors.white.withOpacity(0.01), - blurRadius: 24, - spreadRadius: 24, - ), - ], - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Transform.rotate( - angle: pi / 8, - child: Icon( - CustomGameIcons.elevation_trophy, - size: 64, - color: Theme.of(context).brightness == Brightness.light - ? LevelColors.brighten(LevelColors.gold) - : LevelColors.gold, - ), - ), - ), - Align( - alignment: Alignment.topLeft, - child: Transform.rotate( - angle: -pi / 8, - child: const Icon( - CustomGameIcons.distance_medal, - size: 64, - color: CI.radkulturRed, - ), - ), - ), - ], - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/gamification/challenges/views/challenges_tutorial.dart b/lib/gamification/challenges/views/challenges_tutorial.dart deleted file mode 100644 index 5e9419308..000000000 --- a/lib/gamification/challenges/views/challenges_tutorial.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/common/views/tutorial_page.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/main.dart'; - -/// This tutorial page gives the user a brief introduction to the challenges feature -/// and gives them the option to activate it. -class ChallengesTutorial extends StatelessWidget { - const ChallengesTutorial({super.key}); - - @override - Widget build(BuildContext context) { - return TutorialPage( - confirmButtonLabel: 'Aktivieren', - onConfirmButtonTab: () async { - await getIt().createProfile(); - await getIt().enableFeature(GamificationUserService.challengesFeatureKey); - if (context.mounted) Navigator.of(context).pop(); - }, - contentList: [ - const SizedBox(height: 64 + 16), - Header(text: "PrioBike Challenges", context: context), - const SmallVSpace(), - SubHeader(text: "Absolviere tägliche und wöchentliche Challenges.", context: context), - const SizedBox(height: 82), - ], - ); - } -} diff --git a/lib/gamification/challenges/views/profile/lvl_up_dialog.dart b/lib/gamification/challenges/views/profile/lvl_up_dialog.dart deleted file mode 100644 index 146c35223..000000000 --- a/lib/gamification/challenges/views/profile/lvl_up_dialog.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:confetti/confetti.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; - -/// Dialog widget for when the user reached a new level in their challenge profile. -class LevelUpDialog extends StatefulWidget { - /// The new level of the user. - final Level newLevel; - - /// Additional content on the dialog. - final Widget? content; - - const LevelUpDialog({super.key, required this.newLevel, this.content}); - - @override - State createState() => _LevelUpDialogState(); -} - -class _LevelUpDialogState extends State { - /// Controller to display confetti behind the dialog. - late final ConfettiController _confettiController; - - @override - void initState() { - _confettiController = ConfettiController(duration: const Duration(seconds: 1)); - _confettiController.play(); - super.initState(); - } - - @override - void dispose() { - _confettiController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Stack(alignment: Alignment.center, children: [ - ConfettiWidget( - maximumSize: const Size(15, 10), - minimumSize: const Size(10, 5), - minBlastForce: 20, - maxBlastForce: 40, - numberOfParticles: 40, - emissionFrequency: 0.1, - confettiController: _confettiController, - blastDirectionality: BlastDirectionality.explosive, - colors: [widget.newLevel.color], - ), - CustomDialog( - withGlow: true, - content: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const SmallVSpace(), - BoldContent( - text: 'Du bist ein Level aufgestiegen!', - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onBackground, - height: 1, - ), - const SmallVSpace(), - Header( - text: widget.newLevel.title, - context: context, - color: widget.newLevel.color, - textAlign: TextAlign.center, - ), - BoldSubHeader( - text: 'Level ${levels.indexOf(widget.newLevel)}', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - height: 1, - ), - if (widget.content != null) widget.content!, - const SmallVSpace(), - ], - ), - ), - ), - ]); - } -} diff --git a/lib/gamification/challenges/views/profile/multiple_upgrades_lvl_up.dart.dart b/lib/gamification/challenges/views/profile/multiple_upgrades_lvl_up.dart.dart deleted file mode 100644 index 968de37c0..000000000 --- a/lib/gamification/challenges/views/profile/multiple_upgrades_lvl_up.dart.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/models/profile_upgrade.dart'; -import 'package:priobike/gamification/challenges/views/profile/lvl_up_dialog.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; - -/// Dialog widget for when the user reached a new level in their challenge profile -/// and has the option to apply an upgrade out of multiple options. -class MultipleUpgradesLvlUpDialog extends StatefulWidget { - /// The new level of the user. - final Level newLevel; - - /// The upgrade options the user has. - final List upgrades; - - const MultipleUpgradesLvlUpDialog({ - super.key, - required this.newLevel, - required this.upgrades, - }); - @override - State createState() => _MultipleUpgradesLvlUpDialogState(); -} - -class _MultipleUpgradesLvlUpDialogState extends State with SingleTickerProviderStateMixin { - /// Index of the upgrade selected by the user. - int? _selectedUpgrade; - - /// Update the selected upgrade and close the dialog after a short time. - void _selectUpgrade(int index) async { - setState(() => _selectedUpgrade = index); - await Future.delayed(const MediumDuration()); - if (mounted) Navigator.of(context).pop(widget.upgrades.elementAt(_selectedUpgrade!)); - } - - @override - Widget build(BuildContext context) { - return LevelUpDialog( - newLevel: widget.newLevel, - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - ...widget.upgrades.mapIndexed((i, upgrade) => UpgradeChoice( - visible: _selectedUpgrade == null || _selectedUpgrade == i, - onTap: _selectedUpgrade == i ? null : () => _selectUpgrade(i), - upgrade: upgrade, - color: widget.newLevel.color, - )), - ], - ), - ); - } -} - -/// This is a clickable widget displaying an upgrade the user can apply to their challenge profile. -class UpgradeChoice extends StatelessWidget { - /// Callback for when the choice is selected. - final Function()? onTap; - - /// The choice displayed by this widget. - final ProfileUpgrade upgrade; - - /// Whether the choice should be visible for the user. - final bool visible; - - /// Background color of the widget. - final Color color; - - const UpgradeChoice({ - super.key, - required this.onTap, - required this.upgrade, - required this.visible, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return AnimatedOpacity( - opacity: visible ? 1 : 0, - duration: const ShortDuration(), - child: OnTapAnimation( - onPressed: visible ? onTap : null, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - margin: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: color.withOpacity(0.75), - borderRadius: const BorderRadius.all(Radius.circular(24)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: BoldContent( - text: upgrade.description, - context: context, - textAlign: TextAlign.center, - color: Colors.white, - height: 1, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/challenges/views/profile/profile_view.dart b/lib/gamification/challenges/views/profile/profile_view.dart deleted file mode 100644 index 0e332a4b7..000000000 --- a/lib/gamification/challenges/views/profile/profile_view.dart +++ /dev/null @@ -1,341 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/models/challenges_profile.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; -import 'package:priobike/gamification/challenges/models/profile_upgrade.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/challenges/views/profile/lvl_up_dialog.dart'; -import 'package:priobike/gamification/challenges/views/profile/multiple_upgrades_lvl_up.dart.dart'; -import 'package:priobike/gamification/challenges/views/profile/single_upgrade_lvl_up.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/blink_animation.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/common/views/progress_ring.dart'; -import 'package:priobike/main.dart'; - -/// This view displays the users game state for the challenge feature and provides them with the option to -/// upgrade to the next level, if the current level is finished. -class GameProfileView extends StatefulWidget { - const GameProfileView({super.key}); - - @override - State createState() => _GameProfileViewState(); -} - -class _GameProfileViewState extends State with TickerProviderStateMixin { - /// The service which manages and provides the user profile. - late ChallengesProfileService _profileService; - - /// Controller to animate the trophy icon when a new trophy is gained. - late final AnimationController _trophiesController; - - /// Controller to animate the medal icon when a new medal is gained. - late final AnimationController _medalsController; - - /// The users profile for the challenges feature. - ChallengesProfile? get _profile => _profileService.profile; - - /// Get the current level of the user as a Level object. - Level get _currentLevel { - if (_profile == null) return levels.first; - return levels.elementAt(_profile!.level); - } - - /// Return the next level the user needs to achieve. Returns null, if the user has reached the max level. - Level? get _nextLevel { - if (_profile == null) return null; - int level = _profile!.level; - if (level == levels.length - 1) return null; - return levels[level + 1]; - } - - /// The progress of the user for the next level to reach, as a value between 0 and 1. - double get _levelProgress { - if (_nextLevel == null || _profile == null) return 1; - var progress = (_profile!.xp - _currentLevel.value) / (_nextLevel!.value - _currentLevel.value); - return min(1, max(0, progress)); - } - - /// True, if the user has enough xp to reach the next level. - bool get _canLevelUp => _nextLevel != null && _profileService.profile!.xp >= _nextLevel!.value; - - /// The color representing the current level of the user. - Color get _lvlColor => _currentLevel.color; - - @override - void initState() { - _trophiesController = AnimationController(duration: const LongDuration(), vsync: this); - _medalsController = AnimationController(duration: const LongDuration(), vsync: this); - _profileService = getIt(); - _profileService.addListener(updateProfile); - super.initState(); - } - - @override - void dispose() { - _medalsController.dispose(); - _trophiesController.dispose(); - _profileService.removeListener(updateProfile); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Called when the users challenge profile changes. - void updateProfile() async { - if (mounted) setState(() {}); - // If the medals have changed, animate the medal icon. - if (_profileService.medalsChanged) { - await _medalsController.reverse(from: 1); - _profileService.medalsChanged = false; - if (mounted) setState(() {}); - } - // If the trophies have changed, animate the trophy icon. - else if (_profileService.trophiesChanged) { - await _trophiesController.reverse(from: 1); - _profileService.trophiesChanged = false; - if (mounted) setState(() {}); - } - } - - /// Bounce animation for the trophy or medal icon. - Animation _getAnimation(var controller) => - Tween(begin: 1, end: 3).animate(CurvedAnimation(parent: controller, curve: Curves.bounceIn)); - - /// Opaen level up dialog according to the number of possible upgrades the user can apply with the level up. - Future _showLevelUpDialog() async { - if (_nextLevel == null) return; - var openUpgrades = _profileService.upgradesForNextLevel; - var result = await showDialog( - barrierColor: Colors.black.withOpacity(0.8), - context: context, - barrierDismissible: openUpgrades.length <= 1, - builder: (BuildContext context) { - if (openUpgrades.isEmpty) { - return LevelUpDialog(newLevel: _nextLevel!); - } else if (openUpgrades.length == 1) { - return SingleUpgradeLvlUpDialog(newLevel: _nextLevel!, upgrade: openUpgrades.first); - } else { - return MultipleUpgradesLvlUpDialog(newLevel: _nextLevel!, upgrades: openUpgrades); - } - }, - ); - _profileService.levelUp(result); - } - - /// Returns widget for displaying the count of a collected virtual reward with a matching icon. - Widget _getRewardCounter(int number, IconData icon, Animation animation, bool animate, double size) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ScaleTransition( - scale: animation, - child: AnimatedContainer( - duration: const MediumDuration(), - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: CI.radkulturRed.withOpacity(animate ? 0.2 : 0), - blurRadius: 12, - ), - ], - ), - child: Icon( - icon, - size: size, - color: animate ? CI.radkulturRed : Color.alphaBlend(Colors.white.withOpacity(0.2), CI.radkulturRed), - ), - ), - ), - BoldSubHeader( - text: '$number', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - ], - ); - } - - /// Get the widget that should be displayed inside of the level progress ring. - Widget _getRingContent() { - if (_canLevelUp) { - return BlinkAnimation( - animate: _canLevelUp, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BoldContent( - text: 'Level', - context: context, - color: _lvlColor, - height: 1, - ), - BoldSubHeader( - text: 'Up', - context: context, - color: _lvlColor, - height: 1, - ), - ], - ), - ); - } else if (!_canLevelUp && _nextLevel != null) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SmallVSpace(), - BoldContent( - text: 'Level', - context: context, - color: Color.alphaBlend( - Theme.of(context).colorScheme.onBackground.withOpacity(0.1 * (1 - _levelProgress)), - _lvlColor.withOpacity(_levelProgress), - ), - height: 1, - ), - Header( - text: '${levels.indexOf(_currentLevel)}', - context: context, - color: Color.alphaBlend( - Theme.of(context).colorScheme.onBackground.withOpacity(0.1 * (1 - _levelProgress)), - _lvlColor.withOpacity(_levelProgress), - ), - ), - ], - ); - } else { - return Icon( - Icons.directions_bike, - color: _lvlColor, - size: 56, - ); - } - } - - @override - Widget build(BuildContext context) { - if (_profile == null) return Container(); - double width = MediaQuery.of(context).size.width; - double widthWithoutPadding = width - 48 * 2; - double ringDimensions = widthWithoutPadding / 2.75; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - OnTapAnimation( - onPressed: _canLevelUp && _nextLevel != null ? _showLevelUpDialog : null, - child: Stack( - alignment: Alignment.center, - children: [ - SizedBox.fromSize(size: Size.square(ringDimensions)), - if (_canLevelUp) - BlinkAnimation( - child: Container( - height: 0, - width: 0, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: Theme.of(context).brightness == Brightness.dark - ? [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 30, - spreadRadius: 35, - ), - BoxShadow( - color: Theme.of(context).colorScheme.background, - blurRadius: 15, - spreadRadius: 35, - ), - ] - : [], - ), - ), - ), - if (_nextLevel == null) - Container( - height: 0, - width: 0, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: _lvlColor.withOpacity(0.4), - blurRadius: 30, - spreadRadius: 40, - ), - BoxShadow( - color: Theme.of(context).colorScheme.background, - blurRadius: 15, - spreadRadius: 40, - ), - ], - ), - ), - ProgressRing( - progress: _levelProgress, - ringSize: ringDimensions, - ringColor: _lvlColor, - content: _getRingContent(), - ), - ], - ), - ), - Expanded( - child: SizedBox( - height: ringDimensions, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: BoldContent( - text: _currentLevel.title, - context: context, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Content( - text: (_nextLevel == null) ? '${_profile!.xp} XP' : '${_profile!.xp} / ${_nextLevel!.value} XP', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - ), - Expanded(child: Container()), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _getRewardCounter( - _profile!.medals, - CustomGameIcons.blank_medal, - _getAnimation(_medalsController), - _profileService.medalsChanged, - ringDimensions * 0.375, - ), - _getRewardCounter( - _profile!.trophies, - CustomGameIcons.blank_trophy, - _getAnimation(_trophiesController), - _profileService.trophiesChanged, - ringDimensions * 0.375, - ), - ], - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/lib/gamification/challenges/views/profile/single_upgrade_lvl_up.dart b/lib/gamification/challenges/views/profile/single_upgrade_lvl_up.dart deleted file mode 100644 index cfc9bb5b6..000000000 --- a/lib/gamification/challenges/views/profile/single_upgrade_lvl_up.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/models/profile_upgrade.dart'; -import 'package:priobike/gamification/challenges/views/profile/lvl_up_dialog.dart'; -import 'package:priobike/gamification/challenges/models/level.dart'; - -/// Dialog widget for when the user reached a new level in their challenge profile and gained a single profile upgrade. -class SingleUpgradeLvlUpDialog extends StatelessWidget { - /// The new level of the user. - final Level newLevel; - - /// The upgrade gained by the level up. - final ProfileUpgrade upgrade; - - const SingleUpgradeLvlUpDialog({super.key, required this.newLevel, required this.upgrade}); - - @override - Widget build(BuildContext context) { - return LevelUpDialog( - newLevel: newLevel, - content: Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: newLevel.color.withOpacity(0.75), - borderRadius: const BorderRadius.all(Radius.circular(24)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: BoldContent( - text: upgrade.description, - context: context, - textAlign: TextAlign.center, - color: Colors.white, - height: 1, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/gamification/challenges/views/progress_bar/challenge_reward_dialog.dart b/lib/gamification/challenges/views/progress_bar/challenge_reward_dialog.dart deleted file mode 100644 index a1cec4c22..000000000 --- a/lib/gamification/challenges/views/progress_bar/challenge_reward_dialog.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:confetti/confetti.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/common/database/database.dart'; - -/// Dialog widget for when the user collects their reward for a challenge. -class ChallengeRewardDialog extends StatefulWidget { - /// The challenge which has been completed by the user. - final Challenge challenge; - - /// The color in which the reward should be shown. - final Color color; - - const ChallengeRewardDialog({super.key, required this.color, required this.challenge}); - - @override - State createState() => ChallengeRewardDialogState(); -} - -class ChallengeRewardDialogState extends State with SingleTickerProviderStateMixin { - /// Controller to display confetti behind the dialog. - late final ConfettiController _confettiController; - - /// Animation controller to animate the dialog appearing. - late final AnimationController _animationController; - - @override - void initState() { - _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 5)); - _animationController.forward(); - _confettiController = ConfettiController(duration: const Duration(seconds: 1)); - _confettiController.play(); - super.initState(); - } - - @override - void dispose() { - _animationController.dispose(); - _confettiController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Stack( - alignment: Alignment.center, - children: [ - ConfettiWidget( - maximumSize: const Size(10, 10), - minimumSize: const Size(10, 5), - minBlastForce: 20, - maxBlastForce: 40, - numberOfParticles: 50, - emissionFrequency: 0.1, - confettiController: _confettiController, - blastDirectionality: BlastDirectionality.explosive, - colors: [widget.color.withOpacity(0.5)], - ), - ScaleTransition( - scale: Tween(begin: 0, end: 1).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.fastLinearToSlowEaseIn, - ), - ), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - getChallengeIcon(widget.challenge), - size: 212, - color: widget.color, - ), - Header( - text: '+${widget.challenge.xp} XP', - context: context, - color: Colors.white, - ) - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/gamification/challenges/views/progress_bar/challenge_selection_dialog.dart b/lib/gamification/challenges/views/progress_bar/challenge_selection_dialog.dart deleted file mode 100644 index 13a035699..000000000 --- a/lib/gamification/challenges/views/progress_bar/challenge_selection_dialog.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; - -/// Dialog widget to pop up after multiple challenges were generated, to give the user the option to select one of them. -class ChallengeSelectionDialog extends StatefulWidget { - /// The list of selectable challenges. - final List challenges; - - /// Whether the challenges are weekly, or daily challenges. - final bool isWeekly; - - /// An accent color for the challenges. - final Color color; - - const ChallengeSelectionDialog({ - super.key, - required this.challenges, - required this.isWeekly, - required this.color, - }); - @override - State createState() => _ChallengeSelectionDialogState(); -} - -class _ChallengeSelectionDialogState extends State with SingleTickerProviderStateMixin { - /// The index of the challenge selected by the user. - int? _selectedChallenge; - - /// Select one out of the challenges, make the other choices disappear and close the dialog after half a second. - void _selectChallenge(int index) async { - setState(() => _selectedChallenge = index); - await Future.delayed(const MediumDuration()); - if (mounted) Navigator.of(context).pop(_selectedChallenge); - } - - @override - Widget build(BuildContext context) { - return CustomDialog( - backgroundColor: Colors.transparent, - content: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - ...widget.challenges.mapIndexed( - (i, challenge) => ChallengeWidget( - challenge: challenge, - onTap: () => _selectChallenge(i), - visible: _selectedChallenge == null || _selectedChallenge == i, - color: widget.color, - ), - ), - ], - ), - ); - } -} - -/// This widget displays a single challenge, to be selected by the user. -class ChallengeWidget extends StatelessWidget { - /// Whether the widget should be visible. - final bool visible; - - /// What to do, when the widget is tapped on. - final Function() onTap; - - /// The challenge represented by the widget. - final Challenge challenge; - - /// An accent color for the challenges. - final Color color; - - const ChallengeWidget({ - super.key, - required this.challenge, - required this.onTap, - this.visible = true, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return AnimatedOpacity( - opacity: visible ? 1 : 0, - duration: const ShortDuration(), - child: OnTapAnimation( - onPressed: visible ? onTap : null, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - margin: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - border: Border.all( - width: 0.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: const BorderRadius.all(Radius.circular(24)), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - blurRadius: 4, - ) - ], - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 4), - SubHeader( - text: challenge.description, - context: context, - textAlign: TextAlign.center, - height: 1.1, - ), - const SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - getChallengeIcon(challenge), - size: 48, - color: color.withOpacity(0.75), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - '${challenge.xp} XP', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 28, - height: 1, - fontWeight: FontWeight.w600, - color: color.withOpacity(0.75), - ), - ), - ), - ], - ), - const SizedBox(height: 4), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/challenges/views/progress_bar/progress_bar.dart b/lib/gamification/challenges/views/progress_bar/progress_bar.dart deleted file mode 100644 index 7de58d3ed..000000000 --- a/lib/gamification/challenges/views/progress_bar/progress_bar.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/services/challenge_service.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/challenges/views/progress_bar/challenge_reward_dialog.dart'; -import 'package:priobike/gamification/challenges/views/progress_bar/challenge_selection_dialog.dart'; -import 'package:priobike/gamification/challenges/views/progress_bar/single_challenge_dialog.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/blink_animation.dart'; -import 'package:priobike/gamification/common/views/countdown_timer.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/main.dart'; - -/// A Widget which displays the progress of a given challenge and relevant info about the challenge. -/// It also provides the option to get the rewards, if the challenge was completed. -class ChallengeProgressBar extends StatefulWidget { - /// Determine whether to build the progress bar for weekly or for daily challenges. - final bool isWeekly; - - const ChallengeProgressBar({ - super.key, - required this.isWeekly, - }); - @override - State createState() => _ChallengeProgressBarState(); -} - -class _ChallengeProgressBarState extends State with SingleTickerProviderStateMixin { - /// The service which manages and provides the user profile. - late ChallengesProfileService _profileService; - - /// Get the weekly or daily challenge service, according to the isWeekly variable. - ChallengeService get _service => widget.isWeekly ? getIt() : getIt(); - - /// Get the challenge currently connected to the progress bar, or null if there is none. - Challenge? get _challenge => _service.currentChallenge; - - /// The progress of completion of the challenge as a percentage value between 0 and 100. - double get _progressPercentage => _challenge == null ? 1 : _challenge!.progress / _challenge!.target; - - /// Returns true, if the user completed the challenge. - bool get _isCompleted => _challenge == null ? false : _progressPercentage >= 1; - - /// If there is no active challenge, and there are no available challenge choices, and the service does not allow - /// the generation of a new challenge, this bar ist true and indicates, that a tap on the bar should do nothing. - bool get _deactivateTap => _challenge == null && !_service.allowNew && _service.challengeChoices.isEmpty; - - /// The color representing the current level of the user. - Color get _barColor => CI.radkulturRed; //levels.elementAt(_profileService.profile?.level ?? 0).color; - - /// Time where the challenge ends. - DateTime get _endTime { - var now = DateTime.now(); - if (widget.isWeekly) { - return now.add(Duration(days: 8 - now.weekday)).copyWith(hour: 0, minute: 0, second: 0); - } else { - return now.add(const Duration(days: 1)).copyWith(hour: 0, minute: 0, second: 0); - } - } - - /// Return the type of the current challenge. - get challengeType => _challenge!.isWeekly - ? WeeklyChallengeType.values.elementAt(_challenge!.type) - : DailyChallengeType.values.elementAt(_challenge!.type); - - @override - void initState() { - _profileService = getIt(); - _profileService.addListener(update); - _service.addListener(update); - super.initState(); - } - - @override - void dispose() { - _profileService.removeListener(update); - _service.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired - void update() => {if (mounted) setState(() {})}; - - /// Handle a tap on the progress bar. - void _handleTap() async { - // If there is a non-completed challenge, open dialog field containing information about the challenge. - if (_challenge != null && !_isCompleted) { - return showDialog( - barrierColor: Colors.black.withOpacity(0.8), - context: context, - builder: (context) => SingleChallengeDialog( - challenge: _challenge!, - color: _barColor, - ), - ); - } - // If there are a number of challenges, of which the user can chose from, open the challenge selection dialog. - if (_service.challengeChoices.isNotEmpty) { - await _showChallengeSelection(_service.challengeChoices); - } - // If the challenge has been completed, update it in the db and give haptic feedback again, as the user receives their rewards. - else if (_isCompleted) { - await showDialog( - barrierColor: Colors.black.withOpacity(0.8), - context: context, - builder: (context) => ChallengeRewardDialog(color: _barColor, challenge: _challenge!), - ); - _service.completeChallenge(); - HapticFeedback.heavyImpact(); - } - // If there is no challenge, but the service allows a new one, generate a new one. - else if (_challenge == null && _service.allowNew) { - var result = await _service.generateChallengeChoices(); - if (result != null) { - await _showChallengeSelection(result); - } - } - } - - /// This function opens a dialog, where the user is shown their generated challenge, or is given a choice between - /// a number of challenges, if multiple were generated. - Future _showChallengeSelection(List challenges) async { - // The dialog returns an index of the selected challenge or null, if no selection was made. - var result = await showDialog( - context: context, - barrierColor: Colors.black.withOpacity(0.8), - barrierDismissible: challenges.length == 1, - builder: (BuildContext context) { - if (challenges.length == 1) { - return SingleChallengeDialog( - challenge: challenges.first, - color: _barColor, - ); - } else { - return ChallengeSelectionDialog( - challenges: challenges, - isWeekly: widget.isWeekly, - color: _barColor, - ); - } - }, - ); - // If there is only one challenge to chose from, select that challenge. - if (challenges.length == 1) { - _service.selectAndStartChallenge(0); - } - // If there are multiple challenges to chose from, select a challenge according to the users choice. - else if (result != null) { - _service.selectAndStartChallenge(result); - } - } - - /// Returns a widget which displays the time the user has left for the challenge. - Widget _getTimeLeftWidget() { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - Icons.timer, - size: 16, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - CountdownTimer(timestamp: _endTime), - const SizedBox(width: 16), - ], - ); - } - - /// This widget returns the progress bar corresponding to the challenge state. - Widget _getProgressBar() { - return SizedBox.fromSize( - size: const Size.fromHeight(48), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: OnTapAnimation( - scaleFactor: 0.95, - onPressed: (_deactivateTap) ? null : _handleTap, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(32)), - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: const BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(32))), - clipBehavior: Clip.antiAlias, - alignment: Alignment.centerLeft, - child: FractionallySizedBox( - widthFactor: _isCompleted ? 1 : _progressPercentage, - child: Container( - color: _challenge == null ? _barColor.withOpacity(_deactivateTap ? 0.2 : 0.5) : _barColor, - ), - ), - ), - ), - if (!_deactivateTap) - BlinkAnimation( - animate: _isCompleted, - scaleFactor: 1.025, - child: FractionallySizedBox( - widthFactor: _isCompleted ? 1 : _progressPercentage, - heightFactor: 1, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(32)), - boxShadow: [ - BoxShadow( - color: _barColor.withOpacity(_isCompleted ? 0.8 : 0.3), - blurRadius: 15, - spreadRadius: 0, - ), - ], - ), - ), - ), - ), - Center( - child: BlinkAnimation( - animate: _isCompleted, - scaleFactor: 1.1, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - (_challenge == null) - ? (widget.isWeekly ? CustomGameIcons.blank_trophy : CustomGameIcons.blank_medal) - : getChallengeIcon(_challenge!), - size: 32, - color: _isCompleted ? Colors.white : Theme.of(context).colorScheme.onBackground, - ), - BoldContent( - text: _challenge == null - ? _deactivateTap - ? 'Challenge Abgeschlossen' - : (widget.isWeekly ? 'Neue Wochenchallenge' : 'Neue Tageschallenge') - : _isCompleted - ? 'Belohnung Abholen' - : '${StringFormatter.getRoundStrByChallengeType( - _challenge!.progress, - challengeType, - )}/${StringFormatter.getRoundStrByChallengeType( - _challenge!.target, - challengeType, - )} ${StringFormatter.getLabelForChallengeType(challengeType)}', - context: context, - color: _isCompleted ? Colors.white : Theme.of(context).colorScheme.onBackground, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - if (_profileService.profile == null) return const SizedBox.shrink(); - if (widget.isWeekly && _profileService.profile!.level < 2) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - children: [ - _getProgressBar(), - const SizedBox(height: 4), - _getTimeLeftWidget(), - ], - ), - ); - } -} diff --git a/lib/gamification/challenges/views/progress_bar/single_challenge_dialog.dart b/lib/gamification/challenges/views/progress_bar/single_challenge_dialog.dart deleted file mode 100644 index 63a49530f..000000000 --- a/lib/gamification/challenges/views/progress_bar/single_challenge_dialog.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; - -/// Dialog which displays a single challenge to the user. -class SingleChallengeDialog extends StatelessWidget { - /// The challenge in question. - final Challenge challenge; - - /// An accent color for the challenge. - final Color color; - - const SingleChallengeDialog({ - super.key, - required this.challenge, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return CustomDialog( - content: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 4), - Text( - challenge.isWeekly ? 'Wochenchallenge' : 'Tageschallenge', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 30, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onBackground.withOpacity(1), - ), - ), - const SmallVSpace(), - Row( - children: [ - Expanded( - child: SubHeader( - text: challenge.description, - context: context, - textAlign: TextAlign.center, - height: 1.1, - ), - ), - ], - ), - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - getChallengeIcon(challenge), - size: 60, - color: color.withOpacity(0.75), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Header( - text: '${challenge.xp} XP', - context: context, - height: 1, - color: color.withOpacity(0.75), - ), - ), - ], - ), - const SmallVSpace(), - ], - ), - ), - ); - } -} diff --git a/lib/gamification/common/colors.dart b/lib/gamification/common/colors.dart deleted file mode 100644 index 69a76ec31..000000000 --- a/lib/gamification/common/colors.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; - -/// Custom level colors with at least a 3:1 contrast on white and black. -class LevelColors { - static Color grey = Colors.grey; - static const Color pink = Color.fromRGBO(238, 0, 136, 1); //ee0088 | onWhite: 4.2:1 - onBlack: 4.99:1 - static const Color green = Color.fromRGBO(17, 147, 0, 1); //119300 | onWhite: 4.04:1 - onBlack: 5.19:1 - static const Color gold = Color.fromRGBO(177, 144, 37, 1); //d4af37 | onWhite: 3.04:1 - onBlack: 6.88:1 - static const Color silver = Color.fromRGBO(122, 125, 144, 1); //7A7D90 | onWhite: 4.06:1 - onBlack: 5.16:1 - static const Color bronze = Color.fromRGBO(169, 113, 66, 1); //a97142 | onWhite: 4.09:1 - onBlack: 5.12:1 - static const Color diamond = Color.fromRGBO(0, 156, 235, 1); //009CEB | onWhite: 3.01:1 - onBlack: 6.95:1 - static const Color priobike = CI.radkulturRed; //0073ff | onWhite: 4.28:1 - onBlack: 4.89:1 - - /// Get a color in a slightly lighter tone. - static Color brighten(Color color) => HSLColor.fromColor(color).withLightness(0.58).toColor(); -} diff --git a/lib/gamification/common/custom_game_icons.dart b/lib/gamification/common/custom_game_icons.dart deleted file mode 100644 index e6d389a6b..000000000 --- a/lib/gamification/common/custom_game_icons.dart +++ /dev/null @@ -1,45 +0,0 @@ -/// Flutter icons CustomGame -/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com -/// This font was generated by FlutterIcon.com, which is derived from Fontello. -/// -/// To use this font, place it in your fonts/ directory and include the -/// following in your pubspec.yaml -/// -/// flutter: -/// fonts: -/// - family: CustomGameIcons -/// fonts: -/// - asset: fonts/CustomGameIcons.ttf -/// -/// -/// -library; - -import 'package:flutter/widgets.dart'; - -// ignore_for_file: constant_identifier_names -/// List of custom designed icons used in the gamification. -class CustomGameIcons { - CustomGameIcons._(); - - static const _kFontFam = 'CustomGameIcons'; - static const String? _kFontPkg = null; - - static const IconData blank_medal = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData blank_trophy = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData distance_medal = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData distance_trophy = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData duration_medal = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData duration_trophy = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData elevation_medal = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData elevation_trophy = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData goals = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData explore_medal = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData explore_trophy = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData location_medal = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData location_trophy = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData map_medal = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData map_trophy = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData elevation_gain = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData elevation_loss = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg); -} diff --git a/lib/gamification/common/database/database.dart b/lib/gamification/common/database/database.dart deleted file mode 100644 index 40a57b5f5..000000000 --- a/lib/gamification/common/database/database.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:io'; - -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart' as p; -import 'package:priobike/gamification/common/database/model/achieved_location/achieved_location.dart'; -import 'package:priobike/gamification/common/database/model/challenges/challenge.dart'; -import 'package:priobike/gamification/common/database/model/event_badge/event_badge.dart'; -import 'package:priobike/gamification/common/database/model/ride_summary/ride_summary.dart'; - -part 'database.g.dart'; - -/// Class holding the database required for the gamification data structure. It can be accessed as a Singleton. -@DriftDatabase( - tables: [RideSummaries, Challenges, AchievedLocations, EventBadges], - daos: [RideSummaryDao, ChallengeDao, AchievedLocationDao, EventBadgeDao], -) -class AppDatabase extends _$AppDatabase { - /// Static instance of the class to access it as a singleton. - static final AppDatabase instance = AppDatabase(); - - AppDatabase() : super(_openConnection()); - - @override - int get schemaVersion => 1; -} - -/// Create database file at appropriate location. -LazyDatabase _openConnection() { - return LazyDatabase(() async { - final dbFolder = await getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'db.sqlite')); - return NativeDatabase.createInBackground(file); - }); -} diff --git a/lib/gamification/common/database/database.g.dart b/lib/gamification/common/database/database.g.dart deleted file mode 100644 index 637a8de4f..000000000 --- a/lib/gamification/common/database/database.g.dart +++ /dev/null @@ -1,1390 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'database.dart'; - -// ignore_for_file: type=lint -class $RideSummariesTable extends RideSummaries with TableInfo<$RideSummariesTable, RideSummary> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $RideSummariesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _shortcutIdMeta = const VerificationMeta('shortcutId'); - @override - late final GeneratedColumn shortcutId = - GeneratedColumn('shortcut_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _distanceMetresMeta = const VerificationMeta('distanceMetres'); - @override - late final GeneratedColumn distanceMetres = GeneratedColumn('distance_metres', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); - static const VerificationMeta _durationSecondsMeta = const VerificationMeta('durationSeconds'); - @override - late final GeneratedColumn durationSeconds = GeneratedColumn('duration_seconds', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); - static const VerificationMeta _elevationGainMetresMeta = const VerificationMeta('elevationGainMetres'); - @override - late final GeneratedColumn elevationGainMetres = GeneratedColumn( - 'elevation_gain_metres', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); - static const VerificationMeta _elevationLossMetresMeta = const VerificationMeta('elevationLossMetres'); - @override - late final GeneratedColumn elevationLossMetres = GeneratedColumn( - 'elevation_loss_metres', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); - static const VerificationMeta _averageSpeedKmhMeta = const VerificationMeta('averageSpeedKmh'); - @override - late final GeneratedColumn averageSpeedKmh = GeneratedColumn('average_speed_kmh', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); - static const VerificationMeta _startTimeMeta = const VerificationMeta('startTime'); - @override - late final GeneratedColumn startTime = GeneratedColumn('start_time', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - @override - List get $columns => [ - id, - shortcutId, - distanceMetres, - durationSeconds, - elevationGainMetres, - elevationLossMetres, - averageSpeedKmh, - startTime - ]; - @override - String get aliasedName => _alias ?? 'ride_summaries'; - @override - String get actualTableName => 'ride_summaries'; - @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('shortcut_id')) { - context.handle(_shortcutIdMeta, shortcutId.isAcceptableOrUnknown(data['shortcut_id']!, _shortcutIdMeta)); - } - if (data.containsKey('distance_metres')) { - context.handle( - _distanceMetresMeta, distanceMetres.isAcceptableOrUnknown(data['distance_metres']!, _distanceMetresMeta)); - } else if (isInserting) { - context.missing(_distanceMetresMeta); - } - if (data.containsKey('duration_seconds')) { - context.handle( - _durationSecondsMeta, durationSeconds.isAcceptableOrUnknown(data['duration_seconds']!, _durationSecondsMeta)); - } else if (isInserting) { - context.missing(_durationSecondsMeta); - } - if (data.containsKey('elevation_gain_metres')) { - context.handle(_elevationGainMetresMeta, - elevationGainMetres.isAcceptableOrUnknown(data['elevation_gain_metres']!, _elevationGainMetresMeta)); - } else if (isInserting) { - context.missing(_elevationGainMetresMeta); - } - if (data.containsKey('elevation_loss_metres')) { - context.handle(_elevationLossMetresMeta, - elevationLossMetres.isAcceptableOrUnknown(data['elevation_loss_metres']!, _elevationLossMetresMeta)); - } else if (isInserting) { - context.missing(_elevationLossMetresMeta); - } - if (data.containsKey('average_speed_kmh')) { - context.handle(_averageSpeedKmhMeta, - averageSpeedKmh.isAcceptableOrUnknown(data['average_speed_kmh']!, _averageSpeedKmhMeta)); - } else if (isInserting) { - context.missing(_averageSpeedKmhMeta); - } - if (data.containsKey('start_time')) { - context.handle(_startTimeMeta, startTime.isAcceptableOrUnknown(data['start_time']!, _startTimeMeta)); - } else if (isInserting) { - context.missing(_startTimeMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - RideSummary map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return RideSummary( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - shortcutId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}shortcut_id']), - distanceMetres: - attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}distance_metres'])!, - durationSeconds: - attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}duration_seconds'])!, - elevationGainMetres: - attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}elevation_gain_metres'])!, - elevationLossMetres: - attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}elevation_loss_metres'])!, - averageSpeedKmh: - attachedDatabase.typeMapping.read(DriftSqlType.double, data['${effectivePrefix}average_speed_kmh'])!, - startTime: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}start_time'])!, - ); - } - - @override - $RideSummariesTable createAlias(String alias) { - return $RideSummariesTable(attachedDatabase, alias); - } -} - -class RideSummary extends DataClass implements Insertable { - final int id; - final String? shortcutId; - final double distanceMetres; - final double durationSeconds; - final double elevationGainMetres; - final double elevationLossMetres; - final double averageSpeedKmh; - final DateTime startTime; - const RideSummary( - {required this.id, - this.shortcutId, - required this.distanceMetres, - required this.durationSeconds, - required this.elevationGainMetres, - required this.elevationLossMetres, - required this.averageSpeedKmh, - required this.startTime}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || shortcutId != null) { - map['shortcut_id'] = Variable(shortcutId); - } - map['distance_metres'] = Variable(distanceMetres); - map['duration_seconds'] = Variable(durationSeconds); - map['elevation_gain_metres'] = Variable(elevationGainMetres); - map['elevation_loss_metres'] = Variable(elevationLossMetres); - map['average_speed_kmh'] = Variable(averageSpeedKmh); - map['start_time'] = Variable(startTime); - return map; - } - - RideSummariesCompanion toCompanion(bool nullToAbsent) { - return RideSummariesCompanion( - id: Value(id), - shortcutId: shortcutId == null && nullToAbsent ? const Value.absent() : Value(shortcutId), - distanceMetres: Value(distanceMetres), - durationSeconds: Value(durationSeconds), - elevationGainMetres: Value(elevationGainMetres), - elevationLossMetres: Value(elevationLossMetres), - averageSpeedKmh: Value(averageSpeedKmh), - startTime: Value(startTime), - ); - } - - factory RideSummary.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return RideSummary( - id: serializer.fromJson(json['id']), - shortcutId: serializer.fromJson(json['shortcutId']), - distanceMetres: serializer.fromJson(json['distanceMetres']), - durationSeconds: serializer.fromJson(json['durationSeconds']), - elevationGainMetres: serializer.fromJson(json['elevationGainMetres']), - elevationLossMetres: serializer.fromJson(json['elevationLossMetres']), - averageSpeedKmh: serializer.fromJson(json['averageSpeedKmh']), - startTime: serializer.fromJson(json['startTime']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'shortcutId': serializer.toJson(shortcutId), - 'distanceMetres': serializer.toJson(distanceMetres), - 'durationSeconds': serializer.toJson(durationSeconds), - 'elevationGainMetres': serializer.toJson(elevationGainMetres), - 'elevationLossMetres': serializer.toJson(elevationLossMetres), - 'averageSpeedKmh': serializer.toJson(averageSpeedKmh), - 'startTime': serializer.toJson(startTime), - }; - } - - RideSummary copyWith( - {int? id, - Value shortcutId = const Value.absent(), - double? distanceMetres, - double? durationSeconds, - double? elevationGainMetres, - double? elevationLossMetres, - double? averageSpeedKmh, - DateTime? startTime}) => - RideSummary( - id: id ?? this.id, - shortcutId: shortcutId.present ? shortcutId.value : this.shortcutId, - distanceMetres: distanceMetres ?? this.distanceMetres, - durationSeconds: durationSeconds ?? this.durationSeconds, - elevationGainMetres: elevationGainMetres ?? this.elevationGainMetres, - elevationLossMetres: elevationLossMetres ?? this.elevationLossMetres, - averageSpeedKmh: averageSpeedKmh ?? this.averageSpeedKmh, - startTime: startTime ?? this.startTime, - ); - @override - String toString() { - return (StringBuffer('RideSummary(') - ..write('id: $id, ') - ..write('shortcutId: $shortcutId, ') - ..write('distanceMetres: $distanceMetres, ') - ..write('durationSeconds: $durationSeconds, ') - ..write('elevationGainMetres: $elevationGainMetres, ') - ..write('elevationLossMetres: $elevationLossMetres, ') - ..write('averageSpeedKmh: $averageSpeedKmh, ') - ..write('startTime: $startTime') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, shortcutId, distanceMetres, durationSeconds, elevationGainMetres, - elevationLossMetres, averageSpeedKmh, startTime); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is RideSummary && - other.id == this.id && - other.shortcutId == this.shortcutId && - other.distanceMetres == this.distanceMetres && - other.durationSeconds == this.durationSeconds && - other.elevationGainMetres == this.elevationGainMetres && - other.elevationLossMetres == this.elevationLossMetres && - other.averageSpeedKmh == this.averageSpeedKmh && - other.startTime == this.startTime); -} - -class RideSummariesCompanion extends UpdateCompanion { - final Value id; - final Value shortcutId; - final Value distanceMetres; - final Value durationSeconds; - final Value elevationGainMetres; - final Value elevationLossMetres; - final Value averageSpeedKmh; - final Value startTime; - const RideSummariesCompanion({ - this.id = const Value.absent(), - this.shortcutId = const Value.absent(), - this.distanceMetres = const Value.absent(), - this.durationSeconds = const Value.absent(), - this.elevationGainMetres = const Value.absent(), - this.elevationLossMetres = const Value.absent(), - this.averageSpeedKmh = const Value.absent(), - this.startTime = const Value.absent(), - }); - RideSummariesCompanion.insert({ - this.id = const Value.absent(), - this.shortcutId = const Value.absent(), - required double distanceMetres, - required double durationSeconds, - required double elevationGainMetres, - required double elevationLossMetres, - required double averageSpeedKmh, - required DateTime startTime, - }) : distanceMetres = Value(distanceMetres), - durationSeconds = Value(durationSeconds), - elevationGainMetres = Value(elevationGainMetres), - elevationLossMetres = Value(elevationLossMetres), - averageSpeedKmh = Value(averageSpeedKmh), - startTime = Value(startTime); - static Insertable custom({ - Expression? id, - Expression? shortcutId, - Expression? distanceMetres, - Expression? durationSeconds, - Expression? elevationGainMetres, - Expression? elevationLossMetres, - Expression? averageSpeedKmh, - Expression? startTime, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (shortcutId != null) 'shortcut_id': shortcutId, - if (distanceMetres != null) 'distance_metres': distanceMetres, - if (durationSeconds != null) 'duration_seconds': durationSeconds, - if (elevationGainMetres != null) 'elevation_gain_metres': elevationGainMetres, - if (elevationLossMetres != null) 'elevation_loss_metres': elevationLossMetres, - if (averageSpeedKmh != null) 'average_speed_kmh': averageSpeedKmh, - if (startTime != null) 'start_time': startTime, - }); - } - - RideSummariesCompanion copyWith( - {Value? id, - Value? shortcutId, - Value? distanceMetres, - Value? durationSeconds, - Value? elevationGainMetres, - Value? elevationLossMetres, - Value? averageSpeedKmh, - Value? startTime}) { - return RideSummariesCompanion( - id: id ?? this.id, - shortcutId: shortcutId ?? this.shortcutId, - distanceMetres: distanceMetres ?? this.distanceMetres, - durationSeconds: durationSeconds ?? this.durationSeconds, - elevationGainMetres: elevationGainMetres ?? this.elevationGainMetres, - elevationLossMetres: elevationLossMetres ?? this.elevationLossMetres, - averageSpeedKmh: averageSpeedKmh ?? this.averageSpeedKmh, - startTime: startTime ?? this.startTime, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (shortcutId.present) { - map['shortcut_id'] = Variable(shortcutId.value); - } - if (distanceMetres.present) { - map['distance_metres'] = Variable(distanceMetres.value); - } - if (durationSeconds.present) { - map['duration_seconds'] = Variable(durationSeconds.value); - } - if (elevationGainMetres.present) { - map['elevation_gain_metres'] = Variable(elevationGainMetres.value); - } - if (elevationLossMetres.present) { - map['elevation_loss_metres'] = Variable(elevationLossMetres.value); - } - if (averageSpeedKmh.present) { - map['average_speed_kmh'] = Variable(averageSpeedKmh.value); - } - if (startTime.present) { - map['start_time'] = Variable(startTime.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('RideSummariesCompanion(') - ..write('id: $id, ') - ..write('shortcutId: $shortcutId, ') - ..write('distanceMetres: $distanceMetres, ') - ..write('durationSeconds: $durationSeconds, ') - ..write('elevationGainMetres: $elevationGainMetres, ') - ..write('elevationLossMetres: $elevationLossMetres, ') - ..write('averageSpeedKmh: $averageSpeedKmh, ') - ..write('startTime: $startTime') - ..write(')')) - .toString(); - } -} - -class $ChallengesTable extends Challenges with TableInfo<$ChallengesTable, Challenge> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ChallengesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _xpMeta = const VerificationMeta('xp'); - @override - late final GeneratedColumn xp = - GeneratedColumn('xp', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _startTimeMeta = const VerificationMeta('startTime'); - @override - late final GeneratedColumn startTime = GeneratedColumn('start_time', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - static const VerificationMeta _closingTimeMeta = const VerificationMeta('closingTime'); - @override - late final GeneratedColumn closingTime = GeneratedColumn('closing_time', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - static const VerificationMeta _descriptionMeta = const VerificationMeta('description'); - @override - late final GeneratedColumn description = - GeneratedColumn('description', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _targetMeta = const VerificationMeta('target'); - @override - late final GeneratedColumn target = - GeneratedColumn('target', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _progressMeta = const VerificationMeta('progress'); - @override - late final GeneratedColumn progress = - GeneratedColumn('progress', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _isWeeklyMeta = const VerificationMeta('isWeekly'); - @override - late final GeneratedColumn isWeekly = GeneratedColumn('is_weekly', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("is_weekly" IN (0, 1))', - SqlDialect.mariadb: '', - SqlDialect.postgres: '', - })); - static const VerificationMeta _isOpenMeta = const VerificationMeta('isOpen'); - @override - late final GeneratedColumn isOpen = GeneratedColumn('is_open', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("is_open" IN (0, 1))', - SqlDialect.mariadb: '', - SqlDialect.postgres: '', - })); - static const VerificationMeta _routeIdMeta = const VerificationMeta('routeId'); - @override - late final GeneratedColumn routeId = - GeneratedColumn('route_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedColumn type = - GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => - [id, xp, startTime, closingTime, description, target, progress, isWeekly, isOpen, routeId, type]; - @override - String get aliasedName => _alias ?? 'challenges'; - @override - String get actualTableName => 'challenges'; - @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('xp')) { - context.handle(_xpMeta, xp.isAcceptableOrUnknown(data['xp']!, _xpMeta)); - } else if (isInserting) { - context.missing(_xpMeta); - } - if (data.containsKey('start_time')) { - context.handle(_startTimeMeta, startTime.isAcceptableOrUnknown(data['start_time']!, _startTimeMeta)); - } else if (isInserting) { - context.missing(_startTimeMeta); - } - if (data.containsKey('closing_time')) { - context.handle(_closingTimeMeta, closingTime.isAcceptableOrUnknown(data['closing_time']!, _closingTimeMeta)); - } else if (isInserting) { - context.missing(_closingTimeMeta); - } - if (data.containsKey('description')) { - context.handle(_descriptionMeta, description.isAcceptableOrUnknown(data['description']!, _descriptionMeta)); - } else if (isInserting) { - context.missing(_descriptionMeta); - } - if (data.containsKey('target')) { - context.handle(_targetMeta, target.isAcceptableOrUnknown(data['target']!, _targetMeta)); - } else if (isInserting) { - context.missing(_targetMeta); - } - if (data.containsKey('progress')) { - context.handle(_progressMeta, progress.isAcceptableOrUnknown(data['progress']!, _progressMeta)); - } else if (isInserting) { - context.missing(_progressMeta); - } - if (data.containsKey('is_weekly')) { - context.handle(_isWeeklyMeta, isWeekly.isAcceptableOrUnknown(data['is_weekly']!, _isWeeklyMeta)); - } else if (isInserting) { - context.missing(_isWeeklyMeta); - } - if (data.containsKey('is_open')) { - context.handle(_isOpenMeta, isOpen.isAcceptableOrUnknown(data['is_open']!, _isOpenMeta)); - } else if (isInserting) { - context.missing(_isOpenMeta); - } - if (data.containsKey('route_id')) { - context.handle(_routeIdMeta, routeId.isAcceptableOrUnknown(data['route_id']!, _routeIdMeta)); - } - if (data.containsKey('type')) { - context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); - } else if (isInserting) { - context.missing(_typeMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - Challenge map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Challenge( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - xp: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}xp'])!, - startTime: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}start_time'])!, - closingTime: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}closing_time'])!, - description: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}description'])!, - target: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}target'])!, - progress: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}progress'])!, - isWeekly: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_weekly'])!, - isOpen: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}is_open'])!, - routeId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}route_id']), - type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, - ); - } - - @override - $ChallengesTable createAlias(String alias) { - return $ChallengesTable(attachedDatabase, alias); - } -} - -class Challenge extends DataClass implements Insertable { - final int id; - final int xp; - final DateTime startTime; - final DateTime closingTime; - final String description; - final int target; - final int progress; - final bool isWeekly; - final bool isOpen; - final String? routeId; - final int type; - const Challenge( - {required this.id, - required this.xp, - required this.startTime, - required this.closingTime, - required this.description, - required this.target, - required this.progress, - required this.isWeekly, - required this.isOpen, - this.routeId, - required this.type}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['xp'] = Variable(xp); - map['start_time'] = Variable(startTime); - map['closing_time'] = Variable(closingTime); - map['description'] = Variable(description); - map['target'] = Variable(target); - map['progress'] = Variable(progress); - map['is_weekly'] = Variable(isWeekly); - map['is_open'] = Variable(isOpen); - if (!nullToAbsent || routeId != null) { - map['route_id'] = Variable(routeId); - } - map['type'] = Variable(type); - return map; - } - - ChallengesCompanion toCompanion(bool nullToAbsent) { - return ChallengesCompanion( - id: Value(id), - xp: Value(xp), - startTime: Value(startTime), - closingTime: Value(closingTime), - description: Value(description), - target: Value(target), - progress: Value(progress), - isWeekly: Value(isWeekly), - isOpen: Value(isOpen), - routeId: routeId == null && nullToAbsent ? const Value.absent() : Value(routeId), - type: Value(type), - ); - } - - factory Challenge.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return Challenge( - id: serializer.fromJson(json['id']), - xp: serializer.fromJson(json['xp']), - startTime: serializer.fromJson(json['startTime']), - closingTime: serializer.fromJson(json['closingTime']), - description: serializer.fromJson(json['description']), - target: serializer.fromJson(json['target']), - progress: serializer.fromJson(json['progress']), - isWeekly: serializer.fromJson(json['isWeekly']), - isOpen: serializer.fromJson(json['isOpen']), - routeId: serializer.fromJson(json['routeId']), - type: serializer.fromJson(json['type']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'xp': serializer.toJson(xp), - 'startTime': serializer.toJson(startTime), - 'closingTime': serializer.toJson(closingTime), - 'description': serializer.toJson(description), - 'target': serializer.toJson(target), - 'progress': serializer.toJson(progress), - 'isWeekly': serializer.toJson(isWeekly), - 'isOpen': serializer.toJson(isOpen), - 'routeId': serializer.toJson(routeId), - 'type': serializer.toJson(type), - }; - } - - Challenge copyWith( - {int? id, - int? xp, - DateTime? startTime, - DateTime? closingTime, - String? description, - int? target, - int? progress, - bool? isWeekly, - bool? isOpen, - Value routeId = const Value.absent(), - int? type}) => - Challenge( - id: id ?? this.id, - xp: xp ?? this.xp, - startTime: startTime ?? this.startTime, - closingTime: closingTime ?? this.closingTime, - description: description ?? this.description, - target: target ?? this.target, - progress: progress ?? this.progress, - isWeekly: isWeekly ?? this.isWeekly, - isOpen: isOpen ?? this.isOpen, - routeId: routeId.present ? routeId.value : this.routeId, - type: type ?? this.type, - ); - @override - String toString() { - return (StringBuffer('Challenge(') - ..write('id: $id, ') - ..write('xp: $xp, ') - ..write('startTime: $startTime, ') - ..write('closingTime: $closingTime, ') - ..write('description: $description, ') - ..write('target: $target, ') - ..write('progress: $progress, ') - ..write('isWeekly: $isWeekly, ') - ..write('isOpen: $isOpen, ') - ..write('routeId: $routeId, ') - ..write('type: $type') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, xp, startTime, closingTime, description, target, progress, isWeekly, isOpen, routeId, type); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is Challenge && - other.id == this.id && - other.xp == this.xp && - other.startTime == this.startTime && - other.closingTime == this.closingTime && - other.description == this.description && - other.target == this.target && - other.progress == this.progress && - other.isWeekly == this.isWeekly && - other.isOpen == this.isOpen && - other.routeId == this.routeId && - other.type == this.type); -} - -class ChallengesCompanion extends UpdateCompanion { - final Value id; - final Value xp; - final Value startTime; - final Value closingTime; - final Value description; - final Value target; - final Value progress; - final Value isWeekly; - final Value isOpen; - final Value routeId; - final Value type; - const ChallengesCompanion({ - this.id = const Value.absent(), - this.xp = const Value.absent(), - this.startTime = const Value.absent(), - this.closingTime = const Value.absent(), - this.description = const Value.absent(), - this.target = const Value.absent(), - this.progress = const Value.absent(), - this.isWeekly = const Value.absent(), - this.isOpen = const Value.absent(), - this.routeId = const Value.absent(), - this.type = const Value.absent(), - }); - ChallengesCompanion.insert({ - this.id = const Value.absent(), - required int xp, - required DateTime startTime, - required DateTime closingTime, - required String description, - required int target, - required int progress, - required bool isWeekly, - required bool isOpen, - this.routeId = const Value.absent(), - required int type, - }) : xp = Value(xp), - startTime = Value(startTime), - closingTime = Value(closingTime), - description = Value(description), - target = Value(target), - progress = Value(progress), - isWeekly = Value(isWeekly), - isOpen = Value(isOpen), - type = Value(type); - static Insertable custom({ - Expression? id, - Expression? xp, - Expression? startTime, - Expression? closingTime, - Expression? description, - Expression? target, - Expression? progress, - Expression? isWeekly, - Expression? isOpen, - Expression? routeId, - Expression? type, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (xp != null) 'xp': xp, - if (startTime != null) 'start_time': startTime, - if (closingTime != null) 'closing_time': closingTime, - if (description != null) 'description': description, - if (target != null) 'target': target, - if (progress != null) 'progress': progress, - if (isWeekly != null) 'is_weekly': isWeekly, - if (isOpen != null) 'is_open': isOpen, - if (routeId != null) 'route_id': routeId, - if (type != null) 'type': type, - }); - } - - ChallengesCompanion copyWith( - {Value? id, - Value? xp, - Value? startTime, - Value? closingTime, - Value? description, - Value? target, - Value? progress, - Value? isWeekly, - Value? isOpen, - Value? routeId, - Value? type}) { - return ChallengesCompanion( - id: id ?? this.id, - xp: xp ?? this.xp, - startTime: startTime ?? this.startTime, - closingTime: closingTime ?? this.closingTime, - description: description ?? this.description, - target: target ?? this.target, - progress: progress ?? this.progress, - isWeekly: isWeekly ?? this.isWeekly, - isOpen: isOpen ?? this.isOpen, - routeId: routeId ?? this.routeId, - type: type ?? this.type, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (xp.present) { - map['xp'] = Variable(xp.value); - } - if (startTime.present) { - map['start_time'] = Variable(startTime.value); - } - if (closingTime.present) { - map['closing_time'] = Variable(closingTime.value); - } - if (description.present) { - map['description'] = Variable(description.value); - } - if (target.present) { - map['target'] = Variable(target.value); - } - if (progress.present) { - map['progress'] = Variable(progress.value); - } - if (isWeekly.present) { - map['is_weekly'] = Variable(isWeekly.value); - } - if (isOpen.present) { - map['is_open'] = Variable(isOpen.value); - } - if (routeId.present) { - map['route_id'] = Variable(routeId.value); - } - if (type.present) { - map['type'] = Variable(type.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('ChallengesCompanion(') - ..write('id: $id, ') - ..write('xp: $xp, ') - ..write('startTime: $startTime, ') - ..write('closingTime: $closingTime, ') - ..write('description: $description, ') - ..write('target: $target, ') - ..write('progress: $progress, ') - ..write('isWeekly: $isWeekly, ') - ..write('isOpen: $isOpen, ') - ..write('routeId: $routeId, ') - ..write('type: $type') - ..write(')')) - .toString(); - } -} - -class $AchievedLocationsTable extends AchievedLocations with TableInfo<$AchievedLocationsTable, AchievedLocation> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $AchievedLocationsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _locationIdMeta = const VerificationMeta('locationId'); - @override - late final GeneratedColumn locationId = - GeneratedColumn('location_id', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _eventIdMeta = const VerificationMeta('eventId'); - @override - late final GeneratedColumn eventId = - GeneratedColumn('event_id', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _titleMeta = const VerificationMeta('title'); - @override - late final GeneratedColumn title = - GeneratedColumn('title', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _timestampMeta = const VerificationMeta('timestamp'); - @override - late final GeneratedColumn timestamp = GeneratedColumn('timestamp', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - @override - List get $columns => [id, locationId, eventId, title, timestamp]; - @override - String get aliasedName => _alias ?? 'achieved_locations'; - @override - String get actualTableName => 'achieved_locations'; - @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('location_id')) { - context.handle(_locationIdMeta, locationId.isAcceptableOrUnknown(data['location_id']!, _locationIdMeta)); - } else if (isInserting) { - context.missing(_locationIdMeta); - } - if (data.containsKey('event_id')) { - context.handle(_eventIdMeta, eventId.isAcceptableOrUnknown(data['event_id']!, _eventIdMeta)); - } else if (isInserting) { - context.missing(_eventIdMeta); - } - if (data.containsKey('title')) { - context.handle(_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta)); - } else if (isInserting) { - context.missing(_titleMeta); - } - if (data.containsKey('timestamp')) { - context.handle(_timestampMeta, timestamp.isAcceptableOrUnknown(data['timestamp']!, _timestampMeta)); - } else if (isInserting) { - context.missing(_timestampMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - AchievedLocation map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AchievedLocation( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - locationId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}location_id'])!, - eventId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}event_id'])!, - title: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}title'])!, - timestamp: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}timestamp'])!, - ); - } - - @override - $AchievedLocationsTable createAlias(String alias) { - return $AchievedLocationsTable(attachedDatabase, alias); - } -} - -class AchievedLocation extends DataClass implements Insertable { - final int id; - final int locationId; - final int eventId; - final String title; - final DateTime timestamp; - const AchievedLocation( - {required this.id, - required this.locationId, - required this.eventId, - required this.title, - required this.timestamp}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['location_id'] = Variable(locationId); - map['event_id'] = Variable(eventId); - map['title'] = Variable(title); - map['timestamp'] = Variable(timestamp); - return map; - } - - AchievedLocationsCompanion toCompanion(bool nullToAbsent) { - return AchievedLocationsCompanion( - id: Value(id), - locationId: Value(locationId), - eventId: Value(eventId), - title: Value(title), - timestamp: Value(timestamp), - ); - } - - factory AchievedLocation.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AchievedLocation( - id: serializer.fromJson(json['id']), - locationId: serializer.fromJson(json['locationId']), - eventId: serializer.fromJson(json['eventId']), - title: serializer.fromJson(json['title']), - timestamp: serializer.fromJson(json['timestamp']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'locationId': serializer.toJson(locationId), - 'eventId': serializer.toJson(eventId), - 'title': serializer.toJson(title), - 'timestamp': serializer.toJson(timestamp), - }; - } - - AchievedLocation copyWith({int? id, int? locationId, int? eventId, String? title, DateTime? timestamp}) => - AchievedLocation( - id: id ?? this.id, - locationId: locationId ?? this.locationId, - eventId: eventId ?? this.eventId, - title: title ?? this.title, - timestamp: timestamp ?? this.timestamp, - ); - @override - String toString() { - return (StringBuffer('AchievedLocation(') - ..write('id: $id, ') - ..write('locationId: $locationId, ') - ..write('eventId: $eventId, ') - ..write('title: $title, ') - ..write('timestamp: $timestamp') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, locationId, eventId, title, timestamp); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AchievedLocation && - other.id == this.id && - other.locationId == this.locationId && - other.eventId == this.eventId && - other.title == this.title && - other.timestamp == this.timestamp); -} - -class AchievedLocationsCompanion extends UpdateCompanion { - final Value id; - final Value locationId; - final Value eventId; - final Value title; - final Value timestamp; - const AchievedLocationsCompanion({ - this.id = const Value.absent(), - this.locationId = const Value.absent(), - this.eventId = const Value.absent(), - this.title = const Value.absent(), - this.timestamp = const Value.absent(), - }); - AchievedLocationsCompanion.insert({ - this.id = const Value.absent(), - required int locationId, - required int eventId, - required String title, - required DateTime timestamp, - }) : locationId = Value(locationId), - eventId = Value(eventId), - title = Value(title), - timestamp = Value(timestamp); - static Insertable custom({ - Expression? id, - Expression? locationId, - Expression? eventId, - Expression? title, - Expression? timestamp, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (locationId != null) 'location_id': locationId, - if (eventId != null) 'event_id': eventId, - if (title != null) 'title': title, - if (timestamp != null) 'timestamp': timestamp, - }); - } - - AchievedLocationsCompanion copyWith( - {Value? id, Value? locationId, Value? eventId, Value? title, Value? timestamp}) { - return AchievedLocationsCompanion( - id: id ?? this.id, - locationId: locationId ?? this.locationId, - eventId: eventId ?? this.eventId, - title: title ?? this.title, - timestamp: timestamp ?? this.timestamp, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (locationId.present) { - map['location_id'] = Variable(locationId.value); - } - if (eventId.present) { - map['event_id'] = Variable(eventId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (timestamp.present) { - map['timestamp'] = Variable(timestamp.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AchievedLocationsCompanion(') - ..write('id: $id, ') - ..write('locationId: $locationId, ') - ..write('eventId: $eventId, ') - ..write('title: $title, ') - ..write('timestamp: $timestamp') - ..write(')')) - .toString(); - } -} - -class $EventBadgesTable extends EventBadges with TableInfo<$EventBadgesTable, EventBadge> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $EventBadgesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _iconMeta = const VerificationMeta('icon'); - @override - late final GeneratedColumn icon = - GeneratedColumn('icon', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _eventIdMeta = const VerificationMeta('eventId'); - @override - late final GeneratedColumn eventId = - GeneratedColumn('event_id', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _titleMeta = const VerificationMeta('title'); - @override - late final GeneratedColumn title = - GeneratedColumn('title', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _achievedTimestampMeta = const VerificationMeta('achievedTimestamp'); - @override - late final GeneratedColumn achievedTimestamp = GeneratedColumn( - 'achieved_timestamp', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - @override - List get $columns => [id, icon, eventId, title, achievedTimestamp]; - @override - String get aliasedName => _alias ?? 'event_badges'; - @override - String get actualTableName => 'event_badges'; - @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('icon')) { - context.handle(_iconMeta, icon.isAcceptableOrUnknown(data['icon']!, _iconMeta)); - } else if (isInserting) { - context.missing(_iconMeta); - } - if (data.containsKey('event_id')) { - context.handle(_eventIdMeta, eventId.isAcceptableOrUnknown(data['event_id']!, _eventIdMeta)); - } else if (isInserting) { - context.missing(_eventIdMeta); - } - if (data.containsKey('title')) { - context.handle(_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta)); - } else if (isInserting) { - context.missing(_titleMeta); - } - if (data.containsKey('achieved_timestamp')) { - context.handle(_achievedTimestampMeta, - achievedTimestamp.isAcceptableOrUnknown(data['achieved_timestamp']!, _achievedTimestampMeta)); - } else if (isInserting) { - context.missing(_achievedTimestampMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - EventBadge map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return EventBadge( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - icon: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}icon'])!, - eventId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}event_id'])!, - title: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}title'])!, - achievedTimestamp: - attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}achieved_timestamp'])!, - ); - } - - @override - $EventBadgesTable createAlias(String alias) { - return $EventBadgesTable(attachedDatabase, alias); - } -} - -class EventBadge extends DataClass implements Insertable { - final int id; - final int icon; - final int eventId; - final String title; - final DateTime achievedTimestamp; - const EventBadge( - {required this.id, - required this.icon, - required this.eventId, - required this.title, - required this.achievedTimestamp}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['icon'] = Variable(icon); - map['event_id'] = Variable(eventId); - map['title'] = Variable(title); - map['achieved_timestamp'] = Variable(achievedTimestamp); - return map; - } - - EventBadgesCompanion toCompanion(bool nullToAbsent) { - return EventBadgesCompanion( - id: Value(id), - icon: Value(icon), - eventId: Value(eventId), - title: Value(title), - achievedTimestamp: Value(achievedTimestamp), - ); - } - - factory EventBadge.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return EventBadge( - id: serializer.fromJson(json['id']), - icon: serializer.fromJson(json['icon']), - eventId: serializer.fromJson(json['eventId']), - title: serializer.fromJson(json['title']), - achievedTimestamp: serializer.fromJson(json['achievedTimestamp']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'icon': serializer.toJson(icon), - 'eventId': serializer.toJson(eventId), - 'title': serializer.toJson(title), - 'achievedTimestamp': serializer.toJson(achievedTimestamp), - }; - } - - EventBadge copyWith({int? id, int? icon, int? eventId, String? title, DateTime? achievedTimestamp}) => EventBadge( - id: id ?? this.id, - icon: icon ?? this.icon, - eventId: eventId ?? this.eventId, - title: title ?? this.title, - achievedTimestamp: achievedTimestamp ?? this.achievedTimestamp, - ); - @override - String toString() { - return (StringBuffer('EventBadge(') - ..write('id: $id, ') - ..write('icon: $icon, ') - ..write('eventId: $eventId, ') - ..write('title: $title, ') - ..write('achievedTimestamp: $achievedTimestamp') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, icon, eventId, title, achievedTimestamp); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is EventBadge && - other.id == this.id && - other.icon == this.icon && - other.eventId == this.eventId && - other.title == this.title && - other.achievedTimestamp == this.achievedTimestamp); -} - -class EventBadgesCompanion extends UpdateCompanion { - final Value id; - final Value icon; - final Value eventId; - final Value title; - final Value achievedTimestamp; - const EventBadgesCompanion({ - this.id = const Value.absent(), - this.icon = const Value.absent(), - this.eventId = const Value.absent(), - this.title = const Value.absent(), - this.achievedTimestamp = const Value.absent(), - }); - EventBadgesCompanion.insert({ - this.id = const Value.absent(), - required int icon, - required int eventId, - required String title, - required DateTime achievedTimestamp, - }) : icon = Value(icon), - eventId = Value(eventId), - title = Value(title), - achievedTimestamp = Value(achievedTimestamp); - static Insertable custom({ - Expression? id, - Expression? icon, - Expression? eventId, - Expression? title, - Expression? achievedTimestamp, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (icon != null) 'icon': icon, - if (eventId != null) 'event_id': eventId, - if (title != null) 'title': title, - if (achievedTimestamp != null) 'achieved_timestamp': achievedTimestamp, - }); - } - - EventBadgesCompanion copyWith( - {Value? id, - Value? icon, - Value? eventId, - Value? title, - Value? achievedTimestamp}) { - return EventBadgesCompanion( - id: id ?? this.id, - icon: icon ?? this.icon, - eventId: eventId ?? this.eventId, - title: title ?? this.title, - achievedTimestamp: achievedTimestamp ?? this.achievedTimestamp, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - if (eventId.present) { - map['event_id'] = Variable(eventId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (achievedTimestamp.present) { - map['achieved_timestamp'] = Variable(achievedTimestamp.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('EventBadgesCompanion(') - ..write('id: $id, ') - ..write('icon: $icon, ') - ..write('eventId: $eventId, ') - ..write('title: $title, ') - ..write('achievedTimestamp: $achievedTimestamp') - ..write(')')) - .toString(); - } -} - -abstract class _$AppDatabase extends GeneratedDatabase { - _$AppDatabase(QueryExecutor e) : super(e); - late final $RideSummariesTable rideSummaries = $RideSummariesTable(this); - late final $ChallengesTable challenges = $ChallengesTable(this); - late final $AchievedLocationsTable achievedLocations = $AchievedLocationsTable(this); - late final $EventBadgesTable eventBadges = $EventBadgesTable(this); - late final RideSummaryDao rideSummaryDao = RideSummaryDao(this as AppDatabase); - late final ChallengeDao challengeDao = ChallengeDao(this as AppDatabase); - late final AchievedLocationDao achievedLocationDao = AchievedLocationDao(this as AppDatabase); - late final EventBadgeDao eventBadgeDao = EventBadgeDao(this as AppDatabase); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [rideSummaries, challenges, achievedLocations, eventBadges]; -} diff --git a/lib/gamification/common/database/database_dao.dart b/lib/gamification/common/database/database_dao.dart deleted file mode 100644 index 09b59488c..000000000 --- a/lib/gamification/common/database/database_dao.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:priobike/gamification/common/database/database.dart'; - -/// Abstract database access object (DAO) class to be extendet by actual DAOs with corresponding [Table]. -/// T specifies the [DataClass] held by the table. -abstract class DatabaseDao extends DatabaseAccessor { - DatabaseDao(super.attachedDatabase); - - /// Table corresponding to DAOs extending this class. Needs to be implemented. - TableInfo get table; - - /// Select statement to filter table by a given value of the primary key. Needs to be implemented. - SimpleSelectStatement selectByPrimaryKey(dynamic value); - - /// Simple select statement to select the whole table. - SimpleSelectStatement get _select => select(table); - - /// Convert result list of dynamic objects to list of corrsponding data objects by casting. - List _castResultList(List result) => result.map((r) => r as T).toList(); - - /// Insert object into the database. Returns null if the object wasn't inserted or the object with the new id. - Future createObject(Insertable object) async { - var id = await into(table).insert(object); - return getObjectByPrimaryKey(id); - } - - /// Update object which is already in the database. Returns true if the change was successfully made. - Future updateObject(Insertable object) async { - return update(table).replace(object); - } - - /// Delete object from the database by id. Returns true if an object was deleted successfully. - Future deleteObject(Insertable object) async { - return (await delete(table).delete(object)) > 0; - } - - /// Get specific object from corresponding table by primary key. - Future getObjectByPrimaryKey(dynamic value) async { - return (await selectByPrimaryKey(value).getSingleOrNull()) as T?; - } - - /// Get stream of a specific object from corresponding table by primary key. - Stream streamObjectByPrimaryKey(dynamic value) { - return selectByPrimaryKey(value).watchSingleOrNull().asyncMap((result) => result as T?); - } - - /// Get all objects as a list from the corresponding table. - Future> getAllObjects() async { - return _castResultList(await _select.get()); - } - - /// Get a stream of all objects as a list from the corresponding table. - Stream> streamAllObjects() { - return _select.watch().asyncMap((result) => _castResultList(result)); - } - - Future clearObjects() async { - var objects = await getAllObjects(); - for (var o in objects) { - await deleteObject(o as Insertable); - } - } -} diff --git a/lib/gamification/common/database/model/achieved_location/achieved_location.dart b/lib/gamification/common/database/model/achieved_location/achieved_location.dart deleted file mode 100644 index 036fe18ec..000000000 --- a/lib/gamification/common/database/model/achieved_location/achieved_location.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/database_dao.dart'; -import 'package:priobike/gamification/community_event/model/event.dart'; -import 'package:priobike/gamification/community_event/model/location.dart'; - -part 'achieved_location.g.dart'; - -class AchievedLocations extends Table { - IntColumn get id => integer().autoIncrement()(); - IntColumn get locationId => integer()(); - IntColumn get eventId => integer()(); - TextColumn get title => text()(); - DateTimeColumn get timestamp => dateTime()(); -} - -@DriftAccessor(tables: [AchievedLocations]) -class AchievedLocationDao extends DatabaseDao with _$AchievedLocationDaoMixin { - AchievedLocationDao(super.attachedDatabase); - - @override - TableInfo get table => achievedLocations; - - @override - SimpleSelectStatement selectByPrimaryKey(dynamic value) { - return (select(achievedLocations)..where((tbl) => (tbl as $AchievedLocationsTable).id.equals(value))); - } - - Future createAchievedLocation(EventLocation location, WeekendEvent event) async { - var obj = await (select(achievedLocations)..where((tbl) => tbl.locationId.equals(location.id))).get(); - // If the location was already saved as achieved, return null. - if (obj.isNotEmpty) return null; - // If the location has not been saved yet, create a new object and save it. - return createObject( - AchievedLocationsCompanion.insert( - locationId: location.id, - eventId: event.id, - title: location.title, - timestamp: DateTime.now(), - ), - ); - } - - Stream> streamLocationsForEvent(int eventId) { - return (select(achievedLocations)..where((tbl) => tbl.eventId.equals(eventId))).watch(); - } -} diff --git a/lib/gamification/common/database/model/achieved_location/achieved_location.g.dart b/lib/gamification/common/database/model/achieved_location/achieved_location.g.dart deleted file mode 100644 index 090ff813a..000000000 --- a/lib/gamification/common/database/model/achieved_location/achieved_location.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'achieved_location.dart'; - -// ignore_for_file: type=lint -mixin _$AchievedLocationDaoMixin on DatabaseAccessor { - $AchievedLocationsTable get achievedLocations => attachedDatabase.achievedLocations; -} diff --git a/lib/gamification/common/database/model/challenges/challenge.dart b/lib/gamification/common/database/model/challenges/challenge.dart deleted file mode 100644 index 77c63b1ac..000000000 --- a/lib/gamification/common/database/model/challenges/challenge.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/database_dao.dart'; - -part 'challenge.g.dart'; - -/// This table which holds objects, which represent the challenges a user can do in the game. The objects hold -/// information about the challenge and its state and about the users challenge progress. -class Challenges extends Table { - IntColumn get id => integer().autoIncrement()(); - IntColumn get xp => integer()(); - DateTimeColumn get startTime => dateTime()(); - DateTimeColumn get closingTime => dateTime()(); - TextColumn get description => text()(); - IntColumn get target => integer()(); - IntColumn get progress => integer()(); - BoolColumn get isWeekly => boolean()(); - BoolColumn get isOpen => boolean()(); - TextColumn get routeId => text().nullable()(); - IntColumn get type => integer()(); -} - -@DriftAccessor(tables: [Challenges]) -class ChallengeDao extends DatabaseDao with _$ChallengeDaoMixin { - ChallengeDao(super.attachedDatabase); - - @override - TableInfo get table => challenges; - - @override - SimpleSelectStatement selectByPrimaryKey(dynamic value) { - return (select(challenges)..where((tbl) => (tbl as $ChallengesTable).id.equals(value))); - } - - /// Get all weekly challenges, that are still open. - Future> getOpenWeeklyChallenges() { - return (select(challenges)..where((tbl) => tbl.isOpen & tbl.isWeekly)).get(); - } - - /// Get all daily challenges, that are still open. - Future> getOpenDailyChallenges() { - return (select(challenges)..where((tbl) => tbl.isOpen & tbl.isWeekly.not())).get(); - } - - /// Stream all challenges, that have been completed by the user and that are closed, - /// which means the rewards were collected. - Stream> streamClosedCompletedChallenges() { - return (select(challenges)..where((tbl) => tbl.isOpen.not() & tbl.progress.isBiggerOrEqual(tbl.target))).watch(); - } - - /// Stream all challenges which can be completed in a certain given interval. - Future> getChallengesInInterval(DateTime startDay, int lengthInDays) { - var start = DateTime(startDay.year, startDay.month, startDay.day); - var end = start.add(Duration(days: lengthInDays)); - return (select(challenges) - ..where( - (tbl) => tbl.startTime.isBetweenValues(start, end) & tbl.closingTime.isBetweenValues(start, end), - )) - .get(); - } -} diff --git a/lib/gamification/common/database/model/challenges/challenge.g.dart b/lib/gamification/common/database/model/challenges/challenge.g.dart deleted file mode 100644 index 170c17004..000000000 --- a/lib/gamification/common/database/model/challenges/challenge.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'challenge.dart'; - -// ignore_for_file: type=lint -mixin _$ChallengeDaoMixin on DatabaseAccessor { - $ChallengesTable get challenges => attachedDatabase.challenges; -} diff --git a/lib/gamification/common/database/model/event_badge/event_badge.dart b/lib/gamification/common/database/model/event_badge/event_badge.dart deleted file mode 100644 index adbee5637..000000000 --- a/lib/gamification/common/database/model/event_badge/event_badge.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/database_dao.dart'; -import 'package:priobike/gamification/community_event/model/event.dart'; - -part 'event_badge.g.dart'; - -class EventBadges extends Table { - IntColumn get id => integer().autoIncrement()(); - IntColumn get icon => integer()(); - IntColumn get eventId => integer()(); - TextColumn get title => text()(); - DateTimeColumn get achievedTimestamp => dateTime()(); -} - -@DriftAccessor(tables: [EventBadges]) -class EventBadgeDao extends DatabaseDao with _$EventBadgeDaoMixin { - EventBadgeDao(super.attachedDatabase); - - @override - TableInfo get table => eventBadges; - - @override - SimpleSelectStatement selectByPrimaryKey(dynamic value) { - return (select(eventBadges)..where((tbl) => (tbl as $EventBadgesTable).id.equals(value))); - } - - Future createEventBadge(WeekendEvent event) async { - var obj = await (select(eventBadges)..where((tbl) => tbl.eventId.equals(event.id))).get(); - // If there already is a badge for the event, return null. - if (obj.isNotEmpty) return null; - // If there is no badge for the event, create a new one. - return createObject( - EventBadgesCompanion.insert( - icon: event.iconValue, - eventId: event.id, - title: event.title, - achievedTimestamp: DateTime.now(), - ), - ); - } -} diff --git a/lib/gamification/common/database/model/event_badge/event_badge.g.dart b/lib/gamification/common/database/model/event_badge/event_badge.g.dart deleted file mode 100644 index 0cd04538a..000000000 --- a/lib/gamification/common/database/model/event_badge/event_badge.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'event_badge.dart'; - -// ignore_for_file: type=lint -mixin _$EventBadgeDaoMixin on DatabaseAccessor { - $EventBadgesTable get eventBadges => attachedDatabase.eventBadges; -} diff --git a/lib/gamification/common/database/model/ride_summary/ride_summary.dart b/lib/gamification/common/database/model/ride_summary/ride_summary.dart deleted file mode 100644 index 1d7b6d874..000000000 --- a/lib/gamification/common/database/model/ride_summary/ride_summary.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/database_dao.dart'; -import 'package:priobike/logging/logger.dart'; -import 'package:priobike/statistics/models/summary.dart'; - -part 'ride_summary.g.dart'; - -/// Table which holds ride summary objects, which contain relevant information of rides which the user has done. -@DataClassName('RideSummary') -class RideSummaries extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get shortcutId => text().nullable()(); - RealColumn get distanceMetres => real()(); - RealColumn get durationSeconds => real()(); - RealColumn get elevationGainMetres => real()(); - RealColumn get elevationLossMetres => real()(); - RealColumn get averageSpeedKmh => real()(); - DateTimeColumn get startTime => dateTime()(); -} - -@DriftAccessor(tables: [RideSummaries]) -class RideSummaryDao extends DatabaseDao with _$RideSummaryDaoMixin { - /// The logger for this service. - final logger = Logger("RideDAO"); - - RideSummaryDao(super.attachedDatabase); - - @override - TableInfo get table => rideSummaries; - - @override - SimpleSelectStatement selectByPrimaryKey(dynamic value) { - return (select(rideSummaries)..where((tbl) => (tbl as $RideSummariesTable).id.equals(value))); - } - - /// Store ride summary in database. - void createObjectFromSummary(Summary summary, DateTime startTime, String? shortcutId) { - createObject( - RideSummariesCompanion.insert( - distanceMetres: summary.distanceMeters, - durationSeconds: summary.durationSeconds, - elevationGainMetres: summary.elevationGain, - elevationLossMetres: summary.elevationLoss, - averageSpeedKmh: summary.averageSpeedKmH, - startTime: startTime, - shortcutId: Value(shortcutId), - ), - ); - } - - /// Returns stream of rides which started in a given time intervall, ordered by the start time. - Stream> streamRidesInInterval(DateTime firstTimeStamp, DateTime lastTimeStamp) { - return (select(rideSummaries) - ..where((tbl) { - var startTime = tbl.startTime; - return startTime.isBetweenValues(firstTimeStamp, lastTimeStamp); - }) - ..orderBy( - [(t) => OrderingTerm(expression: t.startTime)], - )) - .watch(); - } - - /// Returns stream of rides on a specific day. - Stream> streamRidesOnDay(DateTime day) { - var firstDay = DateTime(day.year, day.month, day.day); - var lastDay = firstDay.add(const Duration(days: 1)); - return streamRidesInInterval(firstDay, lastDay); - } - - /// Returns stream of rides started in a week starting from a given day. - Stream> streamRidesInWeek(DateTime startDay) { - var firstDay = DateTime(startDay.year, startDay.month, startDay.day); - var lastDay = firstDay.add(const Duration(days: 7)); - return streamRidesInInterval(firstDay, lastDay); - } - - /// Returns stream of rides started in a given month. - Stream> streamRidesInMonth(int year, int month) { - var isDecember = month == 12; - var firstDay = DateTime(year, month, 1); - var lastDay = DateTime(isDecember ? year + 1 : year, (isDecember ? 0 : month + 1), 0); - return streamRidesInInterval(firstDay, lastDay); - } - - /// Returns rides which started in a given time intervall, ordered by the start time. - Future> getRidesInInterval(DateTime firstTimeStamp, DateTime lastTimeStamp) { - return (select(rideSummaries) - ..where((tbl) { - var startTime = tbl.startTime; - return startTime.isBetweenValues(firstTimeStamp, lastTimeStamp); - }) - ..orderBy( - [(t) => OrderingTerm(expression: t.startTime)], - )) - .get(); - } - - /// Returns rides on a specific day. - Future> getRidesOnDay(DateTime day) { - var firstDay = DateTime(day.year, day.month, day.day); - var lastDay = firstDay.add(const Duration(days: 1)); - return getRidesInInterval(firstDay, lastDay); - } - - /// Returns rides started in a week starting from a given day. - Future> getRidesInWeek(DateTime startDay) { - var firstDay = DateTime(startDay.year, startDay.month, startDay.day); - var lastDay = firstDay.add(const Duration(days: 7)); - return getRidesInInterval(firstDay, lastDay); - } - - /// Returns rides started in a given month. - Future> getRidesInMonth(int year, int month) { - var isDecember = month == 12; - var firstDay = DateTime(year, month, 1); - var lastDay = DateTime(isDecember ? year + 1 : year, (isDecember ? 0 : month + 1), 0); - return getRidesInInterval(firstDay, lastDay); - } -} diff --git a/lib/gamification/common/database/model/ride_summary/ride_summary.g.dart b/lib/gamification/common/database/model/ride_summary/ride_summary.g.dart deleted file mode 100644 index e26e8b257..000000000 --- a/lib/gamification/common/database/model/ride_summary/ride_summary.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ride_summary.dart'; - -// ignore_for_file: type=lint -mixin _$RideSummaryDaoMixin on DatabaseAccessor { - $RideSummariesTable get rideSummaries => attachedDatabase.rideSummaries; -} diff --git a/lib/gamification/common/models/evaluation_data.dart b/lib/gamification/common/models/evaluation_data.dart deleted file mode 100644 index 615f3730e..000000000 --- a/lib/gamification/common/models/evaluation_data.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:convert'; - -/// This objects holds data which needs to be send to the gamification service. -class EvaluationData { - /// The url address to send the data to. - final String address; - - /// The data as an encoded json String. - final String jsonData; - - /// Number of attemps of sending this data object to the backend. - int numOfAttemps = 0; - - EvaluationData(this.address, Map map) : jsonData = json.encode(map); - - String toJson() => json.encode({'address': address, 'jsonData': jsonData, 'numOfAttemps': numOfAttemps}); - - EvaluationData.fromJson(String encoded) - : address = json.decode(encoded)['address'], - jsonData = json.decode(encoded)['jsonData'], - numOfAttemps = json.decode(encoded)['numOfAttemps']; -} diff --git a/lib/gamification/common/models/user_profile.dart b/lib/gamification/common/models/user_profile.dart deleted file mode 100644 index cfebd5fa0..000000000 --- a/lib/gamification/common/models/user_profile.dart +++ /dev/null @@ -1,44 +0,0 @@ -/// This class holds general user data concerning the gamification feature. -class UserProfile { - /// The total distance covered by a user while using the app. - double totalDistanceKilometres; - - /// The total duration the user drove while using the app. - double totalDurationMinutes; - - /// The total elevation gain the user covered. - double totalElevationGainMetres; - - /// The total elevation loss the user covered. - double totalElevationLossMetres; - - /// The average speed the user covered the total distance with. - double averageSpeedKmh; - - /// The exact time point the user profile was created. - DateTime joinDate; - - UserProfile({ - this.totalDistanceKilometres = 0, - this.totalDurationMinutes = 0, - this.totalElevationGainMetres = 0, - this.totalElevationLossMetres = 0, - required this.joinDate, - }) : averageSpeedKmh = totalDurationMinutes == 0 ? 0 : (totalDistanceKilometres / totalDurationMinutes) * 3.6; - - Map toJson() => { - 'totalDistanceMetres': totalDistanceKilometres, - 'totalDurationSeconds': totalDurationMinutes, - 'totalElevationGainMetres': totalElevationGainMetres, - 'totalElevationLossMetres': totalElevationLossMetres, - 'joinDate': joinDate.toIso8601String(), - }; - - factory UserProfile.fromJson(Map json) => UserProfile( - totalDistanceKilometres: json['totalDistanceMetres'], - totalDurationMinutes: json['totalDistanceMetres'], - totalElevationGainMetres: json['totalElevationGainMetres'], - totalElevationLossMetres: json['totalElevationLossMetres'], - joinDate: DateTime.parse(json['joinDate']), - ); -} diff --git a/lib/gamification/common/services/evaluation_data_service.dart b/lib/gamification/common/services/evaluation_data_service.dart deleted file mode 100644 index d2a90d9f0..000000000 --- a/lib/gamification/common/services/evaluation_data_service.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:priobike/gamification/common/models/evaluation_data.dart'; -import 'package:priobike/http.dart'; -import 'package:priobike/logging/logger.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/settings/models/backend.dart'; -import 'package:priobike/settings/services/settings.dart'; -import 'package:priobike/user.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// This service handles sending data relevant for the evaluation of the gamification functionality to the backend. -class EvaluationDataService { - final log = Logger("EvaluationDataService"); - - /// Shared prefs key to store unsent elements. - static const String unsentElementsKey = 'priobike.gamification.evaluation.unsentElements'; - - static const int dataSendingAttempsThreshold = 5; - - String get baseUrl => 'https://${getIt().backend.path}/game-service/'; - - /// List of unsent elements. - List unsentElements = []; - - /// Send a given json map to a given address of the gamification service. - Future sendJsonToAddress(String address, Map jsonData) async { - // Add user id and timestamp to the data and create a data object. - final userId = await User.getOrCreateId(); - jsonData.addAll({'userId': userId, 'timestamp': DateTime.now().millisecondsSinceEpoch}); - var data = EvaluationData(address, jsonData); - // Try to send data to service. - var success = await sendEvaluationData(data); - if (success) return; - // If sending was not successful, store the object in the list of unsent elements. - data.numOfAttemps = 1; - unsentElements.add(data); - var prefs = await SharedPreferences.getInstance(); - prefs.setStringList(unsentElementsKey, unsentElements.map((e) => e.toJson()).toList()); - log.e('Saved unsent message: ${data.toJson()}'); - } - - /// Try to send all elements from the list of unsent elements to the service. - Future sendUnsentElements() async { - var prefs = await SharedPreferences.getInstance(); - var tmpList = prefs.getStringList(unsentElementsKey)?.map((e) => EvaluationData.fromJson(e)).toList() ?? []; - for (var element in tmpList) { - var success = await sendEvaluationData(element); - if (success) continue; - element.numOfAttemps += 1; - if (element.numOfAttemps >= dataSendingAttempsThreshold) continue; - unsentElements.add(element); - log.i('Saved unsent message: ${element.toJson()}'); - } - - /// Store elements that still couldn't be sent in the shared prefs. - prefs.setStringList(unsentElementsKey, unsentElements.map((e) => e.toJson()).toList()); - } - - /// Send the data in a given evaluation data object to the corresponding address of the gamification service. - Future sendEvaluationData(EvaluationData data) async { - try { - // Build and parse url to send to. - final postUrl = "$baseUrl${data.address}"; - final postProfileEndpoint = Uri.parse(postUrl); - - log.i('sending gamification evaluation data: ${data.jsonData}'); - - // Try to send json data to url of the gamification service. - final response = await Http.post(postProfileEndpoint, body: data.jsonData).timeout(const Duration(seconds: 4)); - - if (response.statusCode == 200) return true; - log.e("Failed to send gamification data: ${response.statusCode} ${response.body}"); - } catch (e) { - final hint = "Failed to load gamification service response: $e"; - log.e(hint); - } - return false; - } -} diff --git a/lib/gamification/common/services/user_service.dart b/lib/gamification/common/services/user_service.dart deleted file mode 100644 index 83a9d83aa..000000000 --- a/lib/gamification/common/services/user_service.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/model/ride_summary/ride_summary.dart'; -import 'package:priobike/gamification/common/models/user_profile.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/main.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// Service which manages and provides the values of the general user profile. -class GamificationUserService with ChangeNotifier { - /// Key to store whether the gamification as a whole is enabled or not in the shared prefs. - static const gamificationEnabledKey = 'priobike.gamification.enabled'; - - /// Key to store the profile data in the shared prefs. - static const userProfileKey = 'priobike.gamification.userProfile'; - - /// Key to store the list of features, which the user has enabled, in the shared prefs. - static const enabledFeatureListKey = 'priobike.gamification.prefs.enabledFeatures'; - - /// Key describing the statistics feature. - static const statisticsFeatureKey = 'priobike.gamification.feature.statistics'; - - /// Key describing the challenges feature. - static const challengesFeatureKey = 'priobike.gamification.feature.challenges'; - - /// Key describing the community feature. - static const communityFeatureKey = 'priobike.gamification.feature.community'; - - /// List of keys for the features of the gamification system. - static const gamificationFeatures = [challengesFeatureKey, statisticsFeatureKey, communityFeatureKey]; - - /// Instance of the shared preferences. - SharedPreferences? _prefs; - - /// List of the selected game preferences of the user as string keys. - List _enabledFeatures = []; - - /// Object which holds all the user profile values. If it is null, there is no user profile yet. - UserProfile? _profile; - - /// Ride DAOs to access rides. - RideSummaryDao get rideDao => AppDatabase.instance.rideSummaryDao; - - /// The user profile for the gamificaiton functionality. - UserProfile? get profile => _profile; - - /// List of keys of the features enabled by the user. - List get enabledFeatures => _enabledFeatures; - - /// List of keys of the features disabled by the user. - List get disabledFeatures => - gamificationFeatures.whereNot((feature) => enabledFeatures.contains(feature)).toList(); - - /// Returns true, if there is a valid user profile. - bool get hasProfile => _profile != null; - - GamificationUserService() { - _loadData(); - } - - /// Start rides database stream to update user profile accordingly. - void startDatabaseStream() { - rideDao.streamAllObjects().listen((update) => _updateOverallStats(update)); - } - - /// Create a user profile with a given username and save in shared prefs. - Future createProfile() async { - _prefs ??= await SharedPreferences.getInstance(); - // Create profile and set join date to now. - _profile = UserProfile( - joinDate: DateTime.now(), - ); - // Try to save profile in shared prefs and return false if not successful. - if (!(await _prefs?.setString(userProfileKey, jsonEncode(_profile!.toJson())) ?? false)) { - return false; - } - // Try to set profile exists string and return false if not successful - if (!(await _prefs?.setBool(gamificationEnabledKey, true) ?? false)) return false; - // Start the database stream of rides, to update the profile data accordingly. - startDatabaseStream(); - sendProfileDataToBackend(); - return true; - } - - /// Load the profile from shared prefs, if there is one. - Future _loadData() async { - _prefs ??= await SharedPreferences.getInstance(); - // Return, if the profile exists value is not true or set; - if (!(_prefs?.getBool(gamificationEnabledKey) ?? false)) return; - // Try to load profile string from prefs and parse to user profile if possible. - var parsedProfile = _prefs?.getString(userProfileKey); - if (parsedProfile == null) return; - _profile = UserProfile.fromJson(jsonDecode(parsedProfile)); - _enabledFeatures = _prefs!.getStringList(enabledFeatureListKey) ?? []; - // If a profile was loaded, start the database stream of rides, to update the profile data accordingly. - startDatabaseStream(); - } - - /// Update user profile statistics according to database and user prefs and save in shared pref. - Future _updateOverallStats(List rides) async { - // If for some reason there is no user profile, return. - if (_profile == null) return; - // Update profile statistics according to rides. - RideStats stats = RideStats.fromSummaries(rides); - _profile!.totalDistanceKilometres = stats.distanceKilometres; - _profile!.totalDurationMinutes = stats.durationMinutes; - _profile!.totalElevationGainMetres = stats.elevationGainMetres; - _profile!.totalElevationLossMetres = stats.elevationLossMetres; - _profile!.averageSpeedKmh = stats.averageSpeedKmh; - _updateProfile(); - } - - /// Update profile data stored in shared prefs and notify listeners. - Future _updateProfile() async { - // Update profile in shared preferences. - _prefs ??= await SharedPreferences.getInstance(); - _prefs?.setString(userProfileKey, jsonEncode(_profile!.toJson())); - notifyListeners(); - } - - /// Returns true, if a given string key is inside of the list of selected game prefs. - bool isFeatureEnabled(String key) => _enabledFeatures.contains(key); - - /// Enable the feature with the given key. - Future enableFeature(String key) async { - if (_enabledFeatures.contains(key)) return; - _enabledFeatures.add(key); - _prefs ??= await SharedPreferences.getInstance(); - _prefs!.setStringList(enabledFeatureListKey, _enabledFeatures); - sendProfileDataToBackend(); - notifyListeners(); - } - - /// Disable the feature with the given key. - void disableFeature(String key) async { - if (!_enabledFeatures.contains(key)) return; - _enabledFeatures.remove(key); - _prefs ??= await SharedPreferences.getInstance(); - _prefs!.setStringList(enabledFeatureListKey, _enabledFeatures); - sendProfileDataToBackend(); - notifyListeners(); - } - - /// Move the feature with the given key one place down in the feature list, which moves its card one place up. - void moveFeatureUp(String key) { - if (_enabledFeatures.firstOrNull == key) return; - int index = _enabledFeatures.indexOf(key); - _enabledFeatures.remove(key); - _enabledFeatures.insert(index - 1, key); - notifyListeners(); - } - - /// Move the feature with the given key one place up in the feature list, which moves its card one place down. - void moveFeatureDown(String key) { - if (_enabledFeatures.lastOrNull == key) return; - int index = _enabledFeatures.indexOf(key); - _enabledFeatures.remove(key); - _enabledFeatures.insert(index + 1, key); - notifyListeners(); - } - - /// Reset the user profile and all generated gamification data. - Future reset() async { - var prefs = await SharedPreferences.getInstance(); - _profile = null; - _enabledFeatures.clear(); - prefs.remove(userProfileKey); - prefs.remove(gamificationEnabledKey); - prefs.remove(enabledFeatureListKey); - sendProfileDataToBackend(); - notifyListeners(); - } - - /// Send the users profile settings to the backend. - void sendProfileDataToBackend() { - Map profilData = { - 'gamificationEnabled': _profile != null, - 'challengesEnabled': isFeatureEnabled(challengesFeatureKey), - 'statisticsEnabled': isFeatureEnabled(statisticsFeatureKey), - 'communityEnabled': isFeatureEnabled(communityFeatureKey), - }; - getIt().sendJsonToAddress('settings/post/', profilData); - } -} diff --git a/lib/gamification/common/utils.dart b/lib/gamification/common/utils.dart deleted file mode 100644 index ecec5aa1a..000000000 --- a/lib/gamification/common/utils.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:intl/intl.dart'; -import 'package:priobike/gamification/challenges/utils/challenge_generator.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; - -/// Fixed long duration for animations and stuff. -class LongDuration extends Duration { - const LongDuration() : super(milliseconds: 1000); -} - -/// Fixed medium duration for animations and stuff. -class MediumDuration extends Duration { - const MediumDuration() : super(milliseconds: 500); -} - -/// Fixed short duration for animations and stuff. -class ShortDuration extends Duration { - const ShortDuration() : super(milliseconds: 250); -} - -/// A bunch of utility methods for processing ride data. -class ListUtils { - /// Calculate sum of values in list. - static double getListSum(List list) { - if (list.isEmpty) return 0; - return list.reduce((a, b) => a + b); - } - - /// Calculate average of values in list. - static double getListAvg(List list) { - if (list.isEmpty) return 0; - return list.average; - } -} - -/// A bunch of methods to format different information into strings. -class StringFormatter { - /// Get date string as day and month from given date. - static String getDateStr(DateTime date) => - '${DateFormat("dd").format(date)}. ${getMonthStr(date.month)} ${date.year}'; - - static String getShortDateStr(DateTime date) => DateFormat("dd.MM").format(date); - - /// Get time string as hour and monuts from given date. - static String getTimeStr(DateTime date) => DateFormat('hh.mm').format(date); - - /// Get string for a time interval between to dates. - static String getFromToDateStr(DateTime first, DateTime last) { - if (first.month == last.month) { - return '${DateFormat("dd").format(first)}. - ${DateFormat("dd").format(last)}. ${getMonthStr(first.month)} ${first.year}'; - } else { - return '${DateFormat("dd").format(first)}. ${getMonthStr(first.month)} - ${DateFormat("dd").format(last)}. ${getMonthStr(last.month)} ${first.year}'; - } - } - - /// Get formatted string for a given double by rounding it and giving it appending a fitting label. - static String getFormattedStrByStatType(double value, StatType type) { - return '${getRoundedStrByStatType(value, type)} ${getLabelForStatType(type)}'; - } - - /// Returns a fitting label for a given ride type, consisting of the unit describing the type. - static String getLabelForStatType(StatType type) { - if (type == StatType.distance) return 'km'; - if (type == StatType.duration) return 'min'; - if (type == StatType.speed) return 'km/h'; - if (type == StatType.elevationGain) return 'm'; - if (type == StatType.elevationLoss) return 'm'; - return ''; - } - - /// Returns a fitting label for a given ride type, consisting of the unit describing the type. - static String getDescriptionForStatType(StatType type) { - if (type == StatType.distance) return 'Distanz'; - if (type == StatType.duration) return 'Dauer'; - if (type == StatType.speed) return 'Tempo'; - if (type == StatType.elevationGain) return 'Höhenmeter'; - if (type == StatType.elevationLoss) return 'Höhenmeter'; - return ''; - } - - /// Rounds and returns a given value for a given challenge type as a string. - static getRoundStrByChallengeType(int value, var type) { - if (type == DailyChallengeType.distance || type == WeeklyChallengeType.overallDistance) { - double valueInKm = value / 1000; - if (valueInKm - valueInKm.floor() == 0) return valueInKm.toStringAsFixed(0); - return valueInKm.toStringAsFixed(1); - } - return value.toString(); - } - - /// Returns a label for the values of a given challenge type as a string. - static String getLabelForChallengeType(var type) { - if (type == DailyChallengeType.distance) return 'km'; - if (type == WeeklyChallengeType.overallDistance) return 'km'; - if (type == DailyChallengeType.duration) return 'min'; - if (type == WeeklyChallengeType.daysWithGoalsCompleted) return 'Tage'; - if (type == WeeklyChallengeType.routeRidesPerWeek) return 'Fahrten'; - if (type == WeeklyChallengeType.routeStreakInWeek) return 'Tage'; - return ''; - } - - /// Round a given value according to a given ride info type. - static String getRoundedStrByStatType(double value, StatType type) { - if (type == StatType.distance) { - if (value < 1) return truncateToString(value, 2); - if (value < 100) return truncateToString(value, 1); - return truncateToString(value, 0); - } - if (type == StatType.speed && value < 100) { - return truncateToString(value, 1); - } else { - return truncateToString(value, 0); - } - } - - /// Truncate value to avoid rounding and then convert to string. - static String truncateToString(num value, int fractionalDigits) { - var truncated = (value * pow(10, fractionalDigits)).truncate() / pow(10, fractionalDigits); - return truncated.toStringAsFixed(fractionalDigits); - } - - /// Returns a fitting string for a given month and year. - static String getMonthAndYearStr(int month, int year) => '${getMonthStr(month)} $year'; - - /// Convert month index to its name. - static String getMonthStr(int i) { - if (i == 1) return 'Jan.'; - if (i == 2) return 'Feb.'; - if (i == 3) return 'März'; - if (i == 4) return 'April'; - if (i == 5) return 'Mai'; - if (i == 6) return 'Juni'; - if (i == 7) return 'Juli'; - if (i == 8) return 'Aug.'; - if (i == 9) return 'Sept.'; - if (i == 10) return 'Okt.'; - if (i == 11) return 'Nov.'; - return 'Dez.'; - } - - /// Convert weekday index to simple string. - static String getWeekStr(int i) { - if (i == 0) return 'Mo'; - if (i == 1) return 'Di'; - if (i == 2) return 'Mi'; - if (i == 3) return 'Do'; - if (i == 4) return 'Fr'; - if (i == 5) return 'Sa'; - return 'So'; - } -} diff --git a/lib/gamification/common/views/blink_animation.dart b/lib/gamification/common/views/blink_animation.dart deleted file mode 100644 index fe977199d..000000000 --- a/lib/gamification/common/views/blink_animation.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/utils.dart'; - -/// Creates a continous blink animation for the child widget by chaning its scale. -class BlinkAnimation extends StatefulWidget { - /// The widget to get blinking. - final Widget child; - - /// The max scale the widget should have when blinking. - final double scaleFactor; - - /// Whether the blinking should be active. - final bool animate; - - /// Duration of one scale animation. - final Duration duration; - - const BlinkAnimation({ - super.key, - required this.child, - this.scaleFactor = 1.3, - this.animate = true, - this.duration = const MediumDuration(), - }); - - @override - State createState() => _BlinkAnimationState(); -} - -class _BlinkAnimationState extends State with SingleTickerProviderStateMixin { - /// Animation controller to control the blinking animation. - late final AnimationController _animationController; - - @override - void initState() { - _animationController = AnimationController(vsync: this, duration: widget.duration, value: 1); - super.initState(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // Start or stop the blinking animation according to the animate bool. - if (widget.animate) { - _animationController.repeat(reverse: true); - } else { - _animationController.stop(); - } - return ScaleTransition( - scale: Tween(begin: 1, end: widget.scaleFactor).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - ), - ), - child: widget.child, - ); - } -} diff --git a/lib/gamification/common/views/confirm_button.dart b/lib/gamification/common/views/confirm_button.dart deleted file mode 100644 index d89bc575a..000000000 --- a/lib/gamification/common/views/confirm_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; - -class ConfirmButton extends StatelessWidget { - final String label; - - final Color color; - - final Function()? onPressed; - - const ConfirmButton({super.key, required this.label, this.onPressed, this.color = CI.radkulturRed}); - - @override - Widget build(BuildContext context) { - return OnTapAnimation( - onPressed: onPressed, - child: Container( - padding: const EdgeInsets.all(12), - //width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - color: onPressed == null ? Theme.of(context).colorScheme.onBackground.withOpacity(0.25) : color, - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.05), - blurRadius: 4, - spreadRadius: 4, - offset: const Offset(1, 1), - ), - ], - ), - child: SubHeader( - text: label, - context: context, - color: onPressed == null ? Theme.of(context).colorScheme.onBackground : Colors.white, - ), - ), - ); - } -} diff --git a/lib/gamification/common/views/countdown_timer.dart b/lib/gamification/common/views/countdown_timer.dart deleted file mode 100644 index 1f0d02e4b..000000000 --- a/lib/gamification/common/views/countdown_timer.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -/// Timer which counts down to a certain timestamp and updates every second. -class CountdownTimer extends StatefulWidget { - /// The timestamp to count down to. - final DateTime timestamp; - - /// This field can be used to apply a specific style to the countdown text. - final TextStyle? style; - - const CountdownTimer({super.key, required this.timestamp, this.style}); - - @override - State createState() => _CountdownTimerState(); -} - -class _CountdownTimerState extends State { - /// Timer to update the widget periodically. - Timer? _timer; - - @override - void initState() { - /// Create timer which updates the widget every second. - _timer = Timer.periodic( - const Duration(seconds: 1), - (timer) => setState(() {}), - ); - super.initState(); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - /// Returns a string describing how much time the user has left for a challenge. - String _getTimeLeftStr(DateTime date) { - var timeLeft = date.difference(DateTime.now()); - if (timeLeft.isNegative) return 'Zeit abgelaufen'; - var result = ''; - var daysLeft = timeLeft.inDays; - if (daysLeft > 0) result += '$daysLeft ${daysLeft > 1 ? 'Tage' : 'Tag'} '; - var formatter = NumberFormat('00'); - result += '${formatter.format(timeLeft.inHours % 24)}:${formatter.format(timeLeft.inMinutes % 60)}h'; - return result; - } - - @override - Widget build(BuildContext context) { - return Text( - _getTimeLeftStr(widget.timestamp), - style: widget.style ?? - Theme.of(context).textTheme.headlineMedium!.merge(TextStyle( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - )), - ); - } -} diff --git a/lib/gamification/common/views/custom_dialog.dart b/lib/gamification/common/views/custom_dialog.dart deleted file mode 100644 index fe2e668ef..000000000 --- a/lib/gamification/common/views/custom_dialog.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/utils.dart'; - -/// A dialog widget to be used with the showDialog function and which shows a dialog with given content in a uniform style. -class CustomDialog extends StatefulWidget { - /// The content of the dialog widget. - final Widget content; - - /// Background color of the dialog. - final Color? backgroundColor; - - /// Whether to show a glow around the dialog. - final bool withGlow; - - /// Margin on the right and left side of the dialog. - final double horizontalMargin; - - const CustomDialog({ - super.key, - required this.content, - this.backgroundColor, - this.withGlow = false, - this.horizontalMargin = 32, - }); - - @override - State createState() => _CustomDialogState(); -} - -class _CustomDialogState extends State with SingleTickerProviderStateMixin { - /// Animation controller to animate the dialog appearing. - late final AnimationController _animationController; - - @override - void initState() { - _animationController = AnimationController(vsync: this, duration: const MediumDuration()); - _animationController.forward(); - super.initState(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - var lightmode = Theme.of(context).brightness == Brightness.light; - return ScaleTransition( - scale: Tween(begin: 0, end: 1).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.fastLinearToSlowEaseIn, - ), - ), - child: Center( - child: Container( - margin: EdgeInsets.symmetric(horizontal: widget.horizontalMargin), - decoration: BoxDecoration( - color: widget.backgroundColor ?? Theme.of(context).colorScheme.surfaceVariant, - borderRadius: const BorderRadius.all(Radius.circular(24)), - boxShadow: widget.withGlow - ? [ - BoxShadow( - color: Colors.white.withOpacity(lightmode ? 1 : 0.25), - spreadRadius: 0, - blurRadius: 5, - ), - ] - : null, - ), - child: widget.content, - ), - ), - ); - } -} diff --git a/lib/gamification/common/views/dialog_button.dart b/lib/gamification/common/views/dialog_button.dart deleted file mode 100644 index a6581dae7..000000000 --- a/lib/gamification/common/views/dialog_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/common/layout/tiles.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; - -/// A custom button design for a button on a dialog. -class CustomDialogButton extends StatelessWidget { - /// Color of the button. - final Color color; - - /// Text label on the button. - final String label; - - /// Callback function for when the button is pressed. - final Function() onPressed; - - const CustomDialogButton({ - super.key, - required this.color, - required this.label, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return Expanded( - child: OnTapAnimation( - onPressed: onPressed, - child: Tile( - fill: color, - padding: const EdgeInsets.symmetric(vertical: 8), - borderRadius: BorderRadius.circular(24), - content: Center( - child: BoldSubHeader( - text: label, - context: context, - color: Colors.white, - ), - ), - ), - ), - ); - } -} diff --git a/lib/gamification/common/views/feature_card.dart b/lib/gamification/common/views/feature_card.dart deleted file mode 100644 index ec1e4c657..000000000 --- a/lib/gamification/common/views/feature_card.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/animation.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/common/layout/tiles.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; -import 'package:priobike/gamification/common/views/dialog_button.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/main.dart'; - -/// A card to display the state of a given gamification feature on the home screen. -class GamificationFeatureCard extends StatefulWidget { - /// The shared prefs key of the feature. - final String featureKey; - - /// The content of the card, if the feature is enabled. - final Widget featureEnabledContent; - - /// Function that is called, when the feature is enabled by the user. - final Function() onEnabled; - - /// The page, the card navigates to, if the feature is enabled. - final Widget? featurePage; - - /// The content of the card, if the feature is disabled. - final Widget featureDisabledContent; - - const GamificationFeatureCard({ - super.key, - required this.featureKey, - required this.featureEnabledContent, - required this.featureDisabledContent, - this.featurePage, - required this.onEnabled, - }); - - @override - State createState() => _GamificationFeatureCardState(); -} - -class _GamificationFeatureCardState extends State { - /// Profile service to check, whether the feature is enabeld and to listen to changes. - late GamificationUserService _profileService; - - @override - void initState() { - _profileService = getIt(); - _profileService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _profileService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - bool featureEnabled = _profileService.enabledFeatures.contains(widget.featureKey); - - /// Show the fitting card design for whether the feature is enabled or disabled. - if (featureEnabled) { - return EnabledFeatureCard( - featureKey: widget.featureKey, - featurePage: widget.featurePage, - content: widget.featureEnabledContent, - ); - } else { - return DisabledFeatureCard( - onEnabled: widget.onEnabled, - content: widget.featureDisabledContent, - featureKey: widget.featureKey, - ); - } - } -} - -/// A card for displaying a disabled feature on the home screen. -class DisabledFeatureCard extends StatelessWidget { - /// Content to be displayed inside of the feature card. - final Widget content; - - /// The key of the corresponding feature. - final String featureKey; - - /// Function that is called, when the feature is enabled by the user. - final Function() onEnabled; - - const DisabledFeatureCard({ - super.key, - required this.content, - required this.featureKey, - required this.onEnabled, - }); - - /// A dialog which is opened, if the user presses the card to enable a feature. - void _enableFeatureDialog(var context) { - showDialog( - barrierColor: Colors.black.withOpacity(0.8), - context: context, - builder: (context) { - return CustomDialog( - content: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - BoldSubHeader( - text: 'Möchtest Du dieses Feature aktivieren?', - context: context, - textAlign: TextAlign.center, - ), - const VSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CustomDialogButton( - label: 'Abbrechen', - onPressed: () => Navigator.of(context).pop(), - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - const SmallHSpace(), - CustomDialogButton( - label: 'Aktivieren', - onPressed: () { - onEnabled(); - getIt().enableFeature(featureKey); - Navigator.of(context).pop(); - }, - color: CI.radkulturRed, - ), - ], - ), - ], - ), - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return OnTapAnimation( - onPressed: () => _enableFeatureDialog(context), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(24), - border: Border.all( - width: 1, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.07), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 2, - ), - ], - ), - child: content, - ), - ); - } -} - -/// A card for displaying an enabled feature on the home screen. -class EnabledFeatureCard extends StatefulWidget { - /// Content to be displayed inside of the card. - final Widget content; - - /// Page to which the card directs the user to, if pressed. - final Widget? featurePage; - - /// The key of the corresponding feature. - final String featureKey; - - const EnabledFeatureCard({ - super.key, - required this.content, - required this.featureKey, - this.featurePage, - }); - @override - State createState() => _EnabledFeatureCardState(); -} - -class _EnabledFeatureCardState extends State { - ///Whether to show the settings menu of the card, which enabled the user to move the card or disable the feature. - bool _showMenu = false; - - /// User service to apply changes to the feature settings. - GamificationUserService get _userService => getIt(); - - /// Whether the feature card can be moved up in the feature list. - bool get _showMoveUpButton => _userService.enabledFeatures.firstOrNull != widget.featureKey; - - /// Whether the feature card can be moved down in the feature list. - bool get _showMoveDownButton => _userService.enabledFeatures.lastOrNull != widget.featureKey; - - /// A dialog which is opened, if the user presses the disable button in the menu, which asks the user to confirm. - void _showDisableFeatureDialog() { - showDialog( - barrierColor: Colors.black.withOpacity(0.8), - context: context, - builder: (context) { - return CustomDialog( - content: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - BoldSubHeader( - text: 'Möchtest Du dieses Feature wirklich deaktivieren?', - context: context, - textAlign: TextAlign.center, - ), - const VSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CustomDialogButton( - label: 'Abbrechen', - onPressed: () => Navigator.of(context).pop(), - color: CI.radkulturRed, - ), - const SmallHSpace(), - CustomDialogButton( - label: 'Deaktivieren', - onPressed: () { - _userService.disableFeature(widget.featureKey); - Navigator.of(context).pop(); - }, - color: CI.radkulturYellow, - ), - ], - ), - ], - ), - ), - ); - }, - ); - } - - /// The design of a menu item, which invokes a given function when pressed. - Widget _getMenuItem({required IconData icon, Function()? onPressed}) { - return OnTapAnimation( - scaleFactor: 0.8, - onPressed: onPressed, - child: Container( - padding: const EdgeInsets.all(4), - child: Icon(icon, size: 32), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: OnTapAnimation( - scaleFactor: 0.95, - onPressed: widget.featurePage == null - ? null - : () => Navigator.of(context).push( - MaterialPageRoute(builder: (context) => widget.featurePage!), - ), - child: Stack( - alignment: Alignment.topRight, - children: [ - Tile( - padding: const EdgeInsets.all(16), - fill: Theme.of(context).colorScheme.background, - content: IgnorePointer( - ignoring: _showMenu, - child: widget.content, - ), - ), - if (_showMenu) - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _showMenu = false), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background.withOpacity(0.75), - borderRadius: BorderRadius.circular(24), - ), - ), - ), - ), - // If the menu is opened, it is shown above the card at the positin of the menu button. - if (_showMenu) - BlendIn( - duration: const ShortDuration(), - child: GestureDetector( - onTap: () {}, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(24), - border: Border.all( - width: 0.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - spreadRadius: 2, - blurRadius: 2, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox.fromSize(size: const Size.square(32 + 8)), - _getMenuItem( - icon: Icons.not_interested, - onPressed: _showDisableFeatureDialog, - ), - if (_showMoveUpButton) - _getMenuItem( - icon: Icons.keyboard_arrow_up, - onPressed: () => _userService.moveFeatureUp(widget.featureKey), - ), - if (_showMoveDownButton) - _getMenuItem( - icon: Icons.keyboard_arrow_down, - onPressed: () => _userService.moveFeatureDown(widget.featureKey), - ), - const SizedBox(height: 4), - ], - ), - ), - ), - ), - // Button to open or close the menu. - GestureDetector( - onTap: () => setState(() => _showMenu = !_showMenu), - child: Container( - padding: const EdgeInsets.all(8), - child: AnimatedSwitcher( - duration: const ShortDuration(), - transitionBuilder: (child, animation) => ScaleTransition( - scale: animation, - child: child, - ), - child: _showMenu - ? const Icon( - Icons.close_rounded, - size: 32, - key: ValueKey("hide"), - ) - : const Icon( - Icons.more_vert, - size: 32, - key: ValueKey("show"), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/gamification/common/views/map_background.dart b/lib/gamification/common/views/map_background.dart deleted file mode 100644 index f93729fd2..000000000 --- a/lib/gamification/common/views/map_background.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Class which displays a faded map behind the child widget. The child widget has to have a fixed size. -class MapBackground extends StatelessWidget { - /// The child to be displayed above the map. - final Widget child; - - const MapBackground({super.key, required this.child}); - - @override - Widget build(BuildContext context) { - var isLightMode = Theme.of(context).brightness == Brightness.light; - List darkModeGradient = [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withOpacity(0.8), - Theme.of(context).colorScheme.background.withOpacity(0.4), - Theme.of(context).colorScheme.background.withOpacity(0.4), - Theme.of(context).colorScheme.background.withOpacity(0.8), - Theme.of(context).colorScheme.background, - ]; - - List lightModeGradient = [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withOpacity(0.6), - Theme.of(context).colorScheme.background.withOpacity(0.3), - Theme.of(context).colorScheme.background.withOpacity(0.3), - Theme.of(context).colorScheme.background.withOpacity(0.6), - Theme.of(context).colorScheme.background, - ]; - return Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Container( - foregroundDecoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: isLightMode ? lightModeGradient : darkModeGradient, - ), - ), - child: Container( - foregroundDecoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: isLightMode ? lightModeGradient : darkModeGradient, - ), - ), - child: ClipRRect( - child: Image( - image: Theme.of(context).colorScheme.brightness == Brightness.dark - ? const AssetImage('assets/images/map-dark.png') - : const AssetImage('assets/images/map-light.png'), - fit: BoxFit.cover, - ), - ), - ), - ), - ), - child, - ], - ); - } -} diff --git a/lib/gamification/common/views/on_tap_animation.dart b/lib/gamification/common/views/on_tap_animation.dart deleted file mode 100644 index e0c24ba8d..000000000 --- a/lib/gamification/common/views/on_tap_animation.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// A child wrapped by this widget is animated and gives haptic feedback when pressed, if the callback is not null. -class OnTapAnimation extends StatefulWidget { - /// The child wrapped by this widget. - final Widget child; - - /// The callback function for when the widget is pressed. - final Function()? onPressed; - - /// The scale factor with which the animation should happen. - final double scaleFactor; - - /// Whether the widget should register a new click, while it is animating. - final bool blockFastClicking; - - const OnTapAnimation({ - super.key, - required this.child, - this.onPressed, - this.scaleFactor = 0.9, - this.blockFastClicking = true, - }); - - @override - State createState() => _OnTapAnimationState(); -} - -class _OnTapAnimationState extends State with SingleTickerProviderStateMixin { - /// Controller which controls the on pressed scale animation of the widget. - late final AnimationController _animationController; - - /// True, if clicks on the widget should be blocked. - bool _blocked = false; - - @override - void initState() { - _animationController = AnimationController( - vsync: this, - duration: Duration(milliseconds: widget.blockFastClicking ? 50 : 0), - reverseDuration: const Duration(milliseconds: 100), - ); - super.initState(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.onPressed == null) return widget.child; - return GestureDetector( - onTapDown: (_) { - if (widget.blockFastClicking && _animationController.isAnimating) return; - HapticFeedback.selectionClick(); - _animationController.forward(); - }, - onTapUp: (_) async { - if (widget.blockFastClicking) { - if (_blocked) return; - _blocked = true; - if (_animationController.isAnimating) await _animationController.forward(); - await _animationController.reverse(); - _blocked = false; - } else { - _animationController.reverse(); - } - if (widget.onPressed != null) widget.onPressed!(); - }, - onTapCancel: () => _animationController.reverse(), - child: ScaleTransition( - scale: Tween( - begin: 1, - end: widget.scaleFactor, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeIn, - )), - child: widget.child, - ), - ); - } -} diff --git a/lib/gamification/common/views/progress_ring.dart b/lib/gamification/common/views/progress_ring.dart deleted file mode 100644 index 398fa9c32..000000000 --- a/lib/gamification/common/views/progress_ring.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -/// A ring which displays the users progress for something, which can be animated and which can contain an icon. -class ProgressRing extends StatefulWidget { - /// The progress displayed by the ring, as a level between 0 and 1. - final double progress; - - /// The color of the level ring. - final Color ringColor; - - /// The size of the ring. The icon size also depends on this value. - final double ringSize; - - /// The widget to be displayed inside of the ring. - final Widget? content; - - /// Whether to show a border around the level ring. - final bool showBorder; - - /// The color of the backround and border of the ring. - final Color background; - - /// An animation controller animate the level ring. - final AnimationController? animationController; - - const ProgressRing({ - super.key, - required this.ringColor, - required this.ringSize, - this.progress = 1, - this.animationController, - this.showBorder = true, - this.background = Colors.transparent, - this.content, - }); - - @override - ProgressRingState createState() => ProgressRingState(); -} - -class ProgressRingState extends State { - @override - void initState() { - widget.animationController?.addListener(update); - super.initState(); - } - - @override - void dispose() { - widget.animationController?.removeListener(update); - super.dispose(); - } - - /// Update the state of the level ring. Called when the animation controller value changes. - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: widget.showBorder ? Border.all(color: widget.background, width: 2) : null, - color: widget.background, - borderRadius: const BorderRadius.all(Radius.circular(48)), - ), - child: Column(children: [ - Stack(children: [ - SizedBox( - width: widget.ringSize, - height: widget.ringSize, - child: AspectRatio( - aspectRatio: 1.0, - child: CustomPaint( - painter: SolidRingPainter( - progressColor: widget.ringColor, - backgroundColor: Theme.of(context).colorScheme.onBackground.withOpacity(0.05), - radiusThreshold: widget.animationController == null - ? 1 - : Tween(begin: 0, end: 1) - .animate(CurvedAnimation( - parent: widget.animationController!, - curve: Curves.easeIn, - )) - .value, - progress: widget.progress, - ), - ), - ), - ), - if (widget.content != null) - SizedBox( - width: widget.ringSize, - height: widget.ringSize, - child: Center( - child: widget.content, - ), - ), - ]), - ]), - ); - } -} - -/// A painter which paints a simple ring in a certain color, displaying a given progress. -class SolidRingPainter extends CustomPainter { - /// The color showing the current progress. - final Color progressColor; - - /// The background color of the ring, which can be seen if the progress is not 1. - final Color backgroundColor; - - /// The users progress as a value between 0 and 1. - final double progress; - - /// What percentage of the ring should be shown. - final double radiusThreshold; - - SolidRingPainter({ - required this.progressColor, - required this.backgroundColor, - required this.radiusThreshold, - required this.progress, - }); - - @override - void paint(Canvas canvas, Size size) { - final strokeWidth = size.width / 12.0; - final center = Offset(size.width / 2, size.height / 2); - final radius = (size.width - strokeWidth) / 2; - final thresholdRadius = radiusThreshold * (2 * pi); - final progressRadius = progress * (2 * pi); - final backgroundRadius = (2 * pi) - progressRadius; - final paint = Paint() - ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke; - - paint.color = progressColor; - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - pi / 2, - min(progressRadius, thresholdRadius), - false, - paint, - ); - - if (thresholdRadius > progressRadius) { - paint.color = backgroundColor; - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - pi / 2 + progressRadius, - min(backgroundRadius, thresholdRadius - progressRadius), - false, - paint, - ); - } - } - - @override - bool shouldRepaint(covariant SolidRingPainter oldDelegate) => true; -} diff --git a/lib/gamification/common/views/tutorial_page.dart b/lib/gamification/common/views/tutorial_page.dart deleted file mode 100644 index dc166c42f..000000000 --- a/lib/gamification/common/views/tutorial_page.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/fx.dart'; -import 'package:priobike/common/layout/annotated_region.dart'; -import 'package:priobike/common/layout/buttons.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/gamification/common/utils.dart'; - -/// Widget which displays a tutorial page for something regarding the gamification. -/// Each Page consist of a back button at the top, a list of content widgets and a confirmation button at the bottom. -class TutorialPage extends StatefulWidget { - /// Icon which is displayed on the confirmation button. - final IconData confirmButtonIcon; - - /// Text label on the confirmation button. - final String confirmButtonLabel; - - /// Determines wether the content list should be faded at the top and bottom. - final bool withContentFade; - - /// Callback for the back button at the top. - final Function()? onBackButtonTab; - - /// Callback for the confirmation button. - final Function()? onConfirmButtonTab; - - /// List of content widgets displayed on the page. - final List contentList; - - const TutorialPage({ - super.key, - this.confirmButtonIcon = Icons.check, - required this.confirmButtonLabel, - this.withContentFade = true, - this.onBackButtonTab, - required this.onConfirmButtonTab, - required this.contentList, - }); - - @override - State createState() => _TutorialPageState(); -} - -class _TutorialPageState extends State with SingleTickerProviderStateMixin { - late final AnimationController _animationController; - - /// Animation for the confirmation button. The button slides in from the bottom. - Animation get _buttonAnimation => Tween( - begin: const Offset(0.0, 1.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeIn, - )); - - /// Animation for the list of content widgets, which slide in from the top. - Animation get _contentAnimation => Tween( - begin: const Offset(1.0, 0.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeIn, - )); - - @override - void initState() { - _animationController = AnimationController(vsync: this, duration: const MediumDuration()); - _animationController.forward().then((value) => _animationController.duration = const ShortDuration()); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AnnotatedRegionWrapper( - backgroundColor: Theme.of(context).colorScheme.background, - brightness: Theme.of(context).brightness, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - color: Theme.of(context).colorScheme.background, - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - child: SlideTransition( - position: _contentAnimation, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Fade( - stops: widget.withContentFade ? [0, 0.05, 0.85, 0.95] : null, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: widget.contentList, - ), - ), - ), - ), - ), - ), - ), - SafeArea( - child: Column( - children: [ - const SmallVSpace(), - Row( - children: [ - AppBackButton( - onPressed: () async { - await _animationController.reverse(); - if (widget.onBackButtonTab != null) { - widget.onBackButtonTab!(); - } else { - if (mounted) Navigator.of(context).pop(); - } - }, - ), - ], - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: SlideTransition( - position: _buttonAnimation, - child: Pad( - child: BigButton( - icon: widget.confirmButtonIcon, - iconColor: Colors.white, - label: widget.confirmButtonLabel, - onPressed: widget.onConfirmButtonTab == null - ? null - : () async { - await _animationController.reverse(); - widget.onConfirmButtonTab!(); - }, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/community_event/model/event.dart b/lib/gamification/community_event/model/event.dart deleted file mode 100644 index 084bb9196..000000000 --- a/lib/gamification/community_event/model/event.dart +++ /dev/null @@ -1,30 +0,0 @@ -/// This object holds information about a weekend event pulled from the server. -class WeekendEvent { - /// Unique id of the event. - final int id; - - /// Title given to the event. - final String title; - - /// Description of the event. - final String description; - - /// Time where the event starts and the user gets access to its locations. - final DateTime startTime; - - /// Time the event ends. - final DateTime endTime; - - /// Color corresponding to the event as an int. - final int iconValue; - - WeekendEvent(this.id, this.title, this.description, this.startTime, this.endTime, this.iconValue); - - WeekendEvent.fromJson(Map json) - : id = json['id'], - title = json['title'], - description = json['description'], - startTime = DateTime.fromMillisecondsSinceEpoch(json['startTime']), - endTime = DateTime.fromMillisecondsSinceEpoch(json['endTime']), - iconValue = json['icon']; -} diff --git a/lib/gamification/community_event/model/location.dart b/lib/gamification/community_event/model/location.dart deleted file mode 100644 index 58e632936..000000000 --- a/lib/gamification/community_event/model/location.dart +++ /dev/null @@ -1,29 +0,0 @@ -/// A location that is part of a weekly event. -class EventLocation { - /// Unique id of the location. - final int id; - - /// Latitude of the location. - final double lat; - - /// Longitude of the location. - final double lon; - - /// Title describing the location. - final String title; - - EventLocation(this.lat, this.lon, this.title, this.id); - - EventLocation.fromJson(Map json) - : id = json['id'], - lat = json['lat'], - lon = json['lon'], - title = json['title']; - - toJson() => { - 'id': id, - 'lat': lat, - 'lon': lon, - 'title': title, - }; -} diff --git a/lib/gamification/community_event/model/shortcut_event_location.dart b/lib/gamification/community_event/model/shortcut_event_location.dart deleted file mode 100644 index 6f5b3ed3f..000000000 --- a/lib/gamification/community_event/model/shortcut_event_location.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/home/models/shortcut.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/routing/models/waypoint.dart'; -import 'package:priobike/routing/services/boundary.dart'; - -/// The shortcut represents a community event location. -class ShortcutEventLocation implements Shortcut { - /// The unique id of the shortcut. - @override - final String id; - - /// The type of the shortcut. - @override - final String type = "ShortcutLocation"; - - /// The name of the shortcut. - @override - String name; - - /// Whether the location of the shortcut has been achieved. - bool achieved; - - /// The waypoint of the shortcut location. - final Waypoint waypoint; - - ShortcutEventLocation({required this.name, required this.waypoint, required this.id, required this.achieved}); - - factory ShortcutEventLocation.fromJson(Map json) { - return ShortcutEventLocation( - id: json['id'], - name: json['name'], - waypoint: Waypoint.fromJson(json["waypoint"]), - achieved: json['achieved'], - ); - } - - @override - Map toJson() => { - 'type': 'ShortcutLocation', - 'id': id, - 'name': name, - 'waypoint': waypoint.toJSON(), - 'achieved': achieved, - }; - - /// Get the linebreaked name of the shortcut location. The name is split into at most 2 lines, by a limit of 15 characters. - @override - String get linebreakedName { - var result = name; - var insertedLinebreaks = 0; - for (var i = 0; i < name.length; i++) { - if (i % 15 == 0 && i != 0) { - if (insertedLinebreaks == 1) { - // Truncate the name if it is too long - result = result.substring(0, i); - result += '...'; - break; - } - result = result.replaceRange(i, i + 1, '${result[i]}\n'); - insertedLinebreaks++; - } - } - return result; - } - - /// Locations with a waypoint outside of the bounding box of the city are not allowed. - @override - bool isValid() { - final boundaryService = getIt(); - if (boundaryService.checkIfPointIsInBoundary(waypoint.lon, waypoint.lat) == false) { - return false; - } - return true; - } - - /// Trim the addresses of the waypoints, if a factor < 1 is given. - @override - ShortcutEventLocation trim(double factor) { - String? newAddress; - if (waypoint.address == null) { - newAddress = null; - } else { - final int newLength = (waypoint.address!.length * factor).round(); - if (factor >= 1) { - newAddress = waypoint.address; - } else { - newAddress = "${waypoint.address?.substring(0, newLength)}..."; - } - } - - return ShortcutEventLocation( - id: id, - name: name, - achieved: achieved, - waypoint: Waypoint( - waypoint.lat, - waypoint.lon, - address: newAddress, - ), - ); - } - - @override - - /// Methods which returns a list of waypoints. - List getWaypoints() { - return [waypoint]; - } - - /// Returns a String with a short info of the shortcut. - @override - String getShortInfo() { - return waypoint.address ?? ""; - } - - /// Returns the icon of the shortcut type. - @override - Widget getIcon() { - return const Icon(Icons.location_on); - } -} diff --git a/lib/gamification/community_event/service/event_service.dart b/lib/gamification/community_event/service/event_service.dart deleted file mode 100644 index cc8cc0698..000000000 --- a/lib/gamification/community_event/service/event_service.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:latlong2/latlong.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/database/model/achieved_location/achieved_location.dart'; -import 'package:priobike/gamification/common/database/model/event_badge/event_badge.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/community_event/model/event.dart'; -import 'package:priobike/gamification/community_event/model/location.dart'; -import 'package:priobike/http.dart'; -import 'package:priobike/logging/logger.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/positioning/services/positioning.dart'; -import 'package:priobike/settings/models/backend.dart'; -import 'package:priobike/settings/services/settings.dart'; - -/// This service manages the weekend events and pulls all necessary data from the backend. -class EventService with ChangeNotifier { - final log = Logger("EventService"); - - String get baseUrl => 'https://${getIt().backend.path}/game-service/community/'; - - /// Threshold within which a user needs to pass a location to achieve it. - static const double locationThresholdMetres = 100; - - /// Vincenty distance object to measure the distance between to points. - static const vincenty = Distance(roundResult: true, calculator: Vincenty()); - - /// Data access object to access the locations achieved by the user. - final AchievedLocationDao _achievedLocationDao = AppDatabase.instance.achievedLocationDao; - - /// Data access object to access the badges rewarded to the user. - final EventBadgeDao _badgeDao = AppDatabase.instance.eventBadgeDao; - - /// The current open weekend event pulled from the server. If null, the server didn't provide one. - WeekendEvent? _event; - - /// List of locations that belong to the current event. - List _locations = []; - - /// Stream subscription to the stream of achieved locations, to cancel a stream if the event changes. - StreamSubscription? _achievedLocStream; - - /// List of all badges the user has collected. - List userBadges = []; - - /// The number of users that have achieved at least one location of the current event. - int numOfActiveUsers = 0; - - /// The number of achieved locations by all users participating in the current event. - int numOfAchievedLocations = 0; - - /// List of locations that the user has achieved, out of the locations of the current event. - List _achievedLocations = []; - - /// Whether there is an event. - bool get noEvent => _event == null; - - /// Returns true, if the current event has started. - bool get eventStarted => noEvent ? false : DateTime.now().isAfter(_event!.startTime); - - /// Returns true, if the current event has ended. - bool get eventEnded => noEvent ? false : DateTime.now().isAfter(_event!.endTime); - - /// Returns true, if the current event has started, but has not ended yet, which means the user can participate. - bool get activeEvent => eventStarted && !eventEnded; - - /// Returns true, if there is an event, but it hasn't started yet. - bool get waitingForEvent => noEvent ? false : DateTime.now().isBefore(_event!.startTime); - - /// Getter for the current event. - WeekendEvent? get event => _event; - - bool get wasCurrentEventAchieved => userBadges.where((b) => b.eventId == event?.id).isNotEmpty; - - /// Get list of locations from the current event, that the user has not achieved yet. - List get _unachievedLocations => - _locations.where((loc) => _achievedLocations.where((e) => e.locationId == loc.id).isEmpty).toList(); - - /// Getter for the list of locations of the current event. - List get locations => List.from(_locations); - - /// This function checks, whether a given location has been achieved by the user. - bool wasLocationAchieved(EventLocation loc) => !_unachievedLocations.contains(loc); - - EventService() { - _badgeDao.streamAllObjects().listen((results) { - userBadges = results; - notifyListeners(); - }); - } - - /// This function can be called after a ride to check, if the user has passed some of the current locations. - Future checkLocations() async { - try { - if (!activeEvent) return; - - final positioning = getIt(); - final positions = positioning.positions; - if (positions.isEmpty) return; - - final latLngList = positions.map((e) => LatLng(e.latitude, e.longitude)); - - // Iterate through all unachieved locations and check, if the user was close enough to achieve them. - for (var location in _unachievedLocations) { - var hasBeenAchieved = checkIfLocationWasAchieved(location, latLngList); - // If a new location has been achieved, save the corresponding object in the database and send it to the service. - if (hasBeenAchieved) { - var achievedLocation = await _achievedLocationDao.createAchievedLocation(location, _event!); - if (achievedLocation == null) { - log.e('Failed to store achieved location in database'); - continue; - } - // If a location was achieved, create a event badge, if there isn't already one. - _badgeDao.createEventBadge(_event!); - Map json = { - 'eventId': achievedLocation.eventId, - 'locationId': achievedLocation.id, - }; - getIt().sendJsonToAddress('community/send-achieved-location/', json); - } - } - } catch (e) { - log.e('Failed to check event locations after saving route: $e'); - } - } - - /// Check if a location was achieved by iterating through a given list of positions and calculating the distance. - bool checkIfLocationWasAchieved(EventLocation location, Iterable positions) { - for (var pos in positions) { - var distance = vincenty.distance(LatLng(location.lat, location.lon), pos); - log.i('distance to ${location.title} is $distance what'); - if (distance <= locationThresholdMetres) return true; - } - return false; - } - - /// Start a stream to listen to changes in the achieved locations for the current event. - void startAchievedLocationStream(int eventId) async { - await _achievedLocStream?.cancel(); - _achievedLocStream = _achievedLocationDao.streamLocationsForEvent(eventId).listen((locations) { - _achievedLocations = locations; - notifyListeners(); - }); - } - - /// Fetch the current open weekend event from the server. - Future fetchWeekendEvent() async { - var url = '${baseUrl}get-open-event/'; - final endpoint = Uri.parse(url); - log.i('fetching community event...'); - try { - // Try to retreive the current open event. - http.Response response = await Http.get(endpoint).timeout(const Duration(seconds: 4)); - if (response.statusCode != 200) { - final err = "Could not be fetched from endpoint $endpoint: ${response.body}"; - throw Exception(err); - } - - // Try to decode the event - var result = jsonDecode(response.body); - try { - _event = WeekendEvent.fromJson(result); - startAchievedLocationStream(_event!.id); - } on TypeError catch (e) { - final err = "Could not decode the resonse body ${response.body} with error: $e"; - throw Exception(err); - } - } - // Catch the error if there is no connection to the internet or something else went wrong. - catch (e) { - _event = null; - log.e("Failed to load open event: $e"); - } - } - - /// Fetch the locations corresponding to the current event from the server. - Future fetchEventLocations() async { - var url = '${baseUrl}get-locations/'; - final endpoint = Uri.parse(url); - log.i('fetching community event locations...'); - try { - // Try to retrieve list of lcoations for the current event from the gamification service. - http.Response response = await Http.get(endpoint).timeout(const Duration(seconds: 4)); - if (response.statusCode != 200) { - final err = "Could not be fetched from endpoint $endpoint: ${response.body}"; - throw Exception(err); - } - - /// Try to decode the result list and save in locations variale. - try { - var results = jsonDecode(response.body); - List list = []; - for (var location in results) { - list.add(EventLocation.fromJson(location)); - } - _locations = list; - } on TypeError catch (e) { - _locations.clear(); - final err = "Could not decode the resonse body ${response.body} with error: $e"; - throw Exception(err); - } - } - // Catch the error if there is no connection to the internet or something else went wrong. - catch (e) { - log.e("Failed to load locations: $e"); - } - } - - /// Fetch status data about the current event from the service. - Future fetchEventStatus() async { - var url = '${baseUrl}get-event-status/'; - final endpoint = Uri.parse(url); - log.i('fetching community event status...'); - try { - // Try to retrieve status of the current event from the gamification service. - http.Response response = await Http.get(endpoint).timeout(const Duration(seconds: 4)); - if (response.statusCode != 200) { - final err = "Could not be fetched from endpoint $endpoint: ${response.body}"; - throw Exception(err); - } - - /// Try to decode the result list and save in local variales. - try { - var result = jsonDecode(response.body); - numOfActiveUsers = result['numOfUsers']; - numOfAchievedLocations = result['achievedLocations']; - } on TypeError catch (e) { - numOfActiveUsers = 0; - numOfAchievedLocations = 0; - final err = "Could not decode the resonse body ${response.body} with error: $e"; - throw Exception(err); - } - } - // Catch the error if there is no connection to the internet or something else went wrong. - catch (e) { - log.e("Failed to load event status: $e"); - } - } - - /// Fetch all relevant data from the backend and update the total number of achieved badges of the user. - Future fetchData() async { - await fetchWeekendEvent(); - await fetchEventLocations(); - await fetchEventStatus(); - notifyListeners(); - } -} diff --git a/lib/gamification/community_event/views/badge.dart b/lib/gamification/community_event/views/badge.dart deleted file mode 100644 index 306eccccc..000000000 --- a/lib/gamification/community_event/views/badge.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -/// This widget displays a badge with a given size, a given color and with a given icon to display on top of it. -class RewardBadge extends StatelessWidget { - final Color color; - - final double size; - - final int iconIndex; - - final bool achieved; - - const RewardBadge( - {super.key, required this.color, required this.size, required this.iconIndex, required this.achieved}); - - @override - Widget build(BuildContext context) { - return SizedBox.fromSize( - size: Size.square(size), - child: Stack( - children: [ - Icon( - Icons.shield, - color: achieved ? color : Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - size: size, - ), - if (achieved) - Center( - child: Icon( - getIcon(iconIndex), - size: size / 2, - color: Colors.white, - ), - ), - ], - ), - ); - } -} - -IconData getIcon(int value) { - if (value == 0) return Icons.star; - if (value == 1) return Icons.nightlight_round; - if (value == 2) return Icons.public; - if (value == 3) return Icons.water_drop; - if (value == 4) return Icons.location_city; - if (value == 5) return Icons.whatshot; - if (value == 6) return Icons.cyclone; - if (value == 7) return Icons.bolt; - if (value == 8) return Icons.wb_sunny; - if (value == 9) return Icons.landscape; - if (value == 10) return Icons.filter_vintage; - if (value == 11) return Icons.wb_cloudy; - if (value == 12) return Icons.looks; - if (value == 13) return Icons.park; - if (value == 14) return Icons.traffic; - if (value == 15) return Icons.sailing; - if (value == 16) return Icons.forest; - if (value == 17) return Icons.signpost; - if (value == 18) return Icons.air; - if (value == 19) return Icons.grass; - return Icons.question_mark; -} diff --git a/lib/gamification/community_event/views/badge_collection.dart b/lib/gamification/community_event/views/badge_collection.dart deleted file mode 100644 index 329c88852..000000000 --- a/lib/gamification/community_event/views/badge_collection.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/annotated_region.dart'; -import 'package:priobike/common/layout/buttons.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/community_event/views/badge.dart'; - -/// This view displays the badges the user has collected, which represent the achieved locations of the user. -class BadgeCollection extends StatelessWidget { - final List badges; - - const BadgeCollection({super.key, required this.badges}); - - @override - Widget build(BuildContext context) { - return AnnotatedRegionWrapper( - backgroundColor: Theme.of(context).colorScheme.background, - brightness: Theme.of(context).brightness, - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: SafeArea( - child: SingleChildScrollView( - child: Column( - children: [ - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - decoration: BoxDecoration( - border: Border.all( - width: 0.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - ), - child: AppBackButton(onPressed: () => Navigator.pop(context)), - ), - const HSpace(), - SubHeader(text: 'Deine Abzeichen', context: context) - ], - ), - const SmallVSpace(), - ...badges.map( - (badge) => Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: BorderRadius.circular(24), - color: Theme.of(context).colorScheme.surface), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - RewardBadge(color: CI.radkulturRed, size: 64, iconIndex: badge.icon, achieved: true), - Expanded( - child: Column( - children: [ - BoldSubHeader(text: badge.title, context: context), - Content(text: StringFormatter.getDateStr(badge.achievedTimestamp), context: context), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/gamification/community_event/views/event_card.dart b/lib/gamification/community_event/views/event_card.dart deleted file mode 100644 index efe6c1bd9..000000000 --- a/lib/gamification/community_event/views/event_card.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/colors.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/views/countdown_timer.dart'; -import 'package:priobike/gamification/common/views/feature_card.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; -import 'package:priobike/gamification/community_event/views/badge.dart'; -import 'package:priobike/gamification/community_event/views/badge_collection.dart'; -import 'package:priobike/gamification/community_event/views/event_page.dart'; -import 'package:priobike/main.dart'; - -/// This card is displayed on the home view and holds all information about the users participation in the weekend events. -class EventCard extends StatefulWidget { - const EventCard({super.key}); - - @override - State createState() => _EventCardState(); -} - -class _EventCardState extends State { - /// Event service to gather all the relevant data about the current event. - late EventService _eventService; - - @override - void initState() { - _eventService = getIt(); - _eventService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _eventService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - return GamificationFeatureCard( - featureKey: GamificationUserService.communityFeatureKey, - onEnabled: () {}, - featurePage: _eventService.activeEvent - ? const CommunityEventPage() - : (_eventService.userBadges.isNotEmpty ? BadgeCollection(badges: _eventService.userBadges) : null), - featureEnabledContent: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - const SmallHSpace(), - BoldSubHeader( - text: 'Stadtteil-Hopping', - context: context, - textAlign: TextAlign.start, - ), - ], - ), - - /// Display card content according to the current events status. - if (_eventService.activeEvent) ActiveEventView(service: _eventService), - if (_eventService.waitingForEvent) WaitingForEventView(service: _eventService), - if (!_eventService.activeEvent && !_eventService.waitingForEvent) NoEventView(service: _eventService), - ], - ), - featureDisabledContent: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: BoldSubHeader( - text: 'Stadtteil\nHopping', - context: context, - textAlign: TextAlign.center, - ), - ), - const SmallHSpace(), - SizedBox( - width: 96, - height: 80, - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: Container( - width: 0, - height: 0, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: CI.radkulturRed.withOpacity(0.05), - blurRadius: 24, - spreadRadius: 24, - ), - BoxShadow( - color: Colors.white.withOpacity(0.01), - blurRadius: 24, - spreadRadius: 24, - ), - ], - ), - ), - ), - Align( - alignment: Alignment.topLeft, - child: Transform.rotate( - angle: 0, - child: const Icon( - Icons.shield, - size: 56, - color: LevelColors.silver, - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Transform.rotate( - angle: 0, - child: const Icon( - Icons.location_city, - size: 72, - color: CI.radkulturRed, - ), - ), - ), - ], - ), - ) - ], - ), - ), - ); - } -} - -class WaitingForEventView extends StatelessWidget { - final EventService service; - - const WaitingForEventView({super.key, required this.service}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SmallVSpace(), - const RewardBadge( - color: CI.radkulturRed, - size: 64, - iconIndex: 100, - achieved: true, - ), - const SmallVSpace(), - Text( - 'Das nächste Event startet in:', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - ), - CountdownTimer( - timestamp: service.event!.startTime, - style: Theme.of(context).textTheme.titleSmall!.merge( - TextStyle( - color: Theme.of(context).colorScheme.onBackground, - height: 1, - ), - ), - ), - const SmallVSpace(), - if (service.userBadges.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - BoldSmall( - text: 'Abzeichensammlung', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5)), - const Icon(Icons.redo) - ], - ), - ], - ); - } -} - -class ActiveEventView extends StatelessWidget { - final EventService service; - - const ActiveEventView({super.key, required this.service}); - - Widget getInfoIcon(IconData icon, int value, Color color, var context) { - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(icon, size: 52, color: color), - Header(text: '$value', context: context), - ], - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SmallVSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: RewardBadge( - color: CI.radkulturRed, - size: 64, - iconIndex: service.event!.iconValue, - achieved: service.wasCurrentEventAchieved, - ), - ), - ], - ), - Text( - service.event!.title, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'HamburgSans', - fontSize: 30, - fontWeight: FontWeight.w600, - height: 1, - ), - ), - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - BoldSmall( - text: 'endet in ', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5)), - CountdownTimer( - timestamp: service.event!.endTime, - ), - ], - ), - ], - ); - } -} - -class NoEventView extends StatelessWidget { - final EventService service; - - const NoEventView({super.key, required this.service}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SmallVSpace(), - Icon( - Icons.shield, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - size: 80, - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: BoldContent( - text: 'Das nächste Event findet bald statt!', - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - ), - const SmallVSpace(), - if (service.userBadges.isNotEmpty) ...[ - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - BoldSmall( - text: 'Abzeichensammlung', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5)), - const Icon(Icons.redo) - ], - ), - ], - ], - ); - } -} diff --git a/lib/gamification/community_event/views/event_page.dart b/lib/gamification/community_event/views/event_page.dart deleted file mode 100644 index 6f94ee809..000000000 --- a/lib/gamification/community_event/views/event_page.dart +++ /dev/null @@ -1,474 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:priobike/common/layout/annotated_region.dart'; -import 'package:priobike/common/layout/buttons.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/confirm_button.dart'; -import 'package:priobike/gamification/community_event/model/event.dart'; -import 'package:priobike/gamification/community_event/model/location.dart'; -import 'package:priobike/gamification/community_event/model/shortcut_event_location.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; -import 'package:priobike/gamification/community_event/views/badge.dart'; -import 'package:priobike/home/models/shortcut.dart'; -import 'package:priobike/home/models/shortcut_route.dart'; -import 'package:priobike/home/views/shortcuts/selection.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/routing/models/waypoint.dart'; -import 'package:priobike/routing/services/discomfort.dart'; -import 'package:priobike/routing/services/routing.dart'; -import 'package:priobike/routing/views/main.dart'; -import 'package:priobike/status/services/sg.dart'; - -class CommunityEventPage extends StatefulWidget { - const CommunityEventPage({super.key}); - - @override - State createState() => _CommunityEventPageState(); -} - -class _CommunityEventPageState extends State { - late EventService _eventService; - - final ScrollController _scrollController = ScrollController(); - - bool _selectionMode = false; - - bool _showCommunity = false; - - WeekendEvent? get _event => _eventService.event; - - List get _locations => _eventService.locations; - - Map _mappedLocations = {}; - - List get _selectedLocations => - _mappedLocations.entries.where((e) => e.value).map((e) => e.key).toList(); - - bool get _itemsSelected => _selectedLocations.isNotEmpty; - - int get _numOfSelected => _selectedLocations.length; - - @override - void initState() { - _eventService = getIt(); - _eventService.addListener(update); - _updateMappedLocations(); - SchedulerBinding.instance.addPostFrameCallback((timeStamp) => _eventService.fetchData()); - super.initState(); - } - - @override - void dispose() { - _eventService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired - void update() => {if (mounted) setState(_updateMappedLocations)}; - - void _updateMappedLocations() { - Map newMap = {}; - for (var loc in _locations) { - newMap.addAll({loc: false}); - } - for (var map in _mappedLocations.entries) { - if (newMap[map.key] != null) { - newMap[map.key] = map.value; - } - } - _mappedLocations = newMap; - } - - void _startRouteFromShortcut(Shortcut shortcut) { - final shortcutIsValid = shortcut.isValid(); - if (!shortcutIsValid) { - //showInvalidShortcutSheet(context); - return; - } - // Select shortcut for routing. - getIt().selectShortcut(shortcut); - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RoutingView())).then( - (comingNotFromRoutingView) { - if (comingNotFromRoutingView == null) { - getIt().reset(); - getIt().reset(); - getIt().reset(); - } - setState(() {}); - }, - ); - } - - Widget get locationSelection { - const double shortcutRightPad = 16; - final shortcutWidth = (MediaQuery.of(context).size.width / 2) - shortcutRightPad; - final shortcutHeight = max(shortcutWidth - (shortcutRightPad * 3), 128.0); - return SingleChildScrollView( - controller: _scrollController, - scrollDirection: Axis.horizontal, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Row( - children: _mappedLocations.entries.map( - (e) { - var loc = e.key; - var selected = e.value; - var wasAchieved = _eventService.wasLocationAchieved(loc); - var shortcut = ShortcutEventLocation( - name: loc.title, - achieved: wasAchieved, - waypoint: Waypoint( - loc.lat, - loc.lon, - address: loc.title, - ), - id: 'unknown', - ); - return Stack( - children: [ - ShortcutView( - selectionColor: CI.radkulturRed, - onLongPressed: wasAchieved - ? null - : () { - if (!_selectionMode) { - _selectionMode = !_selectionMode; - setState(() => _mappedLocations[loc] = !selected); - } else { - _mappedLocations[loc] = !selected; - setState(() {}); - } - }, - onPressed: wasAchieved - ? () {} - : () { - if (_selectionMode) { - _mappedLocations[loc] = !selected; - if (!_itemsSelected) { - _selectionMode = !_selectionMode; - } - setState(() {}); - } else { - _startRouteFromShortcut(shortcut); - } - }, - shortcut: shortcut, - width: shortcutWidth, - height: shortcutHeight, - rightPad: shortcutRightPad, - selected: selected, - showSplash: false, - ), - if (wasAchieved) - Container( - width: shortcutWidth - 16, - height: shortcutHeight + 34, - decoration: BoxDecoration( - color: CI.radkulturRed.withOpacity(0.25), - borderRadius: BorderRadius.circular(24), - ), - child: const Center( - child: Icon( - Icons.done_rounded, - color: CI.radkulturRed, - size: 96, - ), - ), - ), - ], - ); - }, - ).toList(), - ), - ), - ); - } - - Widget get confirmButton => Container( - height: 64, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: AnimatedSwitcher( - switchInCurve: Curves.easeIn, - duration: const ShortDuration(), - reverseDuration: const Duration(milliseconds: 100), - transitionBuilder: (child, animation) => SlideTransition( - position: Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate(animation), - child: child, - ), - child: (_selectionMode) - ? Padding( - key: const ValueKey('ConfirmButton'), - padding: const EdgeInsets.only(bottom: 8), - child: ConfirmButton( - color: CI.radkulturRed, - label: 'Route anzeigen ($_numOfSelected)', - onPressed: _itemsSelected - ? () { - var waypoints = _selectedLocations - .map((loc) => Waypoint( - loc.lat, - loc.lon, - address: loc.title, - )) - .toList(); - var shortcut = ShortcutRoute( - name: 'unknown', - waypoints: waypoints, - id: 'unknown', - ); - _startRouteFromShortcut(shortcut); - for (var e in _mappedLocations.entries) { - _mappedLocations[e.key] = false; - } - _selectionMode = false; - } - : null, - ), - ) - : Center( - key: const ValueKey('InfoText'), - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), - child: BoldSmall( - text: 'Du kannst auch mehrere Orte gleichzeitig auswählen, indem Du ein Element gedrückt hältst.', - context: context, - textAlign: TextAlign.center, - ), - ), - ), - ), - ); - - @override - Widget build(BuildContext context) { - if (_event == null) return Container(); - return AnnotatedRegionWrapper( - backgroundColor: Theme.of(context).colorScheme.surface, - brightness: Theme.of(context).brightness, - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: SafeArea( - child: Column( - children: [ - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - border: Border.all( - width: 0.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - ), - child: AppBackButton( - onPressed: () => Navigator.pop(context), - ), - ), - const HSpace(), - Text( - _event!.title, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 30, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ], - ), - Expanded( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all( - width: 3, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: BorderRadius.circular(32), - ), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 2, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _showCommunity = false), - child: Column( - children: [ - BoldSubHeader( - text: 'Du', - context: context, - textAlign: TextAlign.center, - ), - Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 4, - decoration: BoxDecoration( - color: _showCommunity - ? Theme.of(context).colorScheme.onBackground.withOpacity(0.25) - : CI.radkulturRed, - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ), - ), - Expanded( - flex: 2, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _showCommunity = true), - child: Column( - children: [ - BoldSubHeader( - text: 'Community', - context: context, - textAlign: TextAlign.center, - ), - Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 4, - decoration: BoxDecoration( - color: _showCommunity - ? CI.radkulturRed - : Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ), - ), - ], - ), - const SmallVSpace(), - if (_showCommunity) ...[ - Expanded(child: Container()), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - const Icon( - Icons.group, - size: 56, - color: CI.radkulturRed, - ), - Header( - text: '${_eventService.numOfActiveUsers}', - context: context, - height: 1, - ) - ], - ), - Column( - children: [ - const Icon( - Icons.location_on, - size: 56, - color: CI.radkulturRed, - ), - Header( - text: '${_eventService.numOfAchievedLocations}', - context: context, - height: 1, - ) - ], - ), - ], - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Content( - text: (_eventService.numOfActiveUsers == 0) - ? 'Du bist der erste, der dieses Wochenende am Stadtteil-Hopping teilnimmt!' - : 'Dieses Wochenende ${_eventService.numOfActiveUsers == 1 ? 'hat' : 'haben'} bereits ${_eventService.numOfActiveUsers} ${_eventService.numOfActiveUsers == 1 ? 'Person' : 'Personen'} am Stadtteil-Hopping teilgenommen. Dabei ${_eventService.numOfActiveUsers == 1 ? 'wurde 1 Ort' : 'wurden ${_eventService.numOfAchievedLocations} Orte'} in ${_event!.title} besucht.', - context: context, - textAlign: TextAlign.center, - ), - ), - Expanded(child: Container()), - ], - if (!_showCommunity) ...[ - Expanded(child: Container()), - Center( - child: RewardBadge( - color: CI.radkulturRed, - size: 96, - iconIndex: _event!.iconValue, - achieved: _eventService.wasCurrentEventAchieved, - ), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: _eventService.wasCurrentEventAchieved - ? BoldSubHeader( - text: - 'Super, Du hast ${_event!.title} besucht und das dieswöchige Abzeichen erhalten!', - context: context, - textAlign: TextAlign.center, - ) - : Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: CI.radkulturRed, - borderRadius: BorderRadius.circular(24), - ), - child: BoldContent( - text: - 'Besuche einen oder mehrere der unten aufgelisteten Orte in ${_event!.title} und hole Dir das Abzeichen der Woche!', - context: context, - textAlign: TextAlign.center, - color: Colors.white, - ), - ), - ), - Expanded(child: Container()), - ], - ], - ), - ), - ), - Container( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.only(top: 8), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - locationSelection, - confirmButton, - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/goals/models/daily_goals.dart b/lib/gamification/goals/models/daily_goals.dart deleted file mode 100644 index 40535197d..000000000 --- a/lib/gamification/goals/models/daily_goals.dart +++ /dev/null @@ -1,29 +0,0 @@ -/// This object holds information about the users daily goals. -class DailyGoals { - static DailyGoals get defaultGoals => DailyGoals(3000, 30, List.filled(DateTime.daysPerWeek, false)); - - /// The users daily distance goals. - double distanceMetres; - - /// The users daily duration goals. - double durationMinutes; - - /// A list which holds 7 bools which determinem whether the users wants to reach their dailay goals on a day. - List weekdays; - - /// Returns the number of weekdays where the goals are activated. - int get numOfDays => weekdays.where((day) => day).length; - - DailyGoals(this.distanceMetres, this.durationMinutes, this.weekdays); - - Map toJson() => { - 'distanceMetres': distanceMetres, - 'durationMinutes': durationMinutes, - 'weekdays': weekdays, - }; - - DailyGoals.fromJson(Map json) - : distanceMetres = json['distanceMetres'], - durationMinutes = json['durationMinutes'], - weekdays = (json['weekdays'] as List).map((v) => v as bool).toList(); -} diff --git a/lib/gamification/goals/models/route_goals.dart b/lib/gamification/goals/models/route_goals.dart deleted file mode 100644 index 7fc95a1c8..000000000 --- a/lib/gamification/goals/models/route_goals.dart +++ /dev/null @@ -1,27 +0,0 @@ -/// This object describes user goals for a specific route. -class RouteGoals { - /// The unique id of the route to identify it. - String routeID; - - /// The name of the route. - String routeName; - - /// A bool which should hold 7 bool values, which determine whether the users wants to drive the route on a day. - List weekdays; - - /// Returns the number of weekdays where the goals are activated. - int get numOfDays => weekdays.where((day) => day).length; - - RouteGoals(this.routeID, this.routeName, this.weekdays); - - Map toJson() => { - 'routeID': routeID, - 'trackName': routeName, - 'weekdays': weekdays, - }; - - RouteGoals.fromJson(Map json) - : routeID = json['routeID'], - routeName = json['trackName'], - weekdays = (json['weekdays'] as List).map((v) => v as bool).toList(); -} diff --git a/lib/gamification/goals/services/goals_service.dart b/lib/gamification/goals/services/goals_service.dart deleted file mode 100644 index 600dd4e0a..000000000 --- a/lib/gamification/goals/services/goals_service.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart' hide Shortcuts; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; -import 'package:priobike/home/services/shortcuts.dart'; -import 'package:priobike/main.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// This service manages the users goal setting. -class GoalsService with ChangeNotifier { - /// A key to store the users daily goals in the shared prefs. - static const dailyGoalsKey = 'priobike.gamification.dailyGoals'; - - /// A key to store the users route goals in the shared prefs. - static const routeGoalsKey = 'priobike.gamification.routeGoals'; - - /// Instance of the shared preferences. - SharedPreferences? _prefs; - - /// The users daily goals. - DailyGoals? _dailyGoals; - - /// The users goals for a specific route. - RouteGoals? _routeGoals; - - /// Get the users daily goals, or the default goals, if they are null. - DailyGoals? get dailyGoals => _dailyGoals; - - /// Get the users route goals or null, if there are none. - RouteGoals? get routeGoals => _routeGoals; - - GoalsService() { - _loadData(); - - /// Listen to shortcut events and remove a route goal, if the corresponding shortcut is deleted. - getIt().addListener(() { - if (_routeGoals == null) return; - var shortcuts = getIt().shortcuts ?? []; - if (shortcuts.where((s) => s.id == _routeGoals!.routeID).isNotEmpty) return; - updateRouteGoals(null); - }); - } - - /// Load goal data from shared prefs. - Future _loadData() async { - _prefs ??= await SharedPreferences.getInstance(); - var dailyGoalsJson = _prefs!.getString(dailyGoalsKey); - if (dailyGoalsJson != null) _dailyGoals = DailyGoals.fromJson(jsonDecode(dailyGoalsJson)); - var routeGoalsJson = _prefs!.getString(routeGoalsKey); - if (routeGoalsJson != null) _routeGoals = RouteGoals.fromJson(jsonDecode(routeGoalsJson)); - } - - /// Update daily goals according to a given goal object. - Future updateDailyGoals(DailyGoals? goals) async { - _prefs ??= await SharedPreferences.getInstance(); - _dailyGoals = goals; - if (goals == null) _prefs!.remove(dailyGoalsKey); - if (goals != null) _prefs!.setString(dailyGoalsKey, jsonEncode(goals.toJson())); - notifyListeners(); - sendGoalsDataToBackend(); - } - - /// Update route goals according to a given goal object. - Future updateRouteGoals(RouteGoals? goals) async { - _prefs ??= await SharedPreferences.getInstance(); - _routeGoals = goals; - if (goals == null) _prefs!.remove(routeGoalsKey); - if (goals != null) _prefs!.setString(routeGoalsKey, jsonEncode(goals.toJson())); - notifyListeners(); - sendGoalsDataToBackend(); - } - - /// Reset all user goals. - Future reset() async { - _prefs ??= await SharedPreferences.getInstance(); - _dailyGoals = null; - _routeGoals = null; - _prefs!.remove(dailyGoalsKey); - _prefs!.remove(routeGoalsKey); - notifyListeners(); - } - - /// Send the users current goal settings to backend. - Future sendGoalsDataToBackend() async { - Map data = { - 'dailyGoalsSet': dailyGoals != null, - 'distanceGoalMetres': dailyGoals?.distanceMetres.toInt(), - 'durationGoalMinutes': dailyGoals?.durationMinutes.toInt(), - 'routeGoalsSet': routeGoals != null, - }; - getIt().sendJsonToAddress('goals/send-goals/', data); - } -} diff --git a/lib/gamification/goals/views/edit_daily_goals.dart b/lib/gamification/goals/views/edit_daily_goals.dart deleted file mode 100644 index 90e443c28..000000000 --- a/lib/gamification/goals/views/edit_daily_goals.dart +++ /dev/null @@ -1,250 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; -import 'package:priobike/gamification/common/views/dialog_button.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/goals/views/weekday_button.dart'; -import 'package:priobike/main.dart'; - -/// This dialog enables the user to edit their daily goals. -class EditDailyGoalsDialog extends StatefulWidget { - const EditDailyGoalsDialog({super.key}); - - @override - State createState() => _EditDailyGoalsDialogState(); -} - -class _EditDailyGoalsDialogState extends State { - /// Default goals to set the values to when no user goals exist. - DailyGoals get _defaultGoals => DailyGoals.defaultGoals; - - /// The users daily distance goals. - late double _distance; - - /// The users daily duration goals. - late double _duration; - - /// The weekdays on which the goals should be implemented. - late List _weekdays; - - @override - void initState() { - var goals = getIt().dailyGoals; - _distance = goals?.distanceMetres ?? _defaultGoals.distanceMetres; - _duration = goals?.durationMinutes ?? _defaultGoals.durationMinutes; - _weekdays = List.from(goals?.weekdays ?? _defaultGoals.weekdays); - super.initState(); - } - - @override - Widget build(BuildContext context) { - var noDaysSelected = _weekdays.where((day) => day).isEmpty; - return CustomDialog( - horizontalMargin: 16, - content: Container( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const SmallVSpace(), - BoldSubHeader( - text: 'Möchtest Du Dir an bestimmten Tagen eine Distanz oder Zeit vornehmen?', - context: context, - textAlign: TextAlign.center, - ), - const SmallVSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: _weekdays - .mapIndexed( - (i, day) => WeekdayButton( - day: i, - onPressed: () => setState(() => _weekdays[i] = !_weekdays[i]), - selected: day, - ), - ) - .toList(), - ), - const VSpace(), - EditGoalWidget( - title: 'Distanz', - value: _distance / 1000, - min: 1, - max: 80, - stepSize: 1, - valueLabel: 'km', - onChanged: noDaysSelected ? null : (value) => setState(() => _distance = value * 1000), - ), - const VSpace(), - EditGoalWidget( - title: 'Fahrtzeit', - value: _duration, - min: 10, - max: 600, - stepSize: 10, - valueLabel: 'min', - onChanged: noDaysSelected ? null : (value) => setState(() => _duration = value), - ), - const VSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SmallHSpace(), - CustomDialogButton( - label: 'Abbrechen', - onPressed: () => Navigator.of(context).pop(), - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - const SmallHSpace(), - CustomDialogButton( - label: 'Speichern', - onPressed: () { - DailyGoals? goals; - if (!noDaysSelected) { - goals = DailyGoals(_distance, _duration, _weekdays); - } - getIt().updateDailyGoals(goals); - Navigator.of(context).pop(); - }, - color: CI.radkulturRed, - ), - const SmallHSpace(), - ], - ), - const SmallVSpace(), - ], - ), - ), - ); - } -} - -/// Widget to edit a given goal value. -class EditGoalWidget extends StatelessWidget { - /// Title to describe the goal. - final String title; - - /// Current value of the goal. - final double value; - - /// Min allowed value. - final double min; - - /// Max allowed value. - final double max; - - /// Step size to edit the goal value with. - final double stepSize; - - /// Label of the goal value. - final String valueLabel; - - /// Callback for when the goal value is changed. - final Function(double)? onChanged; - - const EditGoalWidget({ - super.key, - required this.title, - required this.value, - required this.min, - required this.max, - required this.stepSize, - required this.valueLabel, - this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.025), - borderRadius: BorderRadius.circular(32), - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - EditButton( - icon: Icons.remove, - onPressed: (onChanged == null || value <= min) ? null : () => onChanged!(value - stepSize), - ), - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BoldSubHeader( - text: value.toInt().toString(), - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(onChanged == null ? 0.5 : 1), - ), - const SizedBox(width: 4), - BoldContent( - text: valueLabel, - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(onChanged == null ? 0.5 : 1), - ), - ], - ), - BoldSmall( - text: title, - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(onChanged == null ? 0.3 : 0.5), - ) - ], - ), - ), - EditButton( - icon: Icons.add, - onPressed: (onChanged == null || value >= max) ? null : () => onChanged!(value + stepSize), - ), - ], - ), - ); - } -} - -/// Edit button to either increase or decrease a value. -class EditButton extends StatelessWidget { - /// Icon the button should hold. (Normally either a plus or a minus) - final IconData icon; - - /// Callback for when the button is pressed. - final Function()? onPressed; - - const EditButton({super.key, required this.icon, required this.onPressed}); - - @override - Widget build(BuildContext context) { - bool disable = onPressed == null; - return OnTapAnimation( - scaleFactor: 0.85, - blockFastClicking: false, - onPressed: onPressed, - child: Container( - height: 40, - width: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.onBackground.withOpacity(disable ? 0.05 : 0.1), - ), - child: Center( - child: Icon( - icon, - size: 32, - color: Theme.of(context).colorScheme.onBackground.withOpacity(disable ? 0.25 : 1), - ), - ), - ), - ); - } -} diff --git a/lib/gamification/goals/views/edit_route_goals.dart b/lib/gamification/goals/views/edit_route_goals.dart deleted file mode 100644 index ee1dbec75..000000000 --- a/lib/gamification/goals/views/edit_route_goals.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart' hide Shortcuts; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/views/custom_dialog.dart'; -import 'package:priobike/gamification/common/views/dialog_button.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/goals/views/weekday_button.dart'; -import 'package:priobike/home/models/shortcut.dart'; -import 'package:priobike/home/services/shortcuts.dart'; -import 'package:priobike/home/views/shortcuts/selection.dart'; -import 'package:priobike/main.dart'; - -/// Dialog to edit the route goals set by the user. -class EditRouteGoalsDialog extends StatefulWidget { - const EditRouteGoalsDialog({super.key}); - - @override - State createState() => _EditRouteGoalsDialogState(); -} - -class _EditRouteGoalsDialogState extends State { - /// The associated shortcuts service to display the users saved shortcuts as possible routes to set goals for. - late Shortcuts _shortcutsService; - - /// The shortcut (to a route) currently selected by the user. - Shortcut? _selectedShortcut; - - /// A list of bools which signifies on which weekdays the user wants to drive the selected route. - late List _weekdays; - - /// Get list of exisiting shortcuts from corresponding service. - List get _shortcuts => _shortcutsService.shortcuts?.toList() ?? []; - - /// Check whether the user has selected any days to drive a route on. - bool get _noDaysSelected => _weekdays.where((day) => day).isEmpty; - - @override - void initState() { - var goals = getIt().routeGoals; - _weekdays = List.from(goals?.weekdays ?? List.filled(7, false)); - _shortcutsService = getIt(); - _shortcutsService.addListener(update); - _selectedShortcut = _shortcuts.where((s) => s.id == goals?.routeID).firstOrNull; - super.initState(); - } - - @override - void dispose() { - _shortcutsService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Widget to inform the user that they need to create routes, if they want to set goals for them. - Widget get _noRoutesWidget => CustomDialog( - backgroundColor: Theme.of(context).colorScheme.background.withOpacity(0.9), - horizontalMargin: 16, - content: Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: SubHeader( - text: 'Du kannst Dir eigene Routenziele setzen, sobald Du Deine erste eigene Route erstellt hast.', - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - ), - ], - ), - ), - ); - - @override - Widget build(BuildContext context) { - if (_shortcuts.isEmpty) return _noRoutesWidget; - const double shortcutRightPad = 16; - final shortcutWidth = (MediaQuery.of(context).size.width / 2) - shortcutRightPad; - return CustomDialog( - horizontalMargin: 16, - content: Container( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SmallVSpace(), - BoldSubHeader( - text: 'Möchtest Du Dir vornehmen eine Deiner Routen regelmäßig zu fahren?', - context: context, - textAlign: TextAlign.center, - ), - const SmallVSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: _weekdays - .mapIndexed( - (i, day) => WeekdayButton( - day: i, - onPressed: _selectedShortcut == null - ? null - : () { - _weekdays[i] = !_weekdays[i]; - if (_noDaysSelected) _selectedShortcut = null; - setState(() {}); - }, - selected: day, - ), - ) - .toList(), - ), - const VSpace(), - SingleChildScrollView( - padding: const EdgeInsets.only(left: 8), - controller: ScrollController(), - scrollDirection: Axis.horizontal, - child: Row( - children: _shortcuts - .map( - (shortcut) => ShortcutView( - onPressed: () { - if (_selectedShortcut == shortcut) { - setState(() { - _selectedShortcut = null; - _weekdays = List.filled(7, false); - }); - } else if (_selectedShortcut == null) { - setState(() { - _selectedShortcut = shortcut; - _weekdays = [true, true, true, true, true, false, false]; - }); - } else { - setState(() => _selectedShortcut = shortcut); - } - }, - shortcut: shortcut, - // width == height to make it a square - width: shortcutWidth, - height: shortcutWidth, - rightPad: shortcutRightPad, - selected: _selectedShortcut == shortcut, - showSplash: false, - ), - ) - .toList(), - ), - ), - const SmallVSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SmallHSpace(), - CustomDialogButton( - label: 'Abbrechen', - onPressed: () => Navigator.of(context).pop(), - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - const SmallHSpace(), - CustomDialogButton( - label: 'Speichern', - onPressed: () { - RouteGoals? goals; - if (!_noDaysSelected && _selectedShortcut != null) { - goals = RouteGoals(_selectedShortcut!.id, _selectedShortcut!.name, _weekdays); - } - getIt().updateRouteGoals(goals); - Navigator.of(context).pop(); - }, - color: CI.radkulturRed, - ), - const SmallHSpace(), - ], - ), - const SmallVSpace(), - ], - ), - ), - ); - } -} diff --git a/lib/gamification/goals/views/goals_view.dart b/lib/gamification/goals/views/goals_view.dart deleted file mode 100644 index 1b06ab04c..000000000 --- a/lib/gamification/goals/views/goals_view.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/common/layout/tiles.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/goals/views/edit_daily_goals.dart'; -import 'package:priobike/gamification/goals/views/edit_route_goals.dart'; -import 'package:priobike/tutorial/view.dart'; - -/// This view gives the user to open dialogs to edit their daily and route goals by pressing on corresponding buttons. -class GoalsView extends StatelessWidget { - const GoalsView({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - TutorialView( - padding: const EdgeInsets.fromLTRB(32, 0, 32, 24), - id: 'priobike.gamification.goals.tutorial', - text: - 'Setze Dir tägliche Ziele. Die Ziele die Du für Dich festlegst, werden in Deine Statistiken integriert und bestimmen die Schwierigkeit der für Dich generierten Challenges.', - color: Theme.of(context).colorScheme.onSurface), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - CustomIconButton( - icon: CustomGameIcons.goals, - label: 'Tagesziele', - onPressed: () => showDialog(context: context, builder: (context) => const EditDailyGoalsDialog()), - ), - const SmallHSpace(), - CustomIconButton( - icon: Icons.map, - label: 'Routenziele', - onPressed: () => showDialog(context: context, builder: (context) => const EditRouteGoalsDialog()), - ), - ], - ), - ], - ), - ); - } -} - -/// Custom button design to either open or close a goals edit dialog. -class CustomIconButton extends StatelessWidget { - /// Icon on the left side of the button. - final IconData icon; - - /// Label on the button. - final String label; - - /// Callback function for when the button is pressed. - final Function() onPressed; - - const CustomIconButton({ - super.key, - required this.icon, - required this.label, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return Expanded( - child: OnTapAnimation( - onPressed: onPressed, - child: Tile( - fill: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.all(8), - borderRadius: BorderRadius.circular(24), - showShadow: false, - content: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).colorScheme.onSurface, - ), - const SmallHSpace(), - BoldContent( - text: label, - context: context, - color: Theme.of(context).colorScheme.onSurface, - height: 1, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/goals/views/weekday_button.dart b/lib/gamification/goals/views/weekday_button.dart deleted file mode 100644 index ec06e6387..000000000 --- a/lib/gamification/goals/views/weekday_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; - -/// Button to describe a weekday to activate or deactive goals on this weekday. -class WeekdayButton extends StatelessWidget { - /// Index of the day of week from 1 to 7. - final int day; - - /// Callback for when the button is pressed. - final Function()? onPressed; - - /// Whether the weekday is currently selected, which means it is displayed in blue. - final bool selected; - - const WeekdayButton({super.key, required this.day, required this.onPressed, required this.selected}); - - @override - Widget build(BuildContext context) { - bool disable = onPressed == null; - return OnTapAnimation( - scaleFactor: 0.85, - onPressed: onPressed, - child: Container( - height: 40, - width: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: - selected ? CI.radkulturRed : Theme.of(context).colorScheme.onBackground.withOpacity(disable ? 0.05 : 0.1), - ), - child: Center( - child: BoldSmall( - text: StringFormatter.getWeekStr(day), - context: context, - color: selected ? Colors.white : Theme.of(context).colorScheme.onBackground.withOpacity(disable ? 0.25 : 1), - ), - ), - ), - ); - } -} diff --git a/lib/gamification/intro/intro_card.dart b/lib/gamification/intro/intro_card.dart deleted file mode 100644 index d6dd275a2..000000000 --- a/lib/gamification/intro/intro_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/intro/intro_page.dart'; - -/// Intro card to be displayed on the home view of the app, if the gamification feauture is disabled. -class GameIntroCard extends StatelessWidget { - const GameIntroCard({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: OnTapAnimation( - scaleFactor: 0.95, - onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const IntroPage())), - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - //color: CI.blue, // Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(24), - border: Border.all( - width: 1, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.07), - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - blurRadius: 2, - ), - ], - gradient: LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.topRight, - stops: const [0, 0.5, 1], - colors: [ - Color.alphaBlend(CI.radkulturRed.withOpacity(0.5), Colors.white), - CI.radkulturRed.withOpacity(0.8), - Color.alphaBlend(CI.radkulturRed.withOpacity(0.5), Colors.white), - ], - ), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - const Expanded( - child: Text( - "Beta-Features Aktivieren", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 24, - height: 1, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - SizedBox( - height: 84, - width: 96, - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(right: 1, top: 4), - child: Align( - alignment: Alignment.topCenter, - child: Transform.rotate( - angle: 0, - child: const Icon( - CustomGameIcons.blank_trophy, - size: 36, - color: Colors.white, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 24), - child: Align( - alignment: Alignment.topRight, - child: Transform.rotate( - angle: 0, - child: const Icon( - CustomGameIcons.blank_medal, - size: 40, - color: Colors.white, - ), - ), - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: Transform.rotate( - angle: 0, - child: const Icon( - Icons.bar_chart, - size: 52, - color: Colors.white, - ), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/gamification/intro/intro_page.dart b/lib/gamification/intro/intro_page.dart deleted file mode 100644 index 46376b5bb..000000000 --- a/lib/gamification/intro/intro_page.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/views/tutorial_page.dart'; -import 'package:priobike/main.dart'; - -/// A list item with icon. -class IconItem extends Row { - IconItem({super.key, required IconData icon, required String text, required BuildContext context}) - : super( - children: [ - SizedBox( - width: 56, - height: 56, - child: Icon( - icon, - color: CI.radkulturRed, - size: 56, - semanticLabel: text, - ), - ), - const SmallHSpace(), - Expanded( - child: Content(text: text, context: context), - ), - ], - ); -} - -/// Page to give the user an intro to the gamification feature and let them enable it. -class IntroPage extends StatelessWidget { - const IntroPage({super.key}); - - @override - Widget build(BuildContext context) { - return TutorialPage( - confirmButtonLabel: 'Aktivieren', - onConfirmButtonTab: () { - getIt().createProfile(); - Navigator.of(context).pop(); - }, - contentList: [ - const SizedBox(height: 64 + 16), - Header(text: "Beta-Features Aktivieren", context: context), - const SmallVSpace(), - IconItem( - icon: Icons.query_stats, - text: 'Erhalte einen detaillierten Überblick über die von Dir aufgezeichneten Daten.', - context: context, - ), - const SmallVSpace(), - IconItem( - icon: CustomGameIcons.goals, - text: 'Setze Dir individuelle tägliche Ziele und verfolge Deinen Fortschritt.', - context: context, - ), - const SmallVSpace(), - IconItem( - icon: CustomGameIcons.blank_trophy, - text: - 'Verbinde Deine Fahrten mit täglichen und wöchentlichen Challenges, steige Level auf und sammel virtuelle Belohnungen.', - context: context, - ), - const SmallVSpace(), - IconItem( - icon: Icons.shield, - text: - 'Nimm an den wöchentlichen Stadtteil-Hopping teil und sammel individuelle Abzeichen, indem Du Orte in Hamburg besuchst.', - context: context, - ), - const VSpace(), - Content( - text: - 'Ab jetzt kannst Du die neuen Beta-Features der PrioBike-App ausprobieren! Hierbei handelt es sich um eine Reihe von Funktionen, die es Dir ermöglichen, noch mehr über Deine Fahrradfahrten zu erfahren, Dir Ziele zu setzen und Deine Fahrten mit virtuellen Spielmechaniken zu verbinden.', - context: context, - ), - const SmallVSpace(), - Content( - text: - ' Die Beta-Features wurden im Rahmen eines studentischen Projektes entwickelt. Da zu diesem Projekt auch eine Evaluation gehört, werden Deine Interaktionen mit den Features aufgezeichnet und anonymisiert an einen Server geschickt und dort ausgewertet.', - context: context, - ), - const SizedBox(height: 82), - ], - ); - } -} diff --git a/lib/gamification/main.dart b/lib/gamification/main.dart deleted file mode 100644 index e314c2770..000000000 --- a/lib/gamification/main.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/challenges/views/challenges_card.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/community_event/views/event_card.dart'; -import 'package:priobike/gamification/goals/views/goals_view.dart'; -import 'package:priobike/gamification/intro/intro_card.dart'; -import 'package:priobike/gamification/statistics/views/card/stats_card.dart'; -import 'package:priobike/gamification/statistics/views/overall_stats.dart'; -import 'package:priobike/main.dart'; -import 'package:priobike/tutorial/view.dart'; - -/// This view displays the gamification functionality according to the user settings. -class GameView extends StatefulWidget { - const GameView({super.key}); - - @override - State createState() => _GameViewState(); -} - -class _GameViewState extends State { - /// This service provides user information. - late GamificationUserService _userService; - - /// The gamification features mapped to their corresponding cards. - final Map _featureCards = { - GamificationUserService.challengesFeatureKey: const Column( - children: [ - TutorialView( - padding: EdgeInsets.fromLTRB(48, 16, 48, 8), - id: 'priobike.gamification.challenges.tutorial', - text: - 'Erfülle tägliche Challenges, steige Level auf und lass Dich überraschen, vor was für Herausforderungen Du dabei sonst noch gestellt wirst...', - color: Colors.white, - ), - ChallengesCard(), - ], - ), - GamificationUserService.statisticsFeatureKey: const Column( - children: [ - TutorialView( - padding: EdgeInsets.fromLTRB(48, 16, 48, 8), - id: 'priobike.gamification.statistics.tutorial', - text: - 'Erhalte einen genaueren Überblick über Deine zurückgelegten Fahrten, durch tägliche, wöchentliche und monatliche Statistik-Überblicke.', - color: Colors.white, - ), - RideStatisticsCard(), - ], - ), - GamificationUserService.communityFeatureKey: const Column( - children: [ - TutorialView( - padding: EdgeInsets.fromLTRB(48, 16, 48, 8), - id: 'priobike.gamification.community.tutorial', - text: - 'Besuche jedes Wochenende einen anderen Stadtteil von Hamburg und sammel dabei unterschiedliche Abzeichen.', - color: Colors.white, - ), - EventCard(), - ], - ), - }; - - /// List of feature cards for enabled features. - List get _enabledFeatureCards => - _userService.enabledFeatures.map((key) => _featureCards[key] as Widget).toList(); - - /// List of feature cards for disabled features. - List get _disabledFeatureCards => - _userService.disabledFeatures.map((key) => _featureCards[key] as Widget).toList(); - - /// Whether to give the user the option to set goals, which is only meaningful - /// if the goals are used by some activated features. - bool get _showGoals => - _userService.isFeatureEnabled(GamificationUserService.challengesFeatureKey) || - _userService.isFeatureEnabled(GamificationUserService.statisticsFeatureKey); - - @override - void initState() { - _userService = getIt(); - _userService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _userService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - return Column( - children: (!_userService.hasProfile) - ? [ - Container( - alignment: Alignment.center, - padding: const EdgeInsets.only(top: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - BoldContent( - text: "Da geht doch noch mehr!", - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(height: 4), - Small( - text: "Probiere neue Funktionen aus.", - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onSurface, - ), - ], - ), - ), - const SizedBox(height: 16), - const GameIntroCard(), - ] - : [ - const OverallStatistics(), - if (_showGoals) const GoalsView(), - const SmallVSpace(), - ..._enabledFeatureCards, - ..._disabledFeatureCards, - ], - ); - } -} diff --git a/lib/gamification/statistics/models/ride_stats.dart b/lib/gamification/statistics/models/ride_stats.dart deleted file mode 100644 index e04d160ca..000000000 --- a/lib/gamification/statistics/models/ride_stats.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; - -/// This object holds some kind of aggregation of ride statistics. -class RideStats { - /// Distance in kilometres. - final double distanceKilometres; - - /// Duration in minutes. - final double durationMinutes; - - /// Elevation gain in metres. - final double elevationGainMetres; - - /// Elevation loss in metres. - final double elevationLossMetres; - - /// A goal value for the distance in kilometres. - double? distanceGoalKilometres; - - /// A goal value for the duration in minutes. - double? durationGoalMinutes; - - /// Average speed in kilometres per hour. - double get averageSpeedKmh { - if (durationMinutes > 0) return distanceKilometres / (durationMinutes / 60); - return 0; - } - - /// Get ride stats from a list of summaries, by summing up all the ride values and averaging the speed. - RideStats.fromSummaries(List rides) - : distanceKilometres = ListUtils.getListSum(rides.map((r) => r.distanceMetres / 1000).toList()), - durationMinutes = ListUtils.getListSum(rides.map((r) => r.durationSeconds / 60).toList()), - elevationGainMetres = ListUtils.getListSum(rides.map((r) => r.elevationGainMetres).toList()), - elevationLossMetres = ListUtils.getListSum(rides.map((r) => r.elevationLossMetres).toList()); - - /// Get ride stats from a list of ride stats, by summing up all the ride values and averaging the speed. - RideStats.fromStats(List stats) - : distanceKilometres = ListUtils.getListSum(stats.map((s) => s.distanceKilometres).toList()), - durationMinutes = ListUtils.getListSum(stats.map((s) => s.durationMinutes).toList()), - elevationGainMetres = ListUtils.getListSum(stats.map((s) => s.elevationGainMetres).toList()), - elevationLossMetres = ListUtils.getListSum(stats.map((s) => s.elevationLossMetres).toList()) { - distanceGoalKilometres = - ListUtils.getListSum(stats.map((s) => s.distanceGoalKilometres).whereType().toList()); - durationGoalMinutes = ListUtils.getListSum(stats.map((s) => s.durationGoalMinutes).whereType().toList()); - } - - /// Get ride stat value for a given stat type. - double getStatFromType(StatType type) { - if (type == StatType.distance) return distanceKilometres; - if (type == StatType.duration) return durationMinutes; - if (type == StatType.elevationGain) return elevationGainMetres; - if (type == StatType.elevationLoss) return elevationLossMetres; - if (type == StatType.speed) return averageSpeedKmh; - return 0; - } - - /// Get goal value for a given stat type. - double? getGoalFromType(StatType type) { - if (type == StatType.distance) return distanceGoalKilometres; - if (type == StatType.duration) return durationGoalMinutes; - return null; - } - - /// Get textual description of the time frame the stats are in. - String getTimeDescription(int? index) => ''; - - /// Get ride summaries corresponding to the stats. - List get rides => []; -} - -/// This object holds ride statistics for a concrete day. -class DayStats extends RideStats { - /// The date of the day. - final DateTime date; - - @override - final List rides; - - /// The object can be retreived from given goals and rides and date. - DayStats(int year, int month, int day, this.rides, DailyGoals? goals) - : date = DateTime(year, month, day), - super.fromSummaries(rides) { - setGoals(goals); - } - - /// Create an empty stat object from given date and goals. - DayStats.empty(int year, int month, int day, DailyGoals? goals) - : date = DateTime(year, month, day), - rides = [], - super.fromSummaries([]) { - setGoals(goals); - } - - /// Set goal values of the object according to given goals. - void setGoals(DailyGoals? goals) { - if (goals != null && goals.weekdays[date.weekday - 1]) { - distanceGoalKilometres = goals.distanceMetres / 1000; - durationGoalMinutes = goals.durationMinutes; - } else { - distanceGoalKilometres = null; - durationGoalMinutes = null; - } - } - - /// Whether a given date is on the same day as this day stats. - bool isOnDay(tmpDate) { - return date.year == tmpDate.year && date.month == tmpDate.month && date.day == tmpDate.day; - } - - @override - String getTimeDescription(int? index) => StringFormatter.getDateStr(date); -} - -/// This objects aggregates a list of ride stats in an object holding the concrete list, -/// the corresponding ride stats and, in addition, average values. -class ListOfRideStats extends RideStats { - /// The corresponding list of ride stats. - final List list; - - /// The average distance covered by all ride stats in the list. - final double avgDistanceKilometres; - - /// The average duration of all ride stats in the list. - final double avgDurationMinutes; - - /// The average elevation gain of all ride stats in the list. - final double avgElevationGainMetres; - - /// The average elevation loss of all ride stats in the list. - final double avgElevationLossMetres; - - /// Get object from list of ride stats by calculating averages. - ListOfRideStats(this.list) - : avgDistanceKilometres = list.map((d) => d.distanceKilometres).average, - avgDurationMinutes = list.map((d) => d.durationMinutes).average, - avgElevationGainMetres = list.map((d) => d.elevationGainMetres).average, - avgElevationLossMetres = list.map((d) => d.elevationLossMetres).average, - super.fromStats(list); - - /// Get max of values and goals of all elements in the stat list. - double getMaxForType(StatType type) { - var listOfValues = list.map((e) => e.getStatFromType(type)).toList(); - var listOfGoalValues = list.map((e) => e.getGoalFromType(type)).whereType().toList(); - return (listOfValues + listOfGoalValues).maxOrNull ?? 0; - } - - /// Get average value for given stat type. - double getAvgFromType(StatType type) { - if (type == StatType.distance) return avgDistanceKilometres; - if (type == StatType.duration) return avgDurationMinutes; - if (type == StatType.elevationGain) return avgElevationGainMetres; - if (type == StatType.elevationLoss) return avgElevationLossMetres; - if (type == StatType.speed) return averageSpeedKmh; - return 0; - } - - /// Whether the list hold ride stats for a given day. - int? isDayInList(DateTime? day) { - if (day == null) return null; - for (int i = 0; i < list.length; i++) { - var element = list[i]; - if (element is DayStats && element.isOnDay(day)) return i; - if (element is ListOfRideStats && element.isDayInList(day) != null) return i; - } - return null; - } - - @override - String getTimeDescription(int? index) { - if (index == null || index > list.length - 1) { - var firstElement = list.first; - if (firstElement is WeekStats) { - var lastWeek = list.last as WeekStats; - return StringFormatter.getFromToDateStr(firstElement.mondayDate, lastWeek.list.last.date); - } else { - return '${list.first.getTimeDescription(null)} - ${list.last.getTimeDescription(null)}'; - } - } else { - var element = list.elementAt(index); - return element.getTimeDescription(null); - } - } - - @override - List get rides => list.map((e) => e.rides).reduce((a, b) => a + b); -} - -/// This object holds ride statistics for a week. -class WeekStats extends ListOfRideStats { - /// Date of the monday of the week. - final DateTime mondayDate; - - /// Get stats from list of days of the week. - WeekStats(super.days) : mondayDate = days.first.date; - - @override - String getTimeDescription(int? index) { - if (index == null || index > list.length - 1) { - return StringFormatter.getFromToDateStr(list.first.date, list.last.date); - } else { - return StringFormatter.getDateStr(list.elementAt(index).date); - } - } -} - -/// This object holds ride statistics for a month. -class MonthStats extends ListOfRideStats { - /// Year of the month. - final int year; - - /// Index of the month from 1 to 12. - final int month; - - /// Get stats from list of days of the month. - MonthStats(super.days) - : year = days.first.date.year, - month = days.first.date.month; - - @override - String getTimeDescription(int? index) { - if (index == null || index > list.length - 1) { - return StringFormatter.getMonthAndYearStr(month, year); - } else { - return StringFormatter.getDateStr(list.elementAt(index).date); - } - } -} diff --git a/lib/gamification/statistics/models/stat_type.dart b/lib/gamification/statistics/models/stat_type.dart deleted file mode 100644 index 4ca071e81..000000000 --- a/lib/gamification/statistics/models/stat_type.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; - -/// The different kind of stats that can describe a ride. -enum StatType { - distance, - duration, - elevationGain, - elevationLoss, - speed, -} - -/// Get icon describing a given ride info type. -IconData getIconForInfoType(StatType type) { - if (type == StatType.distance) return Icons.directions_bike; - if (type == StatType.speed) return Icons.speed; - if (type == StatType.duration) return Icons.timer; - if (type == StatType.elevationGain) return CustomGameIcons.elevation_gain; - if (type == StatType.elevationLoss) return CustomGameIcons.elevation_loss; - return Icons.question_mark; -} diff --git a/lib/gamification/statistics/services/statistics_service.dart b/lib/gamification/statistics/services/statistics_service.dart deleted file mode 100644 index 3bc34bde5..000000000 --- a/lib/gamification/statistics/services/statistics_service.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; - -/// Enum which describes different kinds of intervals for displayed statistics. -enum StatInterval { - weeks, - months, - multipleWeeks, -} - -/// This service manages what kind of statistics in what intervals are displayed. -class StatisticService with ChangeNotifier { - /// The interval in which rides shall be displayed. - StatInterval _statInterval = StatInterval.weeks; - - /// The ride info which shall be displayed. - StatType _selectedType = StatType.distance; - - /// The date selected from the stats. - DateTime? _selectedDate; - - /// Get the currently selected stat interval. - StatInterval get statInterval => _statInterval; - - /// Get the currently selected stat type. - StatType get selectedType => _selectedType; - - /// Get the currently selected date. - DateTime? get selectedDate => _selectedDate; - - /// Change selected stat interval. - void setStatInterval(StatInterval type) { - _statInterval = type; - notifyListeners(); - } - - /// Change selected stat type. - void setStatType(StatType type) { - _selectedType = type; - notifyListeners(); - } - - /// Change selected date or set it to null. - void selectDate(DateTime? date) { - _selectedDate = date; - notifyListeners(); - } - - /// Check if given ride info type is currently selected. - bool isTypeSelected(StatType type) => type == _selectedType; -} diff --git a/lib/gamification/statistics/services/stats_view_model.dart b/lib/gamification/statistics/services/stats_view_model.dart deleted file mode 100644 index dc4833171..000000000 --- a/lib/gamification/statistics/services/stats_view_model.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/goals/models/daily_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/main.dart'; - -/// This viewmodel aggregates and managages statistics for all rides the user has made in a given timeframe. -class StatisticsViewModel with ChangeNotifier { - /// Just a duration of one day. - static const Duration _oneDay = Duration(days: 1); - - /// Goal service to update the saved daily goals accordingly. - final GoalsService _goalsService = getIt(); - - /// Ride stream to listen for all the rides the user has made and update the statistics accordingly. - StreamSubscription? _rideStream; - - /// The start date of the time frame to be observed. - DateTime startDate; - - /// The end date of the time frame to be observed. - DateTime endDate; - - /// The daily stats of all days in the observed timeframe. - List days = []; - - /// The stats of all weeks in the observed timeframe. - List weeks = []; - - /// The stats of all months in the observed timeframe. - List months = []; - - /// The current daily distance and duration goals of the user. - DailyGoals? get _goals => _goalsService.dailyGoals; - - /// Just a helper function that returns a list of all dates in the timeframe to be observed by this view model. - List get daysInTimeFrame { - List days = []; - var tmpDate = startDate; - while (!tmpDate.isAfter(endDate)) { - days.add(tmpDate); - tmpDate = tmpDate.add(_oneDay); - } - return days; - } - - StatisticsViewModel({ - required this.startDate, - required this.endDate, - }) { - /// First fill up the days and rides list. - updateRides([]); - - /// Then listen to changes in goals and rides and update the lists accordingly. - _goalsService.addListener(updateGoals); - _rideStream = AppDatabase.instance.rideSummaryDao - .streamRidesInInterval(startDate, endDate.add(_oneDay)) - .listen((rides) => updateRides(rides)); - } - - /// End the data streams and dispose the change notifier. - @override - void dispose() { - _rideStream?.cancel(); - _goalsService.removeListener(updateGoals); - super.dispose(); - } - - /// Update the saved days, weeks, and months according to new daily goals. - void updateGoals() { - for (var day in days) { - day.setGoals(_goals); - } - updateWeeks(); - updateMonths(); - notifyListeners(); - } - - /// Update the saved days, weeks and months according to new ride data. - void updateRides(List rides) { - days.clear(); - for (var day in daysInTimeFrame) { - var ridesOnDay = rides.where((ride) { - var rideDay = ride.startTime; - return rideDay.year == day.year && rideDay.month == day.month && rideDay.day == day.day; - }).toList(); - days.add(DayStats(day.year, day.month, day.day, ridesOnDay, _goals)); - } - updateWeeks(); - updateMonths(); - notifyListeners(); - } - - /// Update the saved weeks according to the saved days. - void updateWeeks() { - weeks.clear(); - var monday = startDate.subtract(Duration(days: startDate.weekday - 1)); - while (!monday.isAfter(endDate)) { - List stats = []; - var tmpDay = monday; - for (int i = 0; i < DateTime.daysPerWeek; i++) { - var weekdayStats = days.firstWhere((day) => day.isOnDay(tmpDay), - orElse: () => DayStats.empty(tmpDay.year, tmpDay.month, tmpDay.day, _goals)); - stats.add(weekdayStats); - tmpDay = tmpDay.add(_oneDay); - } - weeks.add(WeekStats(stats)); - monday = tmpDay; - } - } - - /// Update the saved months according to the saved days. - void updateMonths() { - months.clear(); - var firstDayOfMonth = DateTime(startDate.year, startDate.month, 1); - while (!firstDayOfMonth.isAfter(endDate)) { - List stats = []; - var tmpDay = firstDayOfMonth; - while (tmpDay.month == firstDayOfMonth.month) { - var dayStats = days.firstWhere((day) => day.isOnDay(tmpDay), - orElse: () => DayStats.empty(tmpDay.year, tmpDay.month, tmpDay.day, _goals)); - stats.add(dayStats); - tmpDay = tmpDay.add(_oneDay); - } - months.add(MonthStats(stats)); - firstDayOfMonth = DateTime(tmpDay.year, tmpDay.month, 1); - } - } -} diff --git a/lib/gamification/statistics/views/card/daily_overview.dart b/lib/gamification/statistics/views/card/daily_overview.dart deleted file mode 100644 index 243742974..000000000 --- a/lib/gamification/statistics/views/card/daily_overview.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/progress_ring.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; -import 'package:priobike/main.dart'; - -/// This widget displays a simple overview of the daily statistics of a user, including their daily goals (if set). -class DailyOverview extends StatefulWidget { - /// The day to be displayed. - final DayStats today; - - const DailyOverview({super.key, required this.today}); - - @override - State createState() => _DailyOverviewState(); -} - -class _DailyOverviewState extends State { - /// The associated goals service to get the daily goals and route goals of the user. - late GoalsService _goalsService; - - /// How many rides the user did on the route target for the day, if there is one. - int get _ridesOnRouteGoal => _hasRouteGoal - ? widget.today.rides.where((ride) => ride.shortcutId == _goalsService.routeGoals!.routeID).length - : 0; - - /// Whether the user has set route goals for the day. - bool get _hasRouteGoal => - _goalsService.routeGoals != null && _goalsService.routeGoals!.weekdays.elementAt(widget.today.date.weekday - 1); - - @override - void initState() { - _goalsService = getIt(); - _goalsService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _goalsService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// This widget displays the progress a user has made for a certain value. - Widget _getProgressRing({ - required double value, - required StatType type, - double progress = 1, - double size = 80, - }) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ProgressRing( - ringColor: CI.radkulturRed.withOpacity(progress >= 1 ? 1 : 0.5), - ringSize: size, - progress: progress, - content: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 4), - BoldSubHeader( - text: StringFormatter.getRoundedStrByStatType(value, type), - context: context, - height: 0, - ), - BoldContent( - text: StringFormatter.getLabelForStatType(type), - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - height: 0, - ), - ], - ), - ), - BoldContent(text: StringFormatter.getDescriptionForStatType(type), context: context), - ], - ); - } - - @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - const SmallHSpace(), - BoldSubHeader( - text: 'Heute', - context: context, - textAlign: TextAlign.start, - ), - ], - ), - const SmallVSpace(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _getProgressRing( - value: widget.today.distanceKilometres, - type: StatType.distance, - progress: widget.today.distanceGoalKilometres == null - ? 1 - : widget.today.distanceKilometres / widget.today.distanceGoalKilometres!, - ), - _getProgressRing( - value: widget.today.durationMinutes, - type: StatType.duration, - progress: widget.today.durationGoalMinutes == null - ? 1 - : widget.today.durationMinutes / widget.today.durationGoalMinutes!, - ), - _getProgressRing( - value: widget.today.averageSpeedKmh, - type: StatType.speed, - progress: 1, - ), - ], - ), - Expanded(child: Container()), - Container( - padding: const EdgeInsets.symmetric(vertical: 12), - width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: !_hasRouteGoal - ? Theme.of(context).colorScheme.onBackground.withOpacity(0.1) - : (_ridesOnRouteGoal >= 1 - ? CI.radkulturRed.withOpacity(1) - : Theme.of(context).colorScheme.onBackground.withOpacity(0.1)), - ), - child: BoldContent( - text: !_hasRouteGoal - ? 'Kein Routenziel für Heute' - : '$_ridesOnRouteGoal/1 ${_goalsService.routeGoals!.routeName}', - context: context, - height: 1, - ), - ), - Expanded(child: Container()), - ], - ), - ), - ], - ); - } -} diff --git a/lib/gamification/statistics/views/card/route_stats.dart b/lib/gamification/statistics/views/card/route_stats.dart deleted file mode 100644 index 5c13903c9..000000000 --- a/lib/gamification/statistics/views/card/route_stats.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart' hide Shortcuts; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/views/route_goals_in_week.dart'; -import 'package:priobike/home/models/shortcut.dart'; -import 'package:priobike/home/services/shortcuts.dart'; -import 'package:priobike/home/views/shortcuts/pictogram.dart'; -import 'package:priobike/main.dart'; - -/// A widget displaying the users route stats for a single given week. -class FancyRouteStatsForWeek extends StatefulWidget { - /// The week displayed by the widget. - final WeekStats week; - - const FancyRouteStatsForWeek({super.key, required this.week}); - - @override - State createState() => _FancyRouteStatsForWeekState(); -} - -class _FancyRouteStatsForWeekState extends State { - /// The associated goals service to get the route goals to display, whether the user has reached the goals. - late GoalsService _goalsService; - - /// The current route goals of the user. - RouteGoals? get _goals => _goalsService.routeGoals; - - /// Get shortcut corresponding to the route the users has goals for. - Shortcut? get _routeShortcut => getIt().shortcuts?.where((s) => s.id == _goals!.routeID).firstOrNull; - - @override - void initState() { - _goalsService = getIt(); - _goalsService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _goalsService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - if (_goals == null) { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - Row( - children: [ - const SmallHSpace(), - BoldSubHeader( - text: 'Routenziele', - context: context, - textAlign: TextAlign.start, - ), - ], - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.map, - size: 64, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: BoldContent( - text: 'Noch keine Routenziele gesetzt', - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - ), - ], - ), - ), - ], - ); - } - return Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BoldSubHeader(text: _goals!.routeName, context: context), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: const [0.5, 0.9], - colors: [ - Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.background.withOpacity(0.25), - ], - ), - ), - child: RouteGoalsInWeek( - goals: _goals!, - ridesInWeek: widget.week.rides, - daySize: 32, - ), - ), - ], - ), - ), - if (_routeShortcut != null) - Positioned( - top: 24, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(22), - color: Theme.of(context).colorScheme.background, - ), - padding: const EdgeInsets.all(4), - child: ShortcutPictogram( - key: ValueKey(_routeShortcut!.hashCode), - shortcut: _routeShortcut, - height: 112, - color: CI.radkulturRed, - ), - ), - ), - ], - ); - } -} diff --git a/lib/gamification/statistics/views/card/stats_card.dart b/lib/gamification/statistics/views/card/stats_card.dart deleted file mode 100644 index 77212948d..000000000 --- a/lib/gamification/statistics/views/card/stats_card.dart +++ /dev/null @@ -1,213 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/colors.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/views/feature_card.dart'; -import 'package:priobike/gamification/common/views/map_background.dart'; -import 'package:priobike/gamification/statistics/services/statistics_service.dart'; -import 'package:priobike/gamification/statistics/services/stats_view_model.dart'; -import 'package:priobike/gamification/statistics/views/card/daily_overview.dart'; -import 'package:priobike/gamification/statistics/views/card/route_stats.dart'; -import 'package:priobike/gamification/statistics/views/graphs/month_graph.dart'; -import 'package:priobike/gamification/statistics/views/graphs/multiple_weeks_graph.dart'; -import 'package:priobike/gamification/statistics/views/graphs/week_graph.dart'; -import 'package:priobike/gamification/statistics/views/page/stats_page.dart'; -import 'package:priobike/main.dart'; - -/// This card is displayed on the home view and holds all information and functionality of the statistics feature. -class RideStatisticsCard extends StatelessWidget { - const RideStatisticsCard({super.key}); - - @override - Widget build(BuildContext context) { - return GamificationFeatureCard( - onEnabled: () {}, - featureKey: GamificationUserService.statisticsFeatureKey, - featurePage: const StatisticsView(), - featureEnabledContent: const StatisticsOverview(), - featureDisabledContent: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: BoldSubHeader( - text: 'Deine Fahrtstatistiken', - context: context, - textAlign: TextAlign.center, - ), - ), - const SmallHSpace(), - SizedBox( - width: 96, - height: 80, - child: Stack( - fit: StackFit.expand, - children: [ - Align( - alignment: Alignment.bottomRight, - child: Transform.rotate( - angle: 0, - child: const Icon( - Icons.query_stats, - size: 64, - color: LevelColors.silver, - ), - ), - ), - Align( - alignment: Alignment.topLeft, - child: Transform.rotate( - angle: 0, - child: const Icon( - Icons.bar_chart, - size: 64, - color: CI.radkulturRed, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -/// A page view displaying a reduced stat history of the user for only the recent weeks. -class StatisticsOverview extends StatefulWidget { - const StatisticsOverview({super.key}); - - @override - State createState() => _StatisticsOverviewState(); -} - -class _StatisticsOverviewState extends State with TickerProviderStateMixin { - // Controller for the page view displaying the different pages. - late final PageController _pageController = PageController(); - - /// Controller which connects the tab indicator to the page view. - late final TabController _tabController = TabController(length: 5, vsync: this); - - /// The view model holding the ride stats of the displayed data. - late final StatisticsViewModel _viewModel; - - /// Service to get the current stat interval that should be displayed. - late StatisticService _statsService; - - @override - void initState() { - _statsService = getIt(); - _statsService.addListener(updatePage); - _initViewModel(); - super.initState(); - } - - @override - void dispose() { - _statsService.removeListener(updatePage); - _tabController.dispose(); - _pageController.dispose(); - _viewModel.dispose(); - super.dispose(); - } - - /// Initialize view model to hold data for roughly the last 5 weeks, which is enough for the graphs. - void _initViewModel() { - var today = DateTime.now(); - today = DateTime(today.year, today.month, today.day); - var statsStartDate = today.subtract(const Duration(days: 5 * DateTime.daysPerWeek)); - _viewModel = StatisticsViewModel(startDate: statsStartDate, endDate: today); - _viewModel.addListener(update); - } - - void updatePage() { - var newIndex = _statsService.statInterval.index; - if (_pageController.hasClients && (newIndex - (_pageController.page ?? newIndex)).abs() >= 1) { - _pageController.jumpToPage(newIndex); - } - } - - /// Update the displayed page. - void update() => {if (mounted) setState(() {})}; - - /// Wrap a a graph in a column and add a title above of the graph. - Widget _getGraphWithTitle({required String title, required Widget graph}) { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - Row( - children: [ - const SmallHSpace(), - BoldSubHeader( - text: title, - context: context, - textAlign: TextAlign.start, - ), - ], - ), - Expanded( - child: IgnorePointer(child: graph), - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - SizedBox( - height: 200, - child: MapBackground( - child: PageView( - controller: _pageController, - clipBehavior: Clip.hardEdge, - onPageChanged: (int index) => setState(() { - // Update tab controller index to update the indicator. - _tabController.index = index; - if (index < StatInterval.values.length) { - getIt().setStatInterval( - StatInterval.values[index], - ); - } - }), - children: [ - DailyOverview(today: _viewModel.days.last), - _getGraphWithTitle( - title: 'Diese Woche (Km)', - graph: WeekStatsGraph(week: _viewModel.weeks.last), - ), - _getGraphWithTitle( - title: 'Dieser Monat (Km)', - graph: MonthStatsGraph(month: _viewModel.months.last), - ), - _getGraphWithTitle( - title: '${_viewModel.weeks.length} Wochen Rückblick (Km)', - graph: MultipleWeeksStatsGraph(weeks: _viewModel.weeks), - ), - FancyRouteStatsForWeek(week: _viewModel.weeks.last), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: TabPageSelector( - controller: _tabController, - selectedColor: Theme.of(context).colorScheme.primary, - indicatorSize: 12, - borderStyle: BorderStyle.none, - color: Theme.of(context).colorScheme.primary.withOpacity(0.25), - key: GlobalKey(), - ), - ), - ], - ); - } -} diff --git a/lib/gamification/statistics/views/graphs/month_graph.dart b/lib/gamification/statistics/views/graphs/month_graph.dart deleted file mode 100644 index 5836b20cc..000000000 --- a/lib/gamification/statistics/views/graphs/month_graph.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/views/graphs/ride_stats_graph.dart'; - -/// Graph which displays the data for a given month. -class MonthStatsGraph extends StatelessWidget { - /// The stats of the given month. - final MonthStats month; - - const MonthStatsGraph({super.key, required this.month}); - - /// Label the x axis by adding the day value to every fifth day. - Widget _getTitlesX(double value, TitleMeta meta, TextStyle style) { - if ((value + 1) % 5 > 0) return const SizedBox.shrink(); - return SideTitleWidget( - axisSide: meta.axisSide, - space: 8, - child: Text( - (value.toInt() + 1).toString(), - style: style, - ), - ); - } - - @override - Widget build(BuildContext context) { - return RideStatsGraph( - barWidth: 5, - getTitlesX: (value, meta) => _getTitlesX(value, meta, Theme.of(context).textTheme.labelMedium!), - displayedStats: month, - ); - } -} diff --git a/lib/gamification/statistics/views/graphs/multiple_weeks_graph.dart b/lib/gamification/statistics/views/graphs/multiple_weeks_graph.dart deleted file mode 100644 index 9c46beebe..000000000 --- a/lib/gamification/statistics/views/graphs/multiple_weeks_graph.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/views/graphs/ride_stats_graph.dart'; - -/// Graph which displays the data for a given list of weeks. -class MultipleWeeksStatsGraph extends StatelessWidget { - /// The week stats corresponding to the weeks to be displayed. - final List weeks; - - const MultipleWeeksStatsGraph({super.key, required this.weeks}); - - /// Label the x axis by adding the monday date to each week bar. - Widget _getTitlesX(double value, TitleMeta meta, TextStyle style) { - return SideTitleWidget( - axisSide: meta.axisSide, - space: 8, - child: Text( - StringFormatter.getShortDateStr(weeks.elementAt(value.toInt()).mondayDate), - style: style, - ), - ); - } - - @override - Widget build(BuildContext context) { - var listOfWeeks = ListOfRideStats(weeks); - return RideStatsGraph( - barWidth: 30, - getTitlesX: (value, meta) => _getTitlesX(value, meta, Theme.of(context).textTheme.labelSmall!), - displayedStats: listOfWeeks, - ); - } -} diff --git a/lib/gamification/statistics/views/graphs/ride_stats_graph.dart b/lib/gamification/statistics/views/graphs/ride_stats_graph.dart deleted file mode 100644 index a85cc4a08..000000000 --- a/lib/gamification/statistics/views/graphs/ride_stats_graph.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; -import 'package:priobike/gamification/statistics/services/statistics_service.dart'; -import 'package:priobike/main.dart'; - -/// This widget displays a given list of ride stats in a graph. -class RideStatsGraph extends StatefulWidget { - /// Function which returns title widgets for the x axis. - final Widget Function(double value, TitleMeta meta) getTitlesX; - - /// The stats displayed by the graph. - final ListOfRideStats displayedStats; - - /// The preffered width of the bars. - final double barWidth; - - /// Color of the displayed bars. - final Color barColor; - - const RideStatsGraph({ - super.key, - required this.getTitlesX, - required this.displayedStats, - required this.barWidth, - this.barColor = CI.radkulturRed, - }); - - @override - State createState() => _RideStatsGraphState(); -} - -class _RideStatsGraphState extends State { - /// The stat service to get the current selected stat type to be displayed. - late StatisticService _statsService; - - /// Index of the selected bar or null, if none is selected. - int? _selectedIndex; - - /// Stat type to be displayed. - StatType get _type => _statsService.selectedType; - - @override - void initState() { - _statsService = getIt(); - _statsService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _statsService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Get list of bars according to the given values and goal values. - List _getBars() { - var list = widget.displayedStats.list; - return list.mapIndexed((i, stat) { - var value = stat.getStatFromType(_type); - var selected = _selectedIndex != null && _selectedIndex == i; - var goalForBar = stat.getGoalFromType(_type); - var goalReached = goalForBar == null ? true : value >= goalForBar; - var barColorOpacity = goalReached ? 1.0 : 0.4; - if (_selectedIndex != null) barColorOpacity = selected ? 1.0 : 0.2; - - return BarChartGroupData( - x: i, - barRods: [ - BarChartRodData( - toY: value, - color: widget.barColor.withOpacity(barColorOpacity), - width: widget.barWidth, - borderSide: selected - ? BorderSide(color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), width: 1) - : null, - backDrawRodData: goalReached - ? null - : BackgroundBarChartRodData( - show: true, - toY: goalForBar, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.05), - ), - ), - ], - ); - }).toList(); - } - - /// Get max value for the stats to be displayed, rounded to a fitting interval. - double _getRoundedMax() { - var num = widget.displayedStats.getMaxForType(_type); - if (num == 0) return 1; - if (num <= 5) return num; - if (num <= 10) return num.ceilToDouble(); - if (num <= 50) return _roundUpToInterval(num, 5); - if (num <= 100) return _roundUpToInterval(num, 10); - if (num <= 200) return _roundUpToInterval(num, 25); - if (num <= 500) return _roundUpToInterval(num, 50); - return _roundUpToInterval(num, 100); - } - - /// Round a given double up to a given interval. - double _roundUpToInterval(double num, int interval) => interval * (num / interval).ceilToDouble(); - - @override - Widget build(BuildContext context) { - _selectedIndex = widget.displayedStats.isDayInList(_statsService.selectedDate); - return Padding( - padding: const EdgeInsets.only(top: 8), - child: BarChart( - BarChartData( - barTouchData: BarTouchData( - handleBuiltInTouches: false, - touchCallback: (p0, p1) { - if (p0 is FlTapUpEvent) { - var index = p1?.spot?.touchedBarGroupIndex; - if (index == null) { - return _statsService.selectDate(null); - } else { - var selectedElement = widget.displayedStats.list.elementAt(index); - if (selectedElement is DayStats) { - _statsService.selectDate(selectedElement.date); - } else if (selectedElement is WeekStats) { - _statsService.selectDate(selectedElement.mondayDate); - } - } - } - }, - touchExtraThreshold: const EdgeInsets.all(8)), - borderData: FlBorderData(show: false), - titlesData: FlTitlesData( - leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - rightTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) => SideTitleWidget( - axisSide: AxisSide.left, - space: 4, - child: Text( - meta.formattedValue, - style: Theme.of(context).textTheme.labelSmall, - ), - ), - reservedSize: 30, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - getTitlesWidget: widget.getTitlesX, - showTitles: true, - reservedSize: 27, - ), - ), - ), - maxY: _getRoundedMax(), - gridData: const FlGridData(drawVerticalLine: false), - barGroups: _getBars(), - ), - swapAnimationDuration: Duration.zero, - ), - ); - } -} diff --git a/lib/gamification/statistics/views/graphs/week_graph.dart b/lib/gamification/statistics/views/graphs/week_graph.dart deleted file mode 100644 index 9bda26250..000000000 --- a/lib/gamification/statistics/views/graphs/week_graph.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/views/graphs/ride_stats_graph.dart'; - -/// Graph which displays the data for a given week. -class WeekStatsGraph extends StatelessWidget { - /// The stats corresponding to the displayed week. - final WeekStats week; - - const WeekStatsGraph({super.key, required this.week}); - - /// Label the x axis by adding a short description of the days of the week. - Widget _getTitlesX(double value, TitleMeta meta, TextStyle style) { - return SideTitleWidget( - axisSide: meta.axisSide, - space: 8, - child: Text( - StringFormatter.getWeekStr(value.toInt()), - style: style, - ), - ); - } - - @override - Widget build(BuildContext context) { - return RideStatsGraph( - barWidth: 20, - getTitlesX: (value, meta) => _getTitlesX(value, meta, Theme.of(context).textTheme.labelMedium!), - displayedStats: week, - ); - } -} diff --git a/lib/gamification/statistics/views/overall_stats.dart b/lib/gamification/statistics/views/overall_stats.dart deleted file mode 100644 index 0c78e37ed..000000000 --- a/lib/gamification/statistics/views/overall_stats.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/custom_game_icons.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; -import 'package:priobike/main.dart'; - -/// Widget to display the overall statistics of the users registered rides since enabling the gamification. -class OverallStatistics extends StatefulWidget { - const OverallStatistics({super.key}); - - @override - State createState() => _OverallStatisticsState(); -} - -class _OverallStatisticsState extends State { - /// Service to pull the stats from. - late GamificationUserService _userService; - - @override - void initState() { - _userService = getIt(); - _userService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _userService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Returns an info widget for a given stat, label and icon. - Widget _getStatWidget(IconData icon, double value, StatType type) { - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).colorScheme.onSurface, - ), - BoldContent( - text: StringFormatter.getRoundedStrByStatType(value, type), - context: context, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - height: 1, - ), - BoldSmall( - text: StringFormatter.getLabelForStatType(type), - context: context, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - height: 1, - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - var profile = _userService.profile!; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 16), - BoldContent( - text: 'Beta-Features', context: context, height: 1, color: Theme.of(context).colorScheme.onSurface), - Text( - 'aktiviert am ${StringFormatter.getDateStr(_userService.profile!.joinDate)}', - style: TextStyle( - fontFamily: 'HamburgSans', - fontSize: 10, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - height: 1, - ), - ), - const VSpace(), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _getStatWidget(Icons.directions_bike, profile.totalDistanceKilometres, StatType.distance), - _getStatWidget(Icons.timer, profile.totalDurationMinutes, StatType.duration), - _getStatWidget(Icons.speed, profile.averageSpeedKmh, StatType.speed), - _getStatWidget(CustomGameIcons.elevation_gain, profile.totalElevationGainMetres, StatType.elevationGain), - _getStatWidget(CustomGameIcons.elevation_loss, profile.totalElevationLossMetres, StatType.elevationLoss), - ], - ), - ], - ), - ); - } -} diff --git a/lib/gamification/statistics/views/page/ride_graphs_page_view.dart b/lib/gamification/statistics/views/page/ride_graphs_page_view.dart deleted file mode 100644 index 91994f475..000000000 --- a/lib/gamification/statistics/views/page/ride_graphs_page_view.dart +++ /dev/null @@ -1,205 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/map_background.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; -import 'package:priobike/gamification/statistics/services/statistics_service.dart'; -import 'package:priobike/gamification/statistics/views/graphs/month_graph.dart'; -import 'package:priobike/gamification/statistics/views/graphs/multiple_weeks_graph.dart'; -import 'package:priobike/gamification/statistics/views/graphs/week_graph.dart'; -import 'package:priobike/main.dart'; - -/// This widget displays a list of ride stats in an interactive page view containing the corresponding graphs. -class RideGraphsPageView extends StatefulWidget { - /// The data of the currently displayed page. - final List stats; - - const RideGraphsPageView({ - super.key, - required this.stats, - }); - @override - State createState() => _RideGraphsPageViewState(); -} - -class _RideGraphsPageViewState extends State { - /// Controller to controll the page view. - final PageController _pageController = PageController(initialPage: 0); - - /// Stat service to change selected date and stat type. - late StatisticService _statsService; - - /// Index of the page currently displayed by the page view. - int _displayedPageIndex = 0; - - ListOfRideStats get statsOnPage => widget.stats.elementAt(_displayedPageIndex); - StatType get statType => _statsService.selectedType; - int? get selectedIndex => statsOnPage.isDayInList(_statsService.selectedDate); - RideStats? get selectedElement => selectedIndex == null ? null : statsOnPage.list.elementAt(selectedIndex!); - - @override - void initState() { - _statsService = getIt(); - _statsService.addListener(update); - _pageController.addListener(() => setState(() => _displayedPageIndex = _pageController.page!.round())); - super.initState(); - } - - @override - void dispose() { - _statsService.removeListener(update); - _pageController.dispose(); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Get list of graphs according the stats given to the widget. - List _getGraphs() { - return widget.stats.map((element) { - if (element is WeekStats) return WeekStatsGraph(week: element); - if (element is MonthStats) return MonthStatsGraph(month: element); - if (element is ListOfRideStats) return MultipleWeeksStatsGraph(weeks: element.list); - return const SizedBox.shrink(); - }).toList(); - } - - /// Returns a widget displaying the overall or selected value for a given stat type. - Widget _getStatInfoWidget(StatType type) { - return Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - OnTapAnimation( - onPressed: () => _statsService.setStatType(type), - child: Icon( - getIconForInfoType(type), - size: 40, - ), - ), - Container( - height: 4, - width: 40, - decoration: BoxDecoration( - color: _statsService.selectedType == type ? CI.radkulturRed : Colors.transparent, - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - ), - const SizedBox(height: 8), - BoldSubHeader( - text: StringFormatter.getRoundedStrByStatType( - (selectedElement != null) ? selectedElement!.getStatFromType(type) : statsOnPage.getStatFromType(type), - type, - ), - context: context, - height: 1, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - BoldSmall( - text: StringFormatter.getLabelForStatType(type), - context: context, - height: 1, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => _statsService.selectDate(null), - child: Column( - children: [ - const SizedBox(height: 16), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 16), - SubHeader( - text: StringFormatter.getDescriptionForStatType(_statsService.selectedType), - context: context, - height: 1, - ), - Expanded(child: Container()), - if (selectedElement == null) ...[ - BoldContent( - text: 'ø', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), - ), - const SizedBox(width: 2), - BoldSubHeader( - text: StringFormatter.getFormattedStrByStatType( - statsOnPage.getAvgFromType(_statsService.selectedType), - _statsService.selectedType, - ), - context: context, - height: 1, - ), - ], - const SizedBox(width: 16), - ], - ), - const SizedBox(height: 8), - Container( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 16), - BoldSmall( - text: statsOnPage.getTimeDescription(selectedIndex), - context: context, - ), - ], - ), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SizedBox( - width: MediaQuery.of(context).size.width, - height: 224, - child: MapBackground( - child: PageView( - reverse: true, - controller: _pageController, - clipBehavior: Clip.hardEdge, - children: _getGraphs(), - ), - ), - ), - ), - const SmallVSpace(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _getStatInfoWidget(StatType.distance), - _getStatInfoWidget(StatType.duration), - _getStatInfoWidget(StatType.speed), - _getStatInfoWidget(StatType.elevationGain), - _getStatInfoWidget(StatType.elevationLoss), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/gamification/statistics/views/page/route_goals_history.dart b/lib/gamification/statistics/views/page/route_goals_history.dart deleted file mode 100644 index b65d9bd16..000000000 --- a/lib/gamification/statistics/views/page/route_goals_history.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/common/views/on_tap_animation.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/services/stats_view_model.dart'; -import 'package:priobike/gamification/statistics/views/route_goals_in_week.dart'; -import 'package:priobike/main.dart'; - -/// Display a history of the user reaching their route goal. -class RouteGoalsHistory extends StatefulWidget { - /// The view model with the weeks, for which the history should be displayed. - final StatisticsViewModel viewModel; - - const RouteGoalsHistory({super.key, required this.viewModel}); - - @override - State createState() => _RouteGoalsHistoryState(); -} - -class _RouteGoalsHistoryState extends State { - /// The associated goals service to get the route goals from. - late GoalsService _goalsService; - - /// Index of the currently displayed week. - int _currentWeekIndex = 0; - - /// Return the users route goals. - RouteGoals? get _goals => _goalsService.routeGoals; - - /// Get the list of weeks the history should include. - List get _reversedWeeks => widget.viewModel.weeks.reversed.toList(); - - /// Get the stats of the currently selected week. - WeekStats get _currentWeekStats => _reversedWeeks.elementAt(_currentWeekIndex); - - @override - void initState() { - _goalsService = getIt(); - _goalsService.addListener(update); - super.initState(); - } - - @override - void dispose() { - _goalsService.removeListener(update); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BoldContent( - text: 'Routenziele', - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - textAlign: TextAlign.start, - ), - if (_goals == null) ...[ - const VSpace(), - Icon( - Icons.map, - size: 48, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: BoldContent( - text: 'Du hast noch keine Routenziele gesetzt', - context: context, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.25), - ), - ), - const VSpace(), - ], - if (_goals != null) ...[ - const SmallVSpace(), - BoldSubHeader( - text: _goals!.routeName, - context: context, - textAlign: TextAlign.center, - height: 1, - ), - Container( - margin: const EdgeInsets.all(4), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(24), - ), - child: AnimatedSwitcher( - duration: const ShortDuration(), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: RouteGoalsInWeek( - key: ValueKey(_currentWeekStats.getTimeDescription(null)), - goals: _goals!, - ridesInWeek: _currentWeekStats.rides, - daySize: 40, - ), - ), - ), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - OnTapAnimation( - scaleFactor: 0.8, - onPressed: - _currentWeekIndex >= _reversedWeeks.length - 1 ? null : () => setState(() => _currentWeekIndex++), - child: Icon( - Icons.arrow_back_ios_rounded, - size: 32, - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(_currentWeekIndex >= _reversedWeeks.length - 1 ? 0.25 : 1), - ), - ), - Expanded( - child: SubHeader( - text: _currentWeekStats.getTimeDescription(null), - context: context, - textAlign: TextAlign.center, - height: 1, - ), - ), - OnTapAnimation( - scaleFactor: 0.8, - onPressed: _currentWeekIndex == 0 ? null : () => setState(() => _currentWeekIndex--), - child: Icon( - Icons.arrow_forward_ios_rounded, - size: 32, - color: Theme.of(context).colorScheme.onBackground.withOpacity(_currentWeekIndex == 0 ? 0.25 : 1), - ), - ), - ], - ), - ], - const SmallVSpace(), - ], - ), - ); - } -} diff --git a/lib/gamification/statistics/views/page/stats_page.dart b/lib/gamification/statistics/views/page/stats_page.dart deleted file mode 100644 index 367fbf8fe..000000000 --- a/lib/gamification/statistics/views/page/stats_page.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/annotated_region.dart'; -import 'package:priobike/common/layout/buttons.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/statistics/models/ride_stats.dart'; -import 'package:priobike/gamification/statistics/models/stat_type.dart'; -import 'package:priobike/gamification/statistics/services/statistics_service.dart'; -import 'package:priobike/gamification/statistics/services/stats_view_model.dart'; -import 'package:priobike/gamification/statistics/views/page/ride_graphs_page_view.dart'; -import 'package:priobike/main.dart'; - -/// This view provides the user with detailed statistics about their ride history. -class StatisticsView extends StatefulWidget { - const StatisticsView({super.key}); - - @override - State createState() => _StatisticsViewState(); -} - -class _StatisticsViewState extends State with TickerProviderStateMixin { - /// The view model, which hold all the stats displayed by the view. - late StatisticsViewModel _viewModel; - - /// The interval in which rides shall be displayed. - late StatInterval _statInterval; - - @override - void initState() { - _statInterval = getIt().statInterval; - _initViewModel(); - super.initState(); - } - - @override - void dispose() { - _viewModel.dispose(); - super.dispose(); - } - - /// Called when a listener callback of a ChangeNotifier is fired. - void update() => {if (mounted) setState(() {})}; - - /// Initialize view model to hold data for the last year. - void _initViewModel() { - var today = DateTime.now(); - today = DateTime(today.year, today.month, today.day); - var todayLastYear = DateTime(today.year - 1, today.month, today.day); - _viewModel = StatisticsViewModel(startDate: todayLastYear, endDate: today); - _viewModel.addListener(update); - } - - /// Get ride statistic view according to stat interval. - Widget _getStatsViewFromInterval(StatInterval interval) { - // If the stat interval ist weeks, return a page view for all weeks in the view model. - if (_statInterval == StatInterval.weeks && _viewModel.weeks.isNotEmpty) { - return RideGraphsPageView( - key: const ValueKey('weeks'), - stats: _viewModel.weeks.reversed.toList(), - ); - } - // If the stat interval ist months, return a page view for all months in the view model. - else if (_statInterval == StatInterval.months && _viewModel.months.isNotEmpty) { - return RideGraphsPageView( - key: const ValueKey('months'), - stats: _viewModel.months.reversed.toList(), - ); - } - // If the stat interval ist multiple weeks, built groups of 5 weeks from the weeks - // in the view model and return a page view for the week-groups. - else if (_statInterval == StatInterval.multipleWeeks) { - int weeksPerGraph = 5; - List> displayedStats = []; - var allWeeks = List.from(_viewModel.weeks); - while (allWeeks.length >= weeksPerGraph) { - List weeks = []; - for (int i = 0; i < weeksPerGraph; i++) { - weeks.insert(0, allWeeks.removeLast()); - } - displayedStats.add(ListOfRideStats(weeks)); - } - if (displayedStats.isNotEmpty) { - return RideGraphsPageView( - key: const ValueKey('multiWeeks'), - stats: displayedStats, - ); - } - } - return const SizedBox.shrink(); - } - - /// Reset the selected date and type, to not make the stat card on the home screen confusing. - void _resetGraphs() { - getIt().selectDate(null); - getIt().setStatType(StatType.distance); - } - - @override - Widget build(BuildContext context) { - return AnnotatedRegionWrapper( - backgroundColor: Theme.of(context).colorScheme.background, - brightness: Theme.of(context).brightness, - child: PopScope( - onPopInvoked: (type) { - _resetGraphs(); - }, - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: SafeArea( - child: Column( - children: [ - const SmallVSpace(), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - decoration: BoxDecoration( - border: Border.all( - width: 0.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - ), - child: AppBackButton( - onPressed: () { - _resetGraphs(); - Navigator.pop(context); - }, - ), - ), - const HSpace(), - SubHeader( - text: 'Statistiken', - context: context, - textAlign: TextAlign.center, - ), - ], - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: StatInterval.values - .map((interval) => IntervalSelectionButton( - interval: interval, - selected: _statInterval == interval, - onTap: () => setState(() => _statInterval = interval), - )) - .toList(), - ), - ), - const SmallVSpace(), - AnimatedSwitcher( - duration: const ShortDuration(), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: _getStatsViewFromInterval(_statInterval), - ), - Expanded( - child: Container(), - ), - //RouteGoalsHistory(viewModel: _viewModel), - ], - ), - ), - ), - ), - ); - } -} - -/// Button to change the displayed stat interval. -class IntervalSelectionButton extends StatefulWidget { - /// The stat interval corresponding to the button. - final StatInterval interval; - - /// If the interval is currently selected. - final bool selected; - - /// Callback for when the button is tapped. - final Function() onTap; - - const IntervalSelectionButton({ - super.key, - required this.interval, - required this.onTap, - required this.selected, - }); - - @override - State createState() => _IntervalSelectionButtonState(); -} - -class _IntervalSelectionButtonState extends State { - bool _tapDown = false; - - /// Get widget header title from stat interval. - String _getTitleFromStatInterval(StatInterval interval) { - if (interval == StatInterval.weeks) return 'Woche'; - if (interval == StatInterval.multipleWeeks) return '5 Wochen'; - if (interval == StatInterval.months) return 'Monat'; - return ''; - } - - @override - Widget build(BuildContext context) { - return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTapDown: (_) => setState(() => _tapDown = true), - onTapUp: (_) => setState(() => _tapDown = false), - onTapCancel: () => setState(() => _tapDown = false), - onTap: () { - widget.onTap(); - setState(() {}); - getIt().setStatInterval(widget.interval); - }, - child: Container( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Content( - text: _getTitleFromStatInterval(widget.interval), - context: context, - color: Theme.of(context).colorScheme.onBackground.withOpacity(_tapDown ? 0.1 : 1), - ), - const SmallVSpace(), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 4, - decoration: BoxDecoration( - color: widget.selected - ? CI.radkulturRed - : Theme.of(context).colorScheme.onBackground.withOpacity(_tapDown ? 0.01 : 0.05), - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/gamification/statistics/views/route_goals_in_week.dart b/lib/gamification/statistics/views/route_goals_in_week.dart deleted file mode 100644 index 7fc0f3910..000000000 --- a/lib/gamification/statistics/views/route_goals_in_week.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/ci.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/utils.dart'; -import 'package:priobike/gamification/goals/models/route_goals.dart'; - -/// This widget displays the users route goals progress for given rides in week and the route goals. -class RouteGoalsInWeek extends StatelessWidget { - /// The route goals for the week. - final RouteGoals goals; - - /// The list of rides in the week to be displayed. - final List ridesInWeek; - - /// The size of the widget displaying a single day of the week. - final double daySize; - - const RouteGoalsInWeek({ - super.key, - required this.ridesInWeek, - required this.goals, - required this.daySize, - }); - - /// Returns the number of rides the user did on the route for a given weekday. - int _ridesOnDay(int day) { - var ridesOnDay = ridesInWeek.where((ride) => ride.startTime.weekday == day + 1); - var ridesOnRoute = ridesOnDay.where((ride) => ride.shortcutId == goals.routeID); - return ridesOnRoute.length; - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: goals.weekdays - .mapIndexed( - (i, isGoal) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: daySize, - width: daySize, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _ridesOnDay(i) > 0 && isGoal - ? CI.radkulturRed - : Theme.of(context).colorScheme.onBackground.withOpacity(isGoal ? 0.1 : 0.05), - ), - child: Center( - child: BoldSmall( - text: '${_ridesOnDay(i)}/${isGoal ? '1' : '0'}', - context: context, - color: _ridesOnDay(i) > 0 && isGoal - ? Colors.white - : Theme.of(context).colorScheme.onBackground.withOpacity(isGoal ? 1 : 0.25), - ), - ), - ), - const SizedBox(height: 4), - BoldContent(text: StringFormatter.getWeekStr(i), context: context) - ], - ), - ) - .toList(), - ); - } -} diff --git a/lib/gamification/statistics/views/stats_tutorial.dart b/lib/gamification/statistics/views/stats_tutorial.dart deleted file mode 100644 index 68adb5f06..000000000 --- a/lib/gamification/statistics/views/stats_tutorial.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:priobike/common/layout/spacing.dart'; -import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/common/views/tutorial_page.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/main.dart'; - -/// This tutorial page gives the user a brief introduction to the statistics feature -/// and gives them the option to activate it. -class StatisticsTutorial extends StatelessWidget { - const StatisticsTutorial({super.key}); - - @override - Widget build(BuildContext context) { - return TutorialPage( - confirmButtonLabel: 'Aktivieren', - onConfirmButtonTab: () { - getIt().enableFeature(GamificationUserService.statisticsFeatureKey); - Navigator.of(context).pop(); - }, - contentList: [ - const SizedBox(height: 64 + 16), - Header(text: "Fahrtstatistiken", context: context), - const SmallVSpace(), - SubHeader(text: "Verschaffe Dir einen Überblick über Deine aufgezeichneten Fahrtdaten.", context: context), - const SizedBox(height: 82), - ], - ); - } -} diff --git a/lib/home/views/main.dart b/lib/home/views/main.dart index 84e7a81c2..958b5ffa6 100644 --- a/lib/home/views/main.dart +++ b/lib/home/views/main.dart @@ -8,8 +8,6 @@ import 'package:priobike/common/layout/dialog.dart'; import 'package:priobike/common/layout/modal.dart'; import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; -import 'package:priobike/gamification/main.dart'; import 'package:priobike/home/models/shortcut.dart'; import 'package:priobike/home/services/profile.dart'; import 'package:priobike/home/services/shortcuts.dart'; @@ -247,7 +245,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw await statusHistory.fetch(); await news.getArticles(); await getIt().fetch(); - await getIt().fetchData(); // Wait for one more second, otherwise the user will get impatient. await Future.delayed( const Duration(seconds: 1), @@ -386,11 +383,6 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw ), child: Column( children: [ - if (settings.enableGamification) - const BlendIn( - duration: Duration(milliseconds: 1250), - child: GameView(), - ), const BlendIn( delay: Duration(milliseconds: 1250), child: WikiView(), diff --git a/lib/loader.dart b/lib/loader.dart index c525c4bf5..f7e37a05d 100644 --- a/lib/loader.dart +++ b/lib/loader.dart @@ -9,8 +9,6 @@ import 'package:priobike/common/layout/text.dart'; import 'package:priobike/common/layout/tiles.dart'; import 'package:priobike/common/map/image_cache.dart'; import 'package:priobike/common/map/map_design.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; import 'package:priobike/home/services/profile.dart'; import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/home/views/main.dart'; @@ -92,9 +90,7 @@ class LoaderState extends State { await getIt().fetch(); await getIt().loadBoundaryCoordinates(); await getIt().loadLastRoute(); - await getIt().fetchData(); await MapboxTileImageCache.pruneUnusedImages(); - getIt().sendUnsentElements(); // Only allow portrait mode. await SystemChrome.setPreferredOrientations([ diff --git a/lib/main.dart b/lib/main.dart index de0a108b8..ab5e20800 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,13 +9,6 @@ import 'package:priobike/common/fcm.dart'; import 'package:priobike/common/layout/ci.dart'; import 'package:priobike/common/map/map_design.dart'; import 'package:priobike/feedback/services/feedback.dart'; -import 'package:priobike/gamification/challenges/services/challenge_service.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/common/services/evaluation_data_service.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; -import 'package:priobike/gamification/statistics/services/statistics_service.dart'; import 'package:priobike/home/services/poi.dart'; import 'package:priobike/home/services/profile.dart'; import 'package:priobike/home/services/shortcuts.dart'; @@ -103,16 +96,7 @@ Future main() async { getIt.registerSingleton(Traffic()); getIt.registerSingleton(Boundary()); getIt.registerSingleton(StatusHistory()); - getIt.registerSingleton(GamificationUserService()); getIt.registerSingleton(POI()); - getIt.registerSingleton(StatisticService()); - getIt.registerSingleton(DailyChallengeService()); - getIt.registerSingleton(WeeklyChallengeService()); - getIt.registerSingleton(GoalsService()); - getIt.registerSingleton(ChallengesProfileService()); - getIt.registerSingleton(EvaluationDataService()); - getIt.registerSingleton(EventService()); - try { runApp(const App()); } on Error catch (error, stack) { diff --git a/lib/ride/views/finish_button.dart b/lib/ride/views/finish_button.dart index 5c98665f3..fdd33dffc 100644 --- a/lib/ride/views/finish_button.dart +++ b/lib/ride/views/finish_button.dart @@ -6,7 +6,6 @@ import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; import 'package:priobike/common/layout/tiles.dart'; import 'package:priobike/feedback/views/main.dart'; -import 'package:priobike/gamification/community_event/service/event_service.dart'; import 'package:priobike/home/views/main.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/main.dart'; @@ -77,9 +76,6 @@ class FinishRideButtonState extends State { final statistics = getIt(); await statistics.calculateSummary(); - // If there is an active event, check if the user passed some of the event locations. - await getIt().checkLocations(); - // Disconnect from the mqtt broker. final datastream = getIt(); await datastream.disconnect(); diff --git a/lib/settings/services/settings.dart b/lib/settings/services/settings.dart index 378630acc..5293c7060 100644 --- a/lib/settings/services/settings.dart +++ b/lib/settings/services/settings.dart @@ -372,7 +372,7 @@ class Settings with ChangeNotifier { } static const enableGamificationKey = "priobike.settings.enableGamification"; - static const defaultEnableGamification = true; + static const defaultEnableGamification = false; Future setEnableGamification(bool enableGamification, [SharedPreferences? storage]) async { storage ??= await SharedPreferences.getInstance(); diff --git a/lib/settings/views/internal.dart b/lib/settings/views/internal.dart index 12eeb69a9..8a06e7af5 100644 --- a/lib/settings/views/internal.dart +++ b/lib/settings/views/internal.dart @@ -6,9 +6,6 @@ import 'package:priobike/common/layout/modal.dart'; import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; import 'package:priobike/common/map/image_cache.dart'; -import 'package:priobike/gamification/challenges/services/challenges_profile_service.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; -import 'package:priobike/gamification/goals/services/goals_service.dart'; import 'package:priobike/home/services/shortcuts.dart'; import 'package:priobike/main.dart'; import 'package:priobike/migration/services.dart'; @@ -328,26 +325,6 @@ class InternalSettingsViewState extends State { callback: () => MapboxTileImageCache.deleteAllImages(), ), ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: 'Challenges zurücksetzen', - icon: Icons.recycling, - callback: () => getIt().resetChallenges(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Game-Profil zurücksetzen", - icon: Icons.recycling, - callback: () async { - await getIt().reset(); - await getIt().reset(); - await getIt().reset(); - }, - ), - ), Padding( padding: const EdgeInsets.only(top: 8), child: SettingsElement( diff --git a/lib/statistics/services/statistics.dart b/lib/statistics/services/statistics.dart index 222e04c7b..780f8ce91 100644 --- a/lib/statistics/services/statistics.dart +++ b/lib/statistics/services/statistics.dart @@ -2,12 +2,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; -import 'package:priobike/gamification/common/database/database.dart'; -import 'package:priobike/gamification/common/services/user_service.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/main.dart'; import 'package:priobike/positioning/services/positioning.dart'; -import 'package:priobike/ride/services/ride.dart'; import 'package:priobike/statistics/models/summary.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -129,13 +126,6 @@ class Statistics with ChangeNotifier { elevationLoss: totalElevationLoss, ); - // Store summary in database, if a user profile exists. - final storage = await SharedPreferences.getInstance(); - if (storage.getBool(GamificationUserService.gamificationEnabledKey) ?? false) { - // Get the route or location shortcut used for the ride, if there is one. - AppDatabase.instance.rideSummaryDao.createObjectFromSummary(currentSummary!, start, getIt().shortcutId); - } - addSummary(currentSummary!); } diff --git a/lib/tracking/models/track.dart b/lib/tracking/models/track.dart index d0021d8c1..9e42793ae 100644 --- a/lib/tracking/models/track.dart +++ b/lib/tracking/models/track.dart @@ -165,7 +165,7 @@ class Track { required this.activityType, required this.routes, required this.subVersion, - this.canUseGamification = true, + this.canUseGamification = false, }); /// Convert the track to a json object. diff --git a/pubspec.lock b/pubspec.lock index 47cc0078e..71733814f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -322,7 +322,7 @@ packages: source: hosted version: "7.0.0" drift: - dependency: "direct main" + dependency: transitive description: name: drift sha256: d542088d353585a252f015b81c1e7603c57c996ba59a80d53a3f4644cc47f543 @@ -345,14 +345,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - equatable: - dependency: transitive - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.dev" - source: hosted - version: "2.0.5" event_bus: dependency: transitive description: @@ -449,14 +441,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - fl_chart: - dependency: "direct main" - description: - name: fl_chart - sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" - url: "https://pub.dev" - source: hosted - version: "0.65.0" flutter: dependency: "direct main" description: flutter @@ -833,7 +817,7 @@ packages: source: hosted version: "2.0.1" path: - dependency: "direct main" + dependency: transitive description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" @@ -1142,14 +1126,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - sqlite3_flutter_libs: - dependency: "direct main" - description: - name: sqlite3_flutter_libs - sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871" - url: "https://pub.dev" - source: hosted - version: "0.5.18" sqlparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 93f8575fb..c165f47e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,10 +67,6 @@ dependencies: url_launcher: ^6.2.1 # Used to open URLs in_app_review: ^2.0.8 # Used to rate the app proj4dart: ^2.1.0 # Used to convert coordinates from WGS84 to Mercator and back - drift: ^2.13.2 # Used for the local database - sqlite3_flutter_libs: ^0.5.18 # Needed for the drift database which is based on sqlite - path: ^1.8.2 # Used to determine the database file location - fl_chart: ^0.65.0 # Used to display the ride statistics in the gamification views file_picker: ^6.1.1 # Used to open GPX files gpx: ^2.2.1 @@ -118,9 +114,6 @@ flutter: - asset: assets/fonts/HamburgSans-BoldItalic.ttf weight: 700 style: italic - - family: CustomGameIcons - fonts: - - asset: assets/fonts/CustomGameIcons.ttf # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in