diff --git a/.gitea/workflows/pull_request.yaml b/.gitea/workflows/pull_request.yaml index 43d36d2..5b88cdf 100644 --- a/.gitea/workflows/pull_request.yaml +++ b/.gitea/workflows/pull_request.yaml @@ -6,23 +6,17 @@ on: jobs: lint: runs-on: ubuntu-latest + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install jq - run: | - apt-get update - apt-get install -y jq - - name: Install Flutter (wget) run: | - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + wget --show-progress --progress=bar:force:noscroll:giga https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz tar xf flutter_linux_3.38.2-stable.tar.xz - # Set Git safe directory for Flutter path git config --global --add safe.directory "$(pwd)/flutter" - # Set Flutter path - echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + echo "$(pwd)/flutter/bin" >> $GITEA_PATH - name: Get dependencies run: flutter pub get @@ -32,26 +26,22 @@ jobs: test: runs-on: ubuntu-latest + env: + RUNNER_TOOL_CACHE: /toolcache + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install dependencies - run: | - apt-get update - apt-get install -y jq - - name: Install Flutter (wget) run: | - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + wget --show-progress --progress=bar:force:noscroll:giga https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz tar xf flutter_linux_3.38.2-stable.tar.xz - # Set Git safe directory for Flutter path git config --global --add safe.directory "$(pwd)/flutter" - # Set Flutter path - echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + echo "$(pwd)/flutter/bin" >> $GITEA_PATH - name: Get dependencies run: flutter pub get - name: Run tests - run: flutter test \ No newline at end of file + run: flutter test diff --git a/.gitea/workflows/push.yaml b/.gitea/workflows/push.yaml index 700e96b..20319e2 100644 --- a/.gitea/workflows/push.yaml +++ b/.gitea/workflows/push.yaml @@ -7,44 +7,95 @@ on: - "main" jobs: - format: + test: runs-on: ubuntu-latest - if: false # Needs bot user steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install dependencies + - name: Install Flutter (wget) run: | - apt-get update - apt-get install -y jq + wget --show-progress --progress=bar:force:noscroll:giga https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + tar xf flutter_linux_3.38.2-stable.tar.xz + git config --global --add safe.directory "$(pwd)/flutter" + echo "$(pwd)/flutter/bin" >> $GITEA_PATH + + - name: Get dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + format: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Flutter (wget) run: | - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + wget --show-progress --progress=bar:force:noscroll:giga https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz tar xf flutter_linux_3.38.2-stable.tar.xz - # Set Git safe directory for Flutter path git config --global --add safe.directory "$(pwd)/flutter" - # Set Flutter path - echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + echo "$(pwd)/flutter/bin" >> $GITEA_PATH - - name: Get & upgrade dependencies - run: | - flutter pub get - flutter pub upgrade --major-versions + - name: Get dependencies + run: flutter pub get - - name: Auto-format - run: | - dart format lib - dart fix --apply lib + - name: Check code format + id: check_format + continue-on-error: true + run: flutter analyze lib test - # Needs credentials, push access and the right files need to be staged - - name: Commit Changes + - name: Format code + if: steps.check_format.outcome == 'failure' + env: + GITEA_TOKEN: ${{ secrets.BOT_TOKEN }} run: | - git config --global user.name "Gitea Actions" - git config --global user.email "actions@gitea.com" - git status - git add lib/ - git status - git commit -m "Actions: Auto-formatting [skip ci]" - git push + git fetch origin ${{ gitea.ref_name }} + git checkout ${{ gitea.ref_name }} + + dart fix --apply lib + dart fix --apply test + + if [ -n "$(git status --porcelain lib test)" ]; then + git config --global user.name "Gitea Actions [bot]" + git config --global user.email "actions@yannick-weigert.de" + git add lib test + git commit -m "Auto-format code [skip ci]" + git push origin HEAD:${{ gitea.ref_name }} + else + echo "No changes to commit" + fi + + - name: Verify format + run: flutter analyze lib test + + update_version: + runs-on: ubuntu-latest + needs: format + if: gitea.ref == 'refs/heads/development' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.BOT_TOKEN }} + ref: ${{ gitea.ref_name }} + + - name: Increment version number + uses: https://github.com/stikkyapp/update-pubspec-version@v2 + with: + strategy: 'patch' + path: './pubspec.yaml' + + + - name: Commit version update + env: + GITEA_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + git config --global user.name "Gitea Actions [bot]" + git config --global user.email "actions@yannick-weigert.de" + git add pubspec.yaml + git commit -m "Updated version number [skip ci]" + git push origin HEAD:${{ gitea.ref_name }} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 30e5d08..04172d4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,4 +10,5 @@ linter: prefer_const_declarations: true prefer_const_literals_to_create_immutables: true unnecessary_const: true - lines_longer_than_80_chars: false \ No newline at end of file + lines_longer_than_80_chars: false + constant_identifier_names: false \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index edde6d6..0d5c4b8 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.example.game_tracker" + namespace = "de.liquid.tallee" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -21,7 +21,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.game_tracker" + applicationId = "de.liquid.tallee" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3c48c4a..e722349 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - #E6F1E4 - #0B0B0B + #ef681f \ No newline at end of file diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml index 2971c37..fd4cd85 100644 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,5 +1,4 @@ - - @color/app_icon_background + #EF681F \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8358b1c..27638af 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -152,7 +152,6 @@ B68CF4A64F0B5E45B43D6900 /* Pods-RunnerTests.release.xcconfig */, E754D1191B3E54E52B6DCC49 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -478,7 +477,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; + PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -661,7 +660,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; + PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -684,7 +683,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTracker; + PRODUCT_BUNDLE_IDENTIFIER = de.liquid.tallee; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..94edbb6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..11f8618 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..165a8a8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..b346952 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..8745a0f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..9b48058 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..11c79f1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..6a99099 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..ec9f3f6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..ee015b2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..ebb1b76 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 45d1e86..73d3b7f 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,14 +1 @@ -{ - "images" : [ - { - "filename" : "icon_x1024.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_x1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_x1024.png deleted file mode 100644 index 1abba4a..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_x1024.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json b/ios/Runner/Assets.xcassets/LauncherColor.colorset/Contents.json similarity index 74% rename from ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json rename to ios/Runner/Assets.xcassets/LauncherColor.colorset/Contents.json index 41fe6c8..7356209 100644 --- a/ios/Runner/Assets.xcassets/LauncherBackgroundColor.colorset/Contents.json +++ b/ios/Runner/Assets.xcassets/LauncherColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.043", - "green" : "0.043", - "red" : "0.043" + "blue" : "0.122", + "green" : "0.408", + "red" : "0.937" } }, "idiom" : "universal" diff --git a/ios/Runner/Assets.xcassets/LauncherIcon.imageset/Contents.json b/ios/Runner/Assets.xcassets/LauncherIcon.imageset/Contents.json index 2945b36..06ddd98 100644 --- a/ios/Runner/Assets.xcassets/LauncherIcon.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LauncherIcon.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "icon-transparent.png", + "idiom" : "universal" } ], "info" : { diff --git a/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon-transparent.png b/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon-transparent.png new file mode 100644 index 0000000..f926570 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon-transparent.png differ diff --git a/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon.png b/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon.png deleted file mode 100644 index ee71baa..0000000 Binary files a/ios/Runner/Assets.xcassets/LauncherIcon.imageset/icon.png and /dev/null differ diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index fb29ee2..28d0ddf 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - + - + @@ -20,12 +20,19 @@ + - + @@ -36,8 +43,8 @@ - - + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8db75d8..7e79382 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,10 +2,12 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Game Tracker + Tallee CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - game_tracker + tallee CFBundlePackageType APPL CFBundleShortVersionString @@ -22,13 +24,15 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + https + http + LSRequiresIPhoneOS - LSApplicationQueriesSchemes - - https - http - + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -44,9 +48,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - \ No newline at end of file + diff --git a/lib/core/adaptive_page_route.dart b/lib/core/adaptive_page_route.dart index ba68557..4b0f9b4 100644 --- a/lib/core/adaptive_page_route.dart +++ b/lib/core/adaptive_page_route.dart @@ -1,7 +1,11 @@ import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +/// Returns a platform-adaptive page route based on the current platform. +/// - On iOS, it returns a [CupertinoPageRoute]. +/// - On other platforms, it returns a [MaterialPageRoute]. Route adaptivePageRoute({ required Widget Function(BuildContext) builder, bool fullscreenDialog = false, diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 8d3c8cc..c1bc0fe 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,6 +1,22 @@ +/// Application-wide constants class Constants { Constants._(); // Private constructor to prevent instantiation /// Minimum duration of all app skeletons - static Duration minimumSkeletonDuration = const Duration(milliseconds: 250); + static const Duration MINIMUM_SKELETON_DURATION = Duration(milliseconds: 250); + + /// Maximum length for player names + static const int MAX_PLAYER_NAME_LENGTH = 32; + + /// Maximum length for group names + static const int MAX_GROUP_NAME_LENGTH = 32; + + /// Maximum length for match names + static const int MAX_MATCH_NAME_LENGTH = 32; + + /// Maximum length for game names + static const int MAX_GAME_NAME_LENGTH = 32; + + /// Maximum length for team names + static const int MAX_TEAM_NAME_LENGTH = 32; } diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index a6c6376..9400d3d 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -1,16 +1,37 @@ import 'package:flutter/material.dart'; +/// Theme class that defines colors, border radius, padding, and decorations class CustomTheme { CustomTheme._(); // Private constructor to prevent instantiation // ==================== Colors ==================== - static Color primaryColor = const Color(0xFF7505E4); - static Color secondaryColor = const Color(0xFFAFA2FF); - static Color backgroundColor = const Color(0xFF0B0B0B); - static Color boxColor = const Color(0xFF101010); - static Color onBoxColor = const Color(0xFF181818); - static Color boxBorder = const Color(0xFF272727); - static const Color textColor = Colors.white; + + /// Primary color of the app theme + static const Color primaryColor = Color(0xFFef681f); + + /// Secondary color of the app theme + static const Color secondaryColor = Color(0xFFf2a981); + + /// Background color of the app theme + static const backgroundColor = Color(0xFF0B0B0B); + + /// Default color for boxes and containers + static const Color boxColor = Color(0xFF101010); + + /// Default border color for boxes and containers + static const Color boxBorder = Color(0xFF272727); + + /// Color for boxes and containers displayed on boxes + static const Color onBoxColor = Color(0xFF181818); + + /// Text color used throughout the app + static const Color textColor = Color(0xFFFFFFFF); + + /// Selected color for the [NavbarItem] + static Color navBarItemSelectedColor = primaryColor.withGreen(100); + + /// Unselected color for the [NavbarItem] + static Color navBarItemUnselectedColor = Colors.grey.shade400; // ==================== Border Radius ==================== static const double standardBorderRadius = 12.0; @@ -42,18 +63,18 @@ class CustomTheme { ); // ==================== App Bar Theme ==================== - static AppBarTheme appBarTheme = AppBarTheme( + static const AppBarTheme appBarTheme = AppBarTheme( backgroundColor: backgroundColor, foregroundColor: textColor, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, - titleTextStyle: const TextStyle( + titleTextStyle: TextStyle( color: textColor, fontSize: 20, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), - iconTheme: const IconThemeData(color: textColor), + iconTheme: IconThemeData(color: textColor), ); } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 17a01f6..53288e3 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; /// Button types used for styling the [CustomWidthButton] /// - [ButtonType.primary]: Primary button style. diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 98c602a..086cb2d 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -1,9 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/group_table.dart'; -import 'package:game_tracker/data/db/tables/player_group_table.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/player_group_table.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; part 'group_dao.g.dart'; diff --git a/lib/data/dao/group_match_dao.dart b/lib/data/dao/group_match_dao.dart index d428fb5..de2eaf1 100644 --- a/lib/data/dao/group_match_dao.dart +++ b/lib/data/dao/group_match_dao.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/group_match_table.dart'; -import 'package:game_tracker/data/dto/group.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/group_match_table.dart'; +import 'package:tallee/data/dto/group.dart'; part 'group_match_dao.g.dart'; diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 160686a..cc3a37f 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -1,9 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/match_table.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; part 'match_dao.g.dart'; diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index c8db800..8ac21f6 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/player_table.dart'; +import 'package:tallee/data/dto/player.dart'; part 'player_dao.g.dart'; diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index db45735..23da0c1 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -1,8 +1,8 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/player_group_table.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/player_group_table.dart'; +import 'package:tallee/data/db/tables/player_table.dart'; +import 'package:tallee/data/dto/player.dart'; part 'player_group_dao.g.dart'; diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index 7ebaee6..6700e85 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/db/tables/player_match_table.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/player_match_table.dart'; +import 'package:tallee/data/dto/player.dart'; part 'player_match_dao.g.dart'; diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index e6c322f..4c70b21 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -1,18 +1,18 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:game_tracker/data/dao/group_dao.dart'; -import 'package:game_tracker/data/dao/group_match_dao.dart'; -import 'package:game_tracker/data/dao/match_dao.dart'; -import 'package:game_tracker/data/dao/player_dao.dart'; -import 'package:game_tracker/data/dao/player_group_dao.dart'; -import 'package:game_tracker/data/dao/player_match_dao.dart'; -import 'package:game_tracker/data/db/tables/group_match_table.dart'; -import 'package:game_tracker/data/db/tables/group_table.dart'; -import 'package:game_tracker/data/db/tables/match_table.dart'; -import 'package:game_tracker/data/db/tables/player_group_table.dart'; -import 'package:game_tracker/data/db/tables/player_match_table.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:tallee/data/dao/group_dao.dart'; +import 'package:tallee/data/dao/group_match_dao.dart'; +import 'package:tallee/data/dao/match_dao.dart'; +import 'package:tallee/data/dao/player_dao.dart'; +import 'package:tallee/data/dao/player_group_dao.dart'; +import 'package:tallee/data/dao/player_match_dao.dart'; +import 'package:tallee/data/db/tables/group_match_table.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; +import 'package:tallee/data/db/tables/player_group_table.dart'; +import 'package:tallee/data/db/tables/player_match_table.dart'; +import 'package:tallee/data/db/tables/player_table.dart'; part 'database.g.dart'; diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 6bc493c..4fa56f9 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -527,6 +527,17 @@ class $MatchTableTable extends MatchTable final GeneratedDatabase attachedDatabase; final String? _alias; $MatchTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _winnerIdMeta = const VerificationMeta( + 'winnerId', + ); + @override + late final GeneratedColumn winnerId = GeneratedColumn( + 'winner_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -545,17 +556,6 @@ class $MatchTableTable extends MatchTable type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _winnerIdMeta = const VerificationMeta( - 'winnerId', - ); - @override - late final GeneratedColumn winnerId = GeneratedColumn( - 'winner_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', ); @@ -568,7 +568,7 @@ class $MatchTableTable extends MatchTable requiredDuringInsert: true, ); @override - List get $columns => [id, name, winnerId, createdAt]; + List get $columns => [winnerId, id, name, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -581,6 +581,12 @@ class $MatchTableTable extends MatchTable }) { final context = VerificationContext(); final data = instance.toColumns(true); + if (data.containsKey('winner_id')) { + context.handle( + _winnerIdMeta, + winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), + ); + } if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } else if (isInserting) { @@ -594,12 +600,6 @@ class $MatchTableTable extends MatchTable } else if (isInserting) { context.missing(_nameMeta); } - if (data.containsKey('winner_id')) { - context.handle( - _winnerIdMeta, - winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), - ); - } if (data.containsKey('created_at')) { context.handle( _createdAtMeta, @@ -617,6 +617,10 @@ class $MatchTableTable extends MatchTable MatchTableData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MatchTableData( + winnerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}winner_id'], + ), id: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}id'], @@ -625,10 +629,6 @@ class $MatchTableTable extends MatchTable DriftSqlType.string, data['${effectivePrefix}name'], )!, - winnerId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}winner_id'], - ), createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -643,35 +643,35 @@ class $MatchTableTable extends MatchTable } class MatchTableData extends DataClass implements Insertable { + final String? winnerId; final String id; final String name; - final String? winnerId; final DateTime createdAt; const MatchTableData({ + this.winnerId, required this.id, required this.name, - this.winnerId, required this.createdAt, }); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); if (!nullToAbsent || winnerId != null) { map['winner_id'] = Variable(winnerId); } + map['id'] = Variable(id); + map['name'] = Variable(name); map['created_at'] = Variable(createdAt); return map; } MatchTableCompanion toCompanion(bool nullToAbsent) { return MatchTableCompanion( - id: Value(id), - name: Value(name), winnerId: winnerId == null && nullToAbsent ? const Value.absent() : Value(winnerId), + id: Value(id), + name: Value(name), createdAt: Value(createdAt), ); } @@ -682,9 +682,9 @@ class MatchTableData extends DataClass implements Insertable { }) { serializer ??= driftRuntimeOptions.defaultSerializer; return MatchTableData( + winnerId: serializer.fromJson(json['winnerId']), id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), - winnerId: serializer.fromJson(json['winnerId']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -692,29 +692,29 @@ class MatchTableData extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'winnerId': serializer.toJson(winnerId), 'id': serializer.toJson(id), 'name': serializer.toJson(name), - 'winnerId': serializer.toJson(winnerId), 'createdAt': serializer.toJson(createdAt), }; } MatchTableData copyWith({ + Value winnerId = const Value.absent(), String? id, String? name, - Value winnerId = const Value.absent(), DateTime? createdAt, }) => MatchTableData( + winnerId: winnerId.present ? winnerId.value : this.winnerId, id: id ?? this.id, name: name ?? this.name, - winnerId: winnerId.present ? winnerId.value : this.winnerId, createdAt: createdAt ?? this.createdAt, ); MatchTableData copyWithCompanion(MatchTableCompanion data) { return MatchTableData( + winnerId: data.winnerId.present ? data.winnerId.value : this.winnerId, id: data.id.present ? data.id.value : this.id, name: data.name.present ? data.name.value : this.name, - winnerId: data.winnerId.present ? data.winnerId.value : this.winnerId, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -722,75 +722,75 @@ class MatchTableData extends DataClass implements Insertable { @override String toString() { return (StringBuffer('MatchTableData(') + ..write('winnerId: $winnerId, ') ..write('id: $id, ') ..write('name: $name, ') - ..write('winnerId: $winnerId, ') ..write('createdAt: $createdAt') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, winnerId, createdAt); + int get hashCode => Object.hash(winnerId, id, name, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is MatchTableData && + other.winnerId == this.winnerId && other.id == this.id && other.name == this.name && - other.winnerId == this.winnerId && other.createdAt == this.createdAt); } class MatchTableCompanion extends UpdateCompanion { + final Value winnerId; final Value id; final Value name; - final Value winnerId; final Value createdAt; final Value rowid; const MatchTableCompanion({ + this.winnerId = const Value.absent(), this.id = const Value.absent(), this.name = const Value.absent(), - this.winnerId = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }); MatchTableCompanion.insert({ + this.winnerId = const Value.absent(), required String id, required String name, - this.winnerId = const Value.absent(), required DateTime createdAt, this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), createdAt = Value(createdAt); static Insertable custom({ + Expression? winnerId, Expression? id, Expression? name, - Expression? winnerId, Expression? createdAt, Expression? rowid, }) { return RawValuesInsertable({ + if (winnerId != null) 'winner_id': winnerId, if (id != null) 'id': id, if (name != null) 'name': name, - if (winnerId != null) 'winner_id': winnerId, if (createdAt != null) 'created_at': createdAt, if (rowid != null) 'rowid': rowid, }); } MatchTableCompanion copyWith({ + Value? winnerId, Value? id, Value? name, - Value? winnerId, Value? createdAt, Value? rowid, }) { return MatchTableCompanion( + winnerId: winnerId ?? this.winnerId, id: id ?? this.id, name: name ?? this.name, - winnerId: winnerId ?? this.winnerId, createdAt: createdAt ?? this.createdAt, rowid: rowid ?? this.rowid, ); @@ -799,15 +799,15 @@ class MatchTableCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; + if (winnerId.present) { + map['winner_id'] = Variable(winnerId.value); + } if (id.present) { map['id'] = Variable(id.value); } if (name.present) { map['name'] = Variable(name.value); } - if (winnerId.present) { - map['winner_id'] = Variable(winnerId.value); - } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -820,9 +820,9 @@ class MatchTableCompanion extends UpdateCompanion { @override String toString() { return (StringBuffer('MatchTableCompanion(') + ..write('winnerId: $winnerId, ') ..write('id: $id, ') ..write('name: $name, ') - ..write('winnerId: $winnerId, ') ..write('createdAt: $createdAt, ') ..write('rowid: $rowid') ..write(')')) @@ -2339,17 +2339,17 @@ typedef $$GroupTableTableProcessedTableManager = >; typedef $$MatchTableTableCreateCompanionBuilder = MatchTableCompanion Function({ + Value winnerId, required String id, required String name, - Value winnerId, required DateTime createdAt, Value rowid, }); typedef $$MatchTableTableUpdateCompanionBuilder = MatchTableCompanion Function({ + Value winnerId, Value id, Value name, - Value winnerId, Value createdAt, Value rowid, }); @@ -2414,6 +2414,11 @@ class $$MatchTableTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnFilters get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column), @@ -2424,11 +2429,6 @@ class $$MatchTableTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get winnerId => $composableBuilder( - column: $table.winnerId, - builder: (column) => ColumnFilters(column), - ); - ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column), @@ -2494,6 +2494,11 @@ class $$MatchTableTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnOrderings get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column), @@ -2504,11 +2509,6 @@ class $$MatchTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get winnerId => $composableBuilder( - column: $table.winnerId, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column), @@ -2524,15 +2524,15 @@ class $$MatchTableTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + GeneratedColumn get winnerId => + $composableBuilder(column: $table.winnerId, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - GeneratedColumn get winnerId => - $composableBuilder(column: $table.winnerId, builder: (column) => column); - GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -2618,29 +2618,29 @@ class $$MatchTableTableTableManager $$MatchTableTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ + Value winnerId = const Value.absent(), Value id = const Value.absent(), Value name = const Value.absent(), - Value winnerId = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => MatchTableCompanion( + winnerId: winnerId, id: id, name: name, - winnerId: winnerId, createdAt: createdAt, rowid: rowid, ), createCompanionCallback: ({ + Value winnerId = const Value.absent(), required String id, required String name, - Value winnerId = const Value.absent(), required DateTime createdAt, Value rowid = const Value.absent(), }) => MatchTableCompanion.insert( + winnerId: winnerId, id: id, name: name, - winnerId: winnerId, createdAt: createdAt, rowid: rowid, ), diff --git a/lib/data/db/tables/group_match_table.dart b/lib/data/db/tables/group_match_table.dart index 3f77dcb..a12e83b 100644 --- a/lib/data/db/tables/group_match_table.dart +++ b/lib/data/db/tables/group_match_table.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/tables/group_table.dart'; -import 'package:game_tracker/data/db/tables/match_table.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; class GroupMatchTable extends Table { TextColumn get groupId => diff --git a/lib/data/db/tables/player_group_table.dart b/lib/data/db/tables/player_group_table.dart index da2521b..8d484ef 100644 --- a/lib/data/db/tables/player_group_table.dart +++ b/lib/data/db/tables/player_group_table.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/tables/group_table.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/player_table.dart'; class PlayerGroupTable extends Table { TextColumn get playerId => diff --git a/lib/data/db/tables/player_match_table.dart b/lib/data/db/tables/player_match_table.dart index e155cd5..13ef36f 100644 --- a/lib/data/db/tables/player_match_table.dart +++ b/lib/data/db/tables/player_match_table.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:game_tracker/data/db/tables/match_table.dart'; -import 'package:game_tracker/data/db/tables/player_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; +import 'package:tallee/data/db/tables/player_table.dart'; class PlayerMatchTable extends Table { TextColumn get playerId => diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart index 92dbd09..f02d98f 100644 --- a/lib/data/dto/group.dart +++ b/lib/data/dto/group.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/dto/player.dart'; import 'package:uuid/uuid.dart'; class Group { diff --git a/lib/data/dto/match.dart b/lib/data/dto/match.dart index 9570f66..d3a8333 100644 --- a/lib/data/dto/match.dart +++ b/lib/data/dto/match.dart @@ -1,6 +1,6 @@ import 'package:clock/clock.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; import 'package:uuid/uuid.dart'; class Match { diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 6aee6ee..74d541d 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -3,7 +3,8 @@ "all_players": "Alle Spieler:innen", "all_players_selected": "Alle Spieler:innen ausgewählt", "amount_of_matches": "Anzahl der Spiele", - "app_name": "Game Tracker", + "app_name": "Tallee", + "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", @@ -13,6 +14,7 @@ "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", + "created_on": "Erstellt am", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_exported": "Daten erfolgreich exportiert", @@ -20,6 +22,8 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen", + "delete_group": "Gruppe löschen", + "edit_group": "Gruppe bearbeiten", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", "export_canceled": "Export abgebrochen", @@ -29,6 +33,7 @@ "game_name": "Spielvorlagenname", "group": "Gruppe", "group_name": "Gruppenname", + "group_profile": "Gruppenprofil", "groups": "Gruppen", "home": "Startseite", "import_canceled": "Import abgebrochen", @@ -42,6 +47,7 @@ "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "matches": "Spiele", + "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", "no_groups_created_yet": "Noch keine Gruppen erstellt", @@ -57,6 +63,7 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", + "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", "players_count": "{count} Spieler", @@ -79,7 +86,7 @@ "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", - "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden", + "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "today_at": "Heute um", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c311050..667c5c5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -12,6 +12,9 @@ "@app_name": { "description": "The name of the App" }, + "@best_player": { + "description": "Label for best player statistic" + }, "@cancel": { "description": "Cancel button text" }, @@ -39,6 +42,9 @@ "@create_new_match": { "description": "Button text to create a new match" }, + "@created_on": { + "description": "Label for creation date" + }, "@data": { "description": "Data label" }, @@ -65,6 +71,12 @@ "@delete_all_data": { "description": "Confirmation dialog for deleting all data" }, + "@delete_group": { + "description": "Button text to delete a group" + }, + "@edit_group": { + "description": "Button text to edit a group" + }, "@error_creating_group": { "description": "Error message when group creation fails" }, @@ -92,6 +104,9 @@ "@group_name": { "description": "Placeholder for group name input" }, + "@group_profile": { + "description": "Title for group profile view" + }, "@groups": { "description": "Label for groups" }, @@ -131,6 +146,9 @@ "@matches": { "description": "Label for matches" }, + "@members": { + "description": "Label for group members" + }, "@most_points": { "description": "Title for most points ruleset" }, @@ -176,6 +194,9 @@ "@not_available": { "description": "Abbreviation for not available" }, + "@played_matches": { + "description": "Label for played matches statistic" + }, "@player_name": { "description": "Placeholder for player name input" }, @@ -280,7 +301,8 @@ "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", - "app_name": "Game Tracker", + "app_name": "Tallee", + "best_player": "Best Player", "cancel": "Cancel", "choose_game": "Choose Game", "choose_group": "Choose Group", @@ -289,6 +311,7 @@ "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", + "created_on": "Created on", "create_new_match": "Create new match", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -297,6 +320,8 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", + "delete_group": "Delete Group", + "edit_group": "Edit Group", "error_creating_group": "Error while creating group, please try again", "error_reading_file": "Error reading file", "export_canceled": "Export canceled", @@ -306,6 +331,7 @@ "game_name": "Game Name", "group": "Group", "group_name": "Group name", + "group_profile": "Group Profile", "groups": "Groups", "home": "Home", "import_canceled": "Import canceled", @@ -319,6 +345,7 @@ "match_in_progress": "Match in progress...", "match_name": "Match name", "matches": "Matches", + "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", "no_groups_created_yet": "No groups created yet", @@ -334,6 +361,7 @@ "none": "None", "none_group": "None", "not_available": "Not available", + "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", "players_count": "{count} Players", @@ -356,7 +384,7 @@ "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", "there_is_no_group_matching_your_search": "There is no group matching your search", - "this_cannot_be_undone": "This can't be undone", + "this_cannot_be_undone": "This can't be undone.", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 399dc85..b2a60c0 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -119,9 +119,15 @@ abstract class AppLocalizations { /// The name of the App /// /// In en, this message translates to: - /// **'Game Tracker'** + /// **'Tallee'** String get app_name; + /// Label for best player statistic + /// + /// In en, this message translates to: + /// **'Best Player'** + String get best_player; + /// Cancel button text /// /// In en, this message translates to: @@ -170,6 +176,12 @@ abstract class AppLocalizations { /// **'Create new group'** String get create_new_group; + /// Label for creation date + /// + /// In en, this message translates to: + /// **'Created on'** + String get created_on; + /// Button text to create a new match /// /// In en, this message translates to: @@ -218,6 +230,18 @@ abstract class AppLocalizations { /// **'Delete all data'** String get delete_all_data; + /// Button text to delete a group + /// + /// In en, this message translates to: + /// **'Delete Group'** + String get delete_group; + + /// Button text to edit a group + /// + /// In en, this message translates to: + /// **'Edit Group'** + String get edit_group; + /// Error message when group creation fails /// /// In en, this message translates to: @@ -272,6 +296,12 @@ abstract class AppLocalizations { /// **'Group name'** String get group_name; + /// Title for group profile view + /// + /// In en, this message translates to: + /// **'Group Profile'** + String get group_profile; + /// Label for groups /// /// In en, this message translates to: @@ -350,6 +380,12 @@ abstract class AppLocalizations { /// **'Matches'** String get matches; + /// Label for group members + /// + /// In en, this message translates to: + /// **'Members'** + String get members; + /// Title for most points ruleset /// /// In en, this message translates to: @@ -440,6 +476,12 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; + /// Label for played matches statistic + /// + /// In en, this message translates to: + /// **'Played Matches'** + String get played_matches; + /// Placeholder for player name input /// /// In en, this message translates to: @@ -575,7 +617,7 @@ abstract class AppLocalizations { /// Warning message for irreversible actions /// /// In en, this message translates to: - /// **'This can\'t be undone'** + /// **'This can\'t be undone.'** String get this_cannot_be_undone; /// Date format for today diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index f4d0f8c..2f76fd2 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -18,7 +18,10 @@ class AppLocalizationsDe extends AppLocalizations { String get amount_of_matches => 'Anzahl der Spiele'; @override - String get app_name => 'Game Tracker'; + String get app_name => 'Tallee'; + + @override + String get best_player => 'Beste:r Spieler:in'; @override String get cancel => 'Abbrechen'; @@ -46,6 +49,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_group => 'Neue Gruppe erstellen'; + @override + String get created_on => 'Erstellt am'; + @override String get create_new_match => 'Neues Spiel erstellen'; @@ -72,6 +78,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_all_data => 'Alle Daten löschen'; + @override + String get delete_group => 'Gruppe löschen'; + + @override + String get edit_group => 'Gruppe bearbeiten'; + @override String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; @@ -100,6 +112,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get group_name => 'Gruppenname'; + @override + String get group_profile => 'Gruppenprofil'; + @override String get groups => 'Gruppen'; @@ -139,6 +154,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get matches => 'Spiele'; + @override + String get members => 'Mitglieder'; + @override String get most_points => 'Höchste Punkte'; @@ -185,6 +203,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get played_matches => 'Gespielte Spiele'; + @override String get player_name => 'Spieler:innenname'; @@ -262,7 +283,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get this_cannot_be_undone => - 'Dies kann nicht rückgängig gemacht werden'; + 'Dies kann nicht rückgängig gemacht werden.'; @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 6c4ac74..cfcae20 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -18,7 +18,10 @@ class AppLocalizationsEn extends AppLocalizations { String get amount_of_matches => 'Amount of Matches'; @override - String get app_name => 'Game Tracker'; + String get app_name => 'Tallee'; + + @override + String get best_player => 'Best Player'; @override String get cancel => 'Cancel'; @@ -46,6 +49,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_group => 'Create new group'; + @override + String get created_on => 'Created on'; + @override String get create_new_match => 'Create new match'; @@ -72,6 +78,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data'; + @override + String get delete_group => 'Delete Group'; + + @override + String get edit_group => 'Edit Group'; + @override String get error_creating_group => 'Error while creating group, please try again'; @@ -100,6 +112,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get group_name => 'Group name'; + @override + String get group_profile => 'Group Profile'; + @override String get groups => 'Groups'; @@ -139,6 +154,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get matches => 'Matches'; + @override + String get members => 'Members'; + @override String get most_points => 'Most Points'; @@ -185,6 +203,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get played_matches => 'Played Matches'; + @override String get player_name => 'Player name'; @@ -261,7 +282,7 @@ class AppLocalizationsEn extends AppLocalizations { 'There is no group matching your search'; @override - String get this_cannot_be_undone => 'This can\'t be undone'; + String get this_cannot_be_undone => 'This can\'t be undone.'; @override String get today_at => 'Today at'; diff --git a/lib/main.dart b/lib/main.dart index 2f64e2e..0002531 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/custom_navigation_bar.dart'; void main() { runApp( diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index f6eb381..9e81a34 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -1,15 +1,19 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/settings_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart'; -import 'package:game_tracker/presentation/widgets/navbar_item.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/groups_view.dart'; +import 'package:tallee/presentation/views/main_menu/home_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; +import 'package:tallee/presentation/widgets/navbar_item.dart'; class CustomNavigationBar extends StatefulWidget { + /// A custom navigation bar widget that provides tabbed navigation + /// between different views: Home, Matches, Groups, and Statistics. const CustomNavigationBar({super.key}); @override @@ -71,53 +75,102 @@ class _CustomNavigationBarState extends State backgroundColor: CustomTheme.backgroundColor, body: tabs[currentIndex], extendBody: true, - bottomNavigationBar: SafeArea( - minimum: const EdgeInsets.only(bottom: 30), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: CustomTheme.primaryColor, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: loc.home, - onTabTapped: onTabTapped, + bottomNavigationBar: SizedBox( + height: 70 + MediaQuery.of(context).padding.bottom, + child: Stack( + children: [ + // Dynamically generated blur layers for ultra-smooth transition + ...List.generate(34, (index) { + // Use cubic curve for an even more natural, smoother transition + final progress = index / 34.0; // 0.0 to 1.0 + final cubic = progress * progress * progress; // cubic curve + final blurStrength = + 0.5 + (cubic * 50.0); // Very smooth from 0.5 to 50.5 + + // Height goes completely from 100% to 0% (all the way down) + // With extra density at the bottom for softer transition + final heightFactor = index < 25 + // First 25 layers: 100% to 30% + ? 1.0 - (progress * 0.7) + // Last 10 layers: 30% to 0% (denser) + : 0.3 - ((index - 25) / 34.0); + + return Positioned( + left: 0, + right: 0, + bottom: 0, + height: + (70 + MediaQuery.of(context).padding.bottom) * + heightFactor.clamp(0.05, 1.0), + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: blurStrength, + sigmaY: blurStrength, + ), + child: Container(color: Colors.transparent), ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: loc.matches, - onTabTapped: onTabTapped, + ), + ); + }), + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + CustomTheme.boxColor.withValues(alpha: 1), + CustomTheme.boxColor.withValues(alpha: 0.5), + CustomTheme.boxColor.withValues(alpha: 0.2), + CustomTheme.boxColor.withValues(alpha: 0.0), + ], + stops: const [0.0, 0.4, 0.8, 1], ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: loc.groups, - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: loc.statistics, - onTabTapped: onTabTapped, - ), - ], + ), ), ), - ), + // Navbar content + SafeArea( + child: SizedBox( + height: 70, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: loc.home, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: loc.matches, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: loc.groups, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: loc.statistics, + onTabTapped: onTabTapped, + ), + ], + ), + ), + ), + ], ), ), ); diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 8192c6b..2b7ab86 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/player_selection.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; class CreateGroupView extends StatefulWidget { + /// A view that allows the user to create a new group const CreateGroupView({super.key}); @override @@ -46,6 +48,7 @@ class _CreateGroupViewState extends State { final loc = AppLocalizations.of(context); return ScaffoldMessenger( child: Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_group)), body: SafeArea( @@ -57,6 +60,7 @@ class _CreateGroupViewState extends State { child: TextInputField( controller: _groupNameController, hintText: loc.group_name, + maxLength: Constants.MAX_GROUP_NAME_LENGTH, ), ), Expanded( diff --git a/lib/presentation/views/main_menu/group_view/group_profile_view.dart b/lib/presentation/views/main_menu/group_view/group_profile_view.dart new file mode 100644 index 0000000..d4b71ab --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/group_profile_view.dart @@ -0,0 +1,273 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; +import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; + +class GroupProfileView extends StatefulWidget { + /// A view that displays the profile of a group + /// - [group]: The group to display + const GroupProfileView({ + super.key, + required this.group, + required this.callback, + }); + + /// The group to display + final Group group; + + final VoidCallback callback; + + @override + State createState() => _GroupProfileViewState(); +} + +class _GroupProfileViewState extends State { + late final AppDatabase db; + bool isLoading = true; + + /// Total matches played in this group + int totalMatches = 0; + + String bestPlayer = ''; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _loadStatistics(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(loc.group_profile), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: '${loc.delete_group}?', + content: loc.this_cannot_be_undone, + actions: [ + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + loc.cancel, + style: const TextStyle(color: CustomTheme.textColor), + ), + ), + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + loc.delete, + style: const TextStyle( + color: CustomTheme.secondaryColor, + ), + ), + ), + ], + ), + ).then((confirmed) async { + if (confirmed! && context.mounted) { + await db.groupDao.deleteGroup(groupId: widget.group.id); + if (!context.mounted) return; + Navigator.pop(context); + widget.callback.call(); + } + }); + }, + ), + ], + ), + body: SafeArea( + child: Stack( + alignment: Alignment.center, + children: [ + ListView( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 20, + bottom: 100, + ), + children: [ + const Center( + child: ColoredIconContainer( + icon: Icons.group, + containerSize: 55, + iconSize: 38, + ), + ), + const SizedBox(height: 10), + Text( + widget.group.name, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Text( + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + InfoTile( + title: loc.members, + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: widget.group.members.map((member) { + return TextIconTile( + text: member.name, + iconEnabled: false, + ); + }).toList(), + ), + ), + const SizedBox(height: 15), + InfoTile( + title: loc.statistics, + icon: Icons.bar_chart, + content: AppSkeleton( + enabled: isLoading, + child: Column( + children: [ + _buildStatRow( + loc.members, + widget.group.members.length.toString(), + ), + _buildStatRow( + loc.played_matches, + totalMatches.toString(), + ), + _buildStatRow(loc.best_player, bestPlayer), + ], + ), + ), + ), + ], + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: MainMenuButton( + text: loc.edit_group, + icon: Icons.edit, + onPressed: () { + // TODO: Uncomment when GroupDetailView is implemented + /* + await Navigator.push( + context, + adaptivePageRoute( + builder: (context) { + + return const GroupDetailView(); + }, + ), + );*/ + print('Edit Group pressed'); + }, + ), + ), + ], + ), + ), + ); + } + + /// Builds a single statistic row with a label and value + /// - [label]: The label of the statistic + /// - [value]: The value of the statistic + Widget _buildStatRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + ], + ), + Text( + value, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } + + /// Loads statistics for this group + Future _loadStatistics() async { + final matches = await db.matchDao.getAllMatches(); + final groupMatches = matches + .where((match) => match.group?.id == widget.group.id) + .toList(); + + setState(() { + totalMatches = groupMatches.length; + bestPlayer = _getBestPlayer(groupMatches); + isLoading = false; + }); + } + + /// Determines the best player in the group based on match wins + String _getBestPlayer(List matches) { + final bestPlayerCounts = {}; + + // Count wins for each player + for (var match in matches) { + if (match.winner != null) { + bestPlayerCounts.update( + match.winner!, + (value) => value + 1, + ifAbsent: () => 1, + ); + } + } + + // Sort players by win count + final sortedPlayers = bestPlayerCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + // Get the best player + bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-'; + + return bestPlayer; + } +} diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 239aa23..6035fc8 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/group_profile_view.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class GroupsView extends StatefulWidget { + /// A view that displays a list of groups const GroupsView({super.key}); @override @@ -73,16 +75,31 @@ class _GroupsViewState extends State { height: MediaQuery.paddingOf(context).bottom - 20, ); } - return GroupTile(group: groups[index]); + return GroupTile( + group: groups[index], + onTap: () async { + await Navigator.push( + context, + adaptivePageRoute( + builder: (context) { + return GroupProfileView( + group: groups[index], + callback: loadGroups, + ); + }, + ), + ); + }, + ); }, ), ), ), Positioned( - bottom: MediaQuery.paddingOf(context).bottom, - child: CustomWidthButton( + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( text: loc.create_group, - sizeRelativeToWidth: 0.90, + icon: Icons.group_add, onPressed: () async { await Navigator.push( context, @@ -104,9 +121,12 @@ class _GroupsViewState extends State { } void loadGroups() { + setState(() { + isLoading = true; + }); Future.wait([ db.groupDao.getAllGroups(), - Future.delayed(Constants.minimumSkeletonDuration), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) { loadedGroups = results[0] as List; setState(() { diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index affbe92..63617b2 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -1,20 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; -import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; -import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; -import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/quick_create_button.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; class HomeView extends StatefulWidget { + /// The main home view of the application, displaying quick info, + /// recent matches, and quick create options. const HomeView({super.key}); @override @@ -193,7 +195,7 @@ class _HomeViewState extends State { db.matchDao.getMatchCount(), db.groupDao.getGroupCount(), db.matchDao.getAllMatches(), - Future.delayed(Constants.minimumSkeletonDuration), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) { matchCount = results[0] as int; groupCount = results[1] as int; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 5976f72..447b9c5 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,20 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:tallee/presentation/widgets/tiles/title_description_list_tile.dart'; class ChooseGameView extends StatefulWidget { - final List<(String, String, Ruleset)> games; - final int initialGameIndex; - + /// A view that allows the user to choose a game from a list of available games + /// - [games]: A list of tuples containing the game name, description and ruleset + /// - [initialGameIndex]: The index of the initially selected game const ChooseGameView({ super.key, required this.games, required this.initialGameIndex, }); + /// A list of tuples containing the game name, description and ruleset + final List<(String, String, Ruleset)> games; + + /// The index of the initially selected game + final int initialGameIndex; + @override State createState() => _ChooseGameViewState(); } @@ -37,6 +43,7 @@ class _ChooseGameViewState extends State { final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, + resizeToAvoidBottomInset: false, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios), diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index 9e34460..9c60b16 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -1,21 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class ChooseGroupView extends StatefulWidget { - final List groups; - final String initialGroupId; - + /// A view that allows the user to choose a group from a list of groups. + /// - [groups]: A list of available groups to choose from + /// - [initialGroupId]: The ID of the initially selected group const ChooseGroupView({ super.key, required this.groups, required this.initialGroupId, }); + /// A list of available groups to choose from + final List groups; + + /// The ID of the initially selected group + final String initialGroupId; + @override State createState() => _ChooseGroupViewState(); } @@ -37,6 +43,7 @@ class _ChooseGroupViewState extends State { final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, + resizeToAvoidBottomInset: false, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios), @@ -140,10 +147,11 @@ class _ChooseGroupViewState extends State { filteredGroups.clear(); filteredGroups.addAll( widget.groups.where( - (group) => - group.name.toLowerCase().contains(query.toLowerCase()) || + (group) => + group.name.toLowerCase().contains(query.toLowerCase()) || group.members.any( - (player) => player.name.toLowerCase().contains(query.toLowerCase()), + (player) => + player.name.toLowerCase().contains(query.toLowerCase()), ), ), ); diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart deleted file mode 100644 index ca021af..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; - -class ChooseRulesetView extends StatefulWidget { - final List<(Ruleset, String)> rulesets; - final int initialRulesetIndex; - - const ChooseRulesetView({ - super.key, - required this.rulesets, - required this.initialRulesetIndex, - }); - - @override - State createState() => _ChooseRulesetViewState(); -} - -class _ChooseRulesetViewState extends State { - /// Currently selected ruleset index - late int selectedRulesetIndex; - - @override - void initState() { - selectedRulesetIndex = widget.initialRulesetIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return DefaultTabController( - length: 2, - initialIndex: 0, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - ), - title: Text(loc.choose_ruleset), - ), - body: PopScope( - // This fixes that the Android Back Gesture didn't return the - // selectedRulesetIndex and therefore the selected Ruleset wasn't saved - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) { - return; - } - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.rulesets.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - onPressed: () async { - setState(() { - if (selectedRulesetIndex == index) { - selectedRulesetIndex = -1; - } else { - selectedRulesetIndex = index; - } - }); - }, - title: translateRulesetToString( - widget.rulesets[index].$1, - context, - ), - description: widget.rulesets[index].$2, - isHighlighted: selectedRulesetIndex == index, - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index b9885a4..8182ddb 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -1,26 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; -import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/player_selection.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; +import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; class CreateMatchView extends StatefulWidget { - final VoidCallback? onWinnerChanged; + /// A view that allows creating a new match + /// [onWinnerChanged]: Optional callback invoked when the winner is changed const CreateMatchView({super.key, this.onWinnerChanged}); + /// Optional callback invoked when the winner is changed + final VoidCallback? onWinnerChanged; + @override State createState() => _CreateMatchViewState(); } @@ -53,13 +57,6 @@ class _CreateMatchViewState extends State { /// the [ChooseGroupView] String selectedGroupId = ''; - /// The currently selected ruleset - Ruleset? selectedRuleset; - - /// The index of the currently selected ruleset in [rulesets] to mark it in - /// the [ChooseRulesetView] - int selectedRulesetIndex = -1; - /// The index of the currently selected game in [games] to mark it in /// the [ChooseGameView] int selectedGameIndex = -1; @@ -67,9 +64,6 @@ class _CreateMatchViewState extends State { /// The currently selected players List? selectedPlayers; - /// List of available rulesets with their localized string representations - late final List<(Ruleset, String)> _rulesets; - @override void initState() { super.initState(); @@ -102,15 +96,8 @@ class _CreateMatchViewState extends State { super.didChangeDependencies(); final loc = AppLocalizations.of(context); hintText ??= loc.match_name; - _rulesets = [ - (Ruleset.singleWinner, loc.ruleset_single_winner), - (Ruleset.singleLoser, loc.ruleset_single_loser), - (Ruleset.mostPoints, loc.ruleset_most_points), - (Ruleset.leastPoints, loc.ruleset_least_points), - ]; } - // TODO: Replace when games are implemented List<(String, String, Ruleset)> games = [ ('Example Game 1', 'This is a description', Ruleset.leastPoints), ('Example Game 2', '', Ruleset.singleWinner), @@ -121,6 +108,7 @@ class _CreateMatchViewState extends State { final loc = AppLocalizations.of(context); return ScaffoldMessenger( child: Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_match)), body: SafeArea( @@ -132,6 +120,7 @@ class _CreateMatchViewState extends State { child: TextInputField( controller: _matchNameController, hintText: hintText ?? '', + maxLength: Constants.MAX_MATCH_NAME_LENGTH, ), ), ChooseTile( @@ -151,39 +140,12 @@ class _CreateMatchViewState extends State { setState(() { if (selectedGameIndex != -1) { hintText = games[selectedGameIndex].$1; - selectedRuleset = games[selectedGameIndex].$3; - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); } else { hintText = loc.match_name; - selectedRuleset = null; } }); }, ), - ChooseTile( - title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), - onPressed: () async { - selectedRuleset = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, - ), - ), - ); - if (!mounted) return; - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); - selectedGameIndex = -1; - setState(() {}); - }, - ), ChooseTile( title: loc.group, trailingText: selectedGroup == null @@ -202,7 +164,8 @@ class _CreateMatchViewState extends State { if (selectedGroup != null) { filteredPlayerList = playerList .where( - (p) => !selectedGroup!.members.any((m) => m.id == p.id), + (p) => + !selectedGroup!.members.any((m) => m.id == p.id), ) .toList(); } else { @@ -267,7 +230,6 @@ class _CreateMatchViewState extends State { /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { return (selectedGroup != null || - (selectedPlayers != null && selectedPlayers!.length > 1)) && - selectedRuleset != null; + (selectedPlayers != null && selectedPlayers!.length > 1)); } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 0d624f0..75015f0 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -1,18 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; class MatchResultView extends StatefulWidget { + /// A view that allows selecting and saving the winner of a match + /// [match]: The match for which the winner is to be selected + /// [onWinnerChanged]: Optional callback invoked when the winner is changed + const MatchResultView({super.key, required this.match, this.onWinnerChanged}); + + /// The match for which the winner is to be selected final Match match; + /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; - const MatchResultView({super.key, required this.match, this.onWinnerChanged}); @override State createState() => _MatchResultViewState(); } diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index ecfa9ca..c34a22f 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -1,23 +1,25 @@ import 'dart:core' hide Match; import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class MatchView extends StatefulWidget { + /// A view that displays a list of matches const MatchView({super.key}); @override @@ -104,10 +106,10 @@ class _MatchViewState extends State { ), ), Positioned( - bottom: MediaQuery.paddingOf(context).bottom, - child: CustomWidthButton( - text: loc.create_match, - sizeRelativeToWidth: 0.90, + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( + text: 'Spiel erstellen', + icon: RpgAwesome.clovers_card, onPressed: () async { Navigator.push( context, @@ -128,7 +130,7 @@ class _MatchViewState extends State { void loadGames() { Future.wait([ db.matchDao.getAllMatches(), - Future.delayed(Constants.minimumSkeletonDuration), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) { if (mounted) { setState(() { diff --git a/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart b/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart index 02c3adf..6e58108 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:url_launcher/url_launcher.dart'; class LicenseDetailView extends StatelessWidget { - final Package package; - + /// A detailed view displaying information about a software package license. + /// - [package]: The package data to be displayed. const LicenseDetailView({super.key, required this.package}); + /// The package data to be displayed. + final Package package; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -26,19 +30,11 @@ class LicenseDetailView extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - margin: const EdgeInsetsGeometry.only(right: 15), - width: 60, - height: 60, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.description, - color: CustomTheme.primaryColor, - size: 30, - ), + const ColoredIconContainer( + icon: Icons.description, + margin: EdgeInsetsGeometry.only(right: 15), + containerSize: 60, + iconSize: 30, ), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -93,7 +89,7 @@ class LicenseDetailView extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( fontSize: 12, color: CustomTheme.secondaryColor, decoration: TextDecoration.underline, diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index ef1109c..363ca24 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -8,7 +8,7 @@ // https://pub.dev/packages/dart_pubspec_licenses /// This package. -const thisPackage = _game_tracker; +const thisPackage = _tallee; /// All dependencies including transitive dependencies. const allDependencies = [ @@ -7291,9 +7291,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// game_tracker 0.0.5+127 -const _game_tracker = Package( - name: 'game_tracker', +/// tallee 0.0.5+127 +const _tallee = Package( + name: 'tallee', description: 'Game Tracking App for Card Games', authors: [], version: '0.0.5+127', diff --git a/lib/presentation/views/main_menu/settings_view/licenses_view.dart b/lib/presentation/views/main_menu/settings_view/licenses_view.dart index 603162f..ed98c26 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses_view.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses_view.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; -import 'package:game_tracker/presentation/widgets/tiles/license_tile.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:tallee/presentation/widgets/tiles/license_tile.dart'; class LicensesView extends StatelessWidget { + /// A view that displays a list of open source licenses used in the app const LicensesView({super.key}); @override diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index 1843c90..d063e7c 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -3,17 +3,21 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses_view.dart'; -import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; -import 'package:game_tracker/services/data_transfer_service.dart'; import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:tallee/services/data_transfer_service.dart'; import 'package:url_launcher/url_launcher.dart'; class SettingsView extends StatefulWidget { + /// The settings view of the application, allowing users to manage data + /// and view legal information. const SettingsView({super.key}); @override @@ -37,194 +41,228 @@ class _SettingsViewState extends State { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return ScaffoldMessenger( - child: Scaffold( - appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), - backgroundColor: CustomTheme.backgroundColor, - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 10), - child: Text( - textAlign: TextAlign.start, - loc.settings, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10), - child: Text( - textAlign: TextAlign.start, - loc.data, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ), - SettingsListTile( - title: loc.export_data, - icon: Icons.upload, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () async { - final String json = - await DataTransferService.getAppDataAsJson(context); - final result = await DataTransferService.exportData( - json, - 'game_tracker-data', - ); - if (!context.mounted) return; - showExportSnackBar(context: context, result: result); - }, - ), - SettingsListTile( - title: loc.import_data, - icon: Icons.download, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () async { - final result = await DataTransferService.importData(context); - if (!context.mounted) return; - showImportSnackBar(context: context, result: result); - }, - ), - SettingsListTile( - title: loc.delete_all_data, - icon: Icons.delete, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('${loc.delete_all_data}?'), - content: Text(loc.this_cannot_be_undone), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(loc.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(loc.delete), - ), - ], + child: Builder( + builder: (scaffoldMessengerContext) { + return Scaffold( + appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), + backgroundColor: CustomTheme.backgroundColor, + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 10), + child: Text( + textAlign: TextAlign.start, + loc.settings, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), ), - ).then((confirmed) { - if (confirmed == true && context.mounted) { - DataTransferService.deleteAllData(context); - showSnackbar( - context: context, - message: AppLocalizations.of( - context, - ).data_successfully_deleted, + ), + Padding( + padding: const EdgeInsets.only( + left: 16, + top: 10, + bottom: 10, + ), + child: Text( + textAlign: TextAlign.start, + loc.data, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsListTile( + title: loc.export_data, + icon: Icons.upload, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final String json = + await DataTransferService.getAppDataAsJson( + scaffoldMessengerContext, + ); + final result = await DataTransferService.exportData( + json, + 'tallee-data', ); - } - }); - }, - ), - Padding( - padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10), - child: Text( - textAlign: TextAlign.start, - loc.legal, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, + if (!scaffoldMessengerContext.mounted) return; + showExportSnackBar( + context: scaffoldMessengerContext, + result: result, + ); + }, ), - ), - ), - SettingsListTile( - title: loc.licenses, - icon: Icons.insert_drive_file, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LicensesView(), - ), - ); - }, - ), - SettingsListTile( - title: loc.legal_notice, - icon: Icons.account_balance_sharp, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: null, - ), - SettingsListTile( - title: loc.privacy_policy, - icon: Icons.gpp_good_rounded, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: null, - ), - Padding( - padding: const EdgeInsets.only(top: 30, bottom: 20), - child: Center( - child: Column( - spacing: 4, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 40, - children: [ - GestureDetector( - child: const Icon(Icons.language), - onTap: () => { - launchUrl(Uri.parse('https://liquid-dev.de')), - }, - ), - GestureDetector( - child: const FaIcon(FontAwesomeIcons.github), - onTap: () => { - launchUrl( - Uri.parse( - 'https://github.com/liquiddevelopmentde', - ), + SettingsListTile( + title: loc.import_data, + icon: Icons.download, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final result = await DataTransferService.importData( + scaffoldMessengerContext, + ); + if (!scaffoldMessengerContext.mounted) return; + showImportSnackBar( + context: scaffoldMessengerContext, + result: result, + ); + }, + ), + SettingsListTile( + title: loc.delete_all_data, + icon: Icons.delete, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: '${loc.delete_all_data}?', + content: loc.this_cannot_be_undone, + actions: [ + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + loc.cancel, + style: const TextStyle( + color: CustomTheme.textColor, ), - }, - ), - GestureDetector( - child: Icon( - Platform.isIOS - ? CupertinoIcons.mail_solid - : Icons.email, ), - onTap: () => launchUrl( - Uri.parse('mailto:hi@liquid-dev.de'), + ), + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + loc.delete, + style: const TextStyle( + color: CustomTheme.secondaryColor, + ), ), ), ], ), - ), - Text( - '© ${DateFormat('yyyy').format(DateTime.now())} Liquid Development', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - Text( - 'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], + ).then((confirmed) { + if (confirmed == true && context.mounted) { + DataTransferService.deleteAllData(context); + showSnackbar( + context: scaffoldMessengerContext, + message: AppLocalizations.of( + context, + ).data_successfully_deleted, + ); + } + }); + }, ), - ), + Padding( + padding: const EdgeInsets.only( + left: 16, + top: 10, + bottom: 10, + ), + child: Text( + textAlign: TextAlign.start, + loc.legal, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsListTile( + title: loc.licenses, + icon: Icons.insert_drive_file, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const LicensesView(), + ), + ); + }, + ), + SettingsListTile( + title: loc.legal_notice, + icon: Icons.account_balance_sharp, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: null, + ), + SettingsListTile( + title: loc.privacy_policy, + icon: Icons.gpp_good_rounded, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: null, + ), + Padding( + padding: const EdgeInsets.only(top: 30, bottom: 20), + child: Center( + child: Column( + spacing: 4, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 40, + children: [ + GestureDetector( + child: const Icon(Icons.language), + onTap: () => { + launchUrl( + Uri.parse('https://liquid-dev.de'), + ), + }, + ), + GestureDetector( + child: const FaIcon(FontAwesomeIcons.github), + onTap: () => { + launchUrl( + Uri.parse( + 'https://github.com/liquiddevelopmentde', + ), + ), + }, + ), + GestureDetector( + child: Icon( + Platform.isIOS + ? CupertinoIcons.mail_solid + : Icons.email, + ), + onTap: () => launchUrl( + Uri.parse('mailto:hi@liquid-dev.de'), + ), + ), + ], + ), + ), + Text( + '© ${DateFormat('yyyy').format(DateTime.now())} Liquid Development', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + Text( + 'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], ), - ], - ), - ), + ), + ); + }, ), ); } @@ -285,10 +323,11 @@ class _SettingsViewState extends State { Duration duration = const Duration(seconds: 3), VoidCallback? action, }) { + if (!context.mounted) return; + final loc = AppLocalizations.of(context); - final messenger = ScaffoldMessenger.of(context); - messenger.hideCurrentSnackBar(); - messenger.showSnackBar( + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: CustomTheme.onBoxColor, diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 53569ad..e552e54 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class StatisticsView extends StatefulWidget { + /// A view that displays player statistics const StatisticsView({super.key}); @override @@ -105,7 +106,7 @@ class _StatisticsViewState extends State { Future.wait([ db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), - Future.delayed(Constants.minimumSkeletonDuration), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) async { if (!mounted) return; final matches = results[0] as List; diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 1d74456..98f2ca7 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:skeletonizer/skeletonizer.dart'; -/// A widget that provides a skeleton loading effect to its child widget tree. -/// - [child]: The widget tree to apply the skeleton effect to. -/// - [enabled]: A boolean to enable or disable the skeleton effect. -/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. class AppSkeleton extends StatefulWidget { + /// A widget that provides a skeleton loading effect to its child widget tree. + /// - [child]: The widget tree to apply the skeleton effect to. + /// - [enabled]: A boolean to enable or disable the skeleton effect. + /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. const AppSkeleton({ super.key, required this.child, diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart new file mode 100644 index 0000000..798edfa --- /dev/null +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class AnimatedDialogButton extends StatefulWidget { + /// A custom animated button widget that provides a scaling and opacity effect + /// when pressed. + /// - [onPressed]: Callback function that is triggered when the button is pressed. + /// - [child]: The child widget to be displayed inside the button, typically a text or icon. + const AnimatedDialogButton({ + super.key, + required this.onPressed, + required this.child, + }); + + /// Callback function that is triggered when the button is pressed. + final VoidCallback onPressed; + + /// The child widget to be displayed inside the button, typically a text or icon. + final Widget child; + + @override + State createState() => _AnimatedDialogButtonState(); +} + +class _AnimatedDialogButtonState extends State { + bool _isPressed = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onPressed, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + opacity: _isPressed ? 0.6 : 1.0, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: CustomTheme.standardBoxDecoration, + padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6), + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 7e52648..489ceae 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; -/// A custom button widget that is designed to have a width relative to the screen size. -/// It supports three types of buttons: primary, secondary, and text buttons. -/// - [text]: The text to display on the button. -/// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary]. -/// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen. -/// - [onPressed]: The callback to be invoked when the button is pressed. class CustomWidthButton extends StatelessWidget { + /// A custom button widget that is designed to have a width relative to the screen size. + /// It supports three types of buttons: primary, secondary, and text buttons. + /// - [text]: The text to display on the button. + /// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary]. + /// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen. + /// - [onPressed]: The callback to be invoked when the button is pressed. const CustomWidthButton({ super.key, required this.text, diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart new file mode 100644 index 0000000..747c31e --- /dev/null +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +class MainMenuButton extends StatefulWidget { + /// A button for the main menu with an optional icon and a press animation. + /// - [text]: The text of the button. + /// - [icon]: The icon of the button. + /// - [onPressed]: The callback to be invoked when the button is pressed. + const MainMenuButton({ + super.key, + required this.text, + this.icon, + required this.onPressed, + }); + + /// The text of the button. + final String text; + + /// The icon of the button. + final IconData? icon; + + /// The callback to be invoked when the button is pressed. + final void Function() onPressed; + + @override + State createState() => _MainMenuButtonState(); +} + +class _MainMenuButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: _scaleAnimation, + child: GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) async { + await _animationController.reverse(); + if (mounted) { + widget.onPressed(); + } + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.icon != null) ...[ + Icon(widget.icon, size: 26, color: Colors.black), + const SizedBox(width: 7), + ], + Text( + widget.text, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/widgets/buttons/quick_create_button.dart b/lib/presentation/widgets/buttons/quick_create_button.dart index 40ebeab..f3aa588 100644 --- a/lib/presentation/widgets/buttons/quick_create_button.dart +++ b/lib/presentation/widgets/buttons/quick_create_button.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A button widget designed for quick creating matches in the [HomeView] -/// - [text]: The text to display on the button. -/// - [onPressed]: The callback to be invoked when the button is pressed. class QuickCreateButton extends StatefulWidget { + /// A button widget designed for quick creating matches in the [HomeView] + /// - [text]: The text to display on the button. + /// - [onPressed]: The callback to be invoked when the button is pressed. const QuickCreateButton({ super.key, required this.text, @@ -28,14 +28,18 @@ class _QuickCreateButtonState extends State { onPressed: widget.onPressed, style: ElevatedButton.styleFrom( minimumSize: const Size(140, 45), - backgroundColor: CustomTheme.primaryColor, + backgroundColor: CustomTheme.primaryColor.withAlpha(200).withBlue(40), shape: RoundedRectangleBorder( borderRadius: CustomTheme.standardBorderRadiusAll, ), ), child: Text( widget.text, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), ); } diff --git a/lib/presentation/widgets/colored_icon_container.dart b/lib/presentation/widgets/colored_icon_container.dart new file mode 100644 index 0000000..fe0659f --- /dev/null +++ b/lib/presentation/widgets/colored_icon_container.dart @@ -0,0 +1,57 @@ +import 'package:flutter/cupertino.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class ColoredIconContainer extends StatelessWidget { + /// A customizable container widget that displays an icon with a colored background. + /// - [icon]: The icon to be displayed inside the container. + /// - [containerSize]: The size of the container (width and height). + /// - [iconSize]: The size of the icon inside the container. + /// - [margin]: Optional margin around the container. + /// - [padding]: Optional padding inside the container. + const ColoredIconContainer({ + super.key, + required this.icon, + this.containerSize = 44, + this.iconSize = 28, + this.margin, + this.padding, + }); + + /// The icon to be displayed inside the container. + final IconData icon; + + /// The size of the container (width and height). + final double containerSize; + + /// The size of the icon inside the container. + final double iconSize; + + /// Optional margin around the container. + final EdgeInsetsGeometry? margin; + + /// Optional padding inside the container. + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + width: containerSize, + height: containerSize, + margin: margin, + padding: padding, + decoration: BoxDecoration( + color: CustomTheme.primaryColor.withAlpha(40), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + size: iconSize, + color: CustomTheme.primaryColor.withBlue(40), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/custom_alert_dialog.dart b/lib/presentation/widgets/custom_alert_dialog.dart new file mode 100644 index 0000000..84843b7 --- /dev/null +++ b/lib/presentation/widgets/custom_alert_dialog.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class CustomAlertDialog extends StatelessWidget { + /// A custom alert dialog widget that provides a os unspecific AlertDialog, + /// with consistent colors, borders, and layout that match the app's custom theme. + /// - [title]: The title text displayed at the top of the dialog. + /// - [content]: The main content text displayed in the body of the dialog. + /// - [actions]: A list of action widgets (typically buttons) displayed at the bottom + /// of the dialog. These actions are horizontally spaced around the dialog's width. + const CustomAlertDialog({ + super.key, + required this.title, + required this.content, + required this.actions, + }); + + final String title; + final String content; + final List actions; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(title, style: const TextStyle(color: CustomTheme.textColor)), + content: Text( + content, + style: const TextStyle(color: CustomTheme.textColor), + ), + actions: actions, + backgroundColor: CustomTheme.boxColor, + actionsAlignment: MainAxisAlignment.spaceAround, + shape: RoundedRectangleBorder( + borderRadius: CustomTheme.standardBorderRadiusAll, + side: const BorderSide(color: CustomTheme.boxBorder), + ), + ); + } +} diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart index 13a8d4d..0b08371 100644 --- a/lib/presentation/widgets/navbar_item.dart +++ b/lib/presentation/widgets/navbar_item.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A navigation bar item widget that represents a single tab in a navigation bar. -/// - [index]: The index of the tab. -/// - [isSelected]: A boolean indicating whether the tab is currently selected. -/// - [icon]: The icon to display for the tab. -/// - [label]: The label to display for the tab. -/// - [onTabTapped]: The callback to be invoked when the tab is tapped. class NavbarItem extends StatefulWidget { + /// A navigation bar item widget that represents a single tab in a navigation bar. + /// - [index]: The index of the tab. + /// - [isSelected]: A boolean indicating whether the tab is currently selected. + /// - [icon]: The icon to display for the tab. + /// - [label]: The label to display for the tab. + /// - [onTabTapped]: The callback to be invoked when the tab is tapped. const NavbarItem({ super.key, required this.index, @@ -35,7 +36,45 @@ class NavbarItem extends StatefulWidget { State createState() => _NavbarItemState(); } -class _NavbarItemState extends State { +class _NavbarItemState extends State + with SingleTickerProviderStateMixin { + /// Animation controller for the scale animation + late AnimationController _animationController; + + /// Scale animation for the icon when selected + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOutBack, + ), + ); + + if (widget.isSelected) { + _animationController.forward(); + } + } + + // Retrigger animation on selection change + @override + void didUpdateWidget(NavbarItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isSelected && !oldWidget.isSelected) { + _animationController.forward(); + } else if (!widget.isSelected && oldWidget.isSelected) { + _animationController.reverse(); + } + } + @override Widget build(BuildContext context) { return Expanded( @@ -48,19 +87,29 @@ class _NavbarItemState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - widget.icon, - color: widget.isSelected ? Colors.white : Colors.black, + ScaleTransition( + scale: widget.isSelected + ? _scaleAnimation + : const AlwaysStoppedAnimation(1.0), + child: Icon( + widget.icon, + color: widget.isSelected + ? CustomTheme.navBarItemSelectedColor + : CustomTheme.navBarItemUnselectedColor, + size: 32, + ), ), const SizedBox(height: 4), Text( widget.label, style: TextStyle( - color: widget.isSelected ? Colors.white : Colors.black, - fontSize: 12, + color: widget.isSelected + ? CustomTheme.navBarItemSelectedColor + : CustomTheme.navBarItemUnselectedColor, + fontSize: widget.isSelected ? 12 : 11, fontWeight: widget.isSelected ? FontWeight.bold - : FontWeight.normal, + : FontWeight.w500, ), ), ], @@ -69,4 +118,10 @@ class _NavbarItemState extends State { ), ); } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } } diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 9280ae0..bb3e3b9 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -1,24 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; -/// A widget that allows users to select players from a list, -/// with search functionality and the ability to add new players. -/// - [availablePlayers]: An optional list of players to choose from. If null, all -/// players from the database are used. -/// - [initialSelectedPlayers]: An optional list of players that should be pre-selected. -/// - [onChanged]: A callback function that is invoked whenever the selection changes, -/// providing the updated list of selected players. class PlayerSelection extends StatefulWidget { + /// A widget that allows users to select players from a list, + /// with search functionality and the ability to add new players. + /// - [availablePlayers]: An optional list of players to choose from. If null, + /// all players from the database are used. + /// - [initialSelectedPlayers]: An optional list of players that should be pre-selected. + /// - [onChanged]: A callback function that is invoked whenever the selection + /// changes, providing the updated list of selected players. const PlayerSelection({ super.key, this.availablePlayers, @@ -84,6 +84,7 @@ class _PlayerSelectionState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomSearchBar( + maxLength: Constants.MAX_PLAYER_NAME_LENGTH, controller: _searchBarController, constraints: const BoxConstraints(maxHeight: 45, minHeight: 45), hintText: loc.search_for_players, @@ -181,6 +182,7 @@ class _PlayerSelectionState extends State { icon: Icons.info, title: loc.info, message: _getInfoText(context), + fullscreen: false, ), child: ListView.builder( itemCount: suggestedPlayers.length, @@ -218,7 +220,7 @@ class _PlayerSelectionState extends State { void loadPlayerList() { _allPlayersFuture = Future.wait([ db.playerDao.getAllPlayers(), - Future.delayed(Constants.minimumSkeletonDuration), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) => results[0] as List); if (mounted) { _allPlayersFuture.then((loadedPlayers) { @@ -294,6 +296,7 @@ class _PlayerSelectionState extends State { /// [message] - The message to display in the snackbar. void showSnackBarMessage(String message) { if (!context.mounted) return; + ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: CustomTheme.boxColor, diff --git a/lib/presentation/widgets/text_input/custom_search_bar.dart b/lib/presentation/widgets/text_input/custom_search_bar.dart index bf7971a..aeb71f2 100644 --- a/lib/presentation/widgets/text_input/custom_search_bar.dart +++ b/lib/presentation/widgets/text_input/custom_search_bar.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A custom search bar widget that encapsulates a [SearchBar] with additional customization options. -/// - [controller]: The controller for the search bar's text input. -/// - [hintText]: The hint text displayed in the search bar when it is empty. -/// - [trailingButtonShown]: Whether to show the trailing button. -/// - [trailingButtonicon]: The icon for the trailing button. -/// - [trailingButtonEnabled]: Whether the trailing button is in enabled state. -/// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed. -/// - [onChanged]: The callback invoked when the text in the search bar changes. -/// - [constraints]: The constraints for the search bar. class CustomSearchBar extends StatelessWidget { + /// A custom search bar widget that encapsulates a [SearchBar] with additional customization options. + /// - [controller]: The controller for the search bar's text input. + /// - [hintText]: The hint text displayed in the search bar when it is empty. + /// - [trailingButtonShown]: Whether to show the trailing button. + /// - [trailingButtonicon]: The icon for the trailing button. + /// - [trailingButtonEnabled]: Whether the trailing button is in enabled state. + /// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed. + /// - [onChanged]: The callback invoked when the text in the search bar changes. + /// - [constraints]: The constraints for the search bar. const CustomSearchBar({ super.key, required this.controller, @@ -21,6 +21,7 @@ class CustomSearchBar extends StatelessWidget { this.onTrailingButtonPressed, this.onChanged, this.constraints, + this.maxLength, }); /// The controller for the search bar's text input. @@ -47,8 +48,21 @@ class CustomSearchBar extends StatelessWidget { /// The constraints for the search bar. final BoxConstraints? constraints; + /// Optional parameter for maximum length of the input text. + final int? maxLength; + @override Widget build(BuildContext context) { + /// Enforce maximum length on the input text + if (maxLength != null) { + if (controller.text.length > maxLength!) { + controller.text = controller.text.substring(0, maxLength); + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + } + } + return SearchBar( controller: controller, constraints: @@ -73,7 +87,9 @@ class CustomSearchBar extends StatelessWidget { const SizedBox(width: 5), ], backgroundColor: WidgetStateProperty.all(CustomTheme.boxColor), - side: WidgetStateProperty.all(BorderSide(color: CustomTheme.boxBorder)), + side: WidgetStateProperty.all( + const BorderSide(color: CustomTheme.boxBorder), + ), shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index a409c68..16f5072 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -1,43 +1,53 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:flutter/services.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A custom text input field widget that encapsulates a [TextField] with specific styling. -/// - [controller]: The controller for the text input field. -/// - [onChanged]: The callback invoked when the text in the field changes. -/// - [hintText]: The hint text displayed in the text input field when it is empty class TextInputField extends StatelessWidget { + /// A custom text input field widget that encapsulates a [TextField] with specific styling. + /// - [controller]: The controller for the text input field. + /// - [onChanged]: Optional callback invoked when the text in the field changes. + /// - [hintText]: The hint text displayed in the text input field when it is empty + /// - [maxLength]: Optional parameter for maximum length of the input text. const TextInputField({ super.key, required this.controller, required this.hintText, this.onChanged, + this.maxLength, }); /// The controller for the text input field. final TextEditingController controller; - /// The callback invoked when the text in the field changes. + /// Optional callback invoked when the text in the field changes. final ValueChanged? onChanged; /// The hint text displayed in the text input field when it is empty. final String hintText; + /// Optional parameter for maximum length of the input text. + final int? maxLength; + @override Widget build(BuildContext context) { return TextField( controller: controller, onChanged: onChanged, + maxLength: maxLength, + maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, decoration: InputDecoration( filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, hintStyle: const TextStyle(fontSize: 18), - enabledBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(12)), + // Hides the character counter + counterText: '', + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorder), ), - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(12)), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: CustomTheme.boxBorder), ), floatingLabelBehavior: FloatingLabelBehavior.never, diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index f6ec940..10ded6b 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A tile widget that allows users to choose an option by tapping on it. -/// - [title]: The title text displayed on the tile. -/// - [trailingText]: Optional trailing text displayed on the tile. -/// - [onPressed]: The callback invoked when the tile is tapped. class ChooseTile extends StatefulWidget { + /// A tile widget that allows users to choose an option by tapping on it. + /// - [title]: The title text displayed on the tile. + /// - [trailingText]: Optional trailing text displayed on the tile. + /// - [onPressed]: The callback invoked when the tile is tapped. const ChooseTile({ super.key, required this.title, diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart index 706aabb..2b8e855 100644 --- a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart +++ b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality. -/// - [text]: The text to display next to the radio button. -/// - [value]: The value associated with the radio button. -/// - [onContainerTap]: The callback invoked when the container is tapped. class CustomRadioListTile extends StatelessWidget { + /// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality. + /// - [text]: The text to display next to the radio button. + /// - [value]: The value associated with the radio button. + /// - [onContainerTap]: The callback invoked when the container is tapped. const CustomRadioListTile({ super.key, required this.text, diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 64d9caa..d662918 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -1,13 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; -/// A tile widget that displays information about a group, including its name and members. -/// - [group]: The group data to be displayed. -/// - [isHighlighted]: Whether the tile should be highlighted. -class GroupTile extends StatelessWidget { - const GroupTile({super.key, required this.group, this.isHighlighted = false}); +class GroupTile extends StatefulWidget { + /// A tile widget that displays information about a group, including its name and members. + /// - [group]: The group data to be displayed. + /// - [isHighlighted]: Whether the tile should be highlighted. + /// - [onTap]: Callback function to be executed when the tile is tapped. + const GroupTile({ + super.key, + required this.group, + this.isHighlighted = false, + this.onTap, + }); /// The group data to be displayed. final Group group; @@ -15,61 +21,72 @@ class GroupTile extends StatelessWidget { /// Whether the tile should be highlighted. final bool isHighlighted; + /// Callback function to be executed when the tile is tapped. + final VoidCallback? onTap; + + @override + State createState() => _GroupTileState(); +} + +class _GroupTileState extends State { @override Widget build(BuildContext context) { - return AnimatedContainer( - margin: CustomTheme.standardMargin, - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - decoration: isHighlighted - ? CustomTheme.highlightedBoxDecoration - : CustomTheme.standardBoxDecoration, - duration: const Duration(milliseconds: 150), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - group.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - Row( - children: [ - Text( - '${group.members.length}', + return GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + margin: CustomTheme.standardMargin, + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: widget.isHighlighted + ? CustomTheme.highlightedBoxDecoration + : CustomTheme.standardBoxDecoration, + duration: const Duration(milliseconds: 150), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + widget.group.name, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontWeight: FontWeight.w900, + fontWeight: FontWeight.bold, fontSize: 18, ), ), - const SizedBox(width: 3), - const Icon(Icons.group, size: 22), - ], - ), - ], - ), - const SizedBox(height: 5), - Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12.0, - runSpacing: 8.0, - children: [ - for (var member in [ - ...group.members, - ]..sort((a, b) => a.name.compareTo(b.name))) - TextIconTile(text: member.name, iconEnabled: false), - ], - ), - const SizedBox(height: 2.5), - ], + ), + Row( + children: [ + Text( + '${widget.group.members.length}', + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + const SizedBox(width: 3), + const Icon(Icons.group, size: 22), + ], + ), + ], + ), + const SizedBox(height: 5), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12.0, + runSpacing: 8.0, + children: [ + for (var member in [ + ...widget.group.members, + ]..sort((a, b) => a.name.compareTo(b.name))) + TextIconTile(text: member.name, iconEnabled: false), + ], + ), + const SizedBox(height: 2.5), + ], + ), ), ); } diff --git a/lib/presentation/widgets/tiles/info_tile.dart b/lib/presentation/widgets/tiles/info_tile.dart index 3e11679..fdbd88c 100644 --- a/lib/presentation/widgets/tiles/info_tile.dart +++ b/lib/presentation/widgets/tiles/info_tile.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A tile widget that displays a title with an icon and some content below it. -/// - [title]: The title text displayed on the tile. -/// - [icon]: The icon displayed next to the title. -/// - [content]: The content widget displayed below the title. -/// - [padding]: Optional padding for the tile content. -/// - [height]: Optional height for the tile. -/// - [width]: Optional width for the tile. class InfoTile extends StatefulWidget { + /// A tile widget that displays a title with an icon and some content below it. + /// - [title]: The title text displayed on the tile. + /// - [icon]: The icon displayed next to the title. + /// - [content]: The content widget displayed below the title. + /// - [padding]: Optional padding for the tile content. + /// - [height]: Optional height for the tile. + /// - [width]: Optional width for the tile. const InfoTile({ super.key, required this.title, @@ -17,6 +17,7 @@ class InfoTile extends StatefulWidget { this.padding, this.height, this.width, + this.horizontalAlignment = CrossAxisAlignment.center, }); /// The title text displayed on the tile. @@ -37,6 +38,9 @@ class InfoTile extends StatefulWidget { /// Optional width for the tile. final double? width; + /// The main axis alignment for the content. + final CrossAxisAlignment horizontalAlignment; + @override State createState() => _InfoTileState(); } @@ -51,7 +55,7 @@ class _InfoTileState extends State { decoration: CustomTheme.standardBoxDecoration, child: Column( mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: widget.horizontalAlignment, children: [ Row( children: [ diff --git a/lib/presentation/widgets/tiles/license_tile.dart b/lib/presentation/widgets/tiles/license_tile.dart index 5850d9e..9289ed5 100644 --- a/lib/presentation/widgets/tiles/license_tile.dart +++ b/lib/presentation/widgets/tiles/license_tile.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart'; +import 'package:tallee/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; class LicenseTile extends StatelessWidget { - final Package package; - + /// A tile widget that displays information about a software package license. + /// - [package]: The package data to be displayed. const LicenseTile({super.key, required this.package}); + /// The package data to be displayed. + final Package package; + @override Widget build(BuildContext context) { return GestureDetector( @@ -26,18 +30,10 @@ class LicenseTile extends StatelessWidget { ), child: Row( children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.description, - color: CustomTheme.primaryColor, - size: 32, - ), + const ColoredIconContainer( + icon: Icons.description, + containerSize: 50, + iconSize: 32, ), const SizedBox(width: 16), Expanded( diff --git a/lib/presentation/widgets/tiles/match_summary_tile.dart b/lib/presentation/widgets/tiles/match_summary_tile.dart deleted file mode 100644 index 719037b..0000000 --- a/lib/presentation/widgets/tiles/match_summary_tile.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:skeletonizer/skeletonizer.dart'; - -class MatchSummaryTile extends StatefulWidget { - final String matchTitle; - final String game; - final String ruleset; - final String players; - final String winner; - - const MatchSummaryTile({ - super.key, - required this.matchTitle, - required this.game, - required this.ruleset, - required this.players, - required this.winner, - }); - - @override - State createState() => _MatchSummaryTileState(); -} - -class _MatchSummaryTileState extends State { - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - widget.matchTitle, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 5), - Text( - widget.game, - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - ], - ), - const SizedBox(height: 5), - Container( - padding: const EdgeInsets.symmetric(horizontal: 4), - height: 20, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: CustomTheme.primaryColor, - ), - child: Skeleton.ignore( - child: Text( - widget.ruleset, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - Center( - heightFactor: 1.5, - child: Text( - widget.players, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4), - width: 220, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.yellow.shade300, - ), - child: Skeleton.ignore( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.emoji_events, color: Colors.black, size: 20), - Text( - widget.winner, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 88ae1f1..ab65e5d 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:intl/intl.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; -/// A tile widget that displays information about a match, including its name, -/// creation date, associated group, winner, and players. -/// - [match]: The match data to be displayed. -/// - [onTap]: The callback invoked when the tile is tapped. -/// - [width]: Optional width for the tile. -/// - [compact]: Whether to display the tile in a compact mode class MatchTile extends StatefulWidget { + /// A tile widget that displays information about a match, including its name, + /// creation date, associated group, winner, and players. + /// - [match]: The match data to be displayed. + /// - [onTap]: The callback invoked when the tile is tapped. + /// - [width]: Optional width for the tile. + /// - [compact]: Whether to display the tile in a compact mode const MatchTile({ super.key, required this.match, @@ -230,7 +230,7 @@ class _MatchTileState extends State { } else if (difference.inDays < 7) { return loc.days_ago(difference.inDays); } else { - return DateFormat('MMM d, yyyy').format(dateTime); + return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}'; } } diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart index 839f6c3..5646fa5 100644 --- a/lib/presentation/widgets/tiles/quick_info_tile.dart +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A tile widget that displays a title with an icon and a numeric value below it. -/// - [title]: The title text displayed on the tile. -/// - [icon]: The icon displayed next to the title. -/// - [value]: The numeric value displayed below the title. -/// - [height]: Optional height for the tile. -/// - [width]: Optional width for the tile. -/// - [padding]: Optional padding for the tile content. class QuickInfoTile extends StatefulWidget { + /// A tile widget that displays a title with an icon and a numeric value below it. + /// - [title]: The title text displayed on the tile. + /// - [icon]: The icon displayed next to the title. + /// - [value]: The numeric value displayed below the title. + /// - [height]: Optional height for the tile. + /// - [width]: Optional width for the tile. + /// - [padding]: Optional padding for the tile content. const QuickInfoTile({ super.key, required this.title, diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart index ba05225..de805cd 100644 --- a/lib/presentation/widgets/tiles/settings_list_tile.dart +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; -/// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget. -/// - [icon]: The icon displayed on the left side of the tile. -/// - [title]: The title text displayed next to the icon. -/// - [suffixWidget]: An optional widget displayed on the right side of the tile. -/// - [onPressed]: The callback invoked when the tile is tapped. class SettingsListTile extends StatelessWidget { + /// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget. + /// - [icon]: The icon displayed on the left side of the tile. + /// - [title]: The title text displayed next to the icon. + /// - [suffixWidget]: An optional widget displayed on the right side of the tile. + /// - [onPressed]: The callback invoked when the tile is tapped. const SettingsListTile({ super.key, required this.icon, @@ -46,18 +47,10 @@ class SettingsListTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - size: 28, - color: CustomTheme.primaryColor.withGreen(40), - ), + ColoredIconContainer( + icon: icon, + containerSize: 44, + iconSize: 28, ), const SizedBox(width: 16), Text(title, style: const TextStyle(fontSize: 18)), diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 2c0ced0..bc2f7b6 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,17 +1,17 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; -/// A tile widget that displays statistical data using horizontal bars. -/// - [icon]: The icon displayed next to the title. -/// - [title]: The title text displayed on the tile. -/// - [width]: The width of the tile. -/// - [values]: A list of tuples containing labels and their corresponding numeric values. -/// - [itemCount]: The maximum number of items to display. -/// - [barColor]: The color of the bars representing the values. class StatisticsTile extends StatelessWidget { + /// A tile widget that displays statistical data using horizontal bars. + /// - [icon]: The icon displayed next to the title. + /// - [title]: The title text displayed on the tile. + /// - [width]: The width of the tile. + /// - [values]: A list of tuples containing labels and their corresponding numeric values. + /// - [itemCount]: The maximum number of items to display. + /// - [barColor]: The color of the bars representing the values. const StatisticsTile({ super.key, required this.icon, diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 7d3fe1c..2b29d41 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A list tile widget that displays text with an optional icon button. -/// - [text]: The text to display in the tile. -/// - [onPressed]: The callback to be invoked when the icon is pressed. -/// - [iconEnabled]: A boolean to determine if the icon should be displayed. class TextIconListTile extends StatelessWidget { + /// A list tile widget that displays text with an optional icon button. + /// - [text]: The text to display in the tile. + /// - [onPressed]: The callback to be invoked when the icon is pressed. + /// - [iconEnabled]: A boolean to determine if the icon should be displayed. const TextIconListTile({ super.key, required this.text, diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index 7142b27..f98e0a7 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A tile widget that displays text with an optional icon that can be tapped. -/// - [text]: The text to display in the tile. -/// - [iconEnabled]: A boolean to determine if the icon should be displayed. -/// - [onIconTap]: The callback to be invoked when the icon is tapped. class TextIconTile extends StatelessWidget { + /// A tile widget that displays text with an optional icon that can be tapped. + /// - [text]: The text to display in the tile. + /// - [iconEnabled]: A boolean to determine if the icon should be displayed. + /// - [onIconTap]: The callback to be invoked when the icon is tapped. const TextIconTile({ super.key, required this.text, diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 781149e..9dc8f33 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; +import 'package:tallee/core/custom_theme.dart'; -/// A list tile widget that displays a title and description, with optional highlighting and badge. -/// - [title]: The title text displayed on the tile. -/// - [description]: The description text displayed below the title. -/// - [onPressed]: The callback invoked when the tile is tapped. -/// - [isHighlighted]: A boolean to determine if the tile should be highlighted. -/// - [badgeText]: Optional text to display in a badge on the right side of the title. -/// - [badgeColor]: Optional color for the badge background. class TitleDescriptionListTile extends StatelessWidget { + /// A list tile widget that displays a title and description, with optional highlighting and badge. + /// - [title]: The title text displayed on the tile. + /// - [description]: The description text displayed below the title. + /// - [onPressed]: The callback invoked when the tile is tapped. + /// - [isHighlighted]: A boolean to determine if the tile should be highlighted. + /// - [badgeText]: Optional text to display in a badge on the right side of the title. + /// - [badgeColor]: Optional color for the badge background. const TitleDescriptionListTile({ super.key, required this.title, @@ -73,7 +73,6 @@ class TitleDescriptionListTile extends StatelessWidget { const Spacer(), Container( constraints: const BoxConstraints(maxWidth: 115), - margin: const EdgeInsets.only(top: 4), padding: const EdgeInsets.symmetric( vertical: 2, horizontal: 6, diff --git a/lib/presentation/widgets/top_centered_message.dart b/lib/presentation/widgets/top_centered_message.dart index c15c93d..e651180 100644 --- a/lib/presentation/widgets/top_centered_message.dart +++ b/lib/presentation/widgets/top_centered_message.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -/// A widget that displays a message centered at the top of the screen with an icon, title, and message. -/// - [icon]: The icon to display above the title. -/// - [title]: The title text to display. -/// - [message]: The message text to display below the title. class TopCenteredMessage extends StatelessWidget { + /// A widget that displays a message centered at the top of the screen with an icon, title, and message. + /// - [icon]: The icon to display above the title. + /// - [title]: The title text to display. + /// - [message]: The message text to display below the title. const TopCenteredMessage({ super.key, required this.icon, required this.title, required this.message, + this.fullscreen = true, }); /// The icon to display above the title. @@ -21,13 +22,18 @@ class TopCenteredMessage extends StatelessWidget { /// The message text to display below the title. final String message; + final bool fullscreen; + @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.only(top: 100), + padding: fullscreen ? const EdgeInsets.only(top: 100) : null, margin: const EdgeInsets.symmetric(horizontal: 10), alignment: Alignment.topCenter, child: Column( + mainAxisAlignment: fullscreen + ? MainAxisAlignment.start + : MainAxisAlignment.center, children: [ Icon(icon, size: 45), const SizedBox(height: 10), diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 8767c59..5863d87 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; import 'package:json_schema/json_schema.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; class DataTransferService { /// Deletes all data from the database. @@ -34,22 +34,28 @@ class DataTransferService { 'players': players.map((p) => p.toJson()).toList(), 'groups': groups - .map((g) => { - 'id': g.id, - 'name': g.name, - 'createdAt': g.createdAt.toIso8601String(), - 'memberIds': (g.members).map((m) => m.id).toList(), - }).toList(), + .map( + (g) => { + 'id': g.id, + 'name': g.name, + 'createdAt': g.createdAt.toIso8601String(), + 'memberIds': (g.members).map((m) => m.id).toList(), + }, + ) + .toList(), 'matches': matches - .map((m) => { - 'id': m.id, - 'name': m.name, - 'createdAt': m.createdAt.toIso8601String(), - 'groupId': m.group?.id, - 'playerIds': (m.players ?? []).map((p) => p.id).toList(), - 'winnerId': m.winner?.id, - }).toList(), + .map( + (m) => { + 'id': m.id, + 'name': m.name, + 'createdAt': m.createdAt.toIso8601String(), + 'groupId': m.group?.id, + 'playerIds': (m.players ?? []).map((p) => p.id).toList(), + 'winnerId': m.winner?.id, + }, + ) + .toList(), }; return json.encode(jsonMap); @@ -62,7 +68,7 @@ class DataTransferService { /// [fileName] The desired name for the exported file (without extension). static Future exportData( String jsonString, - String fileName + String fileName, ) async { try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); @@ -76,7 +82,6 @@ class DataTransferService { } else { return ExportResult.success; } - } catch (e, stack) { print('[exportData] $e'); print(stack); @@ -104,11 +109,15 @@ class DataTransferService { final isValid = await _validateJsonSchema(jsonString); if (!isValid) return ImportResult.invalidSchema; - final Map decoded = json.decode(jsonString) as Map; + final Map decoded = + json.decode(jsonString) as Map; - final List playersJson = (decoded['players'] as List?) ?? []; - final List groupsJson = (decoded['groups'] as List?) ?? []; - final List matchesJson = (decoded['matches'] as List?) ?? []; + final List playersJson = + (decoded['players'] as List?) ?? []; + final List groupsJson = + (decoded['groups'] as List?) ?? []; + final List matchesJson = + (decoded['matches'] as List?) ?? []; // Players final List importedPlayers = playersJson @@ -122,7 +131,8 @@ class DataTransferService { // Groups final List importedGroups = groupsJson.map((g) { final map = g as Map; - final memberIds = (map['memberIds'] as List? ?? []).cast(); + final memberIds = (map['memberIds'] as List? ?? []) + .cast(); final members = memberIds .map((id) => playerById[id]) @@ -146,7 +156,8 @@ class DataTransferService { final map = m as Map; final String? groupId = map['groupId'] as String?; - final List playerIds = (map['playerIds'] as List? ?? []).cast(); + final List playerIds = + (map['playerIds'] as List? ?? []).cast(); final String? winnerId = map['winnerId'] as String?; final group = (groupId == null) ? null : groupById[groupId]; @@ -212,4 +223,4 @@ class DataTransferService { return false; } } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 2afff43..dd01e83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ -name: game_tracker -description: "Game Tracking App for Card Games" +name: tallee +description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.5+143 +version: 0.0.12+240 environment: sdk: ^3.8.1 @@ -17,6 +17,7 @@ dependencies: drift_flutter: ^0.2.4 file_picker: ^10.3.6 file_saver: ^0.3.1 + fluttericon: ^2.0.0 font_awesome_flutter: ^10.12.0 intl: any json_schema: ^5.2.2 @@ -27,7 +28,6 @@ dependencies: url_launcher: ^6.3.2 uuid: ^4.5.2 - dev_dependencies: flutter_test: sdk: flutter diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 0ec2cfc..5a6ad25 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -2,10 +2,10 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database; diff --git a/test/db_tests/group_match_test.dart b/test/db_tests/group_match_test.dart index 7d812bd..8c5e6e4 100644 --- a/test/db_tests/group_match_test.dart +++ b/test/db_tests/group_match_test.dart @@ -2,10 +2,10 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database; diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index 5104c65..d87edf3 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -2,9 +2,9 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database; diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart index 2783430..b4a87bc 100644 --- a/test/db_tests/player_group_test.dart +++ b/test/db_tests/player_group_test.dart @@ -2,9 +2,9 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database; diff --git a/test/db_tests/player_match_test.dart b/test/db_tests/player_match_test.dart index 8a4f569..393d0c0 100644 --- a/test/db_tests/player_match_test.dart +++ b/test/db_tests/player_match_test.dart @@ -2,10 +2,10 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database; diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index 5bd10ad..c2a4547 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -2,8 +2,8 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/player.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/player.dart'; void main() { late AppDatabase database;