From 6140311692c0995d2c66fbc6368e3f0ecdb4d263 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 5 Aug 2025 15:35:24 +0530 Subject: [PATCH 01/53] application UI done --- leaderboard_app/.gitignore | 45 ++ leaderboard_app/.metadata | 45 ++ leaderboard_app/README.md | 3 + leaderboard_app/analysis_options.yaml | 1 + leaderboard_app/android/.gitignore | 14 + leaderboard_app/android/app/build.gradle.kts | 44 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 ++ .../example/leaderboard_app/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + leaderboard_app/android/build.gradle.kts | 24 + leaderboard_app/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + leaderboard_app/android/settings.gradle.kts | 26 + leaderboard_app/assets/icons/google.png | Bin 0 -> 1175 bytes .../AlumniSans-Italic-VariableFont_wght.ttf | Bin 0 -> 142496 bytes .../fonts/AlumniSans-VariableFont_wght.ttf | Bin 0 -> 138252 bytes leaderboard_app/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + leaderboard_app/ios/Flutter/Debug.xcconfig | 1 + leaderboard_app/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 616 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + leaderboard_app/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + leaderboard_app/ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../ios/RunnerTests/RunnerTests.swift | 12 + leaderboard_app/lib/components/my_button.dart | 30 + .../lib/components/my_chat_bubble.dart | 130 ++++ .../lib/components/my_textfield.dart | 42 ++ .../lib/components/my_usertile.dart | 43 ++ leaderboard_app/lib/main.dart | 30 + leaderboard_app/lib/pages/chat_page.dart | 447 +++++++++++ leaderboard_app/lib/pages/chatlists_page.dart | 246 ++++++ leaderboard_app/lib/pages/dashboard_page.dart | 493 ++++++++++++ leaderboard_app/lib/pages/files_page.dart | 73 ++ leaderboard_app/lib/pages/home_page.dart | 69 ++ leaderboard_app/lib/pages/media_page.dart | 55 ++ leaderboard_app/lib/pages/profile_page.dart | 211 ++++++ leaderboard_app/lib/pages/settings_page.dart | 263 +++++++ leaderboard_app/lib/pages/signin_page.dart | 143 ++++ leaderboard_app/lib/pages/signup_page.dart | 229 ++++++ .../lib/provider/theme_provider.dart | 17 + leaderboard_app/linux/.gitignore | 1 + leaderboard_app/linux/CMakeLists.txt | 128 ++++ leaderboard_app/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 23 + leaderboard_app/linux/runner/CMakeLists.txt | 26 + leaderboard_app/linux/runner/main.cc | 6 + .../linux/runner/my_application.cc | 144 ++++ leaderboard_app/linux/runner/my_application.h | 18 + leaderboard_app/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + .../macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + leaderboard_app/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + leaderboard_app/pubspec.lock | 253 +++++++ leaderboard_app/pubspec.yaml | 32 + leaderboard_app/web/favicon.png | Bin 0 -> 917 bytes leaderboard_app/web/icons/Icon-192.png | Bin 0 -> 5292 bytes leaderboard_app/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes leaderboard_app/web/index.html | 38 + leaderboard_app/web/manifest.json | 35 + leaderboard_app/windows/.gitignore | 17 + leaderboard_app/windows/CMakeLists.txt | 108 +++ .../windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 23 + leaderboard_app/windows/runner/CMakeLists.txt | 40 + leaderboard_app/windows/runner/Runner.rc | 121 +++ .../windows/runner/flutter_window.cpp | 71 ++ .../windows/runner/flutter_window.h | 33 + leaderboard_app/windows/runner/main.cpp | 43 ++ leaderboard_app/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 14 + leaderboard_app/windows/runner/utils.cpp | 65 ++ leaderboard_app/windows/runner/utils.h | 19 + .../windows/runner/win32_window.cpp | 288 +++++++ leaderboard_app/windows/runner/win32_window.h | 102 +++ 146 files changed, 7261 insertions(+) create mode 100644 leaderboard_app/.gitignore create mode 100644 leaderboard_app/.metadata create mode 100644 leaderboard_app/README.md create mode 100644 leaderboard_app/analysis_options.yaml create mode 100644 leaderboard_app/android/.gitignore create mode 100644 leaderboard_app/android/app/build.gradle.kts create mode 100644 leaderboard_app/android/app/src/debug/AndroidManifest.xml create mode 100644 leaderboard_app/android/app/src/main/AndroidManifest.xml create mode 100644 leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt create mode 100644 leaderboard_app/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 leaderboard_app/android/app/src/main/res/drawable/launch_background.xml create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 leaderboard_app/android/app/src/main/res/values-night/styles.xml create mode 100644 leaderboard_app/android/app/src/main/res/values/styles.xml create mode 100644 leaderboard_app/android/app/src/profile/AndroidManifest.xml create mode 100644 leaderboard_app/android/build.gradle.kts create mode 100644 leaderboard_app/android/gradle.properties create mode 100644 leaderboard_app/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 leaderboard_app/android/settings.gradle.kts create mode 100644 leaderboard_app/assets/icons/google.png create mode 100644 leaderboard_app/fonts/AlumniSans-Italic-VariableFont_wght.ttf create mode 100644 leaderboard_app/fonts/AlumniSans-VariableFont_wght.ttf create mode 100644 leaderboard_app/ios/.gitignore create mode 100644 leaderboard_app/ios/Flutter/AppFrameworkInfo.plist create mode 100644 leaderboard_app/ios/Flutter/Debug.xcconfig create mode 100644 leaderboard_app/ios/Flutter/Release.xcconfig create mode 100644 leaderboard_app/ios/Runner.xcodeproj/project.pbxproj create mode 100644 leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 leaderboard_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 leaderboard_app/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 leaderboard_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 leaderboard_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 leaderboard_app/ios/Runner/AppDelegate.swift create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 leaderboard_app/ios/Runner/Base.lproj/Main.storyboard create mode 100644 leaderboard_app/ios/Runner/Info.plist create mode 100644 leaderboard_app/ios/Runner/Runner-Bridging-Header.h create mode 100644 leaderboard_app/ios/RunnerTests/RunnerTests.swift create mode 100644 leaderboard_app/lib/components/my_button.dart create mode 100644 leaderboard_app/lib/components/my_chat_bubble.dart create mode 100644 leaderboard_app/lib/components/my_textfield.dart create mode 100644 leaderboard_app/lib/components/my_usertile.dart create mode 100644 leaderboard_app/lib/main.dart create mode 100644 leaderboard_app/lib/pages/chat_page.dart create mode 100644 leaderboard_app/lib/pages/chatlists_page.dart create mode 100644 leaderboard_app/lib/pages/dashboard_page.dart create mode 100644 leaderboard_app/lib/pages/files_page.dart create mode 100644 leaderboard_app/lib/pages/home_page.dart create mode 100644 leaderboard_app/lib/pages/media_page.dart create mode 100644 leaderboard_app/lib/pages/profile_page.dart create mode 100644 leaderboard_app/lib/pages/settings_page.dart create mode 100644 leaderboard_app/lib/pages/signin_page.dart create mode 100644 leaderboard_app/lib/pages/signup_page.dart create mode 100644 leaderboard_app/lib/provider/theme_provider.dart create mode 100644 leaderboard_app/linux/.gitignore create mode 100644 leaderboard_app/linux/CMakeLists.txt create mode 100644 leaderboard_app/linux/flutter/CMakeLists.txt create mode 100644 leaderboard_app/linux/flutter/generated_plugin_registrant.cc create mode 100644 leaderboard_app/linux/flutter/generated_plugin_registrant.h create mode 100644 leaderboard_app/linux/flutter/generated_plugins.cmake create mode 100644 leaderboard_app/linux/runner/CMakeLists.txt create mode 100644 leaderboard_app/linux/runner/main.cc create mode 100644 leaderboard_app/linux/runner/my_application.cc create mode 100644 leaderboard_app/linux/runner/my_application.h create mode 100644 leaderboard_app/macos/.gitignore create mode 100644 leaderboard_app/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 leaderboard_app/macos/Flutter/Flutter-Release.xcconfig create mode 100644 leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 leaderboard_app/macos/Runner.xcodeproj/project.pbxproj create mode 100644 leaderboard_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 leaderboard_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 leaderboard_app/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 leaderboard_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 leaderboard_app/macos/Runner/AppDelegate.swift create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 leaderboard_app/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 leaderboard_app/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 leaderboard_app/macos/Runner/Configs/Debug.xcconfig create mode 100644 leaderboard_app/macos/Runner/Configs/Release.xcconfig create mode 100644 leaderboard_app/macos/Runner/Configs/Warnings.xcconfig create mode 100644 leaderboard_app/macos/Runner/DebugProfile.entitlements create mode 100644 leaderboard_app/macos/Runner/Info.plist create mode 100644 leaderboard_app/macos/Runner/MainFlutterWindow.swift create mode 100644 leaderboard_app/macos/Runner/Release.entitlements create mode 100644 leaderboard_app/macos/RunnerTests/RunnerTests.swift create mode 100644 leaderboard_app/pubspec.lock create mode 100644 leaderboard_app/pubspec.yaml create mode 100644 leaderboard_app/web/favicon.png create mode 100644 leaderboard_app/web/icons/Icon-192.png create mode 100644 leaderboard_app/web/icons/Icon-512.png create mode 100644 leaderboard_app/web/icons/Icon-maskable-192.png create mode 100644 leaderboard_app/web/icons/Icon-maskable-512.png create mode 100644 leaderboard_app/web/index.html create mode 100644 leaderboard_app/web/manifest.json create mode 100644 leaderboard_app/windows/.gitignore create mode 100644 leaderboard_app/windows/CMakeLists.txt create mode 100644 leaderboard_app/windows/flutter/CMakeLists.txt create mode 100644 leaderboard_app/windows/flutter/generated_plugin_registrant.cc create mode 100644 leaderboard_app/windows/flutter/generated_plugin_registrant.h create mode 100644 leaderboard_app/windows/flutter/generated_plugins.cmake create mode 100644 leaderboard_app/windows/runner/CMakeLists.txt create mode 100644 leaderboard_app/windows/runner/Runner.rc create mode 100644 leaderboard_app/windows/runner/flutter_window.cpp create mode 100644 leaderboard_app/windows/runner/flutter_window.h create mode 100644 leaderboard_app/windows/runner/main.cpp create mode 100644 leaderboard_app/windows/runner/resource.h create mode 100644 leaderboard_app/windows/runner/resources/app_icon.ico create mode 100644 leaderboard_app/windows/runner/runner.exe.manifest create mode 100644 leaderboard_app/windows/runner/utils.cpp create mode 100644 leaderboard_app/windows/runner/utils.h create mode 100644 leaderboard_app/windows/runner/win32_window.cpp create mode 100644 leaderboard_app/windows/runner/win32_window.h diff --git a/leaderboard_app/.gitignore b/leaderboard_app/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/leaderboard_app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/leaderboard_app/.metadata b/leaderboard_app/.metadata new file mode 100644 index 0000000..131e057 --- /dev/null +++ b/leaderboard_app/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "02da4cc00db9eb97fc48e89d319ef48518c2440a" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: android + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: ios + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: linux + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: macos + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: web + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + - platform: windows + create_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + base_revision: 02da4cc00db9eb97fc48e89d319ef48518c2440a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md new file mode 100644 index 0000000..cbfad68 --- /dev/null +++ b/leaderboard_app/README.md @@ -0,0 +1,3 @@ +# leaderboard_app + +A new Flutter project. diff --git a/leaderboard_app/analysis_options.yaml b/leaderboard_app/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/leaderboard_app/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/leaderboard_app/android/.gitignore b/leaderboard_app/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/leaderboard_app/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/leaderboard_app/android/app/build.gradle.kts b/leaderboard_app/android/app/build.gradle.kts new file mode 100644 index 0000000..5434cd7 --- /dev/null +++ b/leaderboard_app/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.leaderboard_app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.leaderboard_app" + // 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 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/leaderboard_app/android/app/src/debug/AndroidManifest.xml b/leaderboard_app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/leaderboard_app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/leaderboard_app/android/app/src/main/AndroidManifest.xml b/leaderboard_app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d94c1c3 --- /dev/null +++ b/leaderboard_app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt b/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt new file mode 100644 index 0000000..b1e3a68 --- /dev/null +++ b/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.leaderboard_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/leaderboard_app/android/app/src/main/res/drawable-v21/launch_background.xml b/leaderboard_app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/drawable/launch_background.xml b/leaderboard_app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/values-night/styles.xml b/leaderboard_app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/values/styles.xml b/leaderboard_app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/leaderboard_app/android/app/src/profile/AndroidManifest.xml b/leaderboard_app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/leaderboard_app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/leaderboard_app/android/build.gradle.kts b/leaderboard_app/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/leaderboard_app/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/leaderboard_app/android/gradle.properties b/leaderboard_app/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/leaderboard_app/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/leaderboard_app/android/gradle/wrapper/gradle-wrapper.properties b/leaderboard_app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/leaderboard_app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/leaderboard_app/android/settings.gradle.kts b/leaderboard_app/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/leaderboard_app/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/leaderboard_app/assets/icons/google.png b/leaderboard_app/assets/icons/google.png new file mode 100644 index 0000000000000000000000000000000000000000..64b0e95cd32aa8ded9e568d2f03edcc6f49b7e7f GIT binary patch literal 1175 zcmV;I1Zew-P)6F^R5w(CyOKj-{iUy%XFhS`h zMXOj7QKQfJVuBH)L?T2JLqfRuO-XhxRb*7!O zmk+hI4rkh-cGlzm*IwVhclJJKZ6U$vEGwGrb9|Lx)_|;3m=6R54A2d9qFfcUK5S-R zc%ZSdKasjJRl;9uS7nLEY!lcBq$lWi0Q7((W>27@<(Fw0(27UBq2a(s0-J%91i`T} z0!xfL|@1_7i^1{GQI=tmCMttL5^!;@S8wT;CF!0-O^$p z|KAij?O4%Q0}U;q$qJ1dNFTJeDHKhTs|bfYcy^^XG+lQ~=qr0-iE*s=725rQN483J zwKLU91R%V7Ztn2u?5|bD7bowwTDoyg{grc56};Q90qmkb-jRDFzR0m0bm)$Ad0UKG z?LT+*)=a|zL{E_sokb_$Ga)a-M{_XUX=A#orKQPO`B+#^3Kf74!171%zR`-k_|Gwa zhm#2huu#@b9FSRtlnviwuk7q{U{^BX05Vcf5Ln@&waqGJ@l*vi>=`;N$ZON1bIAf0 zx@y9g(<*v5d?nKY{BEKeI+ASwm}dd%Dp^>`rYqPM(Bmo^GLvlp2wA|-Zi*$WPCj3L zum!$SD2|oBG5$s6HA|PBsl0S;vJz_Pr{Y-0<03GHDzZJ!>}xj4d4xA74%a&K_%OPJ z5s`O+*C#V@VslzE&>T;HanDdyJk3&SL@$sYjnWPH9EAN&4hN%I;}%6IzxnLh`pHbh ziN|qL$tU3}-ZLI^nFMc`Qd-A>6?J!|`vM#JmO&v(NOd_4I za!b*mSn?Yx3c7&No0JD2YLnT$i8-Nix5+`fH}$FN9WwkM0|5DT>ldmVXf^?l!O-{1G;{$}PpGxN+d^|U!NAwfiu z@NCqhXI_56{8ksfAd;(z{HW)^0fSpS|H28Pn_3c$?A3Gd(45G98QqEU+7UIK-)mTQ z(F;ZIzfa^CL)3WBfWaxQTc-4nA>vS!I$`jT>>(Eq9Gr&dZ}I$M@vP$NzuJ!BV*=%q zt7JxT)#;YiV~GOs9I$sX((nFZ&L-q9NBWIZifgJ6h9mzFgn?7a=S;5q%srb##d)IF zH%&qQkd@EPCOR~UXvLjVON%G9obqiN@_&r{X;Ts5|7%bKJkP^(yQwp3uS;uR)`ZCZ zB2mM2<&`DHtB+Kp?&0`ez`+^C*Hy_&^e*xvj_ei1GfF$e_V*`>&co-csw!)02gE#$ zvPZ8aYFk%TU0OBO|IS;HzYzHYUKZY4H`(sOBMGmU8X_clmZnhvO+a_5p7#Aol?&+$ zuAn@*#anvi1$m1+LwMcyC%-n{5){0Hc4!HKln$%#^Mh+A;d{sMy_Uq}52ydxL>Wy&O^Fxt z2=7nSSB2yh72t zNi-AzfVKe&7lA0z4ycg`(#sYRdhryfwKkH1@!XDB)j@r-thTrurK_6RDUmX$7Y)Iv zmD6>!K&v;2##&ufW)hLAW=4DyRX4N_9~V)ocB&3WT2oa=1CCa;G~j0S@-$a9HR4-n z{h-YZtu`uN(nX}B{&F#vX$Oi%-4D{s^dK#!xtLv}s1Ie41381m&*C5AqWDEz5?A+;zT0|UrahNi&$nvNW!Cd@?Rh$$ z8MX9BiJ9;P`6EWsZCdK`da0YVn5A0k3mRMpSg++9+H= z@YC=U7^6J+J*hYRe$*fSK>P#^#!t{t`~(fBvGB)ZBn3wAdiXb>*90x0d*I(k_rrgH zo`?Sey#)U?dJFz8+7165`T+jN^ga9w^fUa6^b7pU^c#FHW~C5*m}vqt6g3nHmAr6U8 z;U5)Wz&|a%g8z;92L5*ft;4Ljgt>b~Tp?zEZcKusI0}9=HzUa{xh3FOZVSI7cZA=G z&nu&o}b|__y=z_{vgVj<@b%w1`*o zO85`*8u(B0Q}CbVjqo=y>dznXN67gxe+~FNUnU`2%UJmBB>G$?%M@TYhjapV%T)Mj zGL2ZKOZ2=PE{7v^q#OlUtpt{1CF&z9WfkJ*$R`0mE!V?;R-)hJR=ETITXHx2x8*+g zA4v3@{7jy}h^rY&bSIn(E*>rtGhU6?O_;ua!KkT{D}mVrT9}01!K_uYLW~rn#Aq=_ zj1}X=crif~i;1EH<6nVy=i|+r#VulixK%6^w~0mKZjA9`;&F`V1~mfc=S0=Zyo{Ht z_G-1q>cU)_i}|ryJj+Y*CBke~7_$RhI4F*)F99dtOp8EMmeNYvOOGQ4v5&pD<)v5N z-uuqJ_dodflS79;J96}kW5-XNJazi3Ghcu6?YZy1KY!uxKmPRdKQ8`q>GH3?UGWN* zHb4J>z@Xre&<0@*!y833j%*UuG`d;y7A;%Fw6?d2ZQHJWhmLWb;u8{+l2aT`m)nz? zmY$KB)wxU8Zryui=j4L3=-I1xpT7ME3>;K6c*xMVXPtty0)E64*qwDUxgfTM@3N`M?%-YCty%r|5;gyjv_s z&utP%#8=`QZhh#$C3Ywy8(ATmU8s+u|n`GJ5&yB%pVb>cpk4+k1`me7$&>P9x_KR zP&5&7Se5clxkRp%s{yT7PHs>szCZc6Z32355@={SxZ9S>`Po&v?&~@IqWzj9rnw)u zSicw}9kBZI!(5wkm3OY3(oX1_DR4YL1Uj=3Q zD=5;(bRM+mA9N9vXQ&t`hKXMCDQ*f1)E`vk9$@|-fx%;ek0){&SMy!KtIzYVvN5ox z2bge<+zc$Y6FBZOF<1-|Lqu<~iNA~MJP=3FzDPJdUp8;Z(+1G?&Lv1^1#G zLC0?98C*#Vxt133b#xogqT6{c-N|>*QeH*N`Egptt7(8ZNzd~cdXTr$b9|Ck@&~j{ zwxR8^1HCTW(N398yJRt8`huO}`3%`MC&`$3=krf^Vaz`4E=b3h=9Lv59V_#h|cv(>?qog^QLnjl0l8 z{4yPq^XUt@gpSBX^f}hcQLL7c9853pd9sTF3KXfdg@2}|B8ggxEXo#psJA#o<=lg2 z@nCwKchM$3O>f9J+94fu7(D7(xt4az9`wE}p|!l1UgAsi1iuSPaZolA8^w)cx)?6j z%Ow6#R&WYOgAU##D|s1c;WOefPLRp$00k@oMtl}DY>Ai)IzN-osjp^Nx70v%T@H1e1y&bTb_~+l2`7h-{gA`H9itj9ui-Q zW3riSE~CYLVudK>`(+OM@n85683JzeJXgt3ZYka1=(j-oQ~4#8UB)cv5T@e--a?W6nWc?gHPqmY?I-_!wwrlx!nAf!`Z1YrxCBF8?Nv z%Aaf*wmjPa+bCP9ZKiF$?M~ZD+gjUmw%2UCZ6EnH^=s`H@0aE`*>9oW62BFGkNG|A zx8Lt`zf=Bk{to|f{$>6*`oHD>zW-tWQ~p2r{}#|7U}QjPz>NV516~dIEZ}s&-vcfO zIs!Wf76c9o91}PtusZODz(s*e10M=p7x;YO+kpoHF9!t#MFhnJbqewX%?o-UXl>Aj zpuYtj3;H(bVsKz^WN_=?gy8hx+~C2%j|RUQd@%T2@Gl{LA>kn{LlQ$Kg;a%X2zfD- zL&HLwhqe!Ogmww-8#+9+B(ySgZs@|$6`|`wH;29+`gZ7n&{Ls5HLx{^Y|yqrN`nOr zmNxjX!O^g`VJTr*VZFnu!WM+>3Hz;KP{YWE_J#=!=QP~i@S}!D8=eh!gm(__89pI= ze)#(EZQ;AaKMemo{7m@yM$H?$>x|9jXHfi1@wn^V6OPZ``va!jlO?EZe z-{eS>Z<<_+3W|!3>KK(8l@~QQsyM1LYHrl6QA?v9idrA_O4OdHPohpo{nXUfv~klm zO;ehtH_dB0sOgxdQ<_#cy`kytP4A0t9i10FFnV ztkw8dH?+F7)zVg*TfN@ui&kf2+Q&FzDr4@7SrhX{%z@USt^2ed)_P*=X|1nsy{PpA zt=G1GuJvoJcennq_2;d>ZvB(Jv;8*v-S+Km{Ms~V<7|`JrcayEZKkxjsm&d2*0g!9 z&G9zh#>U1b$6g=%QS61-OKqdurnl|cc2nE)?L6&9wwu}Ro_6cnZEp8^`=IuX+Gn-T zX+NWVUHco`uWA2G`y=hY>EP~gcZVN3{L;~{V}p)uJ7#tq&~akNSsfR4T+;E$j$g;6 z#1+I9#f^(Ai>r;hDQtQxNqVvbqeYf-Kj$-Pp9sk`g9uFskqaO zPIEfl+G$y*%klp45%I0!JH_7{zbbxh{FQ{HgnkJ_6DB3hPq-stMZ%hdFA{!El!@7i zWr=l(e@R@C_(747l)w$I9kaL~$c~`6}*_Guga1C>fbN$U7>rQs3 zxpUnE-No*??meCs9*?J=XRK$2XQk&k&r62bLxGm zPo-{2eLZzg>PM-^(jwDZr^TnGru9hcn>H-1IITQwcG}HpccraNTa&giZCl#zw2#w{ zq&H!1TuHN$IKSJ<cq^WDrZGr!BclogQGG%GeMDJvr@FKa;7sI19Z z)mgV?t)^Y&Uu~Zbl%u`d*|P}gmo$CGPlctF5h&0Tv+u}Wk^NZqrtCMe_ho;c{cZN; zoZy^hIURFSb9&_T%^8+6F{di$rko`?59d6cvn97xsT*Nox3G>NAA11hjLHm z{+MUWi_B}6=g!N`E6SUgH#2X3-ko_X^UmdG=NIM=&o9rf&7YTlXa1`EP5ImM_Z0XQ z^e9+Zu(#k~!HI%%1s8kz_l)d0r03k85A@vL^UGc-y>ff??X|7fhrJH>`m)!zy?*L_ zfA8JBf9un&PkEnP`aITWW1ro9gZuXByQJ?&eNXqjSQt_mQT+mxI3>{L7G#A&rL&9x`vp zz99#P93ArA(2SuyhE5w=J#^*J%|l-udTvOM_K}B2o*MbX z$X`bVjEWeQIjV5f9i#3UwRhC%Q5QyC867-2YIN-Aj1{XUxGdzm5$Y+jwlu*iK_TW4n*-Gj{0M31g>^ePZmZV?P-C z+1S%#e;E7gxPWmH<0g!oI&Rgtwc|F7`(WH5ovw3~lFn_=w$cW_jRq{p?zhB%nL_<; zHeg9T{BjJ~M#+AfWRG|KsJZQu5${j&whs(AklNWc7;sQMID}$ti;eiudT;{~E2kRq zVU#9a1{_XN(0J?hYy=p(O0CY#$PWHigP~vu1^1-G!j}iy=veU5dc34)aQZrI^R){) z>E^u9i1()$HtGPqbQBjE@qzf3Q6F%y(cIjK52ko=-he|W4f2UzekdghqYp&`azGca z$A>|#aTstzaLrc?I2?TM#|GR8e6(552=HCCMtoyv!J&IpZ3o90Nh1un2{g<74H#T0 z)GvCTrWA^jbT}H?rFI6~4CNU83GOulZ|iwl03t|$<>13hAOq$@i(U(Puo!Wbh@E7G zgCXh8pfc;ZAD$=Sxel>KfTsXbrBs7kEW^`Ol;bPS7dHecWmG}g$gSS3z|&y7S3xz< z57z=JM@$KH$i7~e%0vsN;P}&>hwR3y|y2(ey4t)U7g~q`PK2hTKuyd!!a|>+4%4J%53x1->CnV z-I}RtcGprB=4=*Z+!TZ*nE7RhsRf)2SB80-h5D&B6+=!hM>%%XxJ;{u9dYWr*^ufL zT#G)jYwQ2N-b1rd>TJ}g7WxI%B31KR?K{b6OQnWuDp3Q~4%K`4TI~z*T!*|R`08}P z^~cP_ElEZjE3TTyf7Bxm^hw%PW%$P3>?zfI^|6)eMg6_b(5elBWdK@p>n*g^*Xt)~tJ2%2DhraQT+-&F~*HHp*LLP)-14VF%_h;p>3B5 z+yf{}h5uEz6}mj->c6+adse`4L!-Rs5h|zhTo1RB+G+Jhy|lO@#I3;h|HzF)+GvUf z|Dl)B+IvCFhmKXX4Q0|6-HFwp{RUhw>{JhqHtT4{i5F1@oO-rG)RDG$zfp0}v?kEo z-qWVj-Y-R7)eaTL!Tn%_3O3!7c=vkrK@$0EeSmuMT%@Z$FdV~l0=tv?^MhzR95ifN zsNnyk!yI;K-NYn0|j0Y51Ce=!6Ht9FZ&OZ#Wv=1!0qY`|Fv|5+X@ z{v5{h7hij*H^%H8#9iC2+1Lkphmtsw(x4q~Yo(W9uVf|W^daEfS1?~DKrZ02U`da_V*5;&Ilm^=ppvRwC6txp9(x$KrtxG?9Wt`rQpW@G`t5m%`EF-*f=bMtqgg0 zB7YJr9~xrqbr4U%wy6Yr9;@)pV$^M-R>ycNZY0vyKuiCL_j}Ft03MnLYDC_TQ4T`t zjCWij(7O(n8|_3lY6KU7`nD5z7u=D`qg;me&I6!R7s<&(P;Niq(!XFl1|lrR^Dxvw z!T$>(_riCs{ni~6E0kM?xoW1(^8O%{TZlHjgMS)?g##ufVl1&-f4xt^woa} zwGoq$rrM?E!6n6}VRQ7qa-+RZ!hNn_$in7)Rr8@O zTvKr$>ZaTl;La_G69@(RY8uv_YIh89cOBxDGvkatMJ(;yvI_g?Fm3_TVr-JHt7VPT>1t zHw#{BG%+1Q`vqv_o#^)*^nvkQFKmPK*5)&Ivg0+U#-t3^X$w)la-hMm5VQkN+hO^U zfbx|~2mjX@V-cgdyD=_RXtQ$5pk4e9WlaOl&-QLY|0HVg|3@LLKO9z_e?i|~M*CEI zR2x*AOt>BLInWO^=qEUa?nIoUtbZFereKEC=uUIsKUHQd`qT_>G5W54oS8PBQaAwL zGoOu6!^PLc@iNSZAH3&;pZCH)3&ri={ghil9W{sX6UV$idw<3#3pff!U+~eSe%OALs3l#M2=uZ7ERQQ zKnSS?f!zW{0h5S1iK#|yG=kffFrBEqW)|6zFpaTt6#>Q^Oav!LiWq2w0^(qKj5~_kdH$l|`T?y3^G$mj&pz(s*9U2|Q_+C$7 zTyI!*Ky0ddpUL}!V!U zhQJp>R2Ab%Od!Y(AcLUJ5G0`E5tB(w zAqZBWEkK`QI*D=;GjI}!L?wZ6h?yj+30M@20{Cz-i?Lj{aSMqBByJ^;DsdZ$MFd=?4DatC5I*p2AWyKs2XhCy1kx_q~3jo!D z1BEaReI3v`C_RKo@i2);NIXhnHHpUvY$c=~v6jGtK>a74BrqZ3X%g#6JVQ_hLjNZ= zl6a1wM1wv`Y$ovnK>-FC1FAgn62V|myh7Lt6t9ukM&fmXL6~@h#10aFC9#vhjY2RJ ze?-kCvk$rmjrW0af-mYiLXeUA@H{l(I7#KZwa&tMr!H=!FhuIOpb%FwlUrGE%Far}_h4@(zGYs;HZ3MoYaR@1Z7>9F$h=Ykk z)DbdsN8}WN10j?#}KzBwiCA@jwNtZ zj6*){i8~N?B#tBKS#WrP6NnRulZcavQ-~b|9)(@RZekB{DuG4jbm9yG{{&HoI}>*y z?n>N^xI1wV;%wp^;#}f9;(X!);-194h(2k z7$WR&rNHKq#G?q30^@kXSmJTS zt@$?MMZ~ugFDBTu@}0zY5icRWn|LXK7vSZ@_YmJpd>`=&g65i65!Q_$uM!bu7H}M|gw~6->zeD^k z@p}X#Nq(Oo%<_KX4~ah_{+Rdx!Elm4A(+qdr-bc({)}KE$)6L9Cix5EW5maaPY{1e zFyZA>#HWeBA~1IRHSt;EZ`6@SK1ci=@%O~%iGLuzK#(~2N8+D|ez=WU$No>ToA{j%nHAy?kHY8&S(uc%meS4A}NOmL{N3s*i zc#;Vu6G>_)OX$sQ!L zNy3C9mt-Eve3Atudy?!$vNy>-B>R#qB-xK7%n1gN97u8yNti$kCOL%UP?Ez);%p8K z4@Qz4MRGLBF$4=}3G;^WBqxw8COMI03CT$$OG!>9IfWz)D#}PsBRQQU3>apRtRPuQ zvWnzPlGP+@NY;|9BRPxYY?9ZJoI`Rh$?HkZBY6YK8%f?oaz4qwki41XEhHC^yp`la zlDCmuMDliZ%ue1xFp`#ckz7LZZjwt$E+e^|| zK1}ivl8=&HP4Y34kCR+OFqD>0kX%RdNs>>Ie46BXlFyLbK=N6V8%aJ#audntNp2?j z0?8L~8kFQ#k}r{bndB=ZUnTh($!#QGC%K*E8zgs-{42?wB;O?Y7RkSn+(mLX$vq_B zCb^g7J0#yF`5wuAB;P0b0m=O&KZJcR$&X1MAbF7FCnOJ%{FLNjlAn=0Lh^HxM@fD` z@)*hEBu|k1lH^H}r%0YA`4!1CB!U0Is)G;^Sak66LWA%C#^!zR`{0qgf_YBULIwZn zpZ5m}@?P@&DBAliMZq-(r#u|2wHs1?0yXk}>pc(mPkvxq@6^-Cj$atylin{-N+jO8 z1g*fg-k+}eou!rDv)+?nn>!)rNyrebkPm+SpDIyHIRZ^sE6wT8N1(De*V22QdTC{-a{lB`y+JcDI`gbwElt2LLh~m;UQti2G(P~smG;9s zTOn(8g5>nyb+y{0s{eKfW-8Z!Lt!+1gBWLZqyQ zD4c^*u`d8kK?-F5KXQ#By|2WtG1eOXy!-J}dV=}*72+4;Jp~b2*}W)8IrHy!E&pj~ zn$z)XfmxvZf9>K>cQZ!=^s)M>XCuUFY*8;q3={{2z*@Ra1$5C{PKNc>e*d zE#@U=PE-6Mp*C_VWe>CyqcBDbp%GDGeU}Z*PJ8NL%%G!i-_<&rH(J*7eoWu~N? zkoRuj!A@5>Nz(hB_dB$2FMb+jSLjlM?!}2bs8>L76y+zy!z%TXQc{7+kjjSMA<$fv zE|k_zsqg@`(qjCTAJxA_NA1a%&Pb2(}aG99qW+j5XTwhq*%jRIcDZ$fbHmebHup@#^n+pk3)%WLK)t zKm2mgHD%gRp2}&K>ubr?otFEW%Z;AT2)n1H-lDG&#JDVJ`Fb6jYH$nq>QCLfLFkW03ZLL9Nb}Kpz#*C+ruoe^KU(wkQELX+ zhBy5%YJq^I0joD#YWbqzvj*84EDT*5vNHt6`@t83ci$M519w;2wc>{H+Yc>GGbkznqTy1ZIP`H)~|JOo(piSJhJG(zMVIdxkMkx<}xoA_Q5k zhH8h|17#13AEE4#IDK3Vy^jhpgQ4X7v0fP-!13UX!Ifh?LdhQ@ALB&wXjKMs+aPo@ zuMSIavbP}+f7cQK~PZyb@v*;?aMZV}M`iTKzs+cCKL^Vczmbh0ufK!KC#7p88 z@tSyDyn)k)Z;HQ(-QsQWj(AVJFFq6>i%-QD;uP*1I48arKXCvDaR@iyhTMpoa9fVU z3I8K^U|d2(m#K~{Fh!yQb2N5Td za0c8d+#*e6h%C`X^uUSm0-Ow2XWPqgN6Ji5BkFL<9k-w0r2AI!vUn9Mbi3FgcH%~b zU1E>eE8Z3RaDx0J+<0C&O3M~zlD1o_V8YQm-q1pIJbU~5Ak6>!bkZSpWu^xn$O@KlOOqKoMyjdoXl^7 z`zSi#MhXXJdb-S%oweHyM&h=T@v>N!$SGJQ^W=?kK2E(az`6HDa^E%ec`*_HDp*K*6SrFgVpq?anP^eC94h9CyTntVW_L{BrwbfU%M3f$d(iM$`+EfkzJH(*E{?K6KN!CG)t;d2VMURysr%r8E zE*ne!Ue16rRkaQ3Z`C_i4wGMK0=Ye(`iNEHFh_8E+^sMOw<}EG>5|nw3UZJvmDTb^ z`6ZNyX6fo~FRu3#1z7ck-jAv;m9!zC8CUgJ9M=N%toZ~ESa7v^)_f&xnB{7<#_W4v z_K+orwP8N)16D1za;Wcw<4J(?@b%LQz_pnB4%KR`t|;UQ#-sXORYrrhmaT`uIg~idDw^YH7@`b|?p%(OPK%m{$r?dR)atE3FfOYPPzb>~(R?lAjE4mbKwU$ZKg`c6ab zJL-lnbtjd&&FdMhE#SSiR;X2mHW}}#d7cKY`V0&XjsQEJfw{t7%-HR~rJE7A0dnnX zV9$GjOBMmU&I6XNLJtVM0}P_(Hux&VVP1%y`A&7h}Cn2li9gisdQD z3yh~(@MmL9Gg7C)pDt&>pD0V<7h>mzfi(-@cac5d2V!3em_hA(@waNn3z+o@_-UL6 z9}^WmGh{Du85_L}d~^^!_rAou$2~D!kop2}9xTkUNoLI$xkBs#R{y`mZ@`x7U-0Tw zpmXc6M*N@i>;I1H_)qX0K2H5V%%}fL?l19ovQzhZ(vwe$RdQ5?B}vT#rS(a*cY#9d zvem*l9X>!>AR0@;eSo|Qr*O$gpEO|_gaZlM>_Qk3#0!7v(vq^F? zB(o~qZ1dlf&bItH`AnXaU#ZsjkExb zRNA}NWT!UZL)$6MDKxfm;7!$ioyp({(!h~s0)uwO&RAEN`lZ1AAD;63xfdG-RQ z+6R29;!=lz*HrhPjR1!^3YzsX;4UYCPgUnr)%ose(C?N*!(0KLRdKAdz^N)eRdK2F zff*NoFTD*o@^)~h>gKe&VYmj{V{oDOfj@l!IP@WKo)5!F?NMOV$DoB?gPn{gfM1`a zr=YQ4PtO3`K1&%rOjv_yA; zv)1M6RiIYuuqvPVw-WXN$k>PLN!jPYm;VSpw7#qz0ckrL@^*Vo;@0&~e_H>`>gH zr=;={aI;gOIjjC}O6D)(7QR0rn}b_E1%4Uqy7wa7Dow)5{WUqt{@j+-2 zSJ64~uy}+rU>*3eSS=o-1LAR-D%Oa#;t8y`C+Q&8+|y#cy7d(|;%&qop}5NzH=ezK z+vc|5%*$4sRe1>-uUEvY8ZP|?I>~LM^pe}5ncM;08u4eb88b0NvP!(2jkqI5hB#>Rw=V1KwxQb$_nz2NuV~ad84Sz@4PKpx4A* z!7%zO$IWC3?I#gNb1(hP8e z`LO%zPUE35R@l57@c(DRt9VAmF|rLe%lU(j2SRfi4D5ItZ2t;4l&)|CIu3nZLk|bCnqQ}$oCqqhQ{BzWDMDzs>bkIVwgmUYdSLbUGWgka3V>cPle6duw6s{ySx)OR^A1ZzFfW=CVg$_K6R(CM)wp|>#Oc##vPw4_e;a7Fy0;lO@@;~);uGEsTfRDI zN4G#f`VusxuRuro8g99Lowq}Sy#rKv74+CUp~-#=y6j!NoBBa-`ZhGD??89@9`%R* z^nGYh_w$Fi1Nvh=Km(vl{RG<7PoYo!3>wwXp;P?=TGiv=KkkKY^-E}1Pw{EoNBI?W ztY5=&{7abX4TQ{c7&6N_+*kTNG_OC<_nQ9YJPm>t_9y6J{{c+dxK7%zQ{hKNpb-HyM@d4sfi^^j()F?d?xAgn+ff_gPT|Jj>6+jk!lp7B zHhJ@KdtwXOQnr#YpelBHUbdlQGM47zUf*`m!gqikJ`S4rc$pv*WfG*q6m^TWbm10j z547`XGMyr!qt8^l0W|fUAwwy?v; zf{W~hTSj|B@6s1MPZ-?;bG!#>2;{n0a6Nv|t(lz7Tj2ACg5MkfBfTd;;fl2T8;8na zv>5j(j=+tfxIbIno(;M;j)sBuO@Kr>5%)%x(687tC#zX(4h!i!*y2S& z%Y8d;(p*fRViy8dUD%0O0$aQ$aw+yBmeUCAN!&{}Vqanfb|zNB4sSlR;19tJ?=RS+ zcm%r?tFce%N6{{S)ZdKc!dYVQATp&BCYlV*fkc4vN$%Xs#v(BaI@)1R}oXS|ndJR9$2XBf}fT7H+qC`bQ1@@wTeTn<-2 z_Kf0^>dK0M?8+&X6{XXIv#ZN0rf4h190`+9p)_0k+>SYSMR z{PVT$@z2)^SFz{~qnC1x*5(?m%Qbo_S1;Y=@C4^yRb9NQb&SK~GAib}s$xhtJ4`DY z70)pmkaJZ7JkH#Je52#^24dA#ObN)>J5jH^%NT2yHdy$k)2M!~e^0G7Aw7K^tG(iM zxq^FMRb$k^t-qD2HQM2EXwN9k=qgvXt=B~S1C?Ial;v?4O>h&`YBnx7pqpr*XIW~y(A zx?Fm{d7K6QMS3R`)$4>*qt-w(kif)Yqyr&%h&jMmzKL*PMCU5Tf^t zG00Ez8|v%Zd}E{vj1kB;``YN^{QL$(f8V#xw1A;T-wxHMtlvJ**$u@ zU2bD`6c}BemhC@E`)u&2YY~F}h6kXFE2}82gIo@+Ng#HrNukB& z$oa}Lnw+iD{fbLfLUE~9Qc$rrWWKCMS7e)Afl{?76>9J4^D4Ml4-pksJfo_#x~8~d zQnE6t3Me+{tPvVTJ6t)rfyHVPA|hA=vWxklA;lhH3mopZJKgGw|6gRCK7H71pnm#Y~L*y2ON>cL0~F0mF#10QI7%H_yO zjX=v)n4D9To!d8mNOHQ44c*{h6f=`tJ6T^%9({GW9WJ9ruKLZzC|kX#!Ph35y{Hne z>O~b{_M!$~`=;58DpBvn1|^l{l@&9at|G?1*@k#3Zdu9Xss;hR`mUO8JWU47N&WCs;jS69xSR5@EC zq`)i&!b>$jW-y7V9-CUTLHd#f(E6f-^tYz;(GMt^pw#2keX153;B|Im`lCmVn~6V z39#kYRaXWm5A)eJ88LG5_{n~7v6kvNx|(lC4~l4 zN~>o8?@uhRv2vxlgS2teh?NETR+1yfWuPjTwS+PpHnY3Xo5tk#Wb2>vWSgI}O$BQf zpgepnWNN*p)q|1LU}|OM^x}z?votyxan+Mm5r^BEqpwhBPQ6Hn5$W(nf~*d$9`(S?;>>gCy_@GiR9Km>!e$PaL!0@SuWIIpmepG_L1kLAVf-Cx zLB?xXe8yVHRcpbRswU2G1eIOAaB_{+mFrtw?hIonGt9o#3rWq><5TmliU(cM8iLUG zy-cHsOe<@dv7C(1%9`p7C^MFh5hBUIOr!KcdSK=CEv1+npjyoRlVa%&(E-xmsJ;Q z500E{W0hoEt0YsOh0a_B^ML?#pE@ECN- zW6gRDYp(INT8%J?FHf zhJ0HkUJI(U7HOrvNcF%h(V6Em7O4wSVU@mlX5@fWY!JjeA3@AB)>NLkrjW~^k$FBE znPEY7g|z0@X#v zph|Nggy@hq6~gMlTBiE*$Ki4r!{O4%b(N}u>PL^ez(9BfCT0qP*i%+H3G_HhhZLU; zo@bJl0Y`gU{Bg9l@cgSv!Dg7N-D#{pr?qa3)$Yp4w9P~vY>MXjd4TU>YHRvW5zhuC_wxI-&w6#C~eK8=j*-ac4QmKA=^R@P^aiT0z$m1<#*(0 zSP^(5)21lDtQlYHU!&Je4-gqtV^%7p#wX%ved$WgM4V65sRtXMz*h~*=u|7rzXqhd zVp3oY^vD&8>(c>a@I4-boO!MxXC8w_c`O>`UsHCSnmQ^lr;dr@J-G(y$u$R9YpgR< zQ&^xvnSnJ`#U-WYi*9$SO<{CdJH8CA&Q`0~6XmHI*Xkpq2WG3BX$HTQX7(i-YAkQC zA?8%`xQv>(%$fw&s#S^%06?Nt7wCZ5h#Ys2HjOp5%YuBP=<%c(Wu}>BYD3{k&GE0D zU8(v+1y*r}j?3e=)#0l_bruq>(~+nim^F6hc?=}#K~z|sZ*0vR5TLXi2oV)pcP*B4 z=4BWqWSFlw^RkVoY+sZ?5%MgG;LJ1VNS;PVG%S>F;N5)nt)RNAv0uJ{f%Dbk@T=3X zpXM7M0je{Qd%h337Z^yaz=v@24IHUq8r7G0(Ljj#K7^QWAjEuy%No>Oh5fF2GQN)a zU~n={eeoLDuTBrOTm~;yr-v#^ThDb>I!94gHL1d{PH9>KHQy}RlbY{er%jC@JwSqO zmX+!@CY{@wbhg<@^q;M-Ydt_j$ZX%}hSh_S6g1l$)=&%5iqJ+NXmaj{T=0|}!KcE-QvCT!g|6IMZ^Z*e-b4?@|Jl7{&`9PygP?vOr zx};mnb8abwM96?zXrel|v5ei;GFB!w%2G8LR%~PR)20-uoi>d$PMa#badqZWw-jbv zN>Yjwka07LtEZEF#`GD}wKI)2q}#CLs_fd8buw&uv~mNwulEnPEsvVf` z{RF@`SV;(+*$ssio;t@KW4TDc68rY|xi@A`08}DYI~EKp1hkR|IOYz^LHP{Jm7DMp z?LQK0-||QLBJq97Bigs|Uy7}-sTw)(6R`XV#k;B=68}?hv13I~Tn>W&C>fjSpR`TY zx+~66c2zd>`u{dI>#Gda4l}Nhf^9`8?(~krDJ#bRApZ`huMWfBWEW0oZH8^jBe05D z4EvfYSm%s^MNcnS2gTtz26jm{>~f!lrQAW-((Qz;-9}jD-HS7q^I_pvjuXZsP?P)k zUVCl&JV3W{s(o7eA`NbzISx4=)p9-~zUc6BdKz+G=4U!=Ojosd zTYMT5(g@OSZGVZgJ26r4{Ien%$t+18>IT~VeUVqL+5RP z?&Q+WKX>T^XsS5gWo5eRyA^!5>&C=iRUFG!Q`4gXt>(i?Rq4vYeyI3$P(*rjKnr>Q zpulvTv7s9AbJmZYdjWcmcW0gL+zrqQ-rRZBz_WmsaPKZLo%;it%bPt<4OD$Ko@d~H ztu297D0NAL>i~xp(HYm|X(2At)4~(Kf9`w;`S!tWhr{EKiJ(ni7cfW!AX z<9nTP8n`p&aTYA;vc7{m35PPXHp1Ntw-^rZW#PT70=O<1oB!1R?>1MD!UgKol>m;x ziCCNl?t*(0Vqssa`aZ$CJ+qe^v)cQWcdchn>RQA#^gfYNkczpBn@apC*nJ-3JzI^p z_j7NkXTD3#t+w79vsO8VpghWY3J$%Hg|qmXN8r}Nsr7#K{}1;=DcY$U5e3^^39DRn zng-`7Vn)Rbj43c{#Er%G9*h}g+e9M!at+!fz6X|6fA`K;r3jqsb>oZu;ef}ok`czh zg&Cnu4>K?Q{y7t^$vh2r5El>bLbw%foe{43r=iG1O)_tWtHyh%S>^~hd_S`r9L{EB z#v^PF7Y>JB$-o(f44i|=I0pD2+%DX8h;L6)7SzRp3awH)SaDGnL?-0=*@8}+kSEf7*LBd0 zbM3OAZ5FiIgnaFFJ#NOi?zAAQ_O5EzRJ}LwUn5Ouhy_)<@{H6hE6!m-9bCW(xV9p~ z#U|vu;5?(h;XLHrhbw-7A)GH-(0U7c(1I3Q(0mJ8?3`u1TW-aTv7jOg%CR7q1;tv> zA*+-KD~?UbalwMlSkNH{>Wuqr-}2So*Fz4g9gg*m)p{!(D=cWC1d zP>;b^x>%6)T}Pr>14l~-&R#hf&Vo`drhKQrlXB8bO*w2qA6U>X3)*Hun=R-m6LPdv zJdOHB%DrYEq%5$Yxn{nUDhrxy);VR21r=GTy{x!yR$Q`KUcGj-vr?O<0H>(G6n_h{ z;*x(&zM!Wj8?%ZCaiD1GFIgKa4SFm2#pJb!c~E=OpyZ`yT(Xr4Xuh7$7gz6%S;@v| zB~P@V5f(HnxxdPbuRslLKv~I-N@Sx~hJC1+XXO*P|^#+i_+X;Oc!&ZwyY;SC*1!W#yJHw-Axf~;01 zIn45sI+&2J50aviLiHX>VhchkdMgtzSkRfoqk6u?k1c4g1)WLUVWe)g;x?F&57Q(* zZsMH8l_uo8khsKncR{_lxkg-7qBf5chb8);!g_HzDxa2`VL|2_$%*X}BNIWf6ZOL2 z zMTgWIMjYNSAiQBf<1A>1SxQ153+iD)s$}E44h!mFHa!8Q7^y82fUneF0-I3$MGHD( zK}RiUU;JD88}YAN(0U7c(1P~GFE;Ycx8i1*P>O}u;>*pr_%Rk#6tCf=_-^qY3yQZO zy9GtXW5&h<6UJLmr;D9{Z98FxcKX;%?X=g5+hN6Rwc<8dagSSZJ36h@OYXF_6Xv8= z-U2J%Tq|Fd6*t+68)n57T5&m6T!s}FXT?QZae-!B-2Y+iJK&-^wz%(|?Yr!7pfL%oShcZWT%DHh_uTgwz((dK!{CuLpF=FA4J+(k+w>tT@G0))E8-O zH4k}FkWMV5Ur3rQzu1uA5Uj*PoI;er9h4T_5F*lotN%<3i4Cq0 zQc!M+A4a{3?G&ftxJ-{G$$7&B_{68tR`X|IU1St4zkNE<8ChKRI0k(MUXVntf8 zNXrX`B&^)DVAv8?uF}pI)M4ifY7lAFc3SX5$_T0u`A*pRg33kOPLZ}zq_QtZ#ukaOd4bU4z*zisV!?rE`#@+wpwb>2&|#+qG>Einkyatn zPKdN}k+xN&m5Q`4MA~YRw#-iJlqFzMCoQ1ZCTVa0b|+N6fEo6*oqgG@xdu>r&F zxdar6v^HA%++>2bA_F+Q|~+L z7AUr}`GlCF+@8X`(+n#>f6QNq*lH15X12}AUHFR4Xcv}ZhIyh58!r4uE}dAhxyTkf z!<=Ws(#*EHcdq%(VjGx!%uZ${$N6{oH~3fkR{%TVUoO&i+G!8tGyC{&6w`huqTiBZ{XX^Z_)H-G4~|3rV(#2+>A&+||7kM)O?{yzTD2KwW#G}(H@)L^HX zszq9b>4d;%>cq-LzMXbHe;<>r?U5hsF#YM0^J+20GJ6WjVOnG=HqAE8FikWKHx-%k zx?pLhSW~dc&E#Mr_E^7WJI$|Fq+JzhXGGc|k+w&qZ4hbSh_p{d+8)361PS~$_$?O7 z^;;nF%@z5ki?op5zI2fmFVf5+%|oQ=M4Hr2GtIEeXG*inXKbyt)*k~xjM%yUTz7uIB zqAV-zvKW_$w1pz=6_GZ}PJ5Uh7^fM>a-0!bY|JyJ8DovXMxRbTqmvP%jp>xAFTNdi zni28|{cD7LLjU?U*lE7iBCW#LrtQAvzB`5dd^d`;??hUONLwk=Dtxi(pg%~v(1yJt z^34+Yrirw%B5jCoKOsfun#MQVK4yHA?du%hNIR_ypShFf>tQQ5+gE4HZ(}g(;VXr= zq!xQFKJ}d`d@AiJ?BmzxoX-)Tz2M*I^PNu#jrCdfA83oZ@D+<`nPp{ZXNWXrV?i74 zQ{uh`|L!i+z1R5>FMnf4k+Fp^i-A?Ol7q9hp zS-idy<@?x9>%zDE&$Q0I^m79g(hJWXP(n?2UkL8K8o&7;|) zmX_vm)#HrEA%^Yo*ecRWMcNl4ZM8^SCejw!X?P;^ElKh!&|LtwmLxgl>FCqV-$;>H zuHR%}TS-x3jo(OMd&o>vwikU;^$VG4sPe*-DtuwzoP;_98_#h`s5!7Te4W=&hP}i! z`%!5nxci0kyUMWDe4Swx!?uzn&y}7umn|g8W2EO2V84AbYki zZW81p$Q+My-?hMuBvCfhi089>3U1wTeR6vYI#I~-_vGt5aw&#;XgH5=7%c&`PZ@0w>1&+py9j9|WT9(^?=JA& zBnv(Ad^ZDYCq*IWe9^AFoJ?{5&bI}$S~A6Fm2W+022zy5dC)d&CP~h*?o?+g(09As zp)>p;67HVn>;r5S3HR;i>;#OE;*j^^zXM-2DfWzxr`l6R=D1e*p#Iz;GAF^w2leMZ zw3awIdC*pU)mq}b+ykwOwC^~TOVyxNlXpDEOIeM`R@Z)b`$EDUCtKZ@uvVX2Ok2m9 zywhVAzR0Fe)!*@rr7lr;H&QCgW858hPXIUmp#|OqH^)r^lyHqC(I?pFQZeJ7yE^`PV8cj&c40g;kk^qSHz%J$@HLU5;Ke?8qKbCx>$1ux1+<%_ zZ`>K5XwVE~rgynJ^auAf%ybWRe+t+^oEuP0LC)fHRo1iISBU9DW4L<0E;y56n3#;n8h*>oDiD=>2kn$#2A(TsZ$F0mWQuv2`UlQ8-cta7)0u3sT-l2snJ$n|4} zsomzfo&wgxs^(9*9sq_h=T`1bWqpIsacgF!N?4g#hiU6suAlLB+BBggo{lWqlG0)- z*KT5V2=}T2HXiO9!#PU(kgs#2TzJpID%aj|4TjWXNPz3wIEodK0QKu}&=c+>zRo$G z@jY!-y9{^D25qcWtu1n;Z%4F|BG>I+6~L@$%{^}6!0O3Nhf+5aunT;ht03Et9Oo6w za%mz1z1g_qc`_xi-a8$9C) z!mtf^D~H8GmSrwYn%PKknJn|VprND74f%cJIvlj`v0rE7mTTne0)=t=71!)U=SEl` zPVFT0Yb#&jG>GL4ZE-3PF!Z&c6JZF`K7r53Vx+?@IGpMNLJ|&g7`8L)J}h}Kv$AJM zk;{5d)EZ-KN@BVf9Zy%t6pv~zj5gflSETyXWhiL1q)3zJLbbhtB)N`vt_5}+vqhRS zwL))`a7~G0B(O;&oKri}7I=r1-a(2DCD9wfeUTJ<42u2^*i|woV6q3bF}ul}==C1d z{-I?&zHz}AqAlaR(mLtX8X7uD3~IK(t2#c#e=n&2W!uV28<^ zxJ1u9V5MY^%S6vKfQeUR&_^~Yf1;TH$@FeoJbG-8g#N+`hb^Zpa-?x_N zPq`R@&9#=8_qw1(_&3QX9?LvnvG|$f6X%s4D}g;pZF2N@)P@dF`xaBV7Fy*_@dhgOOshO* zy#Z3=y{01FZ1-%?Fl!jH-RYR!2B)s$XPbedj@%_ZtMV?|<4H@X4pXhtmNOq3P{D2yFLF%5qLX&11VO0?x%hV`<_4Q4&+ z5APvzWt*P%+D2G*lVKUK_sMCkTf|;qZ*$EiA#E+c(sinqYCc-Q^bE^o1>}up*bu&_ z{s_a?5}hhO0<#s~1myI>NVSmJ`t>xHwwQ9UAr`TC@6YWrORJV>B%~O^u*+2A zBWO>)Urc$l;7(&PPr@dAAzcgF0+Qr1-SH5xxg;rSx#J#SZ@_kPu4Z6=BSnr*uGH?f zkiLFXU8&t|Abqtr!B^lPRt*y1owVBHqrM=&~5tX);Gw>9PRWb<9A5 z{kz21safmbeY9fD^l*$mNKouJFT4QQ^JI>CxyxW+)5#o7p-UfNv-mm}q5feQ=jnQ? zseaZH=~g|g7nR;|t>gkQ3rP&;T&OH_NTR%-3zcO8nPc4Uav9h#GRNV9%PC;3v`xZC zq7@dfHn~j#Bpbr}A!a2ha`?)H=DGw}feWqwTcmGzqzkS8-$`G6I3%I(?ad5Z?l>IS zJ6OwAI2HlR!LvB3hY~Is8qPGAdj+GTiqX(lGQu$EzH>Ii(AqBBC4tbB*(5--J&bC? z5^`GiVi=wIj&aRK+Ocf3866mg(Pk8K`G5rYHis=o>{6T?oM5r@$ZXvzhBcCzn!UP> zG#9?kVKSrL#_pg4?i(fC36vX*d!Z!Ui&nYga4l{3YvgtL^r)+lWf}>Pdr%4Z0VF`0 z85R#rftjSi*#&$xq{ykr8S@P0_rAW9;S@xI+raudg*!C?`wTW<9xLqzUl;Wv!(Ot! zX`A_=$67(Y8j_^AtfltoC`r=p)za~Jn-pQT#LUMz71LZ#VIC`V`WRRXnG*Vnb2P9^ z(6-IaXfdqciZySBQi;x!VrMRtN^}luKeZEWGe?%gZ_uTAPIAPcJk4U<&&J9#yau<3_ToxVcdaaUHTgL5db(PX`Lp0g3yDDt&a zgO;{TF8NwZw1~x8q(rkpi&+M(S`yNtU4qyJ^5KLE#&tj*Q{q2iGP;38^)?*kzz4yqKWZc zhfQSK%qMUq$|1~R2Nu&>Pv%d1r#b8bcRfkck7jF!BP0p6r=#f^#`8Ev=z`!cz}=U& zS@3GaET%EsOICI0RW-F(t<+&L-fsd~X-^Z{wpgh!CN^ompo#T59I|aEGtA7CP67*=f8oBUNiIkK$4h5!_ zNcH0#3lVz-ce)kpp%v(th3Zm0)r#{NIXfL`UsY25hLwe%@?x5QC7BX5SWj(89hstQ z(Ni088CtCm$^>>3*!G}AV0&Q)*E?nd!)ht$Q^#bKITP#8%j#30!Om#Hsg&@>QRgXG zo#Q0Y@qptZ#9knY>ggB^67DCAg;*xXcDh5N)}Eh0)~iR;a{1|4W4Y+_fc1t4ST)m{ ze^Dve?8uktj#aZ|z7S2R?ISUV@{S(-M@KP2Y&cd|_zd^%vO{&xB z_5r&`iaZ^3JAmCJZ|PcCzc-M#9KT`xzJhT#k~8XHwFx{OT-eICNp&(tKJ0GLT0McRr8P{2=Y?xgCqE2D)lm~@EjFY;@625Ojfzh8bBU zV5j*ysh~5P$!h_Nm1t$0gp}$|1xyFFADC0Xcwlexp-Q2zCUTsziskYX8K}I#umfZn zr&iKleX5w1NtVf5RCGKYA&2Dk0kobQkk?=>)%(fxJVa#%_b|r&2YC&thaxk1lt*037_bb)rnV$m9m6XW_u`;GoZuH4A_dF#XneJq_ zLW;G4gpVSnl9A?YU>e+axI}^R!&i_+Zk2A8z#PdTcPBIL_q(Jtu)~4s&?nXsex))P zu}-+Ja4g>hS~L9Y)U%R4W+fTOCG!lbT{xw=q@2!T-eWOx@|wJlA`)pT$w^tJ!&X*G zDV~T?%z>0jSNoR&(~;8P6aLitc!Cz|zZxm_lY#6>GdsC)qpdCBpA%b=r}^V+3C=B+ z#qc;i3Z@vBXqC&0WXFIdV+TJ=?f`5%Db-~9_W|}Pf5tyk=n0m?IE?NJmTnyx=m2{K zo*7mok}U<+PD=gt{(4|fS=9=iYzMFwQYvr23YyN@rJ7S%LDRF9-L9W%Y7u*xoaB#b zXz9x#he?K!z$-~9f7OK6L?4yP+DtT;Ur|ynE3KUjCT!Ty_fho|-mt`1(xP^Zar*=ga+!HLF z26{fk1WO0oSn8@*UjUDe{3cz=`VkoUib=xGIWTX&=CV^pXL8h!{Ge_Cw}G6NWy@%J z)5uv)t(k_r=drwpkfWShHXGsRXqcW|kyWzG;GBiqE!FB-i1`QO)KCrgqbrhR7Dh`2 z?(q82I2VScVsc}BKTx%r(bl2V=W$jlq1u9R$oLSukzx2U2sb)TL~IAOB9fh; zx))QP6=a$5s&p($h88);8li{WCuFy?PK8l`6S-2dP+1MkKnD5?7V3R+$XIF&1mCNq z)Tu&Q4(ulFA0^FI&2qgDT@|cv1Z?6RsSZ3#V22$fbZh~W&X8mRdxo4epHm(H_C88V z_N5rUl2LAEr>XBjuZObJ)XzYxr6;|(6_YdXK{-^mfleq4v|fNhYt95g!lfWA$ofxl z6lBmCSUAY(OEoZ@g*{k!IKoC2=0tu6TNtf{IRHyF8pfHboL|C!jGR;~r4cdlj8o3S zI)sy07#eEW<HR*do%Axg-Wt?aDQ2ls>*y){qD8;C@=nEN*jsPRK12ouW{zY%tKlH2G7|Kcu zE2iA2iDQd*HZTL`nJRB;|JFh>HwpseF=}UJG}jMk7*e1m zy{IM>u`nCgUUWP?!on@&xXj3MdCL~QXUp|fggI^yqh2TVUV^OCS$Gj_fKav!VSK1V zsn|PJ`>dB@#lYPnrJhGTj{s{%Yaj63i`aLus`|onJ7`ytf=bA7{fcS2SIIz~V1pyD z&Jp?)x?$r5x93d3crE4p*#^D8K^JZVAzd3 zE`<37ON6(DBCZEF2*0WD@%1RYGdzahp76Xd2H&4if8M>g$?&U?56=pVKwFI8C%Gj! z1DL_R%dLR-h4*nbHw!cJ&$w563p~&)VZIPn!rRv&ZWVV7o((?b?!c$PcIMOIH|F7M zH@q5PpTT^4?Z>V3VYq!g3j4Hr+!mj~-Gk499DF>M$M?qRWFc;Wm*W=q>A1uGY2424 zfNw_^;lBE}@$1HJSa)Y$06lSM`j@z?p5B%2i@Va-;RNs}+;eWiJ?F=8^E~c1$G!9P zUh^Dwmw7M#CVunrb!r`7fP2TC`9An!)r}vDJH&nP1P8Z=^TSC93E@Z5d&987q-U<(ZIk*@d zw=NSc?p&Ts9N}Z&bK*|#E+z%Ix%fxY2e%dPAOmnK@$Y0fZX-TGM&lOZ(_|uU8@^0t zz~3*25yz1o91mY)9Qhc3OKkra+Wx=tknk+frr~cm(qWtsj13ZM`wt7w!hinH6Boou z@%b8_LgLe=+5mDLy~TW#Le) zDTT525(_UxZYU9)bCDb51BRBf@$_aqgTy>1vv5Cf(me3hG-4!N7{1~aMt=(8@qry6 z_DaY(5+MmU5>Hf7dti&fKbm2aMY&$1-0;WFaUCqC06a7#!F~UAHaR9x9^8IE&^Cuo zL=4&oG#@~rKF>0qH^npySvW}ykECJiZSWW2Pr?o1N5MK!>}}*LPMLQAs$FQQD<8);U{NT`D_|uQIUP*h9 z{aEjK23qfX#@oa8|CDBJ_Eg%#|F{3vCbw#Py!A4kKjN?Z&%sIH^HbDFBV|sBJr#Aog#|2jLul-n?l$V)4F}T zUH>a(SerblMd7T~%3Xhj|3C3qZ~ytF-YRCUv#$TsAs4ssd z-G7!Jwqg5U$?31~*!%ZE-iPV+LunfyO!xmIq%7^43$=w^!~ZL8x2ykaJ}f* zPoCQZZ*4>H23~PDKWfXd*8gAprfuOmy{7HP{(yQ*JHQ>pKH)G|k3GWw&HLG3@#5ay z6Lk?hM+x2qufivL1NBXWb$+M!Z^8FByzSum%v12P)9H2nUs$yZe#L%-x5|I(x#mHy zHC=ty5bCk!7<{q+8$UIjK57IHHPjo;pMB84SG!5l>8U+~`r%|gSm1xf?tKOR$0jq+ zC7qtvsh5&J`Cf-scotLDP%+qp!B7U{7|diafTAiVsMW?eYht zdrgA<;%GqXeNPKNq|1R*FVKYT3kkOwzXWHzyEp~g9V*!FP>Eiz=X7j$sDihmdz>TN zBg)twQAR!AaWd-r4p76xyD>bHYq2NL@$dx<4`z_&4(g?!)^4_l2i3oDlLx;C{Rxa1(C= z?$4v9yqPxx58wlU2V!@^^Fe$N@L)a|cnBW?oO(p#;SnticoZK6JQ_O-2_M790FUKk zfyeQ2z~k|*7Tz1hu9v4i(g1t#JwQpp{)dORv{c|}Jp9dK@7fc1I*(gu;5{t^cs8F6 zJcrK#p2z0_&*$@j7w`qZd-J`47xIO`2k--c7x6{7${tW*N@Mqx{jfYpXS-_uzZ!{jh(VhqX0uOJ`@R0_OvDkUP1N=RlfAa8a zwi56M{s%zpya6|`-QGr=uJQb4JO}4-_tRFu?fiDY9oP@@@KUxDa2L+mc-#$zdsq0q z@Knb0`>>zm;is$|@Bq&9cFGl>`VG$=R1i^ z;%nK%FTi&Yv>`0eufTT_v?QKU{04j%L3_fF)^6Z?2wIi=4v$&{)(LG3uUdZq-$&5G z@UB$`yqut&$$p%W5m+s>HZ0dc;D>N-z{7?e13V6&TLiZ31n`qMkt6WEgnY5cCoJa$N*oK`MY>f`={w+jklG6>U3-Hy|2)v0j0l!D?0dFSF!0(g$z*|TQ@K(|a zZ^h0MXWT;MB5?tpC`rUKfh0*1r@+Zd58x@16yT|nRN!fnG~hiYJ%Oi7(t&44GLSYC zC-`bf7Cdb!B-xT|;5m{U;3Fg>I1T(^jpUR#n;8RqtYj?kaguSs$4kZopCFk4e4=C` z@JW(Mz$Z&41AkodI6R5H1n*Z;_`P}s@HNS6fW?wxz=Yp)-S+!;u(xH=A+*@N6bmPIT`$B{SLTKvJbFa zQVw`PascoM-oTbgj=^)53_i0?0-lE7EE(pFrGPT*^yz%VP;f+uZ&w~F#ZYv4&59=uCANiO-^T?+Jn#Zi6UIwEvuylfr6RewH*940u*fPP23HD2{T!dLG!A=PlO0Z3WRTAuxV2K19Bv>E8 z?g$n~ur-2}5$ua#Sp=IxNE^XzVekvsj*)DVn4qI(i5W0R5(F3`2>}d~gaO7$;sB`y z5xl26F?*q6_Cmqzg^Jk=1+y0_W-k=XUZ|M8P%wL;l2Ci0V58BQjYdZ{4wY;KD%sdm zvQejG<4wuNn+qFndN$r9Y`jU>c$2X4CSl`E!p56~jW-D!ZxS}%Bp6{!=*bq>OFl@R z+vPXWj!WI7%I?3C7R>BR`33v}8pF@F^Zxt){DZmYVr^vRy}IWsRYIO#@EJOg8_!L` zlZYwYzwiWN3G?OrF2-#$*WFLD57s0l)Q>7gLKBga0Qme&h8NGC@ZXt5a^SZ!A09gk z;iYqk&F}X=$YRmA_ji~p^5LIw`9oIFJmxm~!xMV;M$ha96jLivVm-xqfp!9Z8egzB z$-EGgjTpRQ4Jeb0z`3qTGKRr%3{GHh5{2M$0~b#uz-XSXEL4qp)6jT?@QmGBgZtt* z>n`MzPft`)P6J2F>4|ubdy>IkfMc;v=J1?C$Dp1;CkAmw3A!7D9teLiyD%-~iAaRLF#4hDBJxQoF(4DMxcAA{u#9$@e= zgGU)W#oz@7D;ca|5c_@TQeOstWNO=Bl3-OvaV7~? zViaeRVD&|DoCX3OWAFrnOdfKE;pZ4+vSCd{IWIGKl|d#g)=rdO&mhjAfj2SO%wP+J zSQk;4K_SO+@?xrk*kLfO8_Gh7peym`i_i!z(Ao@NyFm;FGZ?~PD1%`P4r6dQgCiIm z$>1mkM>Dvb!S@(^pTU(3e!$==20vtQHG>~9_%VZ@F!(8hpD|d%;2H+kGWa=zUoiM3 zgI_WDHG|(U_$`CqG59@$KQQKHi^1m^oJ}E7 zFsNcs!=RQyM+OZHva$#(g|Pg|CPv@F;5G(-VenT5e`9brgTFKQ2ZLn{?q~2IgU1;> z$>3=Q&oX$P!HW!DV(oEH3hG*7scsf0To4`GRXUcQ2qI(l> zLaxFcz~6A|@vX>ioERPF&U2MqEuJV_u?vsF-Z>K2K0<={cD}<|Q-B!e#NYG+BYnB6~x&Quc{#pR7Vw zCA%$amix&=KA=Z#$$Tsvb3^EKij5q8wTru1< z+%>d1Nt`rJMNZE-z3epKX`i#NbC7eCbBgmb&M!I_J1=rx=KQJiPUq7u_>#?Kh|2<( zpIvsj9C3AV9qhWqb+zj^uIpU4xE^&q>w3kt-A(4^=N95N)NQQWVz;$!8{D?L?Qy&9 z?&luj9^;b zqsJzXa*tyk*FEYyjh?}t13ia(zU6tqv&ysHOXX$uitu{Q>r1b*UQOO=?_lp9-Z|cV zz5nLD(0h&dH{Rvm$Gp#ZH+i@F`1?fp%=G!u=RS_H)V>B^lW&Y~rf-q&B;Ogn&-wn{ zcZ2Uu-*%(Sh|gz?zQ!P9lrhPeVeD;u1ULImGR`#4FUyQ#S z4;oJzFZu=gMfxTBrTgXiE%967_mSTvlgt!g3O6O0`kEdwjW9iKT4q{fT4%asvid9h z9sT|MgZ)SQPxAlFf2aRG|08CD*~^@09%z2XTx{NGZVX5Ycs^ixz;6Nj0uBe<3k(g+ z3M>dL3Y;AHbl~fOWr5ZpMUZ2VdyrpHNKj1Bf}l5pmIbW}`XcE2pqAiX!To~=2agG! z7Ca|-QSfgeT!?!}bVxdG5PKqIZipphZOFGF8$!xMj)j~HX%Cf#x`z6O28AYt=7&BK zIy`iI=(NyhL*EVkFmz4mx1m3UZVTNVx>@WSvRxS{Xu@b|($4*w~9 zTllW<^Wjb59TA2Ib3|@L--t&dMnuercp>7|h}R>QMXZV_iTEz!=ZIe-evdd5aXO+l zQW@zM85EfkIY07?$o-KgqjXU|QN5#PMg10aEb4kx8*Z6Qjn0Yg8$Bd?bo6V{A4hMA z-Wz=^MiS#5lN?hJQxr2a=INLvG4I8E9P>rY4>8wcYGayWtg(vN*x3Bok+HL5UyEHB z`(5mY*ekKsaV~K_ae;A>afxy1ad~ltaYN$9#!ZWRHg0a*g1E(T@5Ox@_f6b}xb1O! z;ts@}h^vUJj%$ePh?mFf;wQu}kN+@!WBiWzz43?QPsLw~ua0ks??_N4I3@Tb1jE~4 zT0&kzQNqxKu?edZP9;<%R3)?|@`aJWkDnfQ3((!}2q?9vPlV3}IJ^AhAjvj$Mih8`(wnuG> zU&?@#c_}}q>`OVHQkTl5I;47}CZuMij!K=Ex+ryN>Sw86r>;xglKNX}S?bX=eOgdj zLRwndh_nf5i_?BjyV=vb=ct}<_iRs>rEAk&(|yx}(__-dr%y|NCjG_q;`Alyx6+$3 zq#2qF=ZwINh>X09{u!2x*E5!6tjhQ-V^7BZjB6RKnIyA+=A)TUXTFp9Y3Aw7OPSS~ zcQf0vq*>Z5w=82;a8^=Qde-!;MOk~Z4rZOqs>t@p_RG%B?vp(u`)}C`v%k;2oP8tv zc1}o6@0_BXCvu+6c`3(|^G43wIq&CulJjNGk2#xie$DwK=V;FPoa;Hab6Rs{xsJJ> zx&FCfxpBFvxjDIga|h>+%AJz?bnZ*J^K<`|yCV0K+^=(g%Katx_uPZMxL%RH5__fh z>eFjruVKAr^!i7yw|cGU^--_Sdwt)lyw}-0b)F$FIj=BpNZ#nY$$2yKp3i$FZ$aLh zc|YWx$>;Lj^Rx4ZmcO$ch44L;AcW;u?%+dY{!RwpCP*M~+z~mj z1LKM%Rg_@i+Pda?SLErQMhJJ0ukOZwqg(i<7^bT<~#Dc9p7=Lnz@tg|~@0+Ut2d z1DnY%Em0+M2ss`X!}b(t8M%-m2U0nCg{NkurM7g)4aUgW`uh9YhjpqxA|lAgS+6Gz z4Gl!E_YI2>yP1`3&)b^8^IfI6^rzD08GL}i;lSF&K>?Ox-kfXG7kf^3Gq8_md5;5M zOj()U?CI^}WAZUahlTn%xp|xX{QZqCn$dNq)~$N~-KBe~3y0m=^TEm$?`}TakGpM| z?{ue4q0y*xDh1xrlPUQ7GPT^|l9iQ}8lbszzP!BL$H&LoeC6o&?b`(%$V55DvNBCa z?!b62ukf(2U~hxLz5UdWpR6wV**cN{ZDO&B}*kswvgwvSh?iHry} zYPlARB~WQfYHMglw)Y#`%FE>!xBU3yk6SLv8-26;_3M}I+nAb~?5|qTwO!96U03}- zB!=znceTadeu5qx6ghlK_caPDPg9ww8o$=k=nfuC4cwsPr;w>F(0#MM~l%Ub0= zIN-OaGBY!MsvXa7w%Zh8B-9+^+#VbWe;}1mBf(;L*(^pD%F&*#yiY^WB<} zQ?j{v+7}#DS-G<=?;HK%oTs^A7Ihs&8`Uc*HYvf^q8&WAuI|z$kk+gz>eIK=dRf3J z*daIM%5PD_>nUNh5k@41Q$5fWpE7 zMR~#Iu<+Qpj{UpJ_ncM7jhXN`mjL};v&N!bv!?SU-TirvDqVIKs^7Y; zZtu#KD|er?^`NZe{3kmdg;OZ?FjnXf%qv<}?!h0PS*N)R0zyJRG&F6`i*Y4eI=Deb! zqC9h(1vd&jsEIqoezR-dmYN4G?>dn~(7e)a;q_uzXqm&5;Q=ez$sy$MpTY~p9PKi# zu6fW(ZV@?z91n}(=g@WUD-6h^Qr;V4;syQNt!x&PQ>2Y-7um-`2YCpOYo9v{e=EX9(Hi~-s6 zs(zTwu+Fmi!EoS`*$l(LHCCZ z8{ONx%y6niYW5*93 zI9b(5TuuJQRiAwD?eTht9z#Y99o^5YxLC4!)z#Wi^Ohfe-d%S5B(ex3MB!>`xzhxL ze(!F}wQGust)-=kca0#-V4tVmT+%zZ%JO7Ide`vhTFTg-+8_h z`ad-%J;7V4w6?p%_ZU2?Pl#3{mxrefoAK;(ui!N6J&V$8(5rApB$Y1q?8$z(nsaB* zp7rqXkTo4UwtxTrBNuDTUA5_FNFvn66H>sw4j#7RZBNg)7xf#=U4@KcVe|RDdj&ZK zeS6;4_kF-`t~_1W`ot3hl9KZC@^X?RBjaazHXhjV>-OziH~(@`K6cVfE=-ssR9)5% zj!=UTCwH~7wf+E>52w9sEpA4RhOQDEKINpbOCjI8m#gvftFiG4)@v7H1?g2tD?{4B zoUi2{Y`zer^|eWxhj{;(Pgb@WvwBb3xBcfI)~{c8^zh*Wi_E0;tLLEegP z;q_wJoJKyHd0bLbzN^ZO|Q)iuu_cl@HkAuMi>BGZJas4F0i+aj~}>+m)^XA=j_hOKr{mXKVWp!N%Il z{{5W2b?lTTU%QpGQ&x7HuNwYTgKA&9tr!JqqHbL%+r4*F-$%;I9vS-kufP0p{w5A5 z9bKI@txadXUi|tS3*O&V(HhpbXu#lfC+-6JSLv6qckg^tSXf6RTY18|nN%E>n|}8t z7noVNAi3|C-A@=LCX}I9sqexF+^@M=@%W4<3e8-KWxjOyh>?T)SxA_h?18pwLZr}E zLjR=L`iGuKOHTGYKRuh#)$zO_;vYlH`aOI_%DC+1x()+}eQ{gHK}& z8jawx@cZ^XP|{6qq^%^OujSwsO19aVL8EoJ84&45j1b1Jz$fL>A4t~-9wD8*4AIo$5AO>YGP6gD;H@qH^{znc^vh4$c!3*8Fjqk%GU3``{tXkzWDAy!+^n~ z4jvlvXb(TXnCR#TlN&A-bh1ylfwl>kvs;Q5o29^0?oLA@NbQgE9IGquG}lzkoSUEZ z{L7L4oY_*Wxl!lg)Y4LvtPQl+-v^w!va;90-|W@v;Lf;nahaSB8qc-P=RUJ=pV?a1 zHUrUF*#+}6X61R9hcPIwcWuRsocjZr>XL_=R`ffRci&b?5CnG*d)mVw_p4s`>4!lFvW=0UNcH!NZ1( z>>tFnTP(N@yK{GjeHa}hzKst|okA%x-XV`ua4BJl4u!y{XYEg+(Fkcb*oqGjdQ5d!^+F`YgC-yjH!C_c}1gNZoe+eH>Q0; z{Vt;H2Q~OiyWP?V97jYBA$+)7_^22bdglyfcwi;o$sy$MpTcXz9POiOTf5z|3;X|V z^Qqm5L|H4A-Y zqtT4J(`e=*O_1~&wy&22+4g}b^u;sKgJk05A7u8wTv>Zx?&5Lk z)TPG1sC<8(OCF{ke^SQ>=bP*8_SuD=q(qgU-Sy4s+x5*>sY77a$f+aqvy%NIAMIgw zs%<&<%f{`${c*=XHf#K>8IOEXnt^eX6 zl}A~bW00RIz~9>#Jb3u%yZctJetYSTV}m#sc7UX5zk2-man94z)8Mx07xARXHnSup zeYvXtu=xwb73^^27+!Jq^4<2j^C!<7I(X(bZ@_6mLPBb0ithIAFF#xJ(YkZ@oU=wv z7&qn7Fs{uKTE`QtQXK+Q#q*!ElwfZ^?6v*xizep2e_oMI=6sZ3E-Q173JDF5$nTY# zmz^HFqV%U<&(sedG4B46&r3e}XxFJnV9)2P?zBnut&O*D+~Y0!`*$v2J;y~vM7VT4 z@e*3@I@r6~>@(fMzjX_LD~6{*ngq31SX6jeb!`)`^D$La+-UzR;{92%|C7LiPCtbb zlic|m6&Ei)^VIVbQjP69HgDX%d)KzI^XD&KZ+A^7>{}2}v2D}FN>$?2na@lb^622f zj|?oz3Bxp#(9zM_E>S8QT{8v_92nYh>*W4}$FAJE*&@N?=#KhJ_}Q^x#flZX>v)fV zfS?eQs{`L*Yj>fgnvhG^mbxK^?e?ZQ!me{;AZe72yIphZ@)J)!H=&1T+rjTYU%leX zqxIUJqb80yar(qroXi+QBeF6RjOVZX^gR|qmN1onGS7?We&R`Bd3jqS-Geqav?ZC8 zTvK>>Q&-DaCCVYlc~uPCb~D4L{8E}#xc2?vV4RH3!&#CAf5JIl1mu*O18>}zjiY?U zg+pVW-2HQ2VGed*3yZOo;;QD`*exU>p-Lz<4Qaejof$hOCoC+bM~}oXKV#5Thl=&z zmVRG)q-8MH1jU-JD-??w3mDgGM|MnVYf*a2rK&nx*EYOx;?vv^jQNrh3;qNPVsB6B z5dQ71Yp`~3J~+yqhS*zNsC$RVAwvlkgoGUFTQpu(RkL4;$M% zW{scoJfhM{Eedk;w{O1r=1iS!<&}sWJezNxJAUHS;o-x_6nIOjx2{^T!Y??`*F{E8 zD0AiOHI!roHK(PZro{XZSCNjZXG^S{cKw1<+0hEMe@=x zoOEjI>+iLu_DD`nPRz(ijI;2wXIn_5roO92o%)kCJzD?ZTB1zk5ZZlTxA1B)Z12kr z^$*Vc8!mwbGyh;)3kz0y!?A}_e|aeNhCsEi9Rx{^g4lJGZVx6an53Uo| z(FPFbe_@@lj%M7QMl%;_LJb7I?(2kgLcO}J6MhqA8-{)hZ`F7iy}iy}yne^3H5@*C zruP4$27fl8|4uR38qr?&ihBEMKxnPqq6Bu1s(O5i^FSKX$KLmLo@4d)QRf1=<@@Vi z>(#gM)ogBcF*g$iX8Ls7s!@hm3v7-s27?e%T&sI+vh>A*jvTxyx`J@wx9(`m;9(Q4-}H8+ySfLBqCwcz+4pp3bhC5nwS6;>UA~iaCX)Snlkm*Enduw8+2I;h~9Jh6rI-|!lvqgF=+O`KVk4ruYFAaa zaJ?8tEjWC*Qg1(-&!^$mADVF7PXfGfiTwxM$9NUa%x&dmcv=vI&mxxN`dAl(7i7Q# zfelb5-XaSOZeTxynEO_@jUSo0XMy9Wfpa zXZHR2>+ioGu6IcsGH(2|8Dk6LaGq$%pmh;wiDlZo=NFvah?|@093H!}@nvuRu8m6rNcw=a_^d#t<2Egx8s;cS>dp2y`^iz3lN6er{A3cBZ;^n&v2V;1s`Q-7VVJ$m0f4A+9E@|?#Nz(?$ z-@Q|L_SngT`)}PL!M?$PnOVIud|OYiFZpVE#`Yd-q9+Bc&Yr?I%VwcWp4 zQ|0DuP<7n6e6#MjD;$&^-oSjuyk;7G$&_;B{C~ewUkp4GV3VsCE-t)P4 zq=Wl3-o`62i{z6ywN8m=#q$(v*G5F_+BIyLMZw1C_uIdHV3Zz%s9jt2*KVy=F1DJ` zD>u8vH;M7DK%xxSpv07eY_nkF7;PVoI6u>PjlhRwHwb>C^GVUhjPa(nF@te zqNu)nuDZUyT_Ta-jP}bfFadwIZr`OFH+8AQhet(SxngxUnzN_Q;My(C+5Aw{D{4=XH4NscyodAob4+uAJJc=efcCjxU{e0A=tk8qxrEJ7OVJ5)_K zZ{BUJuRMRR6`IN6E8w4g`1M|N|!HoYjeDYX%@UR?V?9EtOH?)+Wo)2X1w#YKeTG|b<( zs`9Q&OnhQYXwM!!=gc0L9TOHD=x_4#xqkJwAu={TDmXPcZT9nHGr6&rm?OtdU%YhX z?8%eY9Aly)LjwH$0G1Clj0|fxDb=Es zf}OP91%ri)=xYDe9bgh^_!@&#-caQXZZ!7ha^Ja{MwV)2|gIe9*|zBSwuMk;VmDEIb;RZ)=|Cln&G#~KK|gncX!wH zA98!os#PoB`{k(cidkcu!a=LjDcLJ#GIC$0l3SdytdG{*y@038-rnAAIB$ck7iy_T zDbkxa)6?;kx){4PtJQvLO!bR*amvA`vJ0?STyBvltyZOw>Se^GtM#oFmo8j5RCe;@ z9j}~Wk3LdV)PLAxWBL!AIQ7}5M-E9(@82&sE6$|%zVpZCpLhMb>#8=rg3ysjdDVm!BWfR+$&4 zs;Q~z@Q6vMYzxU3WS9jRdQ1(ue)Q<^3)iY|RNdD)Iy*F7yL$Qb!9V`kcQs_{*s&vr z4I4IMTr7O3qFP*<| z;rzL?XU?2Carp3Qui?|5cw!QIesurj*of#}!-kKT^wbN_JvY*wmEE&Pd`wJiY`Blk zSJU2bx2duA%4w8%VDDaD)p+{o!NccoMT_Ev2pr!)k`M;m?BcoxQH4%#oOrlx*5 z(_bFkkIS~qlkD7?nJJ8WA57W-~b?xqD(IOqN0>m>$64kos>!^zkq=FjEoFq=%`HT-#_KSJdQq)M}Q+Q zk>(%|e;56L0Vxf~`}F<`d9>T}p!>RzZgMEq8e9@0lWRRgquTBVBn=oapil391N!%! zAJR>@n8c)%9`QYsLqb9#TXEsxIOD^KcyM@l?%1(o$BmybZrr#jcnL7vRvzo6sEoov z0}8bU&%n5>UJ{9eY5t!V8#aD7$fG)VI4A4C!K@6<(IS=Wbn<%^?p~+-^u`PR${x%g zc=sB;h_IHgIVFVY9zM|Bz|~b&U%sMr4;fI{cX!$DbB?)_o|rs+h=1+=JqHFqIxxkL zi?Nv7+uJ2rVcuse%!?Oo>&csLsi~=MH$y|C4ejl=QrVk~*6LH3BoE$fckuM{_j&K5 zmEWGc>)3O|*ijP(nB~>mKUuXVEHo&{oxb=HpbboHYrt7^OJm#q{ql-!FqT{KE~-yf z-@bjbd>VW7ND4PRu!pXNKwa0JN5nTvg;8LC=h6xFlk22I;vGiynJ@1!nbJ%q`}sQC zQM71>_|KP|l1fk6WM?V#(hnNcPqP0EE{76>o3B$#Mn(i%mz3_(fo=qDzZ211$Qj7ej3u?9c?4GVRysP#W%bW^_DI*3k z?d|BeZr5I>r@ZUH(aNZyOiyL9t9CuL+3EqDgJ7$DAxp6W`M5;n6%EeeHghZHt1%cl zI_`Ej1qC??^Q|pa@WWCKPRPMw(+B@P)gXH+YLAQcLRq7*NqBU~u;F88%*EnEGH~F; z*?qa!xlfSQN?a8D9?q^l=2mm7=9_H7s2uf91_xIIOo80NCp0wFW@KKkfmj}09%WXOoYgR{c?Y$edQ+334LGc8=bFy~GNA?|mT@7Ws>S?&MDSg_Dh z_g{~^7f@R2e!Y4x$;)ipLAdQT(NQ=5i&7OK7=cOlQOIAs_+O1gI;%V1lN%hhlKc1X z`zH0DI(6!!DUsX+i(%7|nigK=9?)~ts8Kml#@l-~b=CZP7&tb~4@Dk^c3!2{@GWf} zUeUcq4tuP3QuKVo_djmjwqwtsQ&*dsnl2qK-}dA8wj5ZBF(U^L8aRBy*wNXMxRp!p z927a=DQ-6>Au6qh*e7~_I%u)(JHB6|g@OKTB1asB)+l{NRKbZn`Z`_+l#m9pSaTRG?W2mktt%bMw^@z?7ySKINsjgwqk zSMQNgQjT#%A`u509JHKlZJo62=QuiY`sH$aa%_a6X^8e%IdnU?0v0=H+;WyIU7B_v z3HzA!Boa}Ru=gZ;(-$vUnwPgU*3Hc=cIo=ZAAfv3mV~{m_I>|0kIRBHj`!+ds2sK_ z1Y^NVKaHQ?y3^3maL32Tnj8fwbeOdEVP=E!#+xAZgKb?MUHX~Zw=P|{xMay_XOCvs z1Fx$%`^%a0=Py*l>zgH8w{PCMb7LBuRYVXNt6%FRcC&>8_tf0;avm4Y$Oz;%T&wLK zY`qA&-0C?|-;_KegQgA?J*%pcxwtA^92E9=iKg<+zyITJ$D8so4fy2Eu^ngmYCZ^k zj(t|mKq~24NarhRxz{z=)>2TI9J8bVti&uI0eYU@scp!2_+M_N8H z+%$;*_~qSW_v<>RW_vg6+*vw4nqRoLv10kbM<2n8)zZPt%*@<1TX8%yle%7#nfcL2 zx#>yFy3=hHt7$rgtgsHmc)w`Vce}YCGO=cfWD-X1~q`L}eRY|INC(F^)q-M1%uptd42N653&pTdmIkO$S*okr?z8 zk3+gm{<0No)|RYWnI6gOYHDuo=KC#A11rR<;LH6Q_BT*04NM4y8DJ{;8|XZr2Dtx< z{mJ5q{i}+%Z`-kb+s0z7KrJOn7Af{qElN&KCadpOeD**8^FQBr%DmL%`ST>H7L)LM zycaYQW7wo#3ol(Ta6JqC$7{!tLS)z0?K`rZr*2nVtgJkLvAU_Zudfd@A_;j*vU6(d zZcaL8?%Dsyo`ZYW78MjNFU(7hO__mMc2pV5HY3B9Zpk#)Mr#wk zIGkPCC>A$XQgx~GQVga`-RSGR8{4hsj*aQ{=)=sME6kk0R^xxaEjx227V}o)`kl#e z0jC>xcT2|lz%09iDTz2x8R_8&f&ybFp~nDhkm|_+@+06+JoOc*;v+y)4jTa+ zTMilb1G2#R%d93kds^G3F^A-g>=hfff)=9y4qlh9=H=xkdhcH0s;Rv2QynK_-FnPN z&6%6iOZ>*B%U!r6sh`tgS~noMiYavMXLc?!%*TO`)tx_o{(cud^BbUtV3|TR**(zH z$5F^60U2qF&z(PWrqwEL)873DN(uq%cW+E_9lLVs_%CT$sh-wAp?R~jL;a*TOKM&2 zW+~w~IJgR$&YnHnDsm1FZ*6Vt6MDm$Pcd?zB)w)evvg06XuMTb6Pl%Kly>>+Ar=R^ zBW5rk&~rl5-Ch`n>DlcEGtc=!v;320`E6$TUz_E3o8=!f=F^(j?no%0#uiw`juBAE zm`1PdAZloVRje8{wZJOYZTXP4?wPj5^cnq-a_onco1)4su!~j^4q$SF0{$=4C`UAoaMH zmzzU$W>Tip=(TfaDlS#DTZFCNzJ1fSO=jc$mXWUab{)LV`nIosF;^@U2nHY8-O~)u!usBWw2lxpU{5ac=XFV*-tF^h}&H z8_8eIMxrsxKW@zb3mVDnM0?GhJGE68FV+p#+^D*I=T2Q+UDvE7T=aEu@$<3s^!I=p zzzS=ExcAoI-};}bA<3$emFohvHE+HC)*I&r9XNS~xdran80dbdC(C)W({ltiH8eD& z=J5n9IFqsjJhiuW4rAl9Q}9)fWn{=T_9K-Q75#FJG|kIvC%`PrkpWi3Z*G$i9t4khwH&UswU@hbD3NG`Ot@5r<+@6 z)Ah6GE}uG8(KMpf@`>sGk&CJNr}R7Y&+8xK|FdE=r64lH3FE2VhPd1zJw{r4>EKG7 z9@BZ-5c;ev&b8nPaJHWazQ)O7I1J@S+1}`i8-t7 zW@fbdxVU)OZLtbs^wtj_e)!?(mVrayoCHvEgnWH*vzw-+uS=PbWYB z>_bIdRaN|%_igPYx<;&7jc!u5sG?)Sec>i$&!Hro1ve?DyB6G}Y&Vv*ZRd_G#bCv$<0o$1zjXfW z&lNqw==FQ{@9XdBW633Am&lB?tQ4gP7@Tr;c4Uy_MUwYN6u`Z0 zToz@e=Vht996a^4=YKhS?%I$|)XGg;x9nJx>aV#c_JHB5qquef@!c&QKrINx$T{$6 zKlUAq1GA0yS6L4>Y2XHqwDnre{mXC`yR5;r(y+LM3r%{4vqwVz4$VzTnVIRCsp%MY{=I|76Z&q86pHg+7-x8qZ}k?KG$I^PKiEXgx-Z4{L_H+ot~t^NEqC{$2iBxLa$s%7zP$%GL^=v{1O)!QqVkB%Dsjx8+a;|(DGO9UcMSA#~G(n|;?HP1f1 zA;y(p^Fp#VJ@?F0%dlNQb|q?&-LzJ#2+P{BV@Fn)!cYgDSndEE`~LgypRF6Tq^WNV)N*XEyW7^@)HY*nqX><#F` z?FsBMc7>CXvaxi_V^5{PiR=aJC4Aj76sqNqEY4iXcA&T5L#Klgi5dsdsC!TU0-fJ# zRXa2yV5XCn|11qWw{eVK2Q}BQ z4kp-#&N$8?As%v>uUz5m?uTh~)}+YK%PW5MfrFbjm#p3X^fT$;Bl9?*l;r3(YvpL18O zD_EMlruqnSmmnr5H>){0Ig^uEPEL|OeF6VSVW>DIUCqr)SMI2-p`lr9Tos=H*9?*; z+Q1T1M$FE7s=0oCu)=`z(jC3^osdHs-&jsdhN{`d~C(e5ZBe~ zqeF`~VIyFxJ#k`SK+UJ`)DH}tI6>Dyo=ukuOnKapB^n+vAe>^;>s5QkoGA0D{lNoK78)7!Ow5q$v3` zW^D_m6R~69A^`^nu@i2fkYxU6y50gq;4~XEw4* zV?K2Qtgg-iTfG%9Za#(qy>}%HM*j)MueKW-)55aTKRt~-5J3rZOWB@EX}v)6MLx4{ z-&5&8>fd1g`<>(F-2G1_L^=rsgkBaIyS!wHpV*Sm3y53($YYNs165X&CE-FKhHt13 z2^SlGKL7j;#RJg}ZrlJ*;@Z~Mby1cEYV#rk6#!!R=|NEG9|F}MjZnSd22kyf3bch? zK`3GqS3}$d2o7l#k+$x@lTSYN)Y|_Ki0Iiq2{TFK9X*Q+yiEf{2M;drHl-}aPUN1g z;BIPfZ@_3<7#puq^HP2HNFP?M<}_w<&iJ@=GgLwWxfT-VPud481!4zZXDA_HN>GOg znn!PkQehW)iu(U9zQ%;w$JSOV{lCQ6*atmEHM%P5Z`hD${@Y#y!o#IfTibKz{x3ay zLeE5~(mw_G?Vqu?ICBHz8&+-w)GAX8M})Sv!V&u4dFoEs`gec+w?uyS|d+oK)FJ7Z5ICWC1s>&<+$KUPcufCj}k9ENQ z^zk`_U%pJ2ps}lv*`rASrSou?mh;$StCEuP!3LBZtxEW<605s({_>?Oz1BN`2$bPJ z{%Cs7TrC749s+-{C#%sVb9zj4yM{A~L9bfpY`8n)4n0|$Le|(RBn{AU;NtptXy`c8 z7QG|7V6di?_!5++zayVvdp3uIYvH9JRVOd7IQicz8Hu*W1yOy zV(O#sV4?Vfnvr3Uq|L^=;sfKNzzBpr3br5G^utI759Skgb_Dv+xJB6!;NH_H)~TkC z^ro}g$*dMVFP)fb!Mx0Wr{bT$9Oq+>O#G8XfXnsv#j90Sx0_p9T3aS^bI+XQ%Dh9i z+n)K+Qw0*}&IItTF5`8Nb#(MJG&FWS&$@g1y;t8X=bS!$`o#5~0UB%Qo(If&Zi9YI zS7_8MzuPRo%PjxCS$@A+ev4WD&u01Gn&sD;qAFtnoT{qp+q6LtA59LkQ<{RVp0BBk2=pUP~D0WuQwQ&gy9Uh*6uBf3s zs4HV`Ls7NFuEF6U!9!znTw8Z9xSm5W$o8wX=ux&B`=-A)nth)#n+1(=7LXk+W~Ogo z0B-E_irgrtW?x?j)QW zhKLucsVST%m&s%TNvN0yzvVnIP))IQOLW`Z!k_mz?avp9aUs>N^8G zXH_z@&tsu~$M%+%=IObyzBVz)7X(|!c5nr5e_hj_JW!9fU{ zo%s06BWJ3_W4$8_sM)+8oWV5-40-{sjTlEw&(g2&ru6_-Fv*!{yis%GriM+D65rV9 zc$Hj_baaBb&ATnyP96R7hhxX8XOvmXSLQ+7LZGF{mZD+%;u5QJuE53;_nYaPEtmTG zLQJKY7{`%jdww(MoSOOe%_EKm?*^KxJ1@YjG2mG=X1M|!Vm^O#TsJe;+uOW(=k`1g z2u*s31`X&G65YI1D^*Fb-%8ax(6MV#bv$3amJK8&dfh(-9THE0OtVMm@wocg=#N(a-) zWUvhXOgu*^HD+ei$mwQ=-dTtw>XAp& zKtO#3dmEmYJIQ;pDKMsRc?q@!;$|Kwja7(GU#M*#6v)(M`K!l&VP=M&;VlbhcoZNp z-Me~h74scIO=xI{PjP;Gx4v{8f|`O@jk8!Z)CTU`x(gmCGm-GM01DkMbz(%Lhe$V+O5UX-fUI#;8X0T=rsyh(LhRlxIT$xIVyhL{yIk7y?=Nv?&Zj zX06m1TQSsF9gTO;HPB8ehx{_EgV{^2X#6WQE0aM>Qj3eNlCe5;r*lTl`}peEF;e&X z8-|idPe&~42mQ?}ST*MlmpOYlmk%Yw^W3kLVQbVP8&#>>-o0+6Dw_g=C}3KrWHxR8 zXzB88pqpopli=-QkVz=ny0@uov?FtplC8U(eNN$NOid(P^YICt#ayX)L??pV5g=Q4 zyZD^pWSYp<=tPv*+TC7#`A$nmO*P!2sq32&C|#XD|KiA*mSIU|PJZ5!%!rWqVE4eZ zK&h_d_Sxe<{^aZH=_nCSf^T69R~0@^ws&J}by;FyuynkqhtHQgh}e3#Z^P2GwGDLD z-?(w3kLwuV%ICwehVPSvt6>p7JZmm zpjujGXz9t#v7(;Vrl!2SoXn-hmXPJ(-?t9L~$kIF|w>9^Rg=c20iY&T>rC*HC@GyS0-WmY%jGTct|OT)uhh z(+7*Az-T05v)J6E)U+k}MZr$TkE?BH<=y1u(PN;XI8#-!NM`UTq*@gTxwEr8_RMJg z)oa(zo<(u!90CJELw$WiV-|y-;Ic)ISmA%u>Kg#7H3HO0rl$dIYO`Orc^PQ_e#S0h z72rtlHvDxRo*k=zCs2Qa&o`im{0cs=fh79}*}mmzi(|rK3&0_6`?k`J8=rb?RTPXJ zR_0T+z;tNbOC6wnn;aWed;av(0%hdH1l#_npN6o?pN{|V!w)BaLW72RVP;bd2PF4$X6UuJ<-+6_gb%ADOj78x^fxBgDc~A4#>qbGubI} zH=0VhK`E=-H}1F8-YhEh_g-CUBLKTunXq?A1~I^vMp}3n=vg+D^s1{{Qc#e&Bt0cP zc~N*sXjs^nUwraSb+2VwR_6ILr!M#6UioX*fmdHSJ~UQN=t;aBn=R*#-T3lh4d({* z9bIf_W#+;HzLHvYbjTrcBq+!rK~SaeLm2!|%?HxwDyBI0N>8QB>qoyjeNUrziB4Qq zefr$>j_cQ~tl*xun$%4+SE+frhLd2lIoY67bFX%R@fQw`^ohgJhKHY@%hB@}Yx<^G zj(!2E>Wdd|boBNDdHB^fsEa!k9LTOA(`fb|*q`TTH&TE5<_h#vymHg_5^yap=URt@ zhh|Z@MLD=?mE+c8dYxdsF_x8lM=79Cg_>*&329@-MV*3>PYl#1;ku*27p4emczLCj zZrrkU`<|Zmw)VSgH*Vg!G1U#LuQ-1qF*Pe^N#V*^ee2!o>$Pf3sTO*Y9JEY#^Tl3VzscM;>?6j z9~l|P(HZ=lnUlG}V40enbn@i!A5J&wUBQw`pUf!}C4Dj{#%}!x_;I>l&6*}>uR_Z-gm^a@k0+qk<(GNdY-n@P1ZogiqwRBN9If>YHufO{CYo`Yl zt2eGHi3BBM5ZEvDBGIrzdV0F3zor@P8h5o0uq*@+EQ(JIO^?ndCGqb#4V?llw$nXM z0<(^6=uHjP>G`B~+SI&09mx`9q_YjS)4)$?=$j!FH$B5yP{PAt<@T`0 z;&)iBntR{#?=N@DzWzE%pUuFIXcwpJ$!EPM;0g|Qh6u%G)fiNaf{GZbh#z9Sgo2=? zU1yDzz2$YGlrM}35A~dh1p~Jgtb(Mw6718*z)lV7aL%z~V5_b)X#eQZcABkKrE#q? zpk23VO*hWzYnvsf^Hy^8%hs+=Nn5{u@!~?=NJ{P+;b0=NX4u`k=jT^_)6EUX=}&dF zpC4Sa;MCs(lSFkjGa>_S7*3%mzM;3&ooa}<^0EyewO{8nO*k%#A zX4{U<+qPwcyC2;D8yn^aPKqqc?fF*R$spaJ9g%J;a>ev!8$X>EUl`N5#?k%L`U!Dk1)u+SPE|XL@2T(nEw%SA6D8 z2M+N}^mKYG8i0$&6#DyZXu|aSh*^G}S$?Nk{#CR5KC}Fr#(dgyt1lV~7(K)2w3{wa z$e2dsIOu+Z3^-1w)T5>tDT-;!`XOyinYP80TlA1}?hh$9OO;#TEq1e&D!0H}>=YC; zowLJG#PoZOS^i05J`GO|kLo9drcZR*$w8{01!*06s$8;ZxvEa89Fs<`q4}EC8~ghw z;8CKZvnA!P$aZpc^iL@)Se_Rm^O&uvymbBQ)gGHg8~5znwsU8WhqJ}(=)~Zlokw`_ zs^$CkKd~Wbw(jQLwd>bfr29Wncc;4BF?ZiV@E|ccfhQPF;13?X7u;@x_zCp2HnA98 zYnuGw+01~`6~)nr8~XzuZC)w zH*d}%pzA)IUevCZUJ-LBrrONp9K^H-%!eqOoW>I%lOAv2K$Oia6Bj9pmeIS+;1?SJ z{Ysn#I=#+9nVFdbX6Kyj)a0z}m_w4@-l?fcE`f1*Lb#V867okzdl%4zi)L&5g>la5 zH5oF?zhle?nofD^+6Ni@=M5=?bA}WeC+yK?EyE^hUO=Y}q77@7W{(iSENu`Wfmxb8 z0@^HX5CLwMW{-e3OB+N3FiU%AzldNM%h7WIq1YDek7_7F@0SCx0?`k=Uh*FkSo;S& z@BRyly9fG?#I0RD++15aG*fJe?9QF$vHy(dKBy}EPslKh?ev^Xn$1b0aZZq@vv9oi zX2XZ4HN-Q|fE-trtbbDfBzf-qRf&J~1>fGO8JEZEXz+ znE(rFnn8*z|(5|Ogf)9-kkCNbI;T9Sk5#nqq({(pDwKlhm2$iu(DRAvD7*6w`sT|ik z&hq%>az1ZX0bhK5W%_Xe)(t@mx-r4HUgk^FPUJP=Z^8Eq{0~t}rM4L0a=}&aKjz;^ zy7$ox+Dm8JZr-Z7c6D${V6SlUR66j<&g1XA@z!gf)j)976>Cf`v71?&_*s)_bW`ip)&G4LHwhh(}gDC$t(i3nigNeG}32=!U8#EK+lasS!wcllCx=3Qu5AlfU>4{mP zwH2?NMG#+p$=IR7ptMXPVUKrr_l)6I5`k^d;x+3^i?c(VzL*Z&bZ&zPqlfaRvuBtc<4vaIJsqS9BN0=%$D?N8foUi9p#V_a2 z_F5;c-@0X2Di|j1=H_%fx0vn$a9m_5hR=Ta<(J=n!zRn6+S$3`D$A+Sx#(zpWy5XY zYIpHz*Jyh6tf4o4cXx8~sjo|SL+F$F2(~nBfE0@3{Ta$74h>~xC1k_~gar5phQ}mj zWMwVQj#s5Pl6pSdCZ=fjV+VIXvS%${%$k^)n3w`5Yio%FqKOceT~{k_-DqI(oud+x zqM~DcAQ;0TFNkhBD>HaQ=xp&>+{tOK!HtCAtCMzajeR^PlAP4qzbKKCbEJ!lu1U!T z&-Q7VvRNZwVuetr)i`g6HR2t&?7)Eo%i!9FaArmxn7U!ZhSWg$41+O1cQB%Tp0Xjj zq=tJYph~4ihcs+Jk&q8DX0UQ1S_hBuK0cpP4JnCTU5SYiaUt$raHG{PBq}a3F(oNH zFxq;0TtCYX$=|aR~awen(?;_jED*h4srhX zjeukEe?Ci*51XZbKgIO;Q($D4I&x)^K#;q+q;%u@)$6vTfUfv;>@98>zoFqqwO)c^ z*Fn5M^4#d?IrO5|g8`9gioOKeg2QQM$*SVDtBY2aCWH6hE95M{v=BXw6F}a%Q+Z`t z1mLXHCr}jbmuc)vwdI%d5teIxd}?Xir&j$-!;v z>eXM)43f|4_^XzZmLSm*zA0hhbJZuS2;UZ8GbYPL0Pu`_4)C$ zmDL?%Iw2Um`C{?%#q0eK*X5;#`uIm4Dzgwls2&LN~0#sBR1>C+0;VJ z1#B4q{HL1v(6uSPb@BX#n!Cf(gKgcN-PmkR)y2zamo9N5?pK~Yd%fzym0nS7DI7WB z9%>3uGl1LOnGrN|4T#7sot0+BAu5v|O(BfNyZK(r z$lL%GOMgsn!IzSsmU3=Zu&(Q%O(mmYblE8Xcn|jSXN(afUAKzT_>B}dGq8u@4R#J zW~V{Vi!`KoD!hI8D)th~MhN#Cld!kc++n?b7=4(LqtzwjCq}cAuy^s|6-n3^fKBhK zxj1fRg+BD$BS{a7LB6?%I00mK4kB7yx&Us_k|JLi4r=MKK))nFN>pTFD-?!NwUg8& zs?ESarAKIbld(WGiGX9$(W%F_A5>E!a#oA^B(L1FYv<~;05^!0WwY>7x*O4Q?kCgM zsPXQC*5*eV1g**I>flMT4JDu_U9)B>{Nxrw7=WTR*k%Z}P^*q14{7`w>Uq1Qe z#x-Wwr9(aUsw!!aCxk&yO9q*4QP7*;{cx^+xNzm#sp?NZI{g0S`U22C&j%HsqN1QT zfvFo*G=4VI?Kh3`0WGg~L5HUDPy+0S5KI|M;$>g?5wLkQ66k)H0>p8o#6 z!O@}Ko{{=GkfLYj=EmCU>*|_YXRtc;UzB38kcG{Sfv7Mxhp~iWa&mHVdTeB3e9F=W zw*4doD%?}ckL$$F)JJNc0qq&}WFSI>TAr}H`$Q>3)YNMDYXFK;Yq?ms$|+iTpjKUVlw7?0etnf^$1r*gpW;OLb;b;Z|^Ag z*OBVM)7L}LjFJzwL(30FxBpMj?!i9(9U{_PGy!_F8ktFq8XZ;TEY4h#m70_zw_B8iIE=kMC%mn#- zf5wV!dEc`5S=Xg}7* zV!G>oQ`6?s4I9(!N3WlQxCTG}Tv2oXenUfZFXNz7y4voUa`||7E6B6=z!{@M*VZ>@ zDN{JP`AV^|DVp|eg#Lpiea($ckL=nH4i{%noj84|;{1)Krj{|gu%%gx;pHv3Z?tvS zBU@G$tXvM4cZ1-fQJ_vYH#R-pCrM2OyUB24&7FIln%)U;XVndBG}kVC`st_DgL9Vl z3WXGIF&S(g*P$<^t+u9I$TxAy?(Q5M3%N= z_x2r+?peFMs2HMiWCa3Kcv&si=>!fgHa3J0DAirZL}I+Zs|B7gy9@7dSt`X^zO%D4 ze@-1{W@jWQE8}Eq3$Ukw(UB2W{h2do+I1ZRn2n>WyH~J6OfvT|Xqq$ti(yLj`e2?+ zzjvAC&%Z2SIMvUEn#WffHNJsBNg&qtVX2838Noi5-K~u`FP*)3;q)DD?3&Uwb#?c8 z@RK)ri%K@_$#Wm8tbmJH92N`L)LL-MU1qdeh#`cLO>hgNg9kKA%Vu4Q_VHCx3_HC zwld6N?AX^|et-J(52vdtueM`+c}QGhiYizY5fz&dzsRfY=aZ*S{B-y)FaP=NFOGcq zeN{VQFSGxvi_a=`Kam2F58%JVjnUpx zvDntpmLpe4Y)41Mp0W85w5Me2){^|iPDLwX{o%nBo{+B{#sqvmpFPQDb2{L{YQNOc zCpeoJY7QlyWsvlD-UQ}@=Z z@4WHLuye`gRi%;Gq&jeJZc-=airDajYqeG#Cr+FgwT8>FG9Y!DLls zB^K6ZWzZ|oBcEh?rp}BL^CPF^AWctf0-Uu?<2BN?3|^^mFkS`B&$?s}uNYNCsE56S z*A`*dFJFCe^y}{+0zCB~28om%jJ52c{uXqbG6oWFXfqv;)1T2@#QpWnx^1k(uS~(S zG-a4;D)xfzYkd!4fnVR8gg;HMw>y+)Ya9>kXaQ00d$OPata^lY^X4S%G9EpMn8*v5 z!QP8{DuY4NHD82piCPlxgK#f1z5OHLGR&8WlyH?GI{H#YWo6}{I1Fy{ZQZagD`9C` z{L)P&#mf`;<2|*vZl|Rs2Z>oYOgJ%zg{aFC!a{5yWK2kio1K*aLivy+XLt-kmG+KJ zc2rkak4n6vB11yDx<-Zmsi|pc)cX~bU(OU{wY6xE7FP0{)9SP5-R^S&U2VB!k?6e1FC9v2N_K$(~jXVfo}!t8(@{ z2a>Sh$z2d>p?H1qVz}5SoSmiJGiERdoolgxqS7;j^qu_fhT+*2OE>HW=M!q{!1kf{ z&KAhEfL=JLmmb{m$tRQH7Tf2fmxAic>WQ7(AKA33w0QBFbZ@_)Wjj(q7Wx<5;(7x= zfF(hQ7i0fJ(ElO%g8omOs2}1%;33{n{RtWtnxd`7e#_8l10C*Mjekl@5r!0QA^!2g zl>MvmKstqPWeUbZQ+q=l?P_ysZdGVVq3iU(jG1zXSz{a2EH&O7n8}D;wHa(Z)H^4} zw~-LesUek~vt{rNbcH1l5G>0tI3q118FYmZfw4r|d*Re^&=vmDgu7?0Em;mX{G%~X zpf&E4@Iy*ZsIHZb==MG%0yi!ruIXK!}bLrCM zOAXVKpx}_6zW#|>OGgKp%)v?w%1Ap7c-aQS`CG^5D?-B~+^wvMuC6Xnr)jz-I^n^a z`>kDFlXKw6gzI_-K$*oNbt8szAq`(Pvx?EsyM2(aql47O##$oP>%|aa%RewODmE%2 zKQHW%{nV6xP6xru#-^sk0RaJlLBTNFVgCM>hO;*sA#&<5j34b-eAVh~kfYuKbg9K| zWA9<_!e%dnjr|hOK{_$FePcShbMmel)UiNw_~YY))c;_BC+h3R>0N?O+M;RQ-MV!< zP92Sm+gXA^yk_7$@bVEnW3-U##4@AFggRVJ`;Gbm&4B~(zK{I3JNNBgRTvwaowcOm z{F$>Yg2)XA9)IEhvUkwIhlD8jkfs#^fb~rcs`=eDpa1#KfBv$;%9WvG>!3yVT8Ls% zUvs;m>PL9;@#AW}a$V`hd(F+GmhMrp(W;cZyppGqL9bQL`OSexQzF1Sq0F0YX|3=H z3JtIx>FT?P;)h?jdv|&UPzD1E(>*jeN_sCY+`MB4y_e`UF?c9bIHQ}#nT-HLa94Z# z^;73=)ZV#$8}R%_pWfOpFsQ3{U=rtZ`3_1qcUOf1!tn)yiZ@8<0P6HWyti*|M$h5% zMn{H+hfy%&n)c34t(`K!4&a!Y)C~@dj}DKqWNu-xu?Fm*c3yp3S7&Qm+qP{x9@&!a zK{i~exR{+?f3xNuxFta#p;>rh92}BFLD_KLPjR(5M2Kw+_34oqVrU^$hU5luYgyKh__kEDcFm8JN+60Zri5l*XSLw zay)sV9#3!9e}@rjdQ=1i12i4guhWuHfI{9suwzA0g0p_CsjhX!vi$t~yk(_39@&oN zf_vy=``4uf`^toB9+wfNJ)tc_1PyFy8PAWUw(G3pD5jB)%wj>Uw=_MP=a}=4_S=q*&>M4 zKQ_?Ef*AF7v9YnCb`$)*8x<855T%|yGF}h2-Duv)hSt?kiH0N@zPaj#R;^r}>BFAv zyj$12s-Pe*ed+3r>sJj8ZQkrMH2yS13ocK^eu4i!!PKmxBu_C{&Ec_nZ?{kCexIbb zqg!%^mbjfm&y=zsPthksRH47KrWMa&PpivOOp7G!AK=0{!*EV|rl+A{)0vh)VbkxE zX8G1;`7LJovu61>&GP$=`A8Fok7!YJh~MFZGw7|d@`t}%_MDpA6cW-@rBGDCINpJA z&`+4r@*J{@@yGNLzQc3QlJtDA3K7q(+z7%2@0o_K$Jfs7OPEN8a0`DShpbREqeE&d zLt}>D3mT>NRw}gVuhI0hLIl#*ZjC0tAS*5^%!xZayL2L2D*KyRC;l2s7*?gIl0=0XMp#B?vzn)TLT z?2Qe*m7C>qjk)w#%d}BWH!EtG8Xl=UuasTAZU{08eOyMl~`=>|F zwMVK>pX7O^=Wf&|>Gu&kvA?Lx*l-<@FZ^E3`QC!h6U$|47T#ZZZBosfxLXY#C(++C zp4>>D=giJXsey@?Tl#~2z}8Gg2Z3p|jK~H> z7g4o(^@91+K?A1qXZ{+}q*;DDG-XLjhxni+?t(}7s$K+!#Cf2b?N?; z{@UeZM~)ml`rX$zC!FKs6R+N=?wf&Ns=+}%3O5L*8z8|*bvV4Rth2QAi2=DbQZCco zy?YmehiC^oL9<;0;Z`nQt*;+N&+>3N?Tu}k37u|cbfNT2s_C7bg4SWGyT0yzZEeex z5Z;xyw_)Qoul(Wlm(TRe)^1*25`Yb;o&KZfX9$DTP?~NTcCkKwTP6}Aqm$0cxlOUK@Y|2`&^3a2FOcV$UfrMe7VCUSJdaYsir{E1grqYu*- zY8#Ng657UC@X!OC4T;A=t0Tm$FwkFydt0mAWY%0yJREm}kEffpTP%-i+JB-CpC|y>^#jnZKamIRf)9vN5-w4CE8$`p zY13nyB)2)0nD&DYk*7WS&O2Y>+c2=|GUW*l#zte;cfda!sBeVy9NiE(~#FPyrhMBTld@ZP2A&RPgKz|sYFYS$gfqg)=_3&CH zhFUN;fh5n^+Io}pSfb6CzR@JTkho}sbWg4U(#0mJWqF3wgT!rP8|7daF+e(BdLGjG zT16(co;TD2NOu!`#@1diNsl19jOh@$i|&P5^*csLUoTqE%$`5-wlVd+-zGD22N+^( zgPJ>IY2&&O_JxLV`4ioS(u6;p=kqa=!kaYW5+=sv7_6%#%$eXSy}Xp}9Ii5fAmgwWV?dhnEF(M`dO)~MW0e0e*Ez2p`pTmRa zVJB|kB&5#3N~H?^r2q0?_CY`_#N(7(+W1?@ zd;~%P%Yy42<58w|4tC*)cyYl8$%Am|NXQ9v|09v-AGO5EUu4B2`64S{$Kr@^Ya6S; zz*SKml8At8Ke!S^P|!RM(1p3X7#G@P%Ep@aNS4r?~KKZqOAe5nBEb-AxRCb%j<|fFzIO5p>z(u z2hk}=eHy`PEd4b0t{$$BKTx`C`+OVk7)u`kqa`dX<^%r}!+Zj53s=F4Qlk!sL83L3 zgp(S%I9{m==CXt2L0p&kuyB5iyy!D{pfAA_2=6IXj58O1S$mXOnHQlYAqKi(3QMG1 zl0giW1mT5$(OVwjGF}kZC)VS+@_;x&kRUXmoXE3_j?jG<5o2c)oQh|phQJsA=ZLGs zMa&ERg?Zy_{3B{c_TZPP89_MtB1LgfKFr8|G)C}Bz+Zs9z`=?jmC#S<-^9-YL=p`A zXMj`#ekQPh@*~pihDf&(o-GmB1lqd^1?W9+K}UYRjoeE2m{rMO&ao1Xc=)o_FnWon8yRxe=_lQv@ulw* zS=t}$31B}s7DGXv2P1=0uu}kkqoRx$?CuUo9A?-)Ksm_bg8kqd`KUq^8wjhw5%ZjU z!<|_W|CPmKEs7J0dCm!;(fm+ik*)1nTctAI>xr1fULu?Pyr-fUg~)WDp?N{G`7?3Q zFq`B(W;S=@7pU2E(9hs&sChdBW5F?h{o6R&V?x*kD&UwTjE7WmUl#3KW}km4U%u>7 z(WXs6n$8n(+M}DO8t~7fbPZ-tQXJC1Xp~j2{Dk<_yWYqSnO&xaT?TOd+Ac#Zmb+l5 zec~18#0%vIq8A4F+I>oVX3v3b=N9G35jqHY0jU-?F|qjS*k~J@ zn3y%Cp+T17kkEH*kA#L;S%riy&5Vn-re_6UM!OAZGSP1PyAikM(>ZwYQ^0o-tWX;5 zK~x$rZZk8p3sTFr7*hX8@i4k&+E3pyzUm^U&PPM~`p)%6_z~x$OTPzB3c&lAaUr@urys$-re}eI74bwmoraaf zTp>#8VOVJy=0>fb3MdB9`l#@5z-5LrD3x4qKyE-^SV99bE6F2#>&!TTLXewj&9~=^ zG8193^0O`Y&f)}dY^uOP5WaU&hQL7>y-1jV$EReS%}iaiC?Wr3VO)G3{@tSFlgW!B zGk;pF(k%tJ88~47r9W)d2z!d^(W{1dH8l$0)o%l@rn$(Y6t4y@fZS@yc&N7NFMhRjSl_NJaa2Ee_ui zJ(jt;!?gQ zTGir}3wPZfm_ZPADEaF{uUI5~_^0^zEOO)t;BJdFl$tL}`UuP7*=!)5 zf_E6XmK@s%5J6(_5?TaFPhmumkKuQms?0_=p2U}M?QD(nhUg;lECc%YETVrjgoysp zse5*yahs=>?W9wG!$>BmEXW7g4;0qYr4g;6(~p3CpTbl4=OD}zoPMZz0^x0f)sPQT znid!oj>+{G@Dd^)0}ZQ;mdGgilM4bil00ET<^?~a>`BRL@@1rDSaf0xkH_({#Vs8y zS=L;SC}kdzr|@^?h}rTWMIad+=ELE#!I9Y4f-GDF;)tV#zbe`~ zZGf58QhO8+)ZK#pJ_5r8VIOdH5?UaTbbzmb7Q~n|{^K>U_2`?`KTGtOJoNs~@}&xhf**rx>A0d(q-bku@ee;Cb{ zG4+cyrZGZWJ>k^w;5nCk z-<)Z#MC)(PEaA}bU>|rfNMVK*=%-G*^ax%N8#vsf)T~nc9DfgjwxAPypZ+=186<8w zLLvp{D0QBt+93B@EJx7m$B1I!PWC9Djm|qNCE|%>xCuA%Mid1OE%RqSyv#dJ!L$F) z&wiP`=eM>Xi2C@%Iq`$}fhnGDz@oyq@=!O|C{Jz}$f6*CF8Wq?3vHOc!Ql>ZrH|uc z;4~W?if;%Fwu0Zff5C4+etjoEdK6X#tYk(mO|fU5fjwu+Q#dw0;7^A5GOpEm2SiF? z?odtKg2Kn&!ivD&vt?3Id$7Y$DlO|g0qsM8NZ4VZhk*SA|KOsR?kej4tcRt+BXCDu zo9-IgqRj5a%b&udP%UvPQO8@QLQ54S_ra4%gwnr;1WAczgZ;xgzL zQT^|Xcw0tck(x>m0K3!_FEjzstm>u zRC_2L_A#}GU@h@5q7-_kO5=}UHTr7|RM3kt{bjIP8CqF7pH8xI3k(9Nz$pHPe*uxO z>2|z{t*BQR&F6R_{3elPMgV7`C^MY=%%nR{iWAM#W-cfwjEB1iML#?fk!m&@NHyCl zXs1ZwyG(9pLdnfVLmM|ly}dzOvAN9xfQzy;^xJ0mKDy|A0-BngZI5)&0 zjZ9O?qd?ChKgs&WN#=yVpnE=C8vdDtp$H&XGAk{CN=gYVG!SfA(U_!2wz$zQe=)4c z9l{rQ)wGU_K1@Bt;b=-i)kBq#2w5cm#}cT4#Kxn9rFMjxQ$4W}N_$~1Gb;&b#;oM4 z)Jjt6z&}4WNk_54#40A^5#P!kO@cJy8swC zg;R49myMG39-oq*`rIS_J%pfIXO2xqSP_pyup)^HXvIU)i-E>0 zluA)~z!Zun(j!LrL#LMQHnj0K>U>SNU5GT&Dq$4Q-rE~Dt_pdjO3!Td8((l3ZtVm9vM{u1<_Xa)( z>A-r*{EQcL5U*6ofW zKx6N{EI-|T@vcR2`*LlQa|^vJso8zb6t6%7@IIshSJcfcVO z6#q=FTVYO70+Jr2bAH5Y+MUk-$?P$ZAlwA$gTl$fCKSYgHJk<5(!tEN7fPje(Vt+y z(UKF=Xj=g9Kr#xld!X|s$WTTDN73Pc7@nBr5Eu2hm(Rh7csObYx>2T#{mv1tx^dho zJZzi4FT*oIg@9~-;fy}WzX&Ib913oLK!FrM=(LOY#9z@IL!Tgdu)PgBd4r||)>`%; zx4_v!LYd_PV+2rXx4;9+NFd6lMu~mfaAM(1s`%@7nDY&j(M#zws6n@df zB)h#x(SsArn$c9BPDgYcr5mU_k~5LEiB3gwzzw6+@FH~%qe}yY1$?C2K>OYs=?~2J zy}_6TC(efr>%m=rnOgAwZJ_4ooL?C?U<9Lg$HEydY}At>Q3Pg~K96v@$SMGuDL5H} z8cha-%aV8_aFJBNma_Sh5chyAq%`wXgxYhs9tlBlU?XQ+uzB3z@aQ-J$00=FA8y0t z1txG@mn6HoE=qNbTN)bSY-QmZ6C9G_p1LT0QFv@pl7G09(8e=5G&ao{Q6U;Ow2zQY z0C8ek^N7+nP}~xw!Xp|W# zJ&u2oeZkcN>Vq^I$O1xHL7;|6e~d_n8gQs%tK2}8Mw$=m!^InfI?rXsngy1kB}hg# z)IJC5BLV~OC_IPAfpZ|Oot&3Zk;Vs9AxPchTu2=dM3hl8)MM8!Riwa<&GrVNbpAMC z6Pt`IngA0-J;33lM$;UmMN*Q51v^L$YgYjgCy{8+!%8z{U|z1E6(wF#ylzZIvqh!i zd)&}$xq{6R+B2p;U5qS_A+RG&Q(xYL${O0`P%S`!EP!Phz+wrOWJFET_-J|xrZ@vV zR+>cN0WAgONu^Dz)q(A}Ul(jTLAN3ce0qn#?8i`xsQ zmm#X^V3_C&KwHn3Kn;WSjhbO*cq1EPaKF=+3{=6d^w z#_%XB+ZkJ>SAtSki~GApI0egepCU+t0ncB8vB?mY!^VY`H)y=!PfPL(#}K8MlPXW; z*oQeeM|pEnWvLv6U$_O|A=C$N(zyo)LBO*>yiw=o6ACZM;&8g3!vVt(=Yf6Wt)h(1 zpceB6ltRaM4#!m>hIhoQ1R^I(p*<1m;7=Tte{UbfB5@m>5Qqc{2#ur*$FJ&?Nl|D$ zXc*CszRm3De^GSPJiQoB&`7qX(-8)s^03n>S~aE(mlQh z`cWj^(rt_~=}5YTbcR<48}rA_@&SU!=5x(!gY@|1d@holQJ==PL2~{hB{>I!v=1v~ zgy{?H_(Ns^T01a5E@VHVMgTJCCzNE2Mo>jb#&mihUO-94bh=@Vk?acT)W{5Teb}Vl ze7-3ZRTL+rpl9lvLPKlyOmmTBYzh^UaTes0dyxcboL3|p(@=*(*mgvD==DH65+n|D z4*4*2#^vV1@RDCaa5Dk;SJ2%E81x9_3;J;GCapYSm1L7+FNhhPJJKZO<7 zDcqye(N3e&%dmG%($W5*)8E6cQTxZN9ni~ufZ#G%5fIM;H4bIu1MLQB`Iit`@SmhF zN6VvL!u@bh5WQS<^}0K_tF9SFlMYo=U^JkK4@b3RhE@+Pr50Xd%6Z1#}xDn8n}(>9FHre8$u-KrKqk zg{?4Zc!HJD3z(O&!v!8sRg}8(~k; zWg2QkV<@Nz|6SMHy!D@Nx-&uVT7(o-$d!Q z!;x&oM*2vj1P|&jba({cN^*lx;pe()kR(b;-IWy{?v|vRy)!9NIBW@!1#I#1vn;`b z@i1+3t31eQ!q#@eDM;ShWas0ctHh)5DAy)DbCkhB59pPTbfY+8vEVUj$-=F^CvTEE z%|(`!@RCTi^%B^rfYrDM+7CFJ#-n90OH`^ik}#RnJxW8W2eOFXr<&h8t6lu;eO+ttFx&~W z*|xfVMx*VmtHa>~vnYigGT=!EJIv3(+bLO9MpjS>eP0W0z z!d`5-uDIexHrjb#I3L)f_Z3#s>}ot_F~iGYH|FIxa>~Q|6VB4>!v{>VGOs`)JUv2tdM`c2xb9)|oqoCzF?xS^+U<{?-t~|A6#}?L z^v(K}g}@%ISInvK(o%@QMQ`kRlb-fKjHlP=5Wfn~>=&233v0re?LIg0XWcjP?BsTz zhY9ED@5yZE(QS;b=cO7(cW&uov|D!%;hAH9tf%gtjqSQ05gzEkgSv!$)^8Zn-}w-K zszHQz_)`}>)v@31r;B~PKVZMxUl;q`es?eL7uf%wPQPa??-x)1TkrQ(_CewCr7#1- ztKuJe_gWi(#k_@oo`(XQmrx^Lpyv3O=E3US~g&}mDBo{pqnoUG0>zmEC*W+ z$LMqk$^q19O;6OE=xwDMHY8Y#Mzoj_iplb8N(!R3NCjIOXeYuCsu>%0_oc!P+*>g} zP#lj7k}*(FCrTz+m5rg%`e0F!>Sq7i%*33f-@P(b9g!v3m;GC^955O)mWfp>D;C#J zyhc+lm#e5oIUpOovHCGz=`+P9#>*zCN3}w#HWlzC6K$5*Qziv-q+yrOGVC&Zw>Wxe zVw;A7mYmm}Q7Yv#Dg?N%L7V*#>NARy%X_+YsZ^_5+D(#(R(A)a@2p|sKz}-yzyChV z`O|V~{P?%dgM=Nu0q6VP8b{Kj-G>B%`X5BQ|JQQhp6T+arJaAM!?HSiZ9@lx`;Kp8 zupv;XhQWPDf*Ts5i?6V+u-9>d1TrYMGGQygug}-FUsW{j49l4auC1gT0uEWJ3QTO= z?}nP#AlMamjE*^E%k+TBjw@`YqveoM5@Y55e9a?SjLAaE=(_}UnWktz%}NH)6&{+fWDMB52)7@kKxUXv_{Js62^pnPnYKBe zAR-eIo$SA5c-#fo@{tW=cpzjHOq)t6lO-4%>7&T8{cuwH`R$ z`@LjAz=;Nd+56p*v{#sEJ}@)f9G$_OVSVpmkf8LLRG`8A1hkE)+a6ZvQa6;7{~eYO zcBx^VFv;mJ zP0^euJ~o~8r-h&r&()*jlUjL9FHiAuw#DNKI)nXIH{!tqVaxZto}eoj(!W|e>+=)s z^(j_q-*ft26{wW2PqBKUh5^VMqC>BnrMkL=1WTLt4u4AT;OU9l(|Zy9&kUgd;KKh* z{(W#G7~Yl4t?pAI$6Uf*yzjpEz85trNUQr{;0v~=gX4rv@lj?3RcVhh&4NlS&suzx z7?ua43_&ym_cNTseka(V7a2x!_@8#nh^nYc?!xd#zT^I4U%vK3`RQ|#N1fyU*kESs z(@$?r58dbAk5!yn{EBcpj$9pXzyOT2c(t|5S0`*@+UWvfQCOt(k56CM_KPKpNu+Wu z=thphyM@|#pZhzAePM!SKdYX5e!`*Nd;7M_Ci|4kOf49;GG+~L%2pdDDLOM4%pwiH zAZBXKhS8`RrN~&dRCOBrjiF$0AaAi1-u1R)uR6?S56r%O(lfDYHXwOc9#c+^B*i}a zP^dK6FB!A5>#jfPBM(nrdD`$vbN*z)qEyoP;YdE-C-<2Yx3xc&O-CcKlHXrRK2pdQ zwy)iK$(otH7aX%|Iv`t})_~%%e=b_`?o^!SzWziw6YZ>-cXZawna8%*%s4#q?KN}e zVYJpGi?6XY!fxmt1x^!k3dDTfSm}k?RMc=X+{Tt7gRS2@kdw{s?P|a=SSXg=h~#jD zhw_a|pOg?qYbIN%x}=zBG-s5US`&8P=T_BBHSl_%mQ}s!k?^+BzE*3{3*=eRcp#-DPVOL9Nr=h{)iSiJ(|Ac($cue zDi1Z!5zlE%8qLOP{JreOTyZ9tHCau9Vzs8#^`)}I6&qPsNVuS*BG?PgGzQZ4@@};dQ5^R15@JN; zpwXQe%G3sYMhHIHTE4EDgnn4$mBdViy=$Q66pf04i+oH*zdfIOyEhb{D1Ii@%(&gD zvBX2MX402P+W!sa0@{6z#q*gJ*$SF~mi#{=?S>rRGu$0Sn?b?GpkD}Hc8CO`dRwOA zHcEkLKoY~%pb#9)ijvi3m5kBhyx3m|8zr;TED4@ee<~zOfsiU0oteNja(pP&U#|^> z)PxtkIupTAy*dz1S3@ShQuR7wb1WDuGz7RE?%fK8c`I7tc5RNr$ox8#Zna zzyzTNTNhwe=r2Z$lBLfqiTS)5whEFO1}LsfP|Q|cMkyGf0n}6$9LneeNDN=&lYFIg zv|6gg0u_h3Kh_tmmFtmo)+PB(p=vrjP_2i838!FX-h>(&sMY&ZLveyqSbP+tqVuGP z8Ya)jpdy^`ap9(8ZW70YPr4V*b1$z&CxW2X3%t(9PZ)Cx?{hD3jciDW8Ga|6MLI9Z zf{Q;w_OYuMp8yr#zIgY-@#w8x0}=lo64`J~x6YC-Um^fWfr1cgi(JlZi1@>-aIWDc zXdGzWM%GHLEz+6M8)+!4Tb1AHG;dTStHVji#C8!~#N zRG}0ZW0!bBHry8O_vGfLr>Uo9EenC-KWsTSs4i{URSw7AtYy-gIm|+7sME6G1;S4be+Gi!$7!Cfi-`b09~o)nV=FdB$$Rr7MET=@LVJC$rlS zMQ3i0@>Q;fb6CVPU2RzI2Q_n$m3tZfM&M%6#R<-|NXjCCLY9|;W42gKlEY-SOOSz_ zW{09!Ws_a%lL9?(ty5&lY`3{olWa6v>@H%=hP>c6{FKYf%Ohs5U$Q=M9CV3POjNA8 zc!c4T0-Y_>*0KnbvskUEwOm;(AuzG^5c@aQY&}N(tTyarN3y%12N^J^Y;#Cs!W1L& zz3g-*Go8&&WinHH+ka$HNbjvchP_|2Q^|s`sDD?;HZk;vWqXXo$O_3M>+-9#R^CcZRXc;w(%}MnjMcQ zMa%lR!bC{11}w5#k0*wbazXYF;E$9MV93Q;3(kVE)Lh9`t!~;-%-d}FV(S;$@2SZ~ z-9amfkCf|zJ2YJov3yj{VdCYd=;i;1mk$dm!)rKc2m21n$(Eph$BM&70(g7Y`R9)G zyPY zA%YHlZs@{BRvm&3rTnBuKRmaXGn8s0xzKPWKP{R?Ik2nOR@jAx+j+S*a%x3ud(ZsA z+TO!Y!D)5Ls-d<00PYp&)~0d|OOz{s)^lO;lZbG+31fyWhN94ErT*iywk+oVmVfh- z7?(Fca4X!j5MJFA=W_5vw-guFhD9N*Vo>FBR!V$J5M2#Aaj)Owf7@FGiXAHa$C`!v z{~=i5Fpn=@44Fm1{9Op+I9q%a-<$Z3^J>s>aw8gn8YT^hh(8oR63%4VW%nTb!!N4= zW=tzClQr+cf_E#bADZ8LK)j&h+2gWN8E-J780u5$0GuJX_%&!5R~ahUW9`YH{R=++ zrrRu~obHTOeR4rb4#sRgTeRX=EUH!Z6vE+h1iQ^$2uI6d+2l9N$p#Mjo#m=kS*}`j zu|p3PT-C~kb)~!==QCQrC>8AXLTPNWT63L+b)5$^YXObSE&>^2JTd~B;*vAU0Ip4C z#K$7G;oLw`Hbw(6B{WiQSW?0HC;Z9uRQ9PKjEvXSg_eufV-dUYk9>FT;k&c*o$o!; zt%`3CuJf%r9J>=+3_Ul6U4~cnJ*a74O})l0s-d-ss135eQk!I1|0d!K5&44ee_+4D z?uCV3*W3uBp4GlVZ+Y~Ba(ImGnV2Y*QW|FlEG*G&2e#5XJMDaXc{?BndOQ2dk>~;K ztmM{H4iD_6AMdpDE2LDD$v4-5r-uV`g0Hb)z3ul1g42oYgmRzJ>(rPphd9ppt|+@* zqRruR3L4|p@%sw5zf#|@ygEycp3wlm;dwvss>k#z5kAOB{w41?O9Xg{0uXjie z(eBcQyTPbPW-FMh8EKJm{R%F>1qANJzZp(ps|`8PXvnjuAmcaL*p}^buuqDiNafzr zE`QX2oc|$w+;4H06X_BD^qna|Og>kIMr*YUQ@uDgJ!+Se8@T6|;ix6~T6FbIQP$wOAAvHE1( z)M9aXtylSDicvNxXxn6o#vRz36{MxvAzVxEdSuz>^^?4k;b)+xz+`zAFLRlT6Lrq0vj-1lN&*SaS1uV^-7nww>dfca8HuMn_KCI;>cS z&saD7&clXHN@#lf%0$Lu44X=gfrLqR$wptPl#oLT()`608MZusA+~YP=JAs@l{W0z zG=B2tlC!w}_~EtZuPLlOet7u!^~EzC>G(b6Sgf2*C!^8ieVHgyXvX7d7;^+fKVHMm zXWzr^($MQ+*&$q$fd?C3#gYT|$jU?Era2s0=7Kq4b%STJ&lQ_Y=bUvF{>s|-iAT`suk!B$cbrzfpp49E! zq5-b20-r-8P#%F%h(!;yMWO7HQbynCNTFg@)$K0H6e`DZRn;oO zE5MwYRHNeH){T_OpREU9k5A-O_k9c3*7|~d?r^0Un7d-m9`$ENBhUF^-2fb9)i@8_ z#KQRr*)(tp7{kudZ1Fr5g*6{cH3D+ZyuNwyp5mmEGu0}4;3%k;T7Q7Nm>ps6CPnnF zSbPs#i8Tb7StZR8o0higS#xKLXUEj&Zt?uNi`kF-KmDn{MS>3YI@MoKxs9~?op-eN zig?d=wscl35W~A(e2N#doO+A@XFv1bqQV^w=*f@u32yCBu|L_JcJBHawyT9w=zGU z5dU5L%Uho#p^Tuz$KTRK0b1ad>`(A|{}Ip%i!UOd$9?Pt>|I2~A!rcG1ymduKv;vG z&BNsya`;^G@sH0P;#M84YaGWtGkg0HMi(-9^u_zxzE+yjc(8A`ZdE5BW$7)V2ciLp z_{I+LVAtCyMiDxqwx>z1A_tmgv6TJ$#wxNrIbA40n3^6gHf3X`To{(+3Oj$MT$zmw zL~7H{MlmxwR4ggcvd3JE#ya? zR>^W_j2L1=Y>6MVz?AB+nC;NJ(`FkJ=x>2JS-;Qsu+2iQtpekZEuyI5jwg#;46yB-!O%@t;yPQ-a>B4a)85vQG%7>ZlEc zaLCz*$1e zdEhk^S}r)8T_{@>82lubX;jKiQN%@8@g!`3k~}%)A1s;`RW|2qBNd}4xytEC-VMi# zXK;8IG??N3iFz&@ZN?KduP>O7+xz0RbgVooj?Pp|)8$6S8!JSe_He!A3Jzz5vlbws zBYVN(m)Wom_e>Au+yn$&`>jBZO`_1|KD$t|$^?D72lzUIuQ>1}3xGG}y1?h$MnQDL zBGB*eKN_F^-2g7Q;tFBsC6^FYN(1+6F$S64$4_{M7at+I11fbZQ7O`b7cSQK%Q1Yv zJb-dU*JA(BchH00z0uwe-F<+%qp+pDQ~2=mx|`d3=G^vP!OHz1&RrW|#Y4N;6P-Qh z*XthJsfu-DV0RckBurr@INc>>U}Vs0jYsqNusiO$>n?(H06!iTzG5&zlLTuO2VEoT z!sGTw1n(o($L$Xbw=JCXupJ`=#T72XuF&Xn4jFk)Bn&_kzk+Md0Yur4L0gx_A&!B_ z;$HSn@GL;un~~=V=Xvvil~1e+&hK`feKy>uY4&W(bv9A50BkhtU^U^~p13Vo8&?rw zm+fdySk5B1F_XOVPn8C|DVEhUYimx+)aG@*J~O;#6_WxZO=RTs{*^20CP#E~Y}jO0 zM+afAsZXyfHzRiO)Tw%Xs!7%wW)SV6N^*3f3Ne;pl&`UQ=p3X~0s^d3%~#i}Q)+HZ zaC&fG-DLE>O5M%Gq|rAqIXNIBbiim7!JO+Z$?;*~&OTAFXS0m~F9H%|qdJMxF!jJo z@u>prTBS^KGznI}!~HH|_Q}yiwv^HATg3T}(=Z4>hL+$3eHYfeApIzE54$h^qjo3& ztyGRZZr}L~pO=z;`hmB?%lGT$_v!NmZ1XrKst-Hvq11|2o1&yB~`6*~cN)`@_@Y_)~h9o@&xVx8GBi_Y3{?cb$GOU*0dC!WOOL`C6{6 z_P`9C^-rC5Kgyr>z$}!Qx%2Kiu5EVTh0eL{gbwDvrejkZEuNi4&z8H6D8nh!@5#5R46XZf}cST$|<^wQ1_>jt0=)YS=($% zCI1oV8>y|yTS9Vpyiu+z{ek|zF=!w*Ib@k!Q>~iN-&nOF+JEn!+*tpYL*x0>aO~fbW2w|w z^50^^sq|3)qoFZ6`?YK~gn*XYj&^HzpJBaR)`MY+pyBf&l+Nzrmr6Oluzs!uK1D+k za_xDDtH>LM4XZR7M68|ufzN{^cIXk@X;IKc(m^;c&4C^QJ09(uuFZXv2UHjXK6}FJ z48i6g337I>J}~N&1!KG#3M4#;mNKSSHJj^AifL?Ua3g{zhTd6UuSjxbe0)ZhlrbJk z5p8;`E=l&LR#?Grmb)Y--!gAl;=EXR|6Ae| znof2c_l`i=Sq^azx&GU7z&_nWzn6B6Xd)ij1wQ|0tfuYI8(b8VOgn(ve<&SSs!xOk zOFsJh^TCyOZx=1DklXD@gxHNOH~VSp0!uiAOj7Olf$^VmjKj*r2I0{nE7M=F(z4vj z1Y6U^=m`EL1A7FV2Es8o4Y_KxoC=P4^_XY9^XmAw)x##htB2WlS}ykC);s9a*$EWe zq^<3J-1Df>4l1R!jni4DQ6C<|UxY9-VYUq;P<}jupa_4#F*isjIGOUOunm8C$lf*r ziM*HlBhG}DIH|c<;_#a&?A>bX>Z`INZs*3pyCxF=f4i@*G3%k{<^8vMw9+XFvSMS`Cp?%_tZ7BWMY?^jqNG($Ip7oCiep^JhchD?i7f`G| zKC>b#imlJmPjR=DLvL=Ip zz5#cYhKETIuq1?&4Axvy--m?{_lcrC2La1tsz-9L!^vV+s%04UT+!owKfkYQAZkjG*8)nW4g$_go5 z4L=s7LyK$a$-EO4JdmU>cjXdJIb+TaMb++Y^UN!u38)vC;u$6>%k0s zzH7bfLM>NZD5#yOeK(4BYkxKdwT!jMyYvb>OE?<5=TBb~v4R!Y-!F$=V(BRm+YT$S-DI(&4b$Q-T+{KQ)`myHejfo9&}15{ zlkJ^0Sf6I6V7ZUB{>1uv8Ugm=K%rf@SUXEgnA5OEVV|6x16yKzMNEiUB!8yg9ttR; z9m(sHM$09_B`es!d*&l~9|9V^f|w|*^t483M$jhd5N5t2bRD9jL8Lgt`J3MN=h1hG z>S@~#>!;hbw%S#t-UWrurfA^E<(w;N5zSG5Fd>_7w#cQNw_p`3{o$wvo4Vp=4qvIp z1g{{Z3$yd3njmS& zqHqvLDn@GINZcV>KDAO^aqEgMEC2wQD$f$ytX8ktf#~g(&_@AH4@A%ZJ%}{vRRs`g z&mrPPCh>+GKK&^qIOF zFhN6wVMJRaJ@cg5b2UFBHWY;F5ssvlLd;Cd#)FCBt4226>T^2I6V4lwTp7gCii$5_ zFNEdht#j%e`@UxiSkW+48a?hfPwTpb9xsdYKksf;+4pr=rT|OuO=fJ#Ah>|wtotq8C2rXI$E}VVi zmT5|a`%>RgnXH(#T(&#%{5m%?wO(syX!LjDtwp=kO zn=Q#wzcUNgo-`#93R#Sc7SEg`l( z;_Uy^9CVp|-h3-pXhv+emFMCw!r4=qe3_UzMoMuyrvqA0;vOkjH|!Gi2ZA9zIkga5u^RI&#ud1d%!rug@#a(@=V?VP%;6L=#F5l#_vThL)N+Z;Y#3W<*_MEpDu;dm|CTlBg+={^O$ zE|qF^ck*1Td_H>W?pc4Zx_eI2{5%8$c}&L*8?gos@S2L4zyIBQGDDA}^}7H2$vwbF zyMWpLUrsm2(M2@hI*uqe%%}OT(y#=J4aX>1X8xI%Yxt*f!YP&i3w}cj!H;n+gOKb8 zDiN7~TKY2dAa5(v-lcesUF)X$iyvD&gYAG<4xVDr9H6kF>Uv8@^uY25o27sn>=TrB zEHV6VGT>{=5fZR9W)ekNLd;_(Dy@%;afs+6jfqi{!U#KkGqE& z6H_s7U&Ip`OdWDaxS&Ss;mT+uQ6DH3Y-UgN{}ynadXAIbh|dc}Wd02=WuE2^$4Kz`UE^G?)HiL?7z_$W7ARVZK=rNc}Al&2k z`@iT{{iptKfD|_i|FM4Ifwz*a8W^k_&SrP8r=cxCXUpV(Zp3lXPd0b_?a|wgd+PW* zBDcQn><7N^g$L+^H&S@x%Jv(GV4#pc{`1GzpBy=L?^8FN964og_XW&6`amof^+9nH zXnXJRw?{stHA>VXiRVw~&v{G98%~Ps(AuEqQT$j3tw_@(HCc<4U8J1pST&+Y1p>~& zT(JUOdnkYe47PMORaM0aVfXzWznZNETGz4DaUdd~My;)5(JE;pWUausiAJ?8E`>FL z_%9ObyqfwYEH1zPb<6d)zrpD69BSiCI6O*U!ww)U04BVx-=jjzkL$RI+vkxG+zg7% zoLkw+E^Rh?5(8{$l~py6n3qP$MR`y{;}OAyX?<7^u%$(xglTxKizXK zI)P6xY`7KY6(CuAAf0rA&UHG5sB`5AK?tZ8PtfdA;Nf)j`J7A$dmLt)Q?#o=$>j+L z1UC|?E9AXfiA;C*3ctrlN><9NoP;IzB8&}sA*e}%!u(`8Etm&o!N;T;`?0Hx@bxs_ ztiywN4LGnYR2-B^u^r7J3)ug_N&CBQ0phc&6sysx96mg{V;gEwRN`la$J%xxtyZ&L zYZqcbwgIX-Y?H6IZEQ#cD?Z9igan251oPA2^8;>Y_yBo1gtZ)N4~*$NZh5J;>I^us zgT86bM9k9~vJL_&vU`$`(o6GACMOu#$=p6jmO#$@Jdidu!@A=bd(ziHawXGz<^A)) zIL+l>>On+7o#z&K*<(BIu#%kHynDmouAv;7FehAtxavHBeTN)y0 zG4qCR@zP;Fh7v#TKy`o4h5`g>eA}|&z`z_UZql;2r=B)1Me-rqf)3rhTD7Kz2#3-O7MQ6XD04;QYnq? zo4izm;qLx+-2-Qh>VCMV?gQLsV1uW-TQ|!v!`K}Uj#4`~eNHoq+1FZsaE|s2L2_Yt zo+I<>Jk2xebN>R_9?4GK{!Sy&?bezHhu(jGcho|F$I}yZw{}M@1On}md|H72Mq7VE ziI-B{FY;0jmrw$FHOcIF*zz^1r{ypQH;l{&W|$>7ugxAuVOw5w*6!smlzxA8G0!{k4|IwA)TAnF}#nj zOoHYYyi~*RK7sgt)^M_LKf4I(XP#`2BkgD*zsi}uW{At?G)F=eI-d-)BOHrdf+a_q zaMQ@J#XL8Ys1Fy%RyE_(6_a^xIzBK`7@coMOGnPA)nju>vhx$xpCxOrhIR4G zztRj5)_=!KIo8E97YXZ*#n*%=EchPa9o8s1KiiJKgQx(VAwxVKmYPgQaUkuLEWT`K z3FkTyFEKV&tn$bS|7hz}I9{F{}eA}@r8Q>o$b3lYx8b(pYkPN03Wc;n)YhO0PjIT>S+*#kvvES|sqBcK$Z z|2%&^DbwH#SP3RvFO!HSq^ZEDu`dfRi9g2fZ=IK__}Z;K&Z(u=+MZuhDP_Y)j6QI7 zBJYDD$rJQJ(mHQV2e)z?rq(- zn_M+(x&~cPUBZ$>5bR1}K#dJ8d-Y95%qapROP#1&f<@$7M`TsxX!Ym^cYZSX(CNa( z3-4j^C%)JErMBnUiRfL=zfUzOhKwoKe>eQ(`(GypD*W+9oO~xXs#Ca)s1F5!q#{^W zhPHFm$`vsl1meWLBbuL&@Op{ zV~szdNS(k+c)<|k#O*1xx4uvLw-N71YY{=g=!d_>`+?wx($aPv12}tkMW^l=m(cAN zsLPT9PURzFj9@y$@Xf_bFz2r=USt4Fo!U2yP#eGw!K2rf*$r);VR)odSAidRH_u`~ zR)KD9DmNGwFscE=cHz$$m5FN-s*>0m9ZzEv_C8w9(FwOgk%33iw_Sz_LzPDe*0sO+4l1{>x<_wBP2xhsfw5= z&)F+J7ui2^143Kh*E+9t4YfW7U3Ny(WuY@=6sSv%A0e5l@jt(O>z$!HV>et!GarCH zJJ!}`^?J(4ppTh;C?fIde}3t&?6-RLFKGI#3EB6q77jyf2Eq12TI5o*B6HI}{)UajB!tJZ(jnqT)@L=EHmseU!O1401k-nIgJQHh zgd@enLe+SzSj%P$a96@f6jOs^D>8}r?rq}H$BTXa+jgzSRUMOw(aEWyTrN@!)M}~e zve~6GGwVdiT6E;g2872kyKWB7pP z|Nqq!?LTwX*DxK6cfuky0QeySa{5o!HHzcF>e9^3ZIeFOPRNtD z3aT|=k4B`GaPT=LU$#F~^2=VFM~g~_!*O#Hd@*!2csOBp<8QJKTP|!D4Z~9fL*RVLsSCrBVqL5_UIyQNxte zC2kQFZ@}&g^-1DL5pEjEGomJv#v>$uw%?QA+k=O4Rg%TaJgqC*&MBLk@Utsy$|W{Y zPA3=c19BiC;v~u9&)J>e5}YFtjq!*%k0?HK|NYVX?|0mM^9NIG4N^vazV)vm*c@;> z3@}Ri&JQtClH0rYlWgwaEF3!NpV%U934b=>Pq0t6j!pVfH0LDOu>fc#C}7VUgUV|E zoG@1lRL>F5_I^(KK)@G3U*9NtO4reiCeS^xl3&KD9nL*D9##I6+gmipB3WMazoM%> zqGL8PSQXq8+0{wQP+&;0XJYwT({Nx|?#o3ALyj?oii9GuR3tMskNZ3?s8srf^R0{7 zrR9>n5^bGI&=NQK8K7Mr^;1}*zU{Z(HR|M(-t83iGm5hZF81-(Huj0u;nXfBMC>PQb4{f)P;PDFw-Af3`H}&vm{fC;JE`J=GxKqQcUsf6RY$2u3CB=^)>eZ3 zLd!?oO0r)LH(d+&3g<6?ZwLlx#_Z_c9&nCD^2!8Ow!GDXKIBLvm=S z79F)jE*H*fotiG#t95ok>-y?=C=mZN%}Cww0()EA{*h4XMwXXe=;xWji3qkt~ZIWm!!jZW$oZVI;C~<8PA2yc8gWC$fA)riA1s0?tke z;^BcY(MS~5E>6`OpJ)X2j2)6#&S4#SVDF^%D*}pm45;WO@ zHXjx&WgWjB5xbYsF3BK%la6PB?StoUKf}56DF3!Ioo8&je#h~i<4^YNc)RC>Q|KMC zgi*xQ-?c8TZbw5gageNw05Sz2X=PIolxAJzP$dx>g;r@jlXcO|_mKw}KqlD&4|;+oorQ+1W$Kr)UfmV+xp{Gu*>oe(U)REw3YWTmRMvzIfC9wFi$Iwx`+q`E=;89sGXQB zO*bd~+`a1r>9ot9ke*o`m>(5aUH*ThhtFXTsbBY>e!Bnb>O&lR=#oc)aqJ;3{T&=< zd^Np8UmqyFkXFY4ew+hd*HC`rTZ@r}*tz!fS|M z!{j~Cd&b)P6kjtu*9JH`-Q2U(ix0lQL)?3!1^xa`_K!TWfE1MCY=%7dd0DM-7k*y$ z)!Ugr;IeQ19GN~`Nd3W*ZQp%9PdD9gC^+RJC`e=7%U3pshL?XvFMo)aYxT+QM)l#z z-%4|;)uvLajZz-3PIcFCx}nwGxw%`HEFW6koo7&~)-IK5b@!}0SlvDRwqvS$Go7{I zb#dxp9fzDb_<0F}Wj}vP?c*u92b~Y8@TXy%IT-E0L}xs1;t;p{yNB)MPw8EH3Kom; zbo)JRdB3oq{=U=iHOu=I0t{Y7eSFu^8IN9=5yAgb=iLYR(;k>{0z}JSOXHoT6CgeB zV&7~(p@VsP)_oJt-c8RmoRXJV!|5$6x;Uj$dIKjwPM}h)F8M~Zx_9#%#s3-_3HV$~|7 zh<(OOsemeGI%O6eIT`t3kszoFgTyi$rF*G$(%a1$bkSbQ7StT1^Jhsd9V{da%#c9m z--v2d-wNG;#Hmx$L50(hf_crF+ypWS$(iZ0mOn_XC*pb-(EC@vApAIbpLlQb$4sLq z(=|>{Ds|6NdIGuv11mp^0QvsO#mkUO5BD_+Kfqj&ZBqO0;V{BZLP@7|?5g-njf=*j z#}`hDqH4D|B}wsI770c&W=GcR^%@0-BzIO%XGIGEeojeSrR^0>9*y_*tWxr~@HH)z z&h4&$D&?yMd+^3i?ZbtlURx0=hG!6?tI@VAc)3=;f*noew2}|+P)^)&LG_8!^iWX5 zVzk#X?zq5v{CS&_-h=ns{dM2(UDe%w@mznOukJVPA6}jgP7c!h65Jfly-tLicJJ`? zc5rkBe@bKS^t}vM#KV`oEuP8G1JZn4!7cy2Z-XlFT=q+;k^+w~{7*WbcXseaJ(uy0#a=JqS`dxpv?U zBM@;B3Bkh;4$9IQENM;|`DNpwNYUg$inQ5wimx|k@=KxIt0((CcMbkltekk42t+}CM@EX1VbO{f9}BNf@t{Gq8E_NAVQSx z9)j#j1~CrE%6wTsI>^4p^DN{@23mLUeX)lmI(>AMhxc;OBihG%dbtG3Ka5hr;A5iU zyTSdH@MqFPPrJd6+L>GR8==P=Vx8)lKu)Kj&Tvvg6 ziHN4ic47JZ+eBh#2U>e?q?lFkuC+PJf~VxB3rVDXE>h%bI|-hR{jFsb_>Rz-R=w^# zfN5dv>o_4={XeuO+caE*kJF^T>gAv9@QP7R;rI^fp?fy~(H#(ZHntZz` zEpD7!Lb7GmB1R=Kvr9O!rp9s;aq^0)?wc=1Qr!Qdq`2YoP~eA=K{76Kn+D~NS$-=d z>>5xJXzB z%7~HBLPv@r?Igy>vSTa!iYYgJ#PH0~zN0oDHN{-jEsf@es(;}^B!zfC-cB$66YPDr zaeG-+q-?P~>uOX`e>$?EXt7vEww!=Okw>m+Sj?u>iu#4d*&Ub9ufJjUCdFmnaK=?@ zS6p!7G<5aCR%uG_nN6JaL^ zOTk9rW>ESXNi<}z=JNv^EXk4zBpeWWm7n!$pSGoPSh+6`7D85I)-o~|88s^`I}{#p zihcE1WWs@yK50{SD%YH_B&Awyq=XETHIpS+P2*O8VQ*meh_YF7iZ*8~v2(VyIKC$C zou7ZBzuFjYy*3I3Ry&n>1ZE9!zU|vL9rKJKeZo^K$@9v7rbYI`LcvfEadHAyw$aeU zSx>qrh;BB`ARb~3-Unsi`K@(T>NRcH>lY8CT0PHYf0Ny;woYYgHYUOqfrJLx zlp?t$cRp2)8WG=Hjz+9PpP+s#qg6oeC9OgsU5-nAMt3<9!RZ`OHrTB`WejHA?{lYv zo=oV?PP7Ve{-{!6;blcAU|2APbw8 z4}*B&wW#|!)Wum>ZtHU6#-Ul32DgImhI>9uxZ`7(sZ_G~vf)oeLr@aXOMZfr{iOH( zO(5%E7pL0|r~ZF|SFfKkptlv7gU!njyE)K<_YPWhytmQn|0X6z^z=?lx*XFd#v=9! ze)fz$b#L0l|Eq~MI5p4ZzSy6*IaT$iwZK?vc$Gh6Q|kL&mE zW^n)RB)@<6+6%=k!fJ7g_?)m+m`6B#Pv_cwyx;Npj&A?M9%ul91yF|&`kfYY(7T$@ z#6YTWHj2(&`N2qbnN-!m3KI#NYWIy6S(+Vz+uXh?nA{;(^yJI;u4Xoy&u9|ihi>KA zGlS<{eDN5Xg$3_QoErQp?Jpi2O}0B|n)mvy5w7tCyw`}=_^!N3{lEv*n}~jrw6}x( z2DR~5Z+*h@L*A>Ut9|{xtHrCmpC^CDeK!X8?F-&`AKV(~iE`8|_iNm77d=^prr!n( z0|t^-^-+oE9D1oJ_@&)j)IMU^3*RO^0vl8y4Y6BEmWRHRcgVWxgyO22#;aS`GUC3ZDnw^4T&=D?bQ5{(bK}Z3- zl7ygwl1o+3of%c~rlDqA20HFY#(H&vTHSo;_Ck{-CsOT1!03PF;sQHXI1IM=GR;P# zK{>~Gpc_Vv{KNW|P?Bdxl`YeFNDrk@hPIkP`s>P4y0Fi}FEKWOdEW@(E= zBXIO0GZk7tNpBr^sOdPs6Y_{T*#A(bI`VT+>c|FS1~74GUWu*_M2x}wa6+<)l3+IW z*9&pK(a&Cd)V*?k018ue!%^Fkw_yC(eLP#?o8Q8%bg5%u&E16w#ip!p&@J4#U|Mv9 z`cF)z&xHhPG1ep3kOaE5T4XGn{Xh3Rr}xaLu5RHDj%ZH?V8V_Sx1hVc!DXXGpEr z$%3r6zEg{;qS5hEY8@Ulev8T*@RR{}ow_%S6MW=9gWQc$s!MTe`?=1s9PV3+yf$x{ zMx(WS%kgCSEHRax2|CyZZ^|G=U1&l#Wue4v3YyikZS({!0jJ?7&lQ9ysVT_BDNV>H z8UM4T6>(V}+Egmf((jGM+^U)pNpy0V)m7OT9v1>Dd^S;VtCESi`$Vf%%31BQ^yiHP84xPM6Z+uX=jQ{JMX*9=ez6x2?s4gA!85ow$l{X zrDYW9h*AS&wOT%KlI)Hb5g*$67vTm-Q#QJLJ0OsdrQLZSQqqAWu9XQMNwtv3&vUh_ zGIC@o?_PS(J&Slev}*NCc3R#KE7ILR$*ZckK}F=L z7Aaeyi>F#lBq>{=6df((>u!aoE3A%Gd9KA&q(JEk)loT+tEtGdI24cf=2?tZg6eRk zTAC_1(%nRhq2UZ*^+4BK3n{R|HKDE_9u$GHLUO4?i(%9@1cy{TSo zYQ1`pw2F&bTE&sc>PQD4#7PMs@WJw1A2e)GsW!enM{DD|Xvz3)(9gqqc#KMSd^|=4 zB91APZ?D6B!61QE9f{7|$qja3CD7 z^eaZ&(c6uYJhuHHf3BpbtCJHZGp-q^H%2GTa5U7@wUj4Mwc)%;-RV zEKRti%;#q^Ew_A)a?3Na(`#{x6<`B~H0}vd#Nf-{7haJ5M;P}(p~9OK$%q=@gsb@q zJssco&>Q0Ft<(ZV=)VHG!?iP{jX(L(RU39PV z7D!1NEo?hzy3|jzy7rh@=*SJ``#wGLXs#Wa)Er%J@~f-z6Z5$Di*n;pDAIUyaD&Na zZc;WpHneKfsx_Iy#K`K#?UB@`*+Wt2K`7G1q%he$YKm@I(BT1m8#xBo1HL;c9!cED zh^HV&TKDZR6#aFS5VZY$)tF13$YjObyA@<{WwprLH zIBc<1W3%HDY#LH?aAqP=gw9q5WhVb@z%C&N(jsm8|uW?(<&1YiSjQj#$#K5*!m&HAuN6ZR9VZQ zKH?FH-gxMqF5wSX2`>nbU-_#i#ly{PJsWr8NK5OhWGEs@2DguG;&aFC%iwa{q|#-6 zr|aR)LebY}*CL@a4lNQ|i)nTkDGN`B387W2a~^HX?Zce#iHEM*A^u)Gbrt)$+ucfG z-H;r4qp(+tk{|5pn@mLA-eH66$j^YpA&J99ks$f&ygK)D@Y0M!)LmgJ!abA0<$&9-MEVJ6l?i&YfdFgJCj*Ui3QH zQzro;-TQ;H^l!!c(>6;vPEmud!Te}hcmE9J6ffLA+PwjpEX+_+L^4|7{(0`>6>a{nZn(Wl5Xh5pH10{RKsp~u^DUPd-oO*JGbASJH}_Pw_>D%-{2 zSaX-#d(O}p_W)hZUVM9|a$K+yzWDl5@vscso1p%nW#?iqgKxwBuFH9Jm=R6U#wMn! zSwD%wf@^A4Ck&1gE#bqImeka5RuJY%c}K^abK=VWhR5L+dD_ug+}A9bH?CVZY8I5) zTrF#&9Hm5gn^Pk*!pvy1IVq@qpEKj5G^3SLYG&=)8BcT8VO=|zMm5ZW4kKpaRKDH{ zfO!vK#&;X->^P>zDIAG)iHYTRYWxR&BO;pRxsN(-c%BJ(4xbQws$7)i$rTA%9kN%qz6@#&Uqi7s>ekhYjt`t}* zo`@vf`F>9@l@1}qxm?OR5|AQFxD0}y7%vHBM9;qiV>lDABKBJU4sH#rKu*^WIkr_E z`%RHK5850y#ci{BWVamk+r03Q;I0$;B9s-2(c$!{QS4Wf!{d(vFC)U`$kh2+>cd76 z`V+`1b2&J_jcnc?58VhR;&41}9s&g+>L-3Q>&czr%JwBrwd4|KWy|Nk-^(wz z{`-0S$B=`+^$Or6p8a;LL`V%iJYQikbL-{Rfw}L^1y-hQoY#}okUG50=kX5a@f<$p z1_u5P>?VpWB>(^dfY4Y4WLy211ek}|*iaEF)}UplaE7mMqA-92!KGk{vOrV@{3bE0 z<=VfcKN+7awEmq9#BkIf$9P+R0i>h|o`Tf^`bnpDAR8{VqSAsU^9%jMvc>^P3HP;R zr}LSqf-IQI6GN*_xGK)Nb+$3$0pps?rDw~EsECVFs2Q$}WITPfxq^LS<=DjV(3(}L zxq;%kQBPm0S@ME)m1=1_e90`G4U0k;DPLYbxtvX{uOrl=9bZNIXm2%7@`cCVUFmOFZ4)WRx zA_Lt#B9Ut4n;9gvAbzS{t6mO}u zb>ExNKfs~Er$tO2U1h#}P1@xJ5QSl&(fH#n9N zSqMsjb1RDv37=d1JnXmdyW-rMC|o6OwUP9nK%^d>>?@|bbLS7g@$3C`N51Z@xNPqMSK$`*qoMr0 zcw>w`+8Sry!keFJ9fEbiq2!beRG(AKzjAuc+vDYk7v$wXQlK^|89d8m$(cb(9kD(>J!D{^-;c`W7$GjhMZ1bZj#io z59wGU-96vIK}cl(Ew&{7^E8{^_9a*1e*eRv#3Sqyer|990YE4i@Tj)j-B zvx=Q|DZ&FMcIiwnRL9N{#btd=iJQekMRut;Djp|p5?d!6q$lm1;(`6KcGfqNyi?dL z2ofA5b1VKF#7aScUuw82o$k)1UklLDr6DQ~q!LM|xpjzGWF zHZtkW9=2)JVr#CR4`sK__sNZwEBx{Gs|Rq=#l~&L|5a0giOXOA>+{dQ{MsM?2s@B2n*t+oEDY#hBV6YqZ2?&&vA8ME zBou@ZOhM7&gMQFw8F~BGh(C$T5Bf!aHV`@Tlu3WkhcvP9u}+*cQ#~K zOENRO;#?}_ZSc}>YNepuB4QqqyH~d>!Yd6*ad68bIr#>bS{F|}t!rI8Qv8l^5DViW zUgRdyX(;Uo4W2zU+9w)3RI0(Vr;bv+F6{=b?%n)G39T-bYIX0XlWJ}J>vc(9`yt;U zBRCfZY;|fscp=dR=<%@I9m45w`wS%C8+02VS0Q(Kw~N~y+r`5tL(LmQcN`{dcN{Hk z_g>>W#m9zPPcs}l#hKyHV@+>?c(K;PKArvSIk+>L)N6%p&Vn4uLl7pU&2qO4>GYQjHTQR@>+X8z- zXf3Qol!+Mf1tm3pc*dJKB9^uuvd9dr(J#e@o^U#!2xNU<`a*w&vDO<*V%7dHFwf(d zn8iQhhlD=OwDY)K0n_3U{))>Qg_j?Ztn5jzt<}J78+?ztm(*mqIY3HZ#GH^8+vt?tfPyKan^YIS#J+jS+% zDkW-lAE2AB^meIKtNXxCbVMh)*f#b$ogx|3?MU3~#I0`s$W$iFA6{8ea9&!;4~~q& zSpi>!xYak{wAlq%%4)-P7@8#0)a;6eNia4F#f)M!ubLaL)&_?L>Wxax>vlN;u1L93 z&Ez6O@Y_04V>a_}IXSfw^RL4OPW@Y8LNMeI!+$yMZ|I{HuIbmB=?=Ozg;i)lYZUl= zUl#W=WNq)5m{@Deni^{|Lx);2*73o{7Hir%Zu@%|ub^MOAN2d5e8;BI@1^J$-xq)` zMBy6PA27#0tOiPU+de#u%R^HzC#Dq3=g$efD;`H`-UlQ5&kCIpO9#^Nhnf6`U;Xlz VzxqS#^#%Ng8PVH8KfgB^{vU-Vb(a7D literal 0 HcmV?d00001 diff --git a/leaderboard_app/fonts/AlumniSans-VariableFont_wght.ttf b/leaderboard_app/fonts/AlumniSans-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f68b7dab0724d2b0a93b770f92fd66c229aab975 GIT binary patch literal 138252 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/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 + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/leaderboard_app/ios/Flutter/Debug.xcconfig b/leaderboard_app/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/leaderboard_app/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/leaderboard_app/ios/Flutter/Release.xcconfig b/leaderboard_app/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/leaderboard_app/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/leaderboard_app/ios/Runner.xcodeproj/project.pbxproj b/leaderboard_app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1645a5b --- /dev/null +++ b/leaderboard_app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/leaderboard_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/leaderboard_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/leaderboard_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/leaderboard_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/ios/Runner.xcworkspace/contents.xcworkspacedata b/leaderboard_app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/leaderboard_app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/leaderboard_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/leaderboard_app/ios/Runner/AppDelegate.swift b/leaderboard_app/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/leaderboard_app/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/ios/Runner/Base.lproj/Main.storyboard b/leaderboard_app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/leaderboard_app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/ios/Runner/Info.plist b/leaderboard_app/ios/Runner/Info.plist new file mode 100644 index 0000000..a01f442 --- /dev/null +++ b/leaderboard_app/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Leaderboard App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + leaderboard_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/leaderboard_app/ios/Runner/Runner-Bridging-Header.h b/leaderboard_app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/leaderboard_app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/leaderboard_app/ios/RunnerTests/RunnerTests.swift b/leaderboard_app/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/leaderboard_app/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/leaderboard_app/lib/components/my_button.dart b/leaderboard_app/lib/components/my_button.dart new file mode 100644 index 0000000..505cd46 --- /dev/null +++ b/leaderboard_app/lib/components/my_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class MyButton extends StatelessWidget { + final void Function()? onTap; + final String text; + + const MyButton({ + super.key, + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.all(25), + margin: const EdgeInsets.symmetric(horizontal: 25), + child: Center( + child: Text(text), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_chat_bubble.dart b/leaderboard_app/lib/components/my_chat_bubble.dart new file mode 100644 index 0000000..18ed268 --- /dev/null +++ b/leaderboard_app/lib/components/my_chat_bubble.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; +import 'package:flutter_highlight/themes/github.dart'; +import 'package:flutter_highlight/themes/tomorrow-night.dart'; + +class ChatBubble extends StatelessWidget { + final String message; + final bool isCurrentUser; + + const ChatBubble({ + super.key, + required this.message, + required this.isCurrentUser, + }); + + bool containsPythonCode(String text) { + return text.contains("def") || + text.contains("import") || + text.contains("print(") || + text.contains("input("); + } + + bool isComment(String line) { + return line.trim().startsWith(">"); + } + + List formatMessage(String text, bool isDarkMode, TextStyle defaultStyle) { + List spans = []; + List lines = text.split("\n"); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + bool comment = isComment(line); + bool hasPython = containsPythonCode(line); + + if (comment) { + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: Container( + width: double.infinity, + margin: EdgeInsets.only(top: 4, bottom: i == lines.length - 1 ? 0 : 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isDarkMode ? Colors.black54 : Colors.grey[300], + borderRadius: BorderRadius.circular(4), + border: Border.all(color: isDarkMode ? Colors.white70 : Colors.black54), + ), + child: hasPython + ? HighlightView( + line.substring(1).trim(), + language: 'python', + theme: isDarkMode ? tomorrowNightTheme : githubTheme, + textStyle: const TextStyle(fontSize: 14), + ) + : Text( + line.substring(1).trim(), + style: defaultStyle.copyWith(fontSize: 14), + ), + ), + ), + ); + } else { + spans.add( + TextSpan( + text: i == lines.length - 1 ? line : "$line\n", + style: defaultStyle, + ), + ); + } + } + return spans; + } + + @override + Widget build(BuildContext context) { + final isDarkMode = MediaQuery.platformBrightnessOf(context) == Brightness.dark; + final defaultTextStyle = Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isDarkMode ? Colors.white : Colors.black, + ) ?? + TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + ); + + final bubbleDecoration = BoxDecoration( + color: isCurrentUser + ? Theme.of(context).colorScheme.inversePrimary + : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border( + left: isCurrentUser ? BorderSide.none : const BorderSide(color: Colors.black, width: 2), + right: isCurrentUser ? const BorderSide(color: Colors.black, width: 2) : BorderSide.none, + bottom: const BorderSide(color: Colors.black, width: 2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + offset: isCurrentUser ? const Offset(2, 2) : const Offset(-2, 2), + blurRadius: 4, + ), + ], + ); + + if (!message.contains(">") && containsPythonCode(message)) { + return Container( + decoration: bubbleDecoration, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), + child: HighlightView( + message.trim(), + language: 'python', + theme: isDarkMode ? tomorrowNightTheme : githubTheme, + textStyle: const TextStyle(fontSize: 14), + ), + ); + } + + return Container( + decoration: bubbleDecoration, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), + child: RichText( + text: TextSpan( + children: formatMessage(message.trim(), isDarkMode, defaultTextStyle), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_textfield.dart b/leaderboard_app/lib/components/my_textfield.dart new file mode 100644 index 0000000..4dfd2b0 --- /dev/null +++ b/leaderboard_app/lib/components/my_textfield.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class MyTextfield extends StatelessWidget { + final String hintText; + final bool obscureText; + final TextEditingController controller; + final FocusNode? focusNode; + + const MyTextfield({ + super.key, + required this.hintText, + required this.obscureText, + required this.controller, + this.focusNode, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: TextField( + obscureText: obscureText, + controller: controller, + focusNode: focusNode, + maxLines: obscureText ? 1 : null, + keyboardType: obscureText ? TextInputType.text : TextInputType.multiline, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.tertiary), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.tertiary), + ), + fillColor: Theme.of(context).colorScheme.secondary, + filled: true, + hintText: hintText, + hintStyle: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_usertile.dart b/leaderboard_app/lib/components/my_usertile.dart new file mode 100644 index 0000000..e35137a --- /dev/null +++ b/leaderboard_app/lib/components/my_usertile.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:pixelarticons/pixel.dart'; + +class UserTile extends StatelessWidget { + final String text; + final void Function()? onTap; + + const UserTile({ + super.key, + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 10), + + GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(4), + ), + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), + padding: const EdgeInsets.all(20), + child: Row( + children: [ + const Icon(Pixel.avatar), + + const SizedBox(width: 20), + + Text(text), + ], + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart new file mode 100644 index 0000000..b93855c --- /dev/null +++ b/leaderboard_app/lib/main.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/home_page.dart'; +import 'package:leaderboard_app/pages/signup_page.dart'; +import 'package:leaderboard_app/pages/signin_page.dart'; +import 'package:leaderboard_app/provider/theme_provider.dart'; +import 'package:provider/provider.dart'; + +void main() { + runApp( + ChangeNotifierProvider( + create: (_) => ThemeProvider(), + child: const MainApp(), + ), + ); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: themeProvider.themeData, + home: const HomePage(), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart new file mode 100644 index 0000000..8323813 --- /dev/null +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -0,0 +1,447 @@ +import 'package:flutter/material.dart'; +import 'package:pixelarticons/pixel.dart'; +import 'profile_page.dart'; // <- Assuming this file exists + +class ChatPage extends StatefulWidget { + final String receiverEmail; + final String receiverID; + + const ChatPage({ + super.key, + required this.receiverEmail, + required this.receiverID, + }); + + @override + State createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final FocusNode myFocusNode = FocusNode(); + String? replyTo; + + bool showAttachmentOptions = false; + + final String currentUserId = "uid_me"; + + List> dummyMessages = [ + {"senderID": "uid_me", "type": "image", "timestamp": "12:34 pm"}, + { + "senderID": "uid_me", + "message": "text text text text text text text text text text...", + "timestamp": "12:34 pm", + }, + { + "senderID": "system", + "message": "Duelled", + "timestamp": "12:34 pm", + "icon": Pixel.bullseye, + }, + { + "senderID": "uid_1", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + { + "senderID": "uid_me", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + { + "senderID": "uid_1", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + ]; + + @override + void initState() { + super.initState(); + myFocusNode.addListener(() { + if (myFocusNode.hasFocus) { + Future.delayed(const Duration(milliseconds: 300), scrollDown); + } + }); + Future.delayed(const Duration(milliseconds: 500), scrollDown); + } + + void scrollDown() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent + 80, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + + void sendMessage() { + if (_messageController.text.trim().isEmpty) return; + setState(() { + dummyMessages.add({ + "senderID": currentUserId, + "message": _messageController.text.trim(), + "timestamp": "now", + if (replyTo != null) "replyTo": replyTo, + }); + _messageController.clear(); + replyTo = null; + }); + scrollDown(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: const BackButton(), + titleSpacing: 0, + title: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfilePage()), + ); + }, + child: Row( + children: [ + const CircleAvatar(radius: 20, backgroundColor: Colors.grey), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Penny Valeria", + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + "Online", + style: TextStyle( + color: theme.primary.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: theme.inversePrimary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text("Duel Now!"), + ), + ), + ], + ), + body: Column( + children: [ + Expanded(child: _buildMessageList()), + if (showAttachmentOptions) _buildAttachmentDropdown(), + _buildUserInput(), + ], + ), + ); + } + + Widget _buildMessageList() { + return ListView.builder( + controller: _scrollController, + itemCount: dummyMessages.length, + itemBuilder: (context, index) { + final msg = dummyMessages[index]; + final isMe = msg["senderID"] == currentUserId; + final isSystem = msg["senderID"] == "system"; + final isImage = msg["type"] == "image"; + + if (isSystem) { + return Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + msg["icon"] ?? Icons.info, + size: 16, + color: Colors.white, + ), + const SizedBox(width: 6), + Text( + msg["message"] ?? "", + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + const SizedBox(width: 6), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + ], + ), + ), + ); + } + + if (isImage) { + return Align( + alignment: Alignment.centerRight, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon(Pixel.image, size: 64, color: Colors.grey), + ), + ), + const SizedBox(height: 4), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(fontSize: 10, color: Colors.black54), + ), + ], + ), + ), + ); + } + + return GestureDetector( + onDoubleTap: () { + setState(() { + replyTo = msg["message"]; // set reply target on double-tap + }); + }, + + child: Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isMe + ? Theme.of(context).colorScheme.inversePrimary + : Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: isMe + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [ + if (msg["replyTo"] != null) + Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + msg["replyTo"], + style: TextStyle( + fontSize: 11, + color: isMe ? Colors.black87 : Colors.white60, + ), + ), + ), + Text( + msg["message"] ?? "", + style: TextStyle( + color: isMe ? Colors.black : Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Align( + alignment: isMe + ? Alignment.centerLeft + : Alignment.centerRight, + child: Text( + msg["timestamp"] ?? "", + style: TextStyle( + color: isMe ? Colors.black54 : Colors.white54, + fontSize: 10, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildUserInput() { + final theme = Theme.of(context).colorScheme; + return SafeArea( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: Colors.black, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton( + icon: Icon( + showAttachmentOptions ? Icons.close : Icons.add, + color: Colors.white, + ), + onPressed: () { + setState(() { + showAttachmentOptions = !showAttachmentOptions; + }); + }, + ), + if (replyTo != null) + Padding( + padding: const EdgeInsets.only(bottom: 4, left: 8), + child: Row( + children: [ + Expanded( + child: Text( + "Replying to: $replyTo", + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.close, + size: 16, + color: Colors.white54, + ), + onPressed: () { + setState(() { + replyTo = null; + }); + }, + ), + ], + ), + ), + + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(24), + ), + child: TextField( + controller: _messageController, + focusNode: myFocusNode, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: "Type a message...", + hintStyle: TextStyle(color: Colors.white54), + border: InputBorder.none, + ), + ), + ), + ), + const SizedBox(width: 8), + CircleAvatar( + backgroundColor: theme.primary, + child: IconButton( + onPressed: sendMessage, + icon: const Icon(Pixel.arrowup, color: Colors.black), + ), + ), + ], + ), + ), + ); + } + + Widget _buildAttachmentDropdown() { + return Align( + alignment: Alignment.bottomLeft, + child: Container( + margin: const EdgeInsets.only(left: 20, bottom: 8), + padding: const EdgeInsets.all(12), + width: 180, // ✅ restrict width + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + AttachmentOption(icon: Icons.mic, label: "Audio"), + SizedBox(height: 12), + AttachmentOption(icon: Icons.photo, label: "Photos & Videos"), + SizedBox(height: 12), + AttachmentOption(icon: Icons.attach_file, label: "File"), + ], + ), + ), + ); + } +} + +class AttachmentOption extends StatelessWidget { + final IconData icon; + final String label; + + const AttachmentOption({super.key, required this.icon, required this.label}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(icon, color: Colors.amber, size: 22), + const SizedBox(width: 10), + Text(label, style: const TextStyle(color: Colors.white, fontSize: 14)), + ], + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart new file mode 100644 index 0000000..8f9e0a8 --- /dev/null +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'chat_page.dart'; + +class ChatlistsPage extends StatelessWidget { + ChatlistsPage({super.key}); + + final String currentUserEmail = "me@example.com"; + + final List> dummyUsers = List.generate( + 10, + (index) => { + "name": "Penny Valeria", + "message": "Text text text text....", + "time": "12:35 pm", + "email": "user$index@example.com", + "uid": "uid_$index", + "unread": index != 0, + }, + ); + + final List filters = ["All", "Unread", "Favourites", "Groups"]; + final int selectedFilterIndex = 0; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: theme.surface, + body: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Chats title + Padding( + padding: const EdgeInsets.only(left: 16, top: 16), + child: Text( + 'Chats', + style: TextStyle( + color: theme.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 10), + + // Search + Add Button + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: TextField( + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + hintText: "Search", + hintStyle: TextStyle( + color: theme.primary.withOpacity(0.5), + ), + filled: true, + fillColor: Colors.grey.shade900, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + prefixIcon: Icon(Icons.search, color: theme.primary), + ), + ), + ), + ), + const SizedBox(width: 10), + Padding( + padding: const EdgeInsets.only(right: 16), + child: CircleAvatar( + backgroundColor: theme.secondary, + child: Icon(Icons.add, color: Colors.black), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Filter Buttons + SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: filters.asMap().entries.map((entry) { + final i = entry.key; + final label = entry.value; + final isSelected = i == selectedFilterIndex; + + return ElevatedButton( + onPressed: () { + // Add filter logic here if needed + }, + style: ElevatedButton.styleFrom( + backgroundColor: isSelected + ? theme.secondary + : Colors.grey.shade900, + foregroundColor: isSelected + ? Colors.black + : theme.primary, + padding: const EdgeInsets.symmetric(horizontal: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + elevation: 0, + ), + child: Text( + label, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + + // Chat List + Expanded( + child: ListView.builder( + itemCount: dummyUsers.length, + itemBuilder: (context, index) { + final user = dummyUsers[index]; + return Column( + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatPage( + receiverEmail: user["email"]!, + receiverID: user["uid"]!, + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + children: [ + // Profile picture + Stack( + children: [ + const CircleAvatar( + radius: 24, + backgroundColor: Colors.grey, + ), + Positioned( + bottom: 0, + right: 0, + child: CircleAvatar( + radius: 6, + backgroundColor: Colors.black, + child: CircleAvatar( + radius: 4, + backgroundColor: Colors.green, + ), + ), + ), + ], + ), + const SizedBox(width: 12), + + // Name and message + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + user["name"], + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + user["message"], + style: TextStyle( + color: theme.primary.withOpacity(0.7), + fontSize: 13, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + + // Time and unread dot + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + user["time"], + style: TextStyle( + color: theme.primary.withOpacity(0.6), + fontSize: 12, + ), + ), + const SizedBox(height: 8), + if (user["unread"]) + const CircleAvatar( + radius: 6, + backgroundColor: Colors.amber, + ), + ], + ), + ], + ), + ), + ), + Divider( + height: 1, + thickness: 0.6, + color: Colors.grey.shade800, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart new file mode 100644 index 0000000..54691e9 --- /dev/null +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -0,0 +1,493 @@ +import 'package:flutter/material.dart'; + +class DashboardPage extends StatefulWidget { + const DashboardPage({super.key}); + + @override + State createState() => _DashboardPageState(); +} + +class _DashboardPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + double maxWidth = constraints.maxWidth < 400 + ? constraints.maxWidth + : 400; + + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + color: Colors.grey[900], + child: Row( + children: [ + const CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + child: Icon(Icons.person, color: Colors.black), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "First Name Last Name", + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + "username@email.com", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ], + ), + ), + _buildHeaderButton( + Icons.local_fire_department, + "4", + Colors.amber, + ), + const SizedBox(width: 8), + _buildHeaderButton( + Icons.person_add, + "Invite", + Colors.amber, + ), + ], + ), + ), + + // Scrollable Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCard( + child: Column( + children: [ + const Center( + child: Text( + 'June 5, 2025', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 80, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 7, + itemBuilder: (context, index) { + const days = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + ]; + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 6, + ), + width: 60, + decoration: BoxDecoration( + color: index == 4 + ? Colors.amber + : Colors.grey[900], + borderRadius: BorderRadius.circular( + 10, + ), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon( + Icons.check_circle, + color: Colors.white, + ), + const SizedBox(height: 4), + Text( + days[index], + style: const TextStyle( + color: Colors.white, + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + + _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "10. Regular Expression Matching", + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 12), + LinearProgressIndicator( + value: 0.4, + color: Colors.amber, + minHeight: 12, + borderRadius: BorderRadius.circular(8), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[700], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + ), + onPressed: () {}, + child: const Text( + "Resume >", + style: TextStyle(color: Colors.black), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + + _buildCard( + child: DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: WidgetStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn(label: Text("Place", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Player", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Streak", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Solved", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Badge", style: TextStyle(color: Colors.white, fontSize: 12))), + ], + rows: List.generate( + 5, + (index) => DataRow( + cells: [ + DataCell(Text("${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text("Player ${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Text("12", style: TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Text("1324", style: TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Icon(Icons.star, color: Colors.amber, size: 16)), + ], + ), + ), + ), + ), + + const SizedBox(height: 10), + + _buildCard( + child: DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: WidgetStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn(label: Text("No.", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Title", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Acc.", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Lvl", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text("Prog", style: TextStyle(color: Colors.white, fontSize: 12))), + ], + rows: List.generate( + 4, + (index) => DataRow( + cells: [ + DataCell(Text("${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Text("Problem", style: TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Text("56%", style: TextStyle(color: Colors.white, fontSize: 12))), + const DataCell(Text("Easy", style: TextStyle(color: Colors.green, fontSize: 12))), + const DataCell(Icon(Icons.circle, color: Colors.green, size: 10)), + ], + ), + ), + ), + ), + + const SizedBox(height: 10), + + _buildCard( + child: Column( + children: [ + const Center( + child: Text( + "This Week", + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 10), + _buildBar("Easy", 0.8, Colors.green), + const SizedBox(height: 6), + _buildBar("Medium", 0.4, Colors.amber), + const SizedBox(height: 6), + _buildBar("Hard", 0.2, Colors.red), + ], + ), + ), + + const SizedBox(height: 10), + + _buildCard(child: _CompactCalendar()), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } + + static Widget _buildHeaderButton(IconData icon, String label, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 16), + const SizedBox(width: 4), + Text(label, style: const TextStyle(color: Colors.white)), + ], + ), + ); + } + + static Widget _buildBar(String label, double value, Color color) { + return Row( + children: [ + SizedBox( + width: 60, + child: Text(label, style: const TextStyle(color: Colors.white)), + ), + Expanded( + child: LinearProgressIndicator( + value: value, + color: color, + backgroundColor: Colors.white24, + ), + ), + ], + ); + } + + static Widget _buildCard({required Widget child}) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), + ), + child: child, + ); + } +} + +class _CompactCalendar extends StatefulWidget { + @override + State<_CompactCalendar> createState() => _CompactCalendarState(); +} + +class _CompactCalendarState extends State<_CompactCalendar> { + DateTime _selectedDate = DateTime.now(); + + List _months = const [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + List _weekdays = const [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + ]; + + List _years = List.generate(50, (i) => 2000 + i); // 2000–2049 + + @override + Widget build(BuildContext context) { + int year = _selectedDate.year; + int month = _selectedDate.month; + + DateTime firstOfMonth = DateTime(year, month, 1); + int weekdayOffset = firstOfMonth.weekday == 7 + ? 0 + : firstOfMonth.weekday; // Monday = 1 + int daysInMonth = DateTime(year, month + 1, 0).day; + + List dayWidgets = []; + + // Blank slots before month start + for (int i = 1; i < weekdayOffset; i++) { + dayWidgets.add(Container()); + } + + // Days of month + for (int i = 1; i <= daysInMonth; i++) { + dayWidgets.add( + Container( + margin: const EdgeInsets.all(2), + alignment: Alignment.center, + decoration: BoxDecoration( + color: i == _selectedDate.day ? Colors.amber : Colors.transparent, + shape: BoxShape.circle, + ), + child: Text("$i", style: const TextStyle(color: Colors.white)), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Month + Year dropdown row + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + dropdownColor: Colors.grey[850], + value: month, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: List.generate(12, (index) { + return DropdownMenuItem( + value: index + 1, + child: Text(_months[index]), + ); + }), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(year, val, 1); + }); + } + }, + ), + const SizedBox(width: 16), + DropdownButton( + dropdownColor: Colors.grey[850], + value: year, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: _years.map((yr) { + return DropdownMenuItem(value: yr, child: Text(yr.toString())); + }).toList(), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(val, month, 1); + }); + } + }, + ), + ], + ), + + const SizedBox(height: 10), + + // Weekday headers + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _weekdays.map((day) { + return Expanded( + child: Center( + child: Text( + day, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ); + }).toList(), + ), + + const SizedBox(height: 6), + + // Calendar grid + GridView.count( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + crossAxisCount: 7, + children: dayWidgets, + ), + ], + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/files_page.dart b/leaderboard_app/lib/pages/files_page.dart new file mode 100644 index 0000000..6c6d50b --- /dev/null +++ b/leaderboard_app/lib/pages/files_page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +class FilesPage extends StatelessWidget { + const FilesPage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + final List months = ["This month", "May", "April", "March", "February", "January"]; + + return Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: const BackButton(), + title: const Text("Files", style: TextStyle(fontSize: 16)), + centerTitle: true, + ), + body: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: months.length, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(months[index], style: const TextStyle(color: Colors.white)), + const SizedBox(height: 8), + ListView.builder( + itemCount: 2, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, _) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Colors.grey.shade700, + ), + alignment: Alignment.center, + child: const Text("PNG", style: TextStyle(color: Colors.white, fontSize: 10)), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("blahblah.png", style: TextStyle(color: Colors.white, fontSize: 13)), + Text("4 KB | .png", style: TextStyle(color: Colors.white54, fontSize: 10)), + ], + ), + ], + ), + ); + }, + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/home_page.dart b/leaderboard_app/lib/pages/home_page.dart new file mode 100644 index 0000000..5b0567f --- /dev/null +++ b/leaderboard_app/lib/pages/home_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/dashboard_page.dart'; +import 'package:leaderboard_app/pages/chatlists_page.dart'; +import 'package:leaderboard_app/pages/settings_page.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _selectedIndex = 0; + + final List _pages = [ + const DashboardPage(), + ChatlistsPage(), + Center(child: Text("Stats Page", style: TextStyle(color: Colors.white))), + SettingsPage(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: IndexedStack( + index: _selectedIndex, + children: _pages, + ), + bottomNavigationBar: Theme( + data: Theme.of(context).copyWith( + canvasColor: Colors.grey[900], + primaryColor: Colors.yellow, + textTheme: Theme.of(context).textTheme.copyWith( + bodySmall: const TextStyle(color: Colors.white), + ), + ), + child: BottomNavigationBar( + selectedItemColor: Colors.yellow, + unselectedItemColor: Colors.white, + showSelectedLabels: false, + showUnselectedLabels: false, + type: BottomNavigationBarType.fixed, + currentIndex: _selectedIndex, + onTap: (index) => setState(() => _selectedIndex = index), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.explore, size: 28), + label: 'Discover', + ), + BottomNavigationBarItem( + icon: Icon(Icons.chat, size: 28), + label: 'Chat', + ), + BottomNavigationBarItem( + icon: Icon(Icons.bar_chart, size: 28), + label: 'Stats', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings, size: 28), + label: 'Settings', + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/media_page.dart b/leaderboard_app/lib/pages/media_page.dart new file mode 100644 index 0000000..8e71e94 --- /dev/null +++ b/leaderboard_app/lib/pages/media_page.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class MediaPage extends StatelessWidget { + const MediaPage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + final List months = ["This month", "May", "April"]; + + return Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: const BackButton(), + title: const Text("Media", style: TextStyle(fontSize: 16)), + centerTitle: true, + ), + body: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: months.length, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(months[index], style: const TextStyle(color: Colors.white)), + const SizedBox(height: 8), + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 10, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1, + ), + itemBuilder: (context, _) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(6), + ), + ); + }, + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/profile_page.dart b/leaderboard_app/lib/pages/profile_page.dart new file mode 100644 index 0000000..1896f41 --- /dev/null +++ b/leaderboard_app/lib/pages/profile_page.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/files_page.dart'; +import 'package:leaderboard_app/pages/media_page.dart'; +import 'package:pie_chart/pie_chart.dart'; + +class ProfilePage extends StatelessWidget { + const ProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: const BackButton(), + elevation: 0, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: theme.inversePrimary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text("Duel Now!"), + ), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 10), + + // Avatar + const CircleAvatar(radius: 50, backgroundColor: Colors.grey), + const SizedBox(height: 8), + const Text( + "Penny Valeria", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + + const SizedBox(height: 16), + + // Info Tiles + _infoTile(Icons.people, "Friends for:", "3 Months"), + _infoTile( + Icons.star, + "Rank:", + "Gold", + trailingIcon: Icons.star, + trailingColor: Colors.amber, + ), + _infoTile(Icons.send, "Currently on:", "Title 1"), + + const SizedBox(height: 16), + + // Duel Stats + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Number of Duels:", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + const SizedBox(height: 8), + PieChart( + dataMap: const {"Penny": 6, "You": 10}, + animationDuration: const Duration(milliseconds: 800), + chartLegendSpacing: 16, + chartRadius: 120, + colorList: [Colors.grey.shade800, Colors.amber], + chartType: ChartType.ring, + ringStrokeWidth: 28, + legendOptions: const LegendOptions( + showLegends: true, + legendTextStyle: TextStyle( + color: Colors.white, + fontSize: 12, + ), + legendPosition: LegendPosition.left, + ), + chartValuesOptions: const ChartValuesOptions( + showChartValues: false, + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Bottom Tiles + _bottomTile(context, "Media", "192"), + _bottomTile(context, "Files", "193"), + + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _infoTile( + IconData icon, + String label, + String value, { + IconData? trailingIcon, + Color? trailingColor, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + if (trailingIcon != null) + Icon(trailingIcon, color: trailingColor ?? Colors.white, size: 18), + if (trailingIcon == null) + Text( + value, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ], + ), + ); + } + + Widget _bottomTile(BuildContext context, String title, String count) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey.shade900, + ), + child: Material( + color: + Colors.transparent, // So the original container color shows through + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), // Same radius to clip ripple + onTap: () { + if (title == "Media") { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const MediaPage()), + ); + } else if (title == "Files") { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const FilesPage()), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + Text( + count, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + const SizedBox(width: 6), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: Colors.white54, + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart new file mode 100644 index 0000000..ca4254e --- /dev/null +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Settings'), + centerTitle: true, + backgroundColor: Colors.black, + elevation: 0, + foregroundColor: Colors.white, + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + + // ====== Personal Details ====== + const Text( + 'My Account', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + const SizedBox(height: 10), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Column( + children: [ + const CircleAvatar( + radius: 40, + backgroundColor: Colors.white24, + child: Icon( + Icons.person, + size: 32, + color: Colors.white, + ), + ), + const SizedBox(height: 6), + TextButton( + onPressed: () {}, + child: const Text( + "Edit", + style: TextStyle(color: Colors.amber), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + + const Divider( + height: 1, + thickness: 0.6, + indent: 0, + endIndent: 0, + color: Color.fromARGB(179, 158, 158, 158), + ), + + // First & Last Name side-by-side + Row( + children: [ + Expanded(child: _buildDisplayTile('First Name', 'Penny')), + const SizedBox(width: 10), + Expanded(child: _buildDisplayTile('Last Name', 'Valeria')), + ], + ), + const Divider( + height: 1, + thickness: 0.6, + color: Color.fromARGB(179, 158, 158, 158), + ), + _buildDisplayTile('Username', '@pennyval'), + const Divider( + height: 1, + thickness: 0.6, + color: Color.fromARGB(179, 158, 158, 158), + ), + _buildDisplayTile('Email', 'penny@example.com'), + const Divider( + height: 1, + thickness: 0.6, + color: Color.fromARGB(179, 158, 158, 158), + ), + _buildDisplayTile('Phone Number', '+91 1234567890'), + const Divider( + height: 1, + thickness: 0.6, + color: Color.fromARGB(179, 158, 158, 158), + ), + ], + ), + ), + + const SizedBox(height: 25), + + // ====== Container 2 ====== + const Text( + 'Password and Authentication', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + children: [ + Row( + children: [ + Expanded(child: _buildDisplayTile('Password', '••••••••')), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.amber, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), + ), + child: const Text( + 'Change password', + style: TextStyle(fontSize: 12), + ), + ), + ], + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.centerLeft, + child: Text( + 'Account removal', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + child: const Text('Disable Account'), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + child: const Text('Delete Account'), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 25), + + // ====== Container 3 ====== + const Text( + 'Appearance', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Choose a preferred theme for the website', + style: TextStyle(color: Colors.white54, fontSize: 13), + ), + const SizedBox(height: 12), + Wrap( + spacing: 16, + children: [ + _buildThemeDot(Colors.pink), + _buildThemeDot(Colors.red), + _buildThemeDot(Colors.green), + _buildThemeDot(Colors.teal), + _buildThemeDot(Colors.yellow), + _buildThemeDot(Colors.blueAccent), + _buildThemeDot(Colors.white), + ], + ), + ], + ), + ), + + const SizedBox(height: 100), // Extra space above the bottom + ], + ), + ); + } + + // Non-editable display tile + Widget _buildDisplayTile(String title, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ], + ), + ), + ); + } + + Widget _buildThemeDot(Color color) { + return GestureDetector( + onTap: () {}, + child: CircleAvatar(radius: 14, backgroundColor: color), + ); + } +} diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart new file mode 100644 index 0000000..19b2850 --- /dev/null +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/signup_page.dart'; + +class SignInPage extends StatelessWidget { + const SignInPage({super.key}); + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: const Color(0xFF141316), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Top title + Padding( + padding: const EdgeInsets.only(bottom: 20,), + child: Container( + child: const Center( + child: Text( + 'Sign In', + style: TextStyle( + fontSize: 36, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: screenHeight * 0.80, + padding: const EdgeInsets.all(35), + decoration: BoxDecoration( + color: const Color(0xff11b1a1d), + borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Email or username', + hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 16), + TextField( + obscureText: true, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Password', + hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text( + 'Forgot Password?', + style: TextStyle(color: Color(0xFFD7FE66)), + ), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD7FE66), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () {}, + child: const Text( + 'Sign In', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "New here? ", + style: TextStyle(color: Colors.white), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SignUpPage(), + ), + ); + }, + child: const Text( + "Sign up", + style: TextStyle( + color: Color(0xFFD7FE66), + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ), + const Spacer(), + ], + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart new file mode 100644 index 0000000..0202648 --- /dev/null +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/signin_page.dart'; + +class SignUpPage extends StatelessWidget { + const SignUpPage({super.key}); + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: const Color(0xFF141316), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Top title + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + child: const Center( + child: Text( + 'Sign Up', + style: TextStyle( + fontSize: 36, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: screenHeight * 0.80, + padding: const EdgeInsets.all(35), + decoration: BoxDecoration( + color: const Color(0xff11b1a1d), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF141316), + foregroundColor: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + icon: Image.asset( + 'assets/icons/google.png', + height: 15, + ), + label: const Text('Sign in with Google'), + onPressed: () { + // Handle Google sign-in logic + }, + ), + ), + const SizedBox(height: 10), + Row( + children: [ + const Expanded( + child: Divider(color: Colors.white24, thickness: 1), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'Or', + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + ), + const Expanded( + child: Divider(color: Colors.white24, thickness: 1), + ), + ], + ), + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'First Name', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Last Name', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Username', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Email', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 10), + TextField( + obscureText: true, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Password', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD7FE66), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () {}, + child: const Text( + 'Get Started', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Already have an account? ", + style: TextStyle(color: Colors.white), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SignInPage(), + ), + ); + }, + child: const Text( + "Sign in", + style: TextStyle( + color: Color(0xFFD7FE66), + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ), + const Spacer(), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/leaderboard_app/lib/provider/theme_provider.dart b/leaderboard_app/lib/provider/theme_provider.dart new file mode 100644 index 0000000..c07b8c4 --- /dev/null +++ b/leaderboard_app/lib/provider/theme_provider.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ThemeProvider extends ChangeNotifier { + // Default dark theme configuration + final ThemeData _themeData = ThemeData( + colorScheme: const ColorScheme.dark( + surface: Colors.black, // background containers + primary: Colors.grey, // text & icons + secondary: Colors.amber, // buttons, highlights + tertiary: Colors.grey, // progress bar track, muted UI + inversePrimary: Colors.amber, // badge/gold accent + ), + fontFamily: 'PixelifySans', + ); + + ThemeData get themeData => _themeData; +} \ No newline at end of file diff --git a/leaderboard_app/linux/.gitignore b/leaderboard_app/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/leaderboard_app/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/leaderboard_app/linux/CMakeLists.txt b/leaderboard_app/linux/CMakeLists.txt new file mode 100644 index 0000000..1dc07dd --- /dev/null +++ b/leaderboard_app/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "leaderboard_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.leaderboard_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/leaderboard_app/linux/flutter/CMakeLists.txt b/leaderboard_app/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/leaderboard_app/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/leaderboard_app/linux/flutter/generated_plugin_registrant.cc b/leaderboard_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/leaderboard_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/leaderboard_app/linux/flutter/generated_plugin_registrant.h b/leaderboard_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/leaderboard_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/leaderboard_app/linux/flutter/generated_plugins.cmake b/leaderboard_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/leaderboard_app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/leaderboard_app/linux/runner/CMakeLists.txt b/leaderboard_app/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/leaderboard_app/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/leaderboard_app/linux/runner/main.cc b/leaderboard_app/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/leaderboard_app/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/leaderboard_app/linux/runner/my_application.cc b/leaderboard_app/linux/runner/my_application.cc new file mode 100644 index 0000000..0129099 --- /dev/null +++ b/leaderboard_app/linux/runner/my_application.cc @@ -0,0 +1,144 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "leaderboard_app"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "leaderboard_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/leaderboard_app/linux/runner/my_application.h b/leaderboard_app/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/leaderboard_app/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/leaderboard_app/macos/.gitignore b/leaderboard_app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/leaderboard_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/leaderboard_app/macos/Flutter/Flutter-Debug.xcconfig b/leaderboard_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/leaderboard_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/leaderboard_app/macos/Flutter/Flutter-Release.xcconfig b/leaderboard_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/leaderboard_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..cccf817 --- /dev/null +++ b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/leaderboard_app/macos/Runner.xcodeproj/project.pbxproj b/leaderboard_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1157963 --- /dev/null +++ b/leaderboard_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* leaderboard_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "leaderboard_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* leaderboard_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* leaderboard_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/leaderboard_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/leaderboard_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/leaderboard_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/leaderboard_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/leaderboard_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/leaderboard_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/leaderboard_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/leaderboard_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/leaderboard_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/leaderboard_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/leaderboard_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3647d0 --- /dev/null +++ b/leaderboard_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/leaderboard_app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/leaderboard_app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/leaderboard_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/leaderboard_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/leaderboard_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/leaderboard_app/macos/Runner/AppDelegate.swift b/leaderboard_app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/leaderboard_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/leaderboard_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/macos/Runner/Configs/AppInfo.xcconfig b/leaderboard_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..24cba56 --- /dev/null +++ b/leaderboard_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = leaderboard_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.leaderboardApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/leaderboard_app/macos/Runner/Configs/Debug.xcconfig b/leaderboard_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/leaderboard_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/leaderboard_app/macos/Runner/Configs/Release.xcconfig b/leaderboard_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/leaderboard_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/leaderboard_app/macos/Runner/Configs/Warnings.xcconfig b/leaderboard_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/leaderboard_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/leaderboard_app/macos/Runner/DebugProfile.entitlements b/leaderboard_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/leaderboard_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/leaderboard_app/macos/Runner/Info.plist b/leaderboard_app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/leaderboard_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/leaderboard_app/macos/Runner/MainFlutterWindow.swift b/leaderboard_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/leaderboard_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/leaderboard_app/macos/Runner/Release.entitlements b/leaderboard_app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/leaderboard_app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/leaderboard_app/macos/RunnerTests/RunnerTests.swift b/leaderboard_app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/leaderboard_app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock new file mode 100644 index 0000000..51bd14c --- /dev/null +++ b/leaderboard_app/pubspec.lock @@ -0,0 +1,253 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pie_chart: + dependency: "direct main" + description: + name: pie_chart + sha256: "58e6a46999ac938bfa1c3e5be414d6e149f037647197dca03ba3614324c12c82" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pixelarticons: + dependency: "direct main" + description: + name: pixelarticons + sha256: abe536bb9710cffc2a9a30d27666bbcd43742e391b6ba2f3de89313259d9f1c9 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.9.0-288.0.dev <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml new file mode 100644 index 0000000..a67ad7a --- /dev/null +++ b/leaderboard_app/pubspec.yaml @@ -0,0 +1,32 @@ +name: leaderboard_app +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.9.0-288.0.dev + +dependencies: + flutter: + sdk: flutter + flutter_highlight: ^0.7.0 + pie_chart: ^5.4.0 + pixelarticons: ^0.4.0 + provider: ^6.1.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/icons/ + + fonts: + - family: AlumniSans + fonts: + - asset: fonts/AlumniSans-VariableFont_wght.ttf + - asset: fonts/AlumniSans-Italic-VariableFont_wght.ttf \ No newline at end of file diff --git a/leaderboard_app/web/favicon.png b/leaderboard_app/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/leaderboard_app/web/icons/Icon-192.png b/leaderboard_app/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/web/icons/Icon-512.png b/leaderboard_app/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 zcmd5=2T+s!lYZ%-(h(2@5fr2dC?F^$C=i-}R6$UX8af(!je;W5yC_|HmujSgN*6?W z3knF*TL1$|?oD*=zPbBVex*RUIKsL<(&Rj9%^UD2IK3W?2j>D?eWQgvS-HLymHo9%~|N2Q{~j za?*X-{b9JRowv_*Mh|;*-kPFn>PI;r<#kFaxFqbn?aq|PduQg=2Q;~Qc}#z)_T%x9 zE|0!a70`58wjREmAH38H1)#gof)U3g9FZ^ zF7&-0^Hy{4XHWLoC*hOG(dg~2g6&?-wqcpf{ z&3=o8vw7lMi22jCG9RQbv8H}`+}9^zSk`nlR8?Z&G2dlDy$4#+WOlg;VHqzuE=fM@ z?OI6HEJH4&tA?FVG}9>jAnq_^tlw8NbjNhfqk2rQr?h(F&WiKy03Sn=-;ZJRh~JrD zbt)zLbnabttEZ>zUiu`N*u4sfQaLE8-WDn@tHp50uD(^r-}UsUUu)`!Rl1PozAc!a z?uj|2QDQ%oV-jxUJmJycySBINSKdX{kDYRS=+`HgR2GO19fg&lZKyBFbbXhQV~v~L za^U944F1_GtuFXtvDdDNDvp<`fqy);>Vw=ncy!NB85Tw{&sT5&Ox%-p%8fTS;OzlRBwErvO+ROe?{%q-Zge=%Up|D4L#>4K@Ke=x%?*^_^P*KD zgXueMiS63!sEw@fNLB-i^F|@Oib+S4bcy{eu&e}Xvb^(mA!=U=Xr3||IpV~3K zQWzEsUeX_qBe6fky#M zzOJm5b+l;~>=sdp%i}}0h zO?B?i*W;Ndn02Y0GUUPxERG`3Bjtj!NroLoYtyVdLtl?SE*CYpf4|_${ku2s`*_)k zN=a}V8_2R5QANlxsq!1BkT6$4>9=-Ix4As@FSS;1q^#TXPrBsw>hJ}$jZ{kUHoP+H zvoYiR39gX}2OHIBYCa~6ERRPJ#V}RIIZakUmuIoLF*{sO8rAUEB9|+A#C|@kw5>u0 zBd=F!4I)Be8ycH*)X1-VPiZ+Ts8_GB;YW&ZFFUo|Sw|x~ZajLsp+_3gv((Q#N>?Jz zFBf`~p_#^${zhPIIJY~yo!7$-xi2LK%3&RkFg}Ax)3+dFCjGgKv^1;lUzQlPo^E{K zmCnrwJ)NuSaJEmueEPO@(_6h3f5mFffhkU9r8A8(JC5eOkux{gPmx_$Uv&|hyj)gN zd>JP8l2U&81@1Hc>#*su2xd{)T`Yw< zN$dSLUN}dfx)Fu`NcY}TuZ)SdviT{JHaiYgP4~@`x{&h*Hd>c3K_To9BnQi@;tuoL z%PYQo&{|IsM)_>BrF1oB~+`2_uZQ48z9!)mtUR zdfKE+b*w8cPu;F6RYJiYyV;PRBbThqHBEu_(U{(gGtjM}Zi$pL8Whx}<JwE3RM0F8x7%!!s)UJVq|TVd#hf1zVLya$;mYp(^oZQ2>=ZXU1c$}f zm|7kfk>=4KoQoQ!2&SOW5|JP1)%#55C$M(u4%SP~tHa&M+=;YsW=v(Old9L3(j)`u z2?#fK&1vtS?G6aOt@E`gZ9*qCmyvc>Ma@Q8^I4y~f3gs7*d=ATlP>1S zyF=k&6p2;7dn^8?+!wZO5r~B+;@KXFEn^&C=6ma1J7Au6y29iMIxd7#iW%=iUzq&C=$aPLa^Q zncia$@TIy6UT@69=nbty5epP>*fVW@5qbUcb2~Gg75dNd{COFLdiz3}kODn^U*=@E z0*$7u7Rl2u)=%fk4m8EK1ctR!6%Ve`e!O20L$0LkM#f+)n9h^dn{n`T*^~d+l*Qlx z$;JC0P9+en2Wlxjwq#z^a6pdnD6fJM!GV7_%8%c)kc5LZs_G^qvw)&J#6WSp< zmsd~1-(GrgjC56Pdf6#!dt^y8Rg}!#UXf)W%~PeU+kU`FeSZHk)%sFv++#Dujk-~m zFHvVJC}UBn2jN& zs!@nZ?e(iyZPNo`p1i#~wsv9l@#Z|ag3JR>0#u1iW9M1RK1iF6-RbJ4KYg?B`dET9 zyR~DjZ>%_vWYm*Z9_+^~hJ_|SNTzBKx=U0l9 z9x(J96b{`R)UVQ$I`wTJ@$_}`)_DyUNOso6=WOmQKI1e`oyYy1C&%AQU<0-`(ow)1 zT}gYdwWdm4wW6|K)LcfMe&psE0XGhMy&xS`@vLi|1#Za{D6l@#D!?nW87wcscUZgELT{Cz**^;Zb~7 z(~WFRO`~!WvyZAW-8v!6n&j*PLm9NlN}BuUN}@E^TX*4Or#dMMF?V9KBeLSiLO4?B zcE3WNIa-H{ThrlCoN=XjOGk1dT=xwwrmt<1a)mrRzg{35`@C!T?&_;Q4Ce=5=>z^*zE_c(0*vWo2_#TD<2)pLXV$FlwP}Ik74IdDQU@yhkCr5h zn5aa>B7PWy5NQ!vf7@p_qtC*{dZ8zLS;JetPkHi>IvPjtJ#ThGQD|Lq#@vE2xdl%`x4A8xOln}BiQ92Po zW;0%A?I5CQ_O`@Ad=`2BLPPbBuPUp@Hb%a_OOI}y{Rwa<#h z5^6M}s7VzE)2&I*33pA>e71d78QpF>sNK;?lj^Kl#wU7G++`N_oL4QPd-iPqBhhs| z(uVM}$ItF-onXuuXO}o$t)emBO3Hjfyil@*+GF;9j?`&67GBM;TGkLHi>@)rkS4Nj zAEk;u)`jc4C$qN6WV2dVd#q}2X6nKt&X*}I@jP%Srs%%DS92lpDY^K*Sx4`l;aql$ zt*-V{U&$DM>pdO?%jt$t=vg5|p+Rw?SPaLW zB6nvZ69$ne4Z(s$3=Rf&RX8L9PWMV*S0@R zuIk&ba#s6sxVZ51^4Kon46X^9`?DC9mEhWB3f+o4#2EXFqy0(UTc>GU| zGCJmI|Dn-dX#7|_6(fT)>&YQ0H&&JX3cTvAq(a@ydM4>5Njnuere{J8p;3?1az60* z$1E7Yyxt^ytULeokgDnRVKQw9vzHg1>X@@jM$n$HBlveIrKP5-GJq%iWH#odVwV6cF^kKX(@#%%uQVb>#T6L^mC@)%SMd4DF? zVky!~ge27>cpUP1Vi}Z32lbLV+CQy+T5Wdmva6Fg^lKb!zrg|HPU=5Qu}k;4GVH+x z%;&pN1LOce0w@9i1Mo-Y|7|z}fbch@BPp2{&R-5{GLoeu8@limQmFF zaJRR|^;kW_nw~0V^ zfTnR!Ni*;-%oSHG1yItARs~uxra|O?YJxBzLjpeE-=~TO3Dn`JL5Gz;F~O1u3|FE- zvK2Vve`ylc`a}G`gpHg58Cqc9fMoy1L}7x7T>%~b&irrNMo?np3`q;d3d;zTK>nrK zOjPS{@&74-fA7j)8uT9~*g23uGnxwIVj9HorzUX#s0pcp2?GH6i}~+kv9fWChtPa_ z@T3m+$0pbjdQw7jcnHn;Pi85hk_u2-1^}c)LNvjdam8K-XJ+KgKQ%!?2n_!#{$H|| zLO=%;hRo6EDmnOBKCL9Cg~ETU##@u^W_5joZ%Et%X_n##%JDOcsO=0VL|Lkk!VdRJ z^|~2pB@PUspT?NOeO?=0Vb+fAGc!j%Ufn-cB`s2A~W{Zj{`wqWq_-w0wr@6VrM zbzni@8c>WS!7c&|ZR$cQ;`niRw{4kG#e z70e!uX8VmP23SuJ*)#(&R=;SxGAvq|&>geL&!5Z7@0Z(No*W561n#u$Uc`f9pD70# z=sKOSK|bF~#khTTn)B28h^a1{;>EaRnHj~>i=Fnr3+Fa4 z`^+O5_itS#7kPd20rq66_wH`%?HNzWk@XFK0n;Z@Cx{kx==2L22zWH$Yg?7 zvDj|u{{+NR3JvUH({;b*$b(U5U z7(lF!1bz2%06+|-v(D?2KgwNw7( zJB#Tz+ZRi&U$i?f34m7>uTzO#+E5cbaiQ&L}UxyOQq~afbNB4EI{E04ZWg53w0A{O%qo=lF8d zf~ktGvIgf-a~zQoWf>loF7pOodrd0a2|BzwwPDV}ShauTK8*fmF6NRbO>Iw9zZU}u zw8Ya}?seBnEGQDmH#XpUUkj}N49tP<2jYwTFp!P+&Fd(%Z#yo80|5@zN(D{_pNow*&4%ql zW~&yp@scb-+Qj-EmErY+Tu=dUmf@*BoXY2&oKT8U?8?s1d}4a`Aq>7SV800m$FE~? zjmz(LY+Xx9sDX$;vU`xgw*jLw7dWOnWWCO8o|;}f>cu0Q&`0I{YudMn;P;L3R-uz# zfns_mZED_IakFBPP2r_S8XM$X)@O-xVKi4`7373Jkd5{2$M#%cRhWer3M(vr{S6>h zj{givZJ3(`yFL@``(afn&~iNx@B1|-qfYiZu?-_&Z8+R~v`d6R-}EX9IVXWO-!hL5 z*k6T#^2zAXdardU3Ao~I)4DGdAv2bx{4nOK`20rJo>rmk3S2ZDu}))8Z1m}CKigf0 z3L`3Y`{huj`xj9@`$xTZzZc3je?n^yG<8sw$`Y%}9mUsjUR%T!?k^(q)6FH6Af^b6 zlPg~IEwg0y;`t9y;#D+uz!oE4VP&Je!<#q*F?m5L5?J3i@!0J6q#eu z!RRU`-)HeqGi_UJZ(n~|PSNsv+Wgl{P-TvaUQ9j?ZCtvb^37U$sFpBrkT{7Jpd?HpIvj2!}RIq zH{9~+gErN2+}J`>Jvng2hwM`=PLNkc7pkjblKW|+Fk9rc)G1R>Ww>RC=r-|!m-u7( zc(a$9NG}w#PjWNMS~)o=i~WA&4L(YIW25@AL9+H9!?3Y}sv#MOdY{bb9j>p`{?O(P zIvb`n?_(gP2w3P#&91JX*md+bBEr%xUHMVqfB;(f?OPtMnAZ#rm5q5mh;a2f_si2_ z3oXWB?{NF(JtkAn6F(O{z@b76OIqMC$&oJ_&S|YbFJ*)3qVX_uNf5b8(!vGX19hsG z(OP>RmZp29KH9Ge2kKjKigUmOe^K_!UXP`von)PR8Qz$%=EmOB9xS(ZxE_tnyzo}7 z=6~$~9k0M~v}`w={AeqF?_)9q{m8K#6M{a&(;u;O41j)I$^T?lx5(zlebpY@NT&#N zR+1bB)-1-xj}R8uwqwf=iP1GbxBjneCC%UrSdSxK1vM^i9;bUkS#iRZw2H>rS<2<$ zNT3|sDH>{tXb=zq7XZi*K?#Zsa1h1{h5!Tq_YbKFm_*=A5-<~j63he;4`77!|LBlo zR^~tR3yxcU=gDFbshyF6>o0bdp$qmHS7D}m3;^QZq9kBBU|9$N-~oU?G5;jyFR7>z hN`IR97YZXIo@y!QgFWddJ3|0`sjFx!m))><{BI=FK%f8s literal 0 HcmV?d00001 diff --git a/leaderboard_app/web/icons/Icon-maskable-192.png b/leaderboard_app/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/leaderboard_app/web/icons/Icon-maskable-512.png b/leaderboard_app/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 zcmeFZ_gj-)&^4Nb2tlbLMU<{!p(#yjqEe+=0IA_oih%ScH9@5#MNp&}Y#;;(h=A0@ zh7{>lT2MkSQ344eAvrhici!td|HJuyvJm#Y_w1Q9Yu3!26dNlO-oxUDK_C#XnW^Co z5C{VN6#{~B0)K2j7}*1Xq(Nqemv23A-6&=ZpEijkVnSwVGqLv40?n0=p;k3-U5e5+ z+z3>aS`u9DS=!wg8ROu?X4TFoW6CFLL&{GzoVT)ldhLekLM|+j3tIxRd|*5=c{=s&*vfPdBr(Fyj(v@%eQj1Soy7m4^@VRl1~@-PV7y+c!xz$8436WBn$t{=}mEdK#k`aystimGgI{(IBx$!pAwFoE9Y`^t^;> zKAD)C(Dl^s%`?q5$P|fZf8Xymrtu^Pv(7D`rn>Z-w$Ahs!z9!94WNVxrJuXfHAaxg zC6s@|Z1$7R$(!#t%Jb{{s6(Y?NoQXDYq)!}X@jKPhe`{9KQ@sAU8y-5`xt?S9$jKH zoi}6m5PcG*^{kjvt+kwPpyQzVg4o)a>;LK`aaN2x4@itBD3Aq?yWTM20VRn1rrd+2 zKO=P0rMjEGq_UqpMa`~7B|p?xAN1SCoCp}QxAv8O`jLJ5CVh@umR%c%i^)6!o+~`F zaalSTQcl5iwOLC&H)efzd{8(88mo`GI(56T<(&p7>Qd^;R1hn1Y~jN~tApaL8>##U zd65bo8)79CplWxr#z4!6HvLz&N7_5AN#x;kLG?zQ(#p|lj<8VUlKY=Aw!ATqeL-VG z42gA!^cMNPj>(`ZMEbCrnkg*QTsn*u(nQPWI9pA{MQ=IsPTzd7q5E#7+z>Ch=fx$~ z;J|?(5jTo5UWGvsJa(Sx0?S#56+8SD!I^tftyeh_{5_31l6&Hywtn`bbqYDqGZXI( zCG7hBgvksX2ak8+)hB4jnxlO@A32C_RM&g&qDSb~3kM&)@A_j1*oTO@nicGUyv+%^ z=vB)4(q!ykzT==Z)3*3{atJ5}2PV*?Uw+HhN&+RvKvZL3p9E?gHjv{6zM!A|z|UHK z-r6jeLxbGn0D@q5aBzlco|nG2tr}N@m;CJX(4#Cn&p&sLKwzLFx1A5izu?X_X4x8r@K*d~7>t1~ zDW1Mv5O&WOxbzFC`DQ6yNJ(^u9vJdj$fl2dq`!Yba_0^vQHXV)vqv1gssZYzBct!j zHr9>ydtM8wIs}HI4=E}qAkv|BPWzh3^_yLH(|kdb?x56^BlDC)diWyPd*|f!`^12_U>TD^^94OCN0lVv~Sgvs94ecpE^}VY$w`qr_>Ue zTfH~;C<3H<0dS5Rkf_f@1x$Gms}gK#&k()IC0zb^QbR!YLoll)c$Agfi6MKI0dP_L z=Uou&u~~^2onea2%XZ@>`0x^L8CK6=I{ge;|HXMj)-@o~h&O{CuuwBX8pVqjJ*o}5 z#8&oF_p=uSo~8vn?R0!AMWvcbZmsrj{ZswRt(aEdbi~;HeVqIe)-6*1L%5u$Gbs}| zjFh?KL&U(rC2izSGtwP5FnsR@6$-1toz?RvLD^k~h9NfZgzHE7m!!7s6(;)RKo2z} zB$Ci@h({l?arO+vF;s35h=|WpefaOtKVx>l399}EsX@Oe3>>4MPy%h&^3N_`UTAHJ zI$u(|TYC~E4)|JwkWW3F!Tib=NzjHs5ii2uj0^m|Qlh-2VnB#+X~RZ|`SA*}}&8j9IDv?F;(Y^1=Z0?wWz;ikB zewU>MAXDi~O7a~?jx1x=&8GcR-fTp>{2Q`7#BE#N6D@FCp`?ht-<1|y(NArxE_WIu zP+GuG=Qq>SHWtS2M>34xwEw^uvo4|9)4s|Ac=ud?nHQ>ax@LvBqusFcjH0}{T3ZPQ zLO1l<@B_d-(IS682}5KA&qT1+{3jxKolW+1zL4inqBS-D>BohA!K5++41tM@ z@xe<-qz27}LnV#5lk&iC40M||JRmZ*A##K3+!j93eouU8@q-`W0r%7N`V$cR&JV;iX(@cS{#*5Q>~4BEDA)EikLSP@>Oo&Bt1Z~&0d5)COI%3$cLB_M?dK# z{yv2OqW!al-#AEs&QFd;WL5zCcp)JmCKJEdNsJlL9K@MnPegK23?G|O%v`@N{rIRa zi^7a}WBCD77@VQ-z_v{ZdRsWYrYgC$<^gRQwMCi6);%R~uIi31OMS}=gUTE(GKmCI z$zM>mytL{uNN+a&S38^ez(UT=iSw=l2f+a4)DyCA1Cs_N-r?Q@$3KTYosY!;pzQ0k zzh1G|kWCJjc(oZVBji@kN%)UBw(s{KaYGy=i{g3{)Z+&H8t2`^IuLLKWT6lL<-C(! zSF9K4xd-|VO;4}$s?Z7J_dYqD#Mt)WCDnsR{Kpjq275uUq6`v0y*!PHyS(}Zmv)_{>Vose9-$h8P0|y;YG)Bo}$(3Z%+Gs0RBmFiW!^5tBmDK-g zfe5%B*27ib+7|A*Fx5e)2%kIxh7xWoc3pZcXS2zik!63lAG1;sC1ja>BqH7D zODdi5lKW$$AFvxgC-l-)!c+9@YMC7a`w?G(P#MeEQ5xID#<}W$3bSmJ`8V*x2^3qz zVe<^^_8GHqYGF$nIQm0Xq2kAgYtm#UC1A(=&85w;rmg#v906 zT;RyMgbMpYOmS&S9c38^40oUp?!}#_84`aEVw;T;r%gTZkWeU;;FwM@0y0adt{-OK z(vGnPSlR=Nv2OUN!2=xazlnHPM9EWxXg2EKf0kI{iQb#FoP>xCB<)QY>OAM$Dcdbm zU6dU|%Mo(~avBYSjRc13@|s>axhrPl@Sr81{RSZUdz4(=|82XEbV*JAX6Lfbgqgz584lYgi0 z2-E{0XCVON$wHfvaLs;=dqhQJ&6aLn$D#0i(FkAVrXG9LGm3pSTf&f~RQb6|1_;W> z?n-;&hrq*~L=(;u#jS`*Yvh@3hU-33y_Kv1nxqrsf>pHVF&|OKkoC)4DWK%I!yq?P z=vXo8*_1iEWo8xCa{HJ4tzxOmqS0&$q+>LroMKI*V-rxhOc%3Y!)Y|N6p4PLE>Yek>Y(^KRECg8<|%g*nQib_Yc#A5q8Io z6Ig&V>k|~>B6KE%h4reAo*DfOH)_01tE0nWOxX0*YTJgyw7moaI^7gW*WBAeiLbD?FV9GSB zPv3`SX*^GRBM;zledO`!EbdBO_J@fEy)B{-XUTVQv}Qf~PSDpK9+@I`7G7|>Dgbbu z_7sX9%spVo$%qwRwgzq7!_N;#Td08m5HV#?^dF-EV1o)Q=Oa+rs2xH#g;ykLbwtCh znUnA^dW!XjspJ;otq$yV@I^s9Up(5k7rqhQd@OLMyyxVLj_+$#Vc*}Usevp^I(^vH zmDgHc0VMme|K&X?9&lkN{yq_(If)O`oUPW8X}1R5pSVBpfJe0t{sPA(F#`eONTh_) zxeLqHMfJX#?P(@6w4CqRE@Eiza; z;^5)Kk=^5)KDvd9Q<`=sJU8rjjxPmtWMTmzcH={o$U)j=QBuHarp?=}c??!`3d=H$nrJMyr3L-& zA#m?t(NqLM?I3mGgWA_C+0}BWy3-Gj7bR+d+U?n*mN$%5P`ugrB{PeV>jDUn;eVc- zzeMB1mI4?fVJatrNyq|+zn=!AiN~<}eoM#4uSx^K?Iw>P2*r=k`$<3kT00BE_1c(02MRz4(Hq`L^M&xt!pV2 zn+#U3@j~PUR>xIy+P>51iPayk-mqIK_5rlQMSe5&tDkKJk_$i(X&;K(11YGpEc-K= zq4Ln%^j>Zi_+Ae9eYEq_<`D+ddb8_aY!N;)(&EHFAk@Ekg&41ABmOXfWTo)Z&KotA zh*jgDGFYQ^y=m)<_LCWB+v48DTJw*5dwMm_YP0*_{@HANValf?kV-Ic3xsC}#x2h8 z`q5}d8IRmqWk%gR)s~M}(Qas5+`np^jW^oEd-pzERRPMXj$kS17g?H#4^trtKtq;C?;c ztd|%|WP2w2Nzg@)^V}!Gv++QF2!@FP9~DFVISRW6S?eP{H;;8EH;{>X_}NGj^0cg@ z!2@A>-CTcoN02^r6@c~^QUa={0xwK0v4i-tQ9wQq^=q*-{;zJ{Qe%7Qd!&X2>rV@4 z&wznCz*63_vw4>ZF8~%QCM?=vfzW0r_4O^>UA@otm_!N%mH)!ERy&b!n3*E*@?9d^ zu}s^By@FAhG(%?xgJMuMzuJw2&@$-oK>n z=UF}rt%vuaP9fzIFCYN-1&b#r^Cl6RDFIWsEsM|ROf`E?O(cy{BPO2Ie~kT+^kI^i zp>Kbc@C?}3vy-$ZFVX#-cx)Xj&G^ibX{pWggtr(%^?HeQL@Z( zM-430g<{>vT*)jK4aY9(a{lSy{8vxLbP~n1MXwM527ne#SHCC^F_2@o`>c>>KCq9c(4c$VSyMl*y3Nq1s+!DF| z^?d9PipQN(mw^j~{wJ^VOXDCaL$UtwwTpyv8IAwGOg<|NSghkAR1GSNLZ1JwdGJYm zP}t<=5=sNNUEjc=g(y)1n5)ynX(_$1-uGuDR*6Y^Wgg(LT)Jp><5X|}bt z_qMa&QP?l_n+iVS>v%s2Li_;AIeC=Ca^v1jX4*gvB$?H?2%ndnqOaK5-J%7a} zIF{qYa&NfVY}(fmS0OmXA70{znljBOiv5Yod!vFU{D~*3B3Ka{P8?^ zfhlF6o7aNT$qi8(w<}OPw5fqA7HUje*r*Oa(YV%*l0|9FP9KW@U&{VSW{&b0?@y)M zs%4k1Ax;TGYuZ9l;vP5@?3oQsp3)rjBeBvQQ>^B;z5pc=(yHhHtq6|0m(h4envn_j787fizY@V`o(!SSyE7vlMT zbo=Z1c=atz*G!kwzGB;*uPL$Ei|EbZLh8o+1BUMOpnU(uX&OG1MV@|!&HOOeU#t^x zr9=w2ow!SsTuJWT7%Wmt14U_M*3XiWBWHxqCVZI0_g0`}*^&yEG9RK9fHK8e+S^m? zfCNn$JTswUVbiC#>|=wS{t>-MI1aYPLtzO5y|LJ9nm>L6*wpr_m!)A2Fb1RceX&*|5|MwrvOk4+!0p99B9AgP*9D{Yt|x=X}O% zgIG$MrTB=n-!q%ROT|SzH#A$Xm;|ym)0>1KR}Yl0hr-KO&qMrV+0Ej3d@?FcgZ+B3 ztEk16g#2)@x=(ko8k7^Tq$*5pfZHC@O@}`SmzT1(V@x&NkZNM2F#Q-Go7-uf_zKC( zB(lHZ=3@dHaCOf6C!6i8rDL%~XM@rVTJbZL09?ht@r^Z_6x}}atLjvH^4Vk#Ibf(^LiBJFqorm?A=lE zzFmwvp4bT@Nv2V>YQT92X;t9<2s|Ru5#w?wCvlhcHLcsq0TaFLKy(?nzezJ>CECqj zggrI~Hd4LudM(m{L@ezfnpELsRFVFw>fx;CqZtie`$BXRn#Ns%AdoE$-Pf~{9A8rV zf7FbgpKmVzmvn-z(g+&+-ID=v`;6=)itq8oM*+Uz**SMm_{%eP_c0{<%1JGiZS19o z@Gj7$Se~0lsu}w!%;L%~mIAO;AY-2i`9A*ZfFs=X!LTd6nWOZ7BZH2M{l2*I>Xu)0 z`<=;ObglnXcVk!T>e$H?El}ra0WmPZ$YAN0#$?|1v26^(quQre8;k20*dpd4N{i=b zuN=y}_ew9SlE~R{2+Rh^7%PA1H5X(p8%0TpJ=cqa$65XL)$#ign-y!qij3;2>j}I; ziO@O|aYfn&up5F`YtjGw68rD3{OSGNYmBnl?zdwY$=RFsegTZ=kkzRQ`r7ZjQP!H( zp4>)&zf<*N!tI00xzm-ME_a{_I!TbDCr;8E;kCH4LlL-tqLxDuBn-+xgPk37S&S2^ z2QZumkIimwz!c@!r0)j3*(jPIs*V!iLTRl0Cpt_UVNUgGZzdvs0(-yUghJfKr7;=h zD~y?OJ-bWJg;VdZ^r@vlDoeGV&8^--!t1AsIMZ5S440HCVr%uk- z2wV>!W1WCvFB~p$P$$_}|H5>uBeAe>`N1FI8AxM|pq%oNs;ED8x+tb44E) zTj{^fbh@eLi%5AqT?;d>Es5D*Fi{Bpk)q$^iF!!U`r2hHAO_?#!aYmf>G+jHsES4W zgpTKY59d?hsb~F0WE&dUp6lPt;Pm zcbTUqRryw^%{ViNW%Z(o8}dd00H(H-MmQmOiTq{}_rnwOr*Ybo7*}3W-qBT!#s0Ie z-s<1rvvJx_W;ViUD`04%1pra*Yw0BcGe)fDKUK8aF#BwBwMPU;9`!6E(~!043?SZx z13K%z@$$#2%2ovVlgFIPp7Q6(vO)ud)=*%ZSucL2Dh~K4B|%q4KnSpj#n@(0B})!9 z8p*hY@5)NDn^&Pmo;|!>erSYg`LkO?0FB@PLqRvc>4IsUM5O&>rRv|IBRxi(RX(gJ ztQ2;??L~&Mv;aVr5Q@(?y^DGo%pO^~zijld41aA0KKsy_6FeHIn?fNHP-z>$OoWer zjZ5hFQTy*-f7KENRiCE$ZOp4|+Wah|2=n@|W=o}bFM}Y@0e62+_|#fND5cwa3;P{^pEzlJbF1Yq^}>=wy8^^^$I2M_MH(4Dw{F6hm+vrWV5!q;oX z;tTNhz5`-V={ew|bD$?qcF^WPR{L(E%~XG8eJx(DoGzt2G{l8r!QPJ>kpHeOvCv#w zr=SSwMDaUX^*~v%6K%O~i)<^6`{go>a3IdfZ8hFmz&;Y@P%ZygShQZ2DSHd`m5AR= zx$wWU06;GYwXOf(%MFyj{8rPFXD};JCe85Bdp4$YJ2$TzZ7Gr#+SwCvBI1o$QP0(c zy`P51FEBV2HTisM3bHqpmECT@H!Y2-bv2*SoSPoO?wLe{M#zDTy@ujAZ!Izzky~3k zRA1RQIIoC*Mej1PH!sUgtkR0VCNMX(_!b65mo66iM*KQ7xT8t2eev$v#&YdUXKwGm z7okYAqYF&bveHeu6M5p9xheRCTiU8PFeb1_Rht0VVSbm%|1cOVobc8mvqcw!RjrMRM#~=7xibH&Fa5Imc|lZ{eC|R__)OrFg4@X_ ze+kk*_sDNG5^ELmHnZ7Ue?)#6!O)#Nv*Dl2mr#2)w{#i-;}0*_h4A%HidnmclH#;Q zmQbq+P4DS%3}PpPm7K_K3d2s#k~x+PlTul7+kIKol0@`YN1NG=+&PYTS->AdzPv!> zQvzT=)9se*Jr1Yq+C{wbK82gAX`NkbXFZ)4==j4t51{|-v!!$H8@WKA={d>CWRW+g z*`L>9rRucS`vbXu0rzA1#AQ(W?6)}1+oJSF=80Kf_2r~Qm-EJ6bbB3k`80rCv(0d` zvCf3;L2ovYG_TES%6vSuoKfIHC6w;V31!oqHM8-I8AFzcd^+_86!EcCOX|Ta9k1!s z_Vh(EGIIsI3fb&dF$9V8v(sTBC%!#<&KIGF;R+;MyC0~}$gC}}= zR`DbUVc&Bx`lYykFZ4{R{xRaUQkWCGCQlEc;!mf=+nOk$RUg*7 z;kP7CVLEc$CA7@6VFpsp3_t~m)W0aPxjsA3e5U%SfY{tp5BV5jH-5n?YX7*+U+Zs%LGR>U- z!x4Y_|4{gx?ZPJobISy991O znrmrC3otC;#4^&Rg_iK}XH(XX+eUHN0@Oe06hJk}F?`$)KmH^eWz@@N%wEc)%>?Ft z#9QAroDeyfztQ5Qe{m*#R#T%-h*&XvSEn@N$hYRTCMXS|EPwzF3IIysD2waj`vQD{ zv_#^Pgr?s~I*NE=acf@dWVRNWTr(GN0wrL)Z2=`Dr>}&ZDNX|+^Anl{Di%v1Id$_p zK5_H5`RDjJx`BW7hc85|> zHMMsWJ4KTMRHGu+vy*kBEMjz*^K8VtU=bXJYdhdZ-?jTXa$&n)C?QQIZ7ln$qbGlr zS*TYE+ppOrI@AoPP=VI-OXm}FzgXRL)OPvR$a_=SsC<3Jb+>5makX|U!}3lx4tX&L z^C<{9TggZNoeX!P1jX_K5HkEVnQ#s2&c#umzV6s2U-Q;({l+j^?hi7JnQ7&&*oOy9 z(|0asVTWUCiCnjcOnB2pN0DpuTglKq;&SFOQ3pUdye*eT<2()7WKbXp1qq9=bhMWlF-7BHT|i3TEIT77AcjD(v=I207wi-=vyiw5mxgPdTVUC z&h^FEUrXwWs9en2C{ywZp;nvS(Mb$8sBEh-*_d-OEm%~p1b2EpcwUdf<~zmJmaSTO zSX&&GGCEz-M^)G$fBvLC2q@wM$;n4jp+mt0MJFLuJ%c`tSp8$xuP|G81GEd2ci$|M z4XmH{5$j?rqDWoL4vs!}W&!?!rtj=6WKJcE>)?NVske(p;|#>vL|M_$as=mi-n-()a*OU3Okmk0wC<9y7t^D(er-&jEEak2!NnDiOQ99Wx8{S8}=Ng!e0tzj*#T)+%7;aM$ z&H}|o|J1p{IK0Q7JggAwipvHvko6>Epmh4RFRUr}$*2K4dz85o7|3#Bec9SQ4Y*;> zXWjT~f+d)dp_J`sV*!w>B%)#GI_;USp7?0810&3S=WntGZ)+tzhZ+!|=XlQ&@G@~3 z-dw@I1>9n1{+!x^Hz|xC+P#Ab`E@=vY?3%Bc!Po~e&&&)Qp85!I|U<-fCXy*wMa&t zgDk!l;gk;$taOCV$&60z+}_$ykz=Ea*)wJQ3-M|p*EK(cvtIre0Pta~(95J7zoxBN zS(yE^3?>88AL0Wfuou$BM{lR1hkrRibz=+I9ccwd`ZC*{NNqL)3pCcw^ygMmrG^Yp zn5f}Xf>%gncC=Yq96;rnfp4FQL#{!Y*->e82rHgY4Zwy{`JH}b9*qr^VA{%~Z}jtp z_t$PlS6}5{NtTqXHN?uI8ut8rOaD#F1C^ls73S=b_yI#iZDOGz3#^L@YheGd>L;<( z)U=iYj;`{>VDNzIxcjbTk-X3keXR8Xbc`A$o5# zKGSk-7YcoBYuAFFSCjGi;7b<;n-*`USs)IX z=0q6WZ=L!)PkYtZE-6)azhXV|+?IVGTOmMCHjhkBjfy@k1>?yFO3u!)@cl{fFAXnRYsWk)kpT?X{_$J=|?g@Q}+kFw|%n!;Zo}|HE@j=SFMvT8v`6Y zNO;tXN^036nOB2%=KzxB?n~NQ1K8IO*UE{;Xy;N^ZNI#P+hRZOaHATz9(=)w=QwV# z`z3+P>9b?l-@$@P3<;w@O1BdKh+H;jo#_%rr!ute{|YX4g5}n?O7Mq^01S5;+lABE+7`&_?mR_z7k|Ja#8h{!~j)| zbBX;*fsbUak_!kXU%HfJ2J+G7;inu#uRjMb|8a){=^))y236LDZ$$q3LRlat1D)%7K0!q5hT5V1j3qHc7MG9 z_)Q=yQ>rs>3%l=vu$#VVd$&IgO}Za#?aN!xY>-<3PhzS&q!N<=1Q7VJBfHjug^4|) z*fW^;%3}P7X#W3d;tUs3;`O&>;NKZBMR8au6>7?QriJ@gBaorz-+`pUWOP73DJL=M z(33uT6Gz@Sv40F6bN|H=lpcO z^AJl}&=TIjdevuDQ!w0K*6oZ2JBOhb31q!XDArFyKpz!I$p4|;c}@^bX{>AXdt7Bm zaLTk?c%h@%xq02reu~;t@$bv`b3i(P=g}~ywgSFpM;}b$zAD+=I!7`V~}ARB(Wx0C(EAq@?GuxOL9X+ffbkn3+Op0*80TqmpAq~EXmv%cq36celXmRz z%0(!oMp&2?`W)ALA&#|fu)MFp{V~~zIIixOxY^YtO5^FSox8v$#d0*{qk0Z)pNTt0QVZ^$`4vImEB>;Lo2!7K05TpY-sl#sWBz_W-aDIV`Ksabi zvpa#93Svo!70W*Ydh)Qzm{0?CU`y;T^ITg-J9nfWeZ-sbw)G@W?$Eomf%Bg2frfh5 zRm1{|E0+(4zXy){$}uC3%Y-mSA2-^I>Tw|gQx|7TDli_hB>``)Q^aZ`LJC2V3U$SABP}T)%}9g2pF9dT}aC~!rFFgkl1J$ z`^z{Arn3On-m%}r}TGF8KQe*OjSJ=T|caa_E;v89A{t@$yT^(G9=N9F?^kT*#s3qhJq!IH5|AhnqFd z0B&^gm3w;YbMNUKU>naBAO@fbz zqw=n!@--}o5;k6DvTW9pw)IJVz;X}ncbPVrmH>4x);8cx;q3UyiML1PWp%bxSiS|^ zC5!kc4qw%NSOGQ*Kcd#&$30=lDvs#*4W4q0u8E02U)7d=!W7+NouEyuF1dyH$D@G& zaFaxo9Ex|ZXA5y{eZT*i*dP~INSMAi@mvEX@q5i<&o&#sM}Df?Og8n8Ku4vOux=T% zeuw~z1hR}ZNwTn8KsQHKLwe2>p^K`YWUJEdVEl|mO21Bov!D0D$qPoOv=vJJ`)|%_ z>l%`eexY7t{BlVKP!`a^U@nM?#9OC*t76My_E_<16vCz1x_#82qj2PkWiMWgF8bM9 z(1t4VdHcJ;B~;Q%x01k_gQ0>u2*OjuEWNOGX#4}+N?Gb5;+NQMqp}Puqw2HnkYuKA zzKFWGHc&K>gwVgI1Sc9OT1s6fq=>$gZU!!xsilA$fF`kLdGoX*^t}ao@+^WBpk>`8 z4v_~gK|c2rCq#DZ+H)$3v~Hoi=)=1D==e3P zpKrRQ+>O^cyTuWJ%2}__0Z9SM_z9rptd*;-9uC1tDw4+A!=+K%8~M&+Zk#13hY$Y$ zo-8$*8dD5@}XDi19RjK6T^J~DIXbF5w&l?JLHMrf0 zLv0{7*G!==o|B%$V!a=EtVHdMwXLtmO~vl}P6;S(R2Q>*kTJK~!}gloxj)m|_LYK{ zl(f1cB=EON&wVFwK?MGn^nWuh@f95SHatPs(jcwSY#Dnl1@_gkOJ5=f`%s$ZHljRH0 z+c%lrb=Gi&N&1>^L_}#m>=U=(oT^vTA&3!xXNyqi$pdW1BDJ#^{h|2tZc{t^vag3& zAD7*8C`chNF|27itjBUo^CCDyEpJLX3&u+(L;YeeMwnXEoyN(ytoEabcl$lSgx~Ltatn}b$@j_yyMrBb03)shJE*$;Mw=;mZd&8e>IzE+4WIoH zCSZE7WthNUL$|Y#m!Hn?x7V1CK}V`KwW2D$-7&ODy5Cj;!_tTOOo1Mm%(RUt)#$@3 zhurA)t<7qik%%1Et+N1?R#hdBB#LdQ7{%-C zn$(`5e0eFh(#c*hvF>WT*07fk$N_631?W>kfjySN8^XC9diiOd#s?4tybICF;wBjp zIPzilX3{j%4u7blhq)tnaOBZ_`h_JqHXuI7SuIlNTgBk9{HIS&3|SEPfrvcE<@}E` zKk$y*nzsqZ{J{uWW9;#n=de&&h>m#A#q)#zRonr(?mDOYU&h&aQWD;?Z(22wY?t$U3qo`?{+amA$^TkxL+Ex2dh`q7iR&TPd0Ymwzo#b? zP$#t=elB5?k$#uE$K>C$YZbYUX_JgnXA`oF_Ifz4H7LEOW~{Gww&3s=wH4+j8*TU| zSX%LtJWqhr-xGNSe{;(16kxnak6RnZ{0qZ^kJI5X*It_YuynSpi(^-}Lolr{)#z_~ zw!(J-8%7Ybo^c3(mED`Xz8xecP35a6M8HarxRn%+NJBE;dw>>Y2T&;jzRd4FSDO3T zt*y+zXCtZQ0bP0yf6HRpD|WmzP;DR^-g^}{z~0x~z4j8m zucTe%k&S9Nt-?Jb^gYW1w6!Y3AUZ0Jcq;pJ)Exz%7k+mUOm6%ApjjSmflfKwBo6`B zhNb@$NHTJ>guaj9S{@DX)!6)b-Shav=DNKWy(V00k(D!v?PAR0f0vDNq*#mYmUp6> z76KxbFDw5U{{qx{BRj(>?|C`82ICKbfLxoldov-M?4Xl+3;I4GzLHyPOzYw7{WQST zPNYcx5onA%MAO9??41Po*1zW(Y%Zzn06-lUp{s<3!_9vv9HBjT02On0Hf$}NP;wF) zP<`2p3}A^~1YbvOh{ePMx$!JGUPX-tbBzp3mDZMY;}h;sQ->!p97GA)9a|tF(Gh{1$xk7 zUw?ELkT({Xw!KIr);kTRb1b|UL`r2_`a+&UFVCdJ)1T#fdh;71EQl9790Br0m_`$x z9|ZANuchFci8GNZ{XbP=+uXSJRe(;V5laQz$u18#?X*9}x7cIEbnr%<=1cX3EIu7$ zhHW6pe5M(&qEtsqRa>?)*{O;OJT+YUhG5{km|YI7I@JL_3Hwao9aXneiSA~a* z|Lp@c-oMNyeAEuUz{F?kuou3x#C*gU?lon!RC1s37gW^0Frc`lqQWH&(J4NoZg3m8 z;Lin#8Q+cFPD7MCzj}#|ws7b@?D9Q4dVjS4dpco=4yX5SSH=A@U@yqPdp@?g?qeia zH=Tt_9)G=6C2QIPsi-QipnK(mc0xXIN;j$WLf@n8eYvMk;*H-Q4tK%(3$CN}NGgO8n}fD~+>?<3UzvsrMf*J~%i;VKQHbF%TPalFi=#sgj)(P#SM^0Q=Tr>4kJVw8X3iWsP|e8tj}NjlMdWp z@2+M4HQu~3!=bZpjh;;DIDk&X}=c8~kn)FWWH z2KL1w^rA5&1@@^X%MjZ7;u(kH=YhH2pJPFQe=hn>tZd5RC5cfGYis8s9PKaxi*}-s6*W zRA^PwR=y^5Z){!(4D9-KC;0~;b*ploznFOaU`bJ_7U?qAi#mTo!&rIECRL$_y@yI27x2?W+zqDBD5~KCVYKFZLK+>ABC(Kj zeAll)KMgIlAG`r^rS{loBrGLtzhHY8$)<_S<(Dpkr(Ym@@vnQ&rS@FC*>2@XCH}M+an74WcRDcoQ+a3@A z9tYhl5$z7bMdTvD2r&jztBuo37?*k~wcU9GK2-)MTFS-lux-mIRYUuGUCI~V$?s#< z?1qAWb(?ZLm(N>%S%y10COdaq_Tm5c^%ooIxpR=`3e4C|@O5wY+eLik&XVi5oT7oe zmxH)Jd*5eo@!7t`x8!K=-+zJ-Sz)B_V$)s1pW~CDU$=q^&ABvf6S|?TOMB-RIm@CoFg>mjIQE)?+A1_3s6zmFU_oW&BqyMz1mY*IcP_2knjq5 zqw~JK(cVsmzc7*EvTT2rvpeqhg)W=%TOZ^>f`rD4|7Z5fq*2D^lpCttIg#ictgqZ$P@ru6P#f$x#KfnfTZj~LG6U_d-kE~`;kU_X)`H5so@?C zWmb!7x|xk@0L~0JFall*@ltyiL^)@3m4MqC7(7H0sH!WidId1#f#6R{Q&A!XzO1IAcIx;$k66dumt6lpUw@nL2MvqJ5^kbOVZ<^2jt5-njy|2@`07}0w z;M%I1$FCoLy`8xp8Tk)bFr;7aJeQ9KK6p=O$U0-&JYYy8woV*>b+FB?xLX`=pirYM z5K$BA(u)+jR{?O2r$c_Qvl?M{=Ar{yQ!UVsVn4k@0!b?_lA;dVz9uaQUgBH8Oz(Sb zrEs;&Ey>_ex8&!N{PmQjp+-Hlh|OA&wvDai#GpU=^-B70V0*LF=^bi+Nhe_o|azZ%~ZZ1$}LTmWt4aoB1 zPgccm$EwYU+jrdBaQFxQfn5gd(gM`Y*Ro1n&Zi?j=(>T3kmf94vdhf?AuS8>$Va#P zGL5F+VHpxdsCUa}+RqavXCobI-@B;WJbMphpK2%6t=XvKWWE|ruvREgM+|V=i6;;O zx$g=7^`$XWn0fu!gF=Xe9cMB8Z_SelD>&o&{1XFS`|nInK3BXlaeD*rc;R-#osyIS zWv&>~^TLIyBB6oDX+#>3<_0+2C4u2zK^wmHXXDD9_)kmLYJ!0SzM|%G9{pi)`X$uf zW}|%%#LgyK7m(4{V&?x_0KEDq56tk|0YNY~B(Sr|>WVz-pO3A##}$JCT}5P7DY+@W z#gJv>pA5>$|E3WO2tV7G^SuymB?tY`ooKcN3!vaQMnBNk-WATF{-$#}FyzgtJ8M^; zUK6KWSG)}6**+rZ&?o@PK3??uN{Q)#+bDP9i1W&j)oaU5d0bIWJ_9T5ac!qc?x66Q z$KUSZ`nYY94qfN_dpTFr8OW~A?}LD;Yty-BA)-be5Z3S#t2Io%q+cAbnGj1t$|qFR z9o?8B7OA^KjCYL=-!p}w(dkC^G6Nd%_I=1))PC0w5}ZZGJxfK)jP4Fwa@b-SYBw?% zdz9B-<`*B2dOn(N;mcTm%Do)rIvfXRNFX&1h`?>Rzuj~Wx)$p13nrDlS8-jwq@e@n zNIj_|8or==8~1h*Ih?w*8K7rYkGlwlTWAwLKc5}~dfz3y`kM&^Q|@C%1VAp_$wnw6zG~W4O+^ z>i?NY?oXf^Puc~+fDM$VgRNBpOZj{2cMP~gCqWAX4 z7>%$ux8@a&_B(pt``KSt;r+sR-$N;jdpY>|pyvPiN)9ohd*>mVST3wMo)){`B(&eX z1?zZJ-4u9NZ|~j1rdZYq4R$?swf}<6(#ex%7r{kh%U@kT)&kWuAszS%oJts=*OcL9 zaZwK<5DZw%1IFHXgFplP6JiL^dk8+SgM$D?8X+gE4172hXh!WeqIO>}$I9?Nry$*S zQ#f)RuH{P7RwA3v9f<-w>{PSzom;>(i&^l{E0(&Xp4A-*q-@{W1oE3K;1zb{&n28dSC2$N+6auXe0}e4b z)KLJ?5c*>@9K#I^)W;uU_Z`enquTUxr>mNq z1{0_puF-M7j${rs!dxxo3EelGodF1TvjV;Zpo;s{5f1pyCuRp=HDZ?s#IA4f?h|-p zGd|Mq^4hDa@Bh!c4ZE?O&x&XZ_ptZGYK4$9F4~{%R!}G1leCBx`dtNUS|K zL-7J5s4W@%mhXg1!}a4PD%!t&Qn%f_oquRajn3@C*)`o&K9o7V6DwzVMEhjVdDJ1fjhr#@=lp#@4EBqi=CCQ>73>R(>QKPNM&_Jpe5G`n4wegeC`FYEPJ{|vwS>$-`fuRSp3927qOv|NC3T3G-0 zA{K`|+tQy1yqE$ShWt8ny&5~)%ITb@^+x$w0)f&om;P8B)@}=Wzy59BwUfZ1vqw87 za2lB8J(&*l#(V}Id8SyQ0C(2amzkz3EqG&Ed0Jq1)$|&>4_|NIe=5|n=3?siFV0fI z{As5DLW^gs|B-b4C;Hd(SM-S~GQhzb>HgF2|2Usww0nL^;x@1eaB)=+Clj+$fF@H( z-fqP??~QMT$KI-#m;QC*&6vkp&8699G3)Bq0*kFZXINw=b9OVaed(3(3kS|IZ)CM? zJdnW&%t8MveBuK21uiYj)_a{Fnw0OErMzMN?d$QoPwkhOwcP&p+t>P)4tHlYw-pPN z^oJ=uc$Sl>pv@fZH~ZqxSvdhF@F1s=oZawpr^-#l{IIOGG=T%QXjtwPhIg-F@k@uIlr?J->Ia zpEUQ*=4g|XYn4Gez&aHr*;t$u3oODPmc2Ku)2Og|xjc%w;q!Zz+zY)*3{7V8bK4;& zYV82FZ+8?v)`J|G1w4I0fWdKg|2b#iaazCv;|?(W-q}$o&Y}Q5d@BRk^jL7#{kbCK zSgkyu;=DV+or2)AxCBgq-nj5=@n^`%T#V+xBGEkW4lCqrE)LMv#f;AvD__cQ@Eg3`~x| zW+h9mofSXCq5|M)9|ez(#X?-sxB%Go8};sJ?2abp(Y!lyi>k)|{M*Z$c{e1-K4ky` MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/leaderboard_app/web/index.html b/leaderboard_app/web/index.html new file mode 100644 index 0000000..921b04e --- /dev/null +++ b/leaderboard_app/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + leaderboard_app + + + + + + diff --git a/leaderboard_app/web/manifest.json b/leaderboard_app/web/manifest.json new file mode 100644 index 0000000..112a7bd --- /dev/null +++ b/leaderboard_app/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "leaderboard_app", + "short_name": "leaderboard_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/leaderboard_app/windows/.gitignore b/leaderboard_app/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/leaderboard_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/leaderboard_app/windows/CMakeLists.txt b/leaderboard_app/windows/CMakeLists.txt new file mode 100644 index 0000000..f7b848d --- /dev/null +++ b/leaderboard_app/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(leaderboard_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "leaderboard_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/leaderboard_app/windows/flutter/CMakeLists.txt b/leaderboard_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/leaderboard_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/leaderboard_app/windows/flutter/generated_plugin_registrant.cc b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/leaderboard_app/windows/flutter/generated_plugin_registrant.h b/leaderboard_app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/leaderboard_app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/leaderboard_app/windows/flutter/generated_plugins.cmake b/leaderboard_app/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/leaderboard_app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/leaderboard_app/windows/runner/CMakeLists.txt b/leaderboard_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/leaderboard_app/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/leaderboard_app/windows/runner/Runner.rc b/leaderboard_app/windows/runner/Runner.rc new file mode 100644 index 0000000..6f9a0b5 --- /dev/null +++ b/leaderboard_app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "leaderboard_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "leaderboard_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "leaderboard_app.exe" "\0" + VALUE "ProductName", "leaderboard_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/leaderboard_app/windows/runner/flutter_window.cpp b/leaderboard_app/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/leaderboard_app/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/leaderboard_app/windows/runner/flutter_window.h b/leaderboard_app/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/leaderboard_app/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/leaderboard_app/windows/runner/main.cpp b/leaderboard_app/windows/runner/main.cpp new file mode 100644 index 0000000..bef765f --- /dev/null +++ b/leaderboard_app/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"leaderboard_app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/leaderboard_app/windows/runner/resource.h b/leaderboard_app/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/leaderboard_app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/leaderboard_app/windows/runner/resources/app_icon.ico b/leaderboard_app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/windows/runner/runner.exe.manifest b/leaderboard_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/leaderboard_app/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/leaderboard_app/windows/runner/utils.cpp b/leaderboard_app/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/leaderboard_app/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/leaderboard_app/windows/runner/utils.h b/leaderboard_app/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/leaderboard_app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/leaderboard_app/windows/runner/win32_window.cpp b/leaderboard_app/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/leaderboard_app/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/leaderboard_app/windows/runner/win32_window.h b/leaderboard_app/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/leaderboard_app/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From eb6440e7f24c7334b8ad62384c3263f61bb95c71 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 5 Aug 2025 15:48:28 +0530 Subject: [PATCH 02/53] integrated provider for chats and chat lists --- leaderboard_app/lib/main.dart | 8 +- leaderboard_app/lib/pages/chat_page.dart | 390 ++++++------------ leaderboard_app/lib/pages/chatlists_page.dart | 83 ++-- .../lib/provider/chat_provider.dart | 80 ++++ .../lib/provider/chatlists_provider.dart | 30 ++ 5 files changed, 280 insertions(+), 311 deletions(-) create mode 100644 leaderboard_app/lib/provider/chat_provider.dart create mode 100644 leaderboard_app/lib/provider/chatlists_provider.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index b93855c..40d5ee4 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -2,13 +2,17 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/pages/home_page.dart'; import 'package:leaderboard_app/pages/signup_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; +import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/provider/theme_provider.dart'; import 'package:provider/provider.dart'; void main() { runApp( - ChangeNotifierProvider( - create: (_) => ThemeProvider(), + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => ThemeProvider()), + ChangeNotifierProvider(create: (_) => ChatListProvider()..loadDummyChats(),), + ], child: const MainApp(), ), ); diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index 8323813..0483bb7 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/provider/chat_provider.dart'; import 'package:pixelarticons/pixel.dart'; -import 'profile_page.dart'; // <- Assuming this file exists +import 'package:provider/provider.dart'; +import 'profile_page.dart'; -class ChatPage extends StatefulWidget { +class ChatPage extends StatelessWidget { final String receiverEmail; final String receiverID; @@ -13,48 +15,25 @@ class ChatPage extends StatefulWidget { }); @override - State createState() => _ChatPageState(); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => ChatProvider(receiverID: receiverID), + child: const ChatView(), + ); + } +} + +class ChatView extends StatefulWidget { + const ChatView({super.key}); + + @override + State createState() => _ChatViewState(); } -class _ChatPageState extends State { +class _ChatViewState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final FocusNode myFocusNode = FocusNode(); - String? replyTo; - - bool showAttachmentOptions = false; - - final String currentUserId = "uid_me"; - - List> dummyMessages = [ - {"senderID": "uid_me", "type": "image", "timestamp": "12:34 pm"}, - { - "senderID": "uid_me", - "message": "text text text text text text text text text text...", - "timestamp": "12:34 pm", - }, - { - "senderID": "system", - "message": "Duelled", - "timestamp": "12:34 pm", - "icon": Pixel.bullseye, - }, - { - "senderID": "uid_1", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - { - "senderID": "uid_me", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - { - "senderID": "uid_1", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - ]; @override void initState() { @@ -77,24 +56,10 @@ class _ChatPageState extends State { } } - void sendMessage() { - if (_messageController.text.trim().isEmpty) return; - setState(() { - dummyMessages.add({ - "senderID": currentUserId, - "message": _messageController.text.trim(), - "timestamp": "now", - if (replyTo != null) "replyTo": replyTo, - }); - _messageController.clear(); - replyTo = null; - }); - scrollDown(); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; + final provider = Provider.of(context); return Scaffold( backgroundColor: theme.surface, @@ -117,21 +82,8 @@ class _ChatPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Penny Valeria", - style: TextStyle( - color: theme.primary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - "Online", - style: TextStyle( - color: theme.primary.withOpacity(0.6), - fontSize: 12, - ), - ), + Text("Penny Valeria", style: TextStyle(color: theme.primary, fontWeight: FontWeight.bold, fontSize: 16)), + Text("Online", style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12)), ], ), ], @@ -145,13 +97,8 @@ class _ChatPageState extends State { style: ElevatedButton.styleFrom( backgroundColor: theme.inversePrimary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), child: const Text("Duel Now!"), ), @@ -160,166 +107,128 @@ class _ChatPageState extends State { ), body: Column( children: [ - Expanded(child: _buildMessageList()), - if (showAttachmentOptions) _buildAttachmentDropdown(), - _buildUserInput(), + Expanded(child: _buildMessageList(provider)), + if (provider.showAttachmentOptions) _buildAttachmentDropdown(), + _buildUserInput(provider), ], ), ); } - Widget _buildMessageList() { + Widget _buildMessageList(ChatProvider provider) { return ListView.builder( controller: _scrollController, - itemCount: dummyMessages.length, + itemCount: provider.messages.length, itemBuilder: (context, index) { - final msg = dummyMessages[index]; - final isMe = msg["senderID"] == currentUserId; + final msg = provider.messages[index]; + final isMe = msg["senderID"] == provider.currentUserID; final isSystem = msg["senderID"] == "system"; final isImage = msg["type"] == "image"; if (isSystem) { - return Center( - child: Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - msg["icon"] ?? Icons.info, - size: 16, - color: Colors.white, - ), - const SizedBox(width: 6), - Text( - msg["message"] ?? "", - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - const SizedBox(width: 6), - Text( - msg["timestamp"] ?? "", - style: const TextStyle(color: Colors.white54, fontSize: 10), - ), - ], - ), - ), - ); + return _systemMessage(msg); } if (isImage) { - return Align( - alignment: Alignment.centerRight, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - width: 180, - height: 180, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon(Pixel.image, size: 64, color: Colors.grey), - ), - ), - const SizedBox(height: 4), - Text( - msg["timestamp"] ?? "", - style: const TextStyle(fontSize: 10, color: Colors.black54), - ), - ], - ), - ), - ); + return _imageMessage(msg); } return GestureDetector( onDoubleTap: () { - setState(() { - replyTo = msg["message"]; // set reply target on double-tap - }); + provider.setReplyTo(msg["message"]); }, + child: _textMessage(msg, isMe, provider), + ); + }, + ); + } - child: Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isMe - ? Theme.of(context).colorScheme.inversePrimary - : Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), + Widget _systemMessage(Map msg) => Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: Colors.grey.shade800, borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), + const SizedBox(width: 6), + Text(msg["message"] ?? "", style: const TextStyle(color: Colors.white, fontSize: 12)), + const SizedBox(width: 6), + Text(msg["timestamp"] ?? "", style: const TextStyle(color: Colors.white54, fontSize: 10)), + ], + ), + ), + ); + + Widget _imageMessage(Map msg) => Align( + alignment: Alignment.centerRight, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 180, + height: 180, + decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12)), + child: const Center(child: Icon(Pixel.image, size: 64, color: Colors.grey)), ), - constraints: const BoxConstraints(maxWidth: 280), - child: Column( - crossAxisAlignment: isMe - ? CrossAxisAlignment.start - : CrossAxisAlignment.end, - children: [ - if (msg["replyTo"] != null) - Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - msg["replyTo"], - style: TextStyle( - fontSize: 11, - color: isMe ? Colors.black87 : Colors.white60, - ), - ), - ), - Text( - msg["message"] ?? "", - style: TextStyle( - color: isMe ? Colors.black : Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - Align( - alignment: isMe - ? Alignment.centerLeft - : Alignment.centerRight, - child: Text( - msg["timestamp"] ?? "", - style: TextStyle( - color: isMe ? Colors.black54 : Colors.white54, - fontSize: 10, - ), - ), + const SizedBox(height: 4), + Text(msg["timestamp"] ?? "", style: const TextStyle(fontSize: 10, color: Colors.black54)), + ], + ), + ), + ); + + Widget _textMessage(Map msg, bool isMe, ChatProvider provider) => Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isMe ? Theme.of(context).colorScheme.inversePrimary : Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: isMe ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + if (msg["replyTo"] != null) + Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.black.withOpacity(0.1), borderRadius: BorderRadius.circular(6)), + child: Text( + msg["replyTo"], + style: TextStyle(fontSize: 11, color: isMe ? Colors.black87 : Colors.white60), ), - ], + ), + Text( + msg["message"] ?? "", + style: TextStyle(color: isMe ? Colors.black : Colors.white, fontSize: 14), ), - ), + const SizedBox(height: 4), + Align( + alignment: isMe ? Alignment.centerLeft : Alignment.centerRight, + child: Text( + msg["timestamp"] ?? "", + style: TextStyle(color: isMe ? Colors.black54 : Colors.white54, fontSize: 10), + ), + ), + ], ), - ); - }, - ); - } + ), + ); - Widget _buildUserInput() { + Widget _buildUserInput(ChatProvider provider) { final theme = Theme.of(context).colorScheme; + return SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -328,62 +237,30 @@ class _ChatPageState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ IconButton( - icon: Icon( - showAttachmentOptions ? Icons.close : Icons.add, - color: Colors.white, - ), - onPressed: () { - setState(() { - showAttachmentOptions = !showAttachmentOptions; - }); - }, + icon: Icon(provider.showAttachmentOptions ? Icons.close : Icons.add, color: Colors.white), + onPressed: provider.toggleAttachmentOptions, ), - if (replyTo != null) - Padding( - padding: const EdgeInsets.only(bottom: 4, left: 8), + if (provider.replyTo != null) + Expanded( child: Row( children: [ - Expanded( - child: Text( - "Replying to: $replyTo", - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ), + Text("Replying to: ${provider.replyTo}", style: const TextStyle(color: Colors.white70, fontSize: 12)), IconButton( - icon: const Icon( - Icons.close, - size: 16, - color: Colors.white54, - ), - onPressed: () { - setState(() { - replyTo = null; - }); - }, + icon: const Icon(Icons.close, size: 16, color: Colors.white54), + onPressed: provider.clearReplyTo, ), ], ), ), - Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(24), - ), + decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(24)), child: TextField( controller: _messageController, focusNode: myFocusNode, style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: "Type a message...", - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - ), + decoration: const InputDecoration(hintText: "Type a message...", hintStyle: TextStyle(color: Colors.white54), border: InputBorder.none), ), ), ), @@ -391,7 +268,11 @@ class _ChatPageState extends State { CircleAvatar( backgroundColor: theme.primary, child: IconButton( - onPressed: sendMessage, + onPressed: () { + provider.sendMessage(_messageController.text.trim()); + _messageController.clear(); + scrollDown(); + }, icon: const Icon(Pixel.arrowup, color: Colors.black), ), ), @@ -407,11 +288,8 @@ class _ChatPageState extends State { child: Container( margin: const EdgeInsets.only(left: 20, bottom: 8), padding: const EdgeInsets.all(12), - width: 180, // ✅ restrict width - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(10), - ), + width: 180, + decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(10)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 8f9e0a8..299b9f0 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -1,37 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/provider/chatlists_provider.dart'; +import 'package:provider/provider.dart'; import 'chat_page.dart'; class ChatlistsPage extends StatelessWidget { - ChatlistsPage({super.key}); + const ChatlistsPage({super.key}); - final String currentUserEmail = "me@example.com"; - - final List> dummyUsers = List.generate( - 10, - (index) => { - "name": "Penny Valeria", - "message": "Text text text text....", - "time": "12:35 pm", - "email": "user$index@example.com", - "uid": "uid_$index", - "unread": index != 0, - }, - ); - - final List filters = ["All", "Unread", "Favourites", "Groups"]; - final int selectedFilterIndex = 0; + final List filters = const ["All", "Unread", "Favourites", "Groups"]; @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; + final chatProvider = Provider.of(context); + final chatUsers = chatProvider.chatUsers; return Scaffold( backgroundColor: theme.surface, body: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () { - FocusScope.of(context).unfocus(); - }, + onTap: () => FocusScope.of(context).unfocus(), child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -39,14 +26,12 @@ class ChatlistsPage extends StatelessWidget { // Chats title Padding( padding: const EdgeInsets.only(left: 16, top: 16), - child: Text( - 'Chats', - style: TextStyle( - color: theme.primary, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), + child: Text('Chats', + style: TextStyle( + color: theme.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + )), ), const SizedBox(height: 10), @@ -60,15 +45,12 @@ class ChatlistsPage extends StatelessWidget { style: TextStyle(color: theme.primary), decoration: InputDecoration( hintText: "Search", - hintStyle: TextStyle( - color: theme.primary.withOpacity(0.5), - ), + hintStyle: + TextStyle(color: theme.primary.withOpacity(0.5)), filled: true, fillColor: Colors.grey.shade900, contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + horizontal: 16, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -95,32 +77,26 @@ class ChatlistsPage extends StatelessWidget { height: 40, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: filters.asMap().entries.map((entry) { - final i = entry.key; - final label = entry.value; - final isSelected = i == selectedFilterIndex; - + children: filters.map((label) { + final isSelected = label == "All"; // static for now return ElevatedButton( onPressed: () { - // Add filter logic here if needed + // Add filtering logic later }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? theme.secondary : Colors.grey.shade900, - foregroundColor: isSelected - ? Colors.black - : theme.primary, + foregroundColor: + isSelected ? Colors.black : theme.primary, padding: const EdgeInsets.symmetric(horizontal: 20), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), elevation: 0, ), - child: Text( - label, - style: const TextStyle(fontWeight: FontWeight.w600), - ), + child: Text(label, + style: const TextStyle(fontWeight: FontWeight.w600)), ); }).toList(), ), @@ -130,19 +106,20 @@ class ChatlistsPage extends StatelessWidget { // Chat List Expanded( child: ListView.builder( - itemCount: dummyUsers.length, + itemCount: chatUsers.length, itemBuilder: (context, index) { - final user = dummyUsers[index]; + final user = chatUsers[index]; return Column( children: [ InkWell( onTap: () { + chatProvider.markAsRead(user["email"]); Navigator.push( context, MaterialPageRoute( builder: (context) => ChatPage( - receiverEmail: user["email"]!, - receiverID: user["uid"]!, + receiverEmail: user["email"], + receiverID: user["uid"], ), ), ); @@ -216,7 +193,7 @@ class ChatlistsPage extends StatelessWidget { ), ), const SizedBox(height: 8), - if (user["unread"]) + if (user["unread"] == true) const CircleAvatar( radius: 6, backgroundColor: Colors.amber, @@ -243,4 +220,4 @@ class ChatlistsPage extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart new file mode 100644 index 0000000..78a69bc --- /dev/null +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:pixelarticons/pixel.dart'; + +class ChatProvider extends ChangeNotifier { + final String receiverID; + final String currentUserID = "uid_me"; + + ChatProvider({required this.receiverID}); + + final List> _messages = [ + {"senderID": "uid_me", "type": "image", "timestamp": "12:34 pm"}, + { + "senderID": "uid_me", + "message": "text text text text text text text text text text...", + "timestamp": "12:34 pm", + }, + { + "senderID": "system", + "message": "Duelled", + "timestamp": "12:34 pm", + "icon": Pixel.bullseye, + }, + { + "senderID": "uid_1", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + { + "senderID": "uid_me", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + { + "senderID": "uid_1", + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + ]; + + List> get messages => _messages; + + String? _replyTo; + String? get replyTo => _replyTo; + + bool _showAttachmentOptions = false; + bool get showAttachmentOptions => _showAttachmentOptions; + + /// Send a new message + void sendMessage(String text) { + if (text.trim().isEmpty) return; + + _messages.add({ + "senderID": currentUserID, + "message": text.trim(), + "timestamp": "now", + if (_replyTo != null) "replyTo": _replyTo, + }); + + _replyTo = null; + notifyListeners(); + } + + /// Set reply-to target + void setReplyTo(String? message) { + _replyTo = message; + notifyListeners(); + } + + /// Clear reply-to state + void clearReplyTo() { + _replyTo = null; + notifyListeners(); + } + + /// Toggle attachment options visibility + void toggleAttachmentOptions() { + _showAttachmentOptions = !_showAttachmentOptions; + notifyListeners(); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart new file mode 100644 index 0000000..8df5933 --- /dev/null +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class ChatListProvider extends ChangeNotifier { + List> _chatUsers = []; + + List> get chatUsers => _chatUsers; + + void loadDummyChats() { + _chatUsers = List.generate( + 10, + (index) => { + "name": "Penny Valeria", + "message": "Text text text text....", + "time": "12:35 pm", + "email": "user$index@example.com", + "uid": "uid_$index", + "unread": index != 0, + }, + ); + notifyListeners(); + } + + void markAsRead(String email) { + final index = _chatUsers.indexWhere((user) => user["email"] == email); + if (index != -1) { + _chatUsers[index]["unread"] = false; + notifyListeners(); + } + } +} From 68a26dcca38b7dc855a9a5d25830feec268c38bc Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 5 Aug 2025 17:26:28 +0530 Subject: [PATCH 03/53] integrated provider in more pages + fixed chat replies and expanded message input box --- leaderboard_app/lib/main.dart | 2 + leaderboard_app/lib/pages/chat_page.dart | 252 +++++++++++------- leaderboard_app/lib/pages/dashboard_page.dart | 219 ++++++++++++--- leaderboard_app/lib/pages/home_page.dart | 23 +- .../lib/provider/user_provider.dart | 18 ++ 5 files changed, 378 insertions(+), 136 deletions(-) create mode 100644 leaderboard_app/lib/provider/user_provider.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 40d5ee4..a02aad8 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -4,6 +4,7 @@ import 'package:leaderboard_app/pages/signup_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/provider/theme_provider.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; void main() { @@ -12,6 +13,7 @@ void main() { providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ChatListProvider()..loadDummyChats(),), + ChangeNotifierProvider(create: (_) => UserProvider()), ], child: const MainApp(), ), diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index 0483bb7..bef1b83 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -61,56 +61,59 @@ class _ChatViewState extends State { final theme = Theme.of(context).colorScheme; final provider = Provider.of(context); - return Scaffold( - backgroundColor: theme.surface, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: const BackButton(), - titleSpacing: 0, - title: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const ProfilePage()), - ); - }, - child: Row( - children: [ - const CircleAvatar(radius: 20, backgroundColor: Colors.grey), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Penny Valeria", style: TextStyle(color: theme.primary, fontWeight: FontWeight.bold, fontSize: 16)), - Text("Online", style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12)), - ], - ), - ], + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: const BackButton(), + titleSpacing: 0, + title: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfilePage()), + ); + }, + child: Row( + children: [ + const CircleAvatar(radius: 20, backgroundColor: Colors.grey), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Penny Valeria", style: TextStyle(color: theme.primary, fontWeight: FontWeight.bold, fontSize: 16)), + Text("Online", style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12)), + ], + ), + ], + ), ), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: theme.inversePrimary, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: theme.inversePrimary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + ), + child: const Text("Duel Now!"), ), - child: const Text("Duel Now!"), ), - ), - ], - ), - body: Column( - children: [ - Expanded(child: _buildMessageList(provider)), - if (provider.showAttachmentOptions) _buildAttachmentDropdown(), - _buildUserInput(provider), - ], + ], + ), + body: Column( + children: [ + Expanded(child: _buildMessageList(provider)), + if (provider.showAttachmentOptions) _buildAttachmentDropdown(), + _buildUserInput(provider), + ], + ), ), ); } @@ -227,60 +230,123 @@ class _ChatViewState extends State { ); Widget _buildUserInput(ChatProvider provider) { - final theme = Theme.of(context).colorScheme; + final theme = Theme.of(context).colorScheme; - return SafeArea( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: Colors.black, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - IconButton( - icon: Icon(provider.showAttachmentOptions ? Icons.close : Icons.add, color: Colors.white), - onPressed: provider.toggleAttachmentOptions, - ), - if (provider.replyTo != null) - Expanded( - child: Row( - children: [ - Text("Replying to: ${provider.replyTo}", style: const TextStyle(color: Colors.white70, fontSize: 12)), - IconButton( - icon: const Icon(Icons.close, size: 16, color: Colors.white54), - onPressed: provider.clearReplyTo, + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Reply section (above input) + if (provider.replyTo != null) + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(24), ), - ], + child: Row( + children: [ + Expanded( + child: Text( + "Replying to: ${provider.replyTo}", + style: const TextStyle(color: Colors.white70, fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: provider.clearReplyTo, + child: const Icon(Icons.close, size: 16, color: Colors.white54), + ), + ], + ), + ), ), ), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(24)), - child: TextField( - controller: _messageController, - focusNode: myFocusNode, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration(hintText: "Type a message...", hintStyle: TextStyle(color: Colors.white54), border: InputBorder.none), + + // Input + Buttons row + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Add / Close Button + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: IconButton( + icon: Icon( + provider.showAttachmentOptions ? Icons.close : Icons.add, + color: Colors.white, + ), + onPressed: provider.toggleAttachmentOptions, ), ), - ), - const SizedBox(width: 8), - CircleAvatar( - backgroundColor: theme.primary, - child: IconButton( - onPressed: () { - provider.sendMessage(_messageController.text.trim()); - _messageController.clear(); - scrollDown(); - }, - icon: const Icon(Pixel.arrowup, color: Colors.black), + + // Expanding TextField + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(24), + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + maxHeight: 150, + ), + child: Scrollbar( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _messageController, + focusNode: myFocusNode, + maxLines: null, + keyboardType: TextInputType.multiline, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: "Type a message...", + hintStyle: TextStyle(color: Colors.white54), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ), + ), + ), ), - ), - ], - ), + + const SizedBox(width: 8), + + // Send Button + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: CircleAvatar( + backgroundColor: theme.primary, + radius: 22, + child: IconButton( + onPressed: () { + provider.sendMessage(_messageController.text.trim()); + _messageController.clear(); + scrollDown(); + }, + icon: const Icon(Pixel.arrowup, color: Colors.black), + ), + ), + ), + ], + ), + ], ), - ); - } + ), + ); +} Widget _buildAttachmentDropdown() { return Align( diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 54691e9..5ff0f08 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:provider/provider.dart'; -class DashboardPage extends StatefulWidget { +class DashboardPage extends StatelessWidget { const DashboardPage({super.key}); - @override - State createState() => _DashboardPageState(); -} - -class _DashboardPageState extends State { @override Widget build(BuildContext context) { + final user = Provider.of(context); + return Scaffold( backgroundColor: Colors.black, body: SafeArea( @@ -39,21 +38,21 @@ class _DashboardPageState extends State { child: Icon(Icons.person, color: Colors.black), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "First Name Last Name", - style: TextStyle( + user.name, + style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold, ), ), Text( - "username@email.com", - style: TextStyle( + user.email, + style: const TextStyle( color: Colors.grey, fontSize: 12, ), @@ -63,7 +62,7 @@ class _DashboardPageState extends State { ), _buildHeaderButton( Icons.local_fire_department, - "4", + "${user.streak}", Colors.amber, ), const SizedBox(width: 8), @@ -204,21 +203,99 @@ class _DashboardPageState extends State { Colors.grey[900], ), columns: const [ - DataColumn(label: Text("Place", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Player", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Streak", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Solved", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Badge", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn( + label: Text( + "Place", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Player", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Streak", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Solved", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Badge", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), ], rows: List.generate( 5, (index) => DataRow( cells: [ - DataCell(Text("${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text("Player ${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Text("12", style: TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Text("1324", style: TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Icon(Icons.star, color: Colors.amber, size: 16)), + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Text( + "Player ${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "12", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "1324", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), ], ), ), @@ -237,21 +314,99 @@ class _DashboardPageState extends State { Colors.grey[900], ), columns: const [ - DataColumn(label: Text("No.", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Title", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Acc.", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Lvl", style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text("Prog", style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn( + label: Text( + "No.", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Title", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Acc.", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Lvl", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Prog", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), ], rows: List.generate( 4, (index) => DataRow( cells: [ - DataCell(Text("${index + 1}", style: const TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Text("Problem", style: TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Text("56%", style: TextStyle(color: Colors.white, fontSize: 12))), - const DataCell(Text("Easy", style: TextStyle(color: Colors.green, fontSize: 12))), - const DataCell(Icon(Icons.circle, color: Colors.green, size: 10)), + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "Problem", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "56%", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "Easy", + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ), + const DataCell( + Icon( + Icons.circle, + color: Colors.green, + size: 10, + ), + ), ], ), ), diff --git a/leaderboard_app/lib/pages/home_page.dart b/leaderboard_app/lib/pages/home_page.dart index 5b0567f..8078e4b 100644 --- a/leaderboard_app/lib/pages/home_page.dart +++ b/leaderboard_app/lib/pages/home_page.dart @@ -13,21 +13,18 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { int _selectedIndex = 0; - final List _pages = [ - const DashboardPage(), - ChatlistsPage(), - Center(child: Text("Stats Page", style: TextStyle(color: Colors.white))), - SettingsPage(), - ]; + List get _pages => [ + const DashboardPage(), // Rebuilds on each access + ChatlistsPage(), + const Center(child: Text("Stats Page", style: TextStyle(color: Colors.white))), + SettingsPage(), + ]; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, - body: IndexedStack( - index: _selectedIndex, - children: _pages, - ), + body: _pages[_selectedIndex], // <-- Not using IndexedStack anymore bottomNavigationBar: Theme( data: Theme.of(context).copyWith( canvasColor: Colors.grey[900], @@ -43,7 +40,11 @@ class _HomePageState extends State { showUnselectedLabels: false, type: BottomNavigationBarType.fixed, currentIndex: _selectedIndex, - onTap: (index) => setState(() => _selectedIndex = index), + onTap: (index) { + setState(() { + _selectedIndex = index; + }); + }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.explore, size: 28), diff --git a/leaderboard_app/lib/provider/user_provider.dart b/leaderboard_app/lib/provider/user_provider.dart new file mode 100644 index 0000000..63e727b --- /dev/null +++ b/leaderboard_app/lib/provider/user_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class UserProvider extends ChangeNotifier { + String _name = 'First Name Last Name'; + String _email = 'username@email.com'; + int _streak = 9; + + String get name => _name; + String get email => _email; + int get streak => _streak; + + void updateUser({required String name, required String email, required int streak}) { + _name = name; + _email = email; + _streak = streak; + notifyListeners(); + } +} From 97fa2f088cc1ed664263c5ceff312dd59eeae305 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 5 Aug 2025 17:30:56 +0530 Subject: [PATCH 04/53] removed redundant files --- leaderboard_app/lib/components/my_button.dart | 30 ---- .../lib/components/my_chat_bubble.dart | 130 ------------------ .../lib/components/my_textfield.dart | 42 ------ .../lib/components/my_usertile.dart | 43 ------ 4 files changed, 245 deletions(-) delete mode 100644 leaderboard_app/lib/components/my_button.dart delete mode 100644 leaderboard_app/lib/components/my_chat_bubble.dart delete mode 100644 leaderboard_app/lib/components/my_textfield.dart delete mode 100644 leaderboard_app/lib/components/my_usertile.dart diff --git a/leaderboard_app/lib/components/my_button.dart b/leaderboard_app/lib/components/my_button.dart deleted file mode 100644 index 505cd46..0000000 --- a/leaderboard_app/lib/components/my_button.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyButton extends StatelessWidget { - final void Function()? onTap; - final String text; - - const MyButton({ - super.key, - required this.text, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.all(25), - margin: const EdgeInsets.symmetric(horizontal: 25), - child: Center( - child: Text(text), - ), - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_chat_bubble.dart b/leaderboard_app/lib/components/my_chat_bubble.dart deleted file mode 100644 index 18ed268..0000000 --- a/leaderboard_app/lib/components/my_chat_bubble.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_highlight/flutter_highlight.dart'; -import 'package:flutter_highlight/themes/github.dart'; -import 'package:flutter_highlight/themes/tomorrow-night.dart'; - -class ChatBubble extends StatelessWidget { - final String message; - final bool isCurrentUser; - - const ChatBubble({ - super.key, - required this.message, - required this.isCurrentUser, - }); - - bool containsPythonCode(String text) { - return text.contains("def") || - text.contains("import") || - text.contains("print(") || - text.contains("input("); - } - - bool isComment(String line) { - return line.trim().startsWith(">"); - } - - List formatMessage(String text, bool isDarkMode, TextStyle defaultStyle) { - List spans = []; - List lines = text.split("\n"); - - for (int i = 0; i < lines.length; i++) { - String line = lines[i].trim(); - bool comment = isComment(line); - bool hasPython = containsPythonCode(line); - - if (comment) { - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, - child: Container( - width: double.infinity, - margin: EdgeInsets.only(top: 4, bottom: i == lines.length - 1 ? 0 : 4), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isDarkMode ? Colors.black54 : Colors.grey[300], - borderRadius: BorderRadius.circular(4), - border: Border.all(color: isDarkMode ? Colors.white70 : Colors.black54), - ), - child: hasPython - ? HighlightView( - line.substring(1).trim(), - language: 'python', - theme: isDarkMode ? tomorrowNightTheme : githubTheme, - textStyle: const TextStyle(fontSize: 14), - ) - : Text( - line.substring(1).trim(), - style: defaultStyle.copyWith(fontSize: 14), - ), - ), - ), - ); - } else { - spans.add( - TextSpan( - text: i == lines.length - 1 ? line : "$line\n", - style: defaultStyle, - ), - ); - } - } - return spans; - } - - @override - Widget build(BuildContext context) { - final isDarkMode = MediaQuery.platformBrightnessOf(context) == Brightness.dark; - final defaultTextStyle = Theme.of(context).textTheme.bodyMedium?.copyWith( - color: isDarkMode ? Colors.white : Colors.black, - ) ?? - TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ); - - final bubbleDecoration = BoxDecoration( - color: isCurrentUser - ? Theme.of(context).colorScheme.inversePrimary - : Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - border: Border( - left: isCurrentUser ? BorderSide.none : const BorderSide(color: Colors.black, width: 2), - right: isCurrentUser ? const BorderSide(color: Colors.black, width: 2) : BorderSide.none, - bottom: const BorderSide(color: Colors.black, width: 2), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - offset: isCurrentUser ? const Offset(2, 2) : const Offset(-2, 2), - blurRadius: 4, - ), - ], - ); - - if (!message.contains(">") && containsPythonCode(message)) { - return Container( - decoration: bubbleDecoration, - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), - child: HighlightView( - message.trim(), - language: 'python', - theme: isDarkMode ? tomorrowNightTheme : githubTheme, - textStyle: const TextStyle(fontSize: 14), - ), - ); - } - - return Container( - decoration: bubbleDecoration, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), - child: RichText( - text: TextSpan( - children: formatMessage(message.trim(), isDarkMode, defaultTextStyle), - ), - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_textfield.dart b/leaderboard_app/lib/components/my_textfield.dart deleted file mode 100644 index 4dfd2b0..0000000 --- a/leaderboard_app/lib/components/my_textfield.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyTextfield extends StatelessWidget { - final String hintText; - final bool obscureText; - final TextEditingController controller; - final FocusNode? focusNode; - - const MyTextfield({ - super.key, - required this.hintText, - required this.obscureText, - required this.controller, - this.focusNode, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 25), - child: TextField( - obscureText: obscureText, - controller: controller, - focusNode: focusNode, - maxLines: obscureText ? 1 : null, - keyboardType: obscureText ? TextInputType.text : TextInputType.multiline, - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.tertiary), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.tertiary), - ), - fillColor: Theme.of(context).colorScheme.secondary, - filled: true, - hintText: hintText, - hintStyle: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/components/my_usertile.dart b/leaderboard_app/lib/components/my_usertile.dart deleted file mode 100644 index e35137a..0000000 --- a/leaderboard_app/lib/components/my_usertile.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pixelarticons/pixel.dart'; - -class UserTile extends StatelessWidget { - final String text; - final void Function()? onTap; - - const UserTile({ - super.key, - required this.text, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: 10), - - GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - borderRadius: BorderRadius.circular(4), - ), - margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), - padding: const EdgeInsets.all(20), - child: Row( - children: [ - const Icon(Pixel.avatar), - - const SizedBox(width: 20), - - Text(text), - ], - ), - ), - ), - ], - ); - } -} \ No newline at end of file From e63f961244a82c9c44726aeec1f49011a41a2266 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 5 Aug 2025 20:12:35 +0530 Subject: [PATCH 05/53] made the code more modular --- .../lib/components/compact_calendar.dart | 148 ++++ .../lib/components/leaderboard_table.dart | 123 ++++ .../lib/components/problem_table.dart | 123 ++++ .../lib/components/resume_activity.dart | 55 ++ leaderboard_app/lib/components/week_view.dart | 65 ++ .../lib/components/weekly_stats.dart | 49 ++ leaderboard_app/lib/pages/dashboard_page.dart | 640 ++---------------- leaderboard_app/lib/pages/home_page.dart | 2 +- .../lib/provider/user_provider.dart | 2 +- 9 files changed, 631 insertions(+), 576 deletions(-) create mode 100644 leaderboard_app/lib/components/compact_calendar.dart create mode 100644 leaderboard_app/lib/components/leaderboard_table.dart create mode 100644 leaderboard_app/lib/components/problem_table.dart create mode 100644 leaderboard_app/lib/components/resume_activity.dart create mode 100644 leaderboard_app/lib/components/week_view.dart create mode 100644 leaderboard_app/lib/components/weekly_stats.dart diff --git a/leaderboard_app/lib/components/compact_calendar.dart b/leaderboard_app/lib/components/compact_calendar.dart new file mode 100644 index 0000000..3f95d5f --- /dev/null +++ b/leaderboard_app/lib/components/compact_calendar.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; + +class CompactCalendar extends StatefulWidget { + const CompactCalendar({super.key}); + + @override + State createState() => _CompactCalendarState(); +} + +class _CompactCalendarState extends State { + DateTime _selectedDate = DateTime.now(); + + final List _months = const [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + final List _weekdays = const [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + ]; + + final List _years = List.generate(50, (i) => 2000 + i); // 2000–2049 + + @override + Widget build(BuildContext context) { + int year = _selectedDate.year; + int month = _selectedDate.month; + + DateTime firstOfMonth = DateTime(year, month, 1); + int weekdayOffset = firstOfMonth.weekday == 7 ? 0 : firstOfMonth.weekday; + int daysInMonth = DateTime(year, month + 1, 0).day; + + List dayWidgets = []; + + // Blank slots before the month starts + for (int i = 1; i < weekdayOffset; i++) { + dayWidgets.add(Container()); + } + + // Day buttons + for (int i = 1; i <= daysInMonth; i++) { + dayWidgets.add( + Container( + margin: const EdgeInsets.all(2), + alignment: Alignment.center, + decoration: BoxDecoration( + color: i == _selectedDate.day ? Colors.amber : Colors.transparent, + shape: BoxShape.circle, + ), + child: Text( + "$i", + style: const TextStyle(color: Colors.white), + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Month + Year dropdowns + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + dropdownColor: Colors.grey[850], + value: month, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: List.generate(12, (index) { + return DropdownMenuItem( + value: index + 1, + child: Text(_months[index]), + ); + }), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(year, val, 1); + }); + } + }, + ), + const SizedBox(width: 16), + DropdownButton( + dropdownColor: Colors.grey[850], + value: year, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: _years.map((yr) { + return DropdownMenuItem(value: yr, child: Text(yr.toString())); + }).toList(), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(val, month, 1); + }); + } + }, + ), + ], + ), + const SizedBox(height: 10), + + // Weekday headers + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _weekdays.map((day) { + return Expanded( + child: Center( + child: Text( + day, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ); + }).toList(), + ), + + const SizedBox(height: 6), + + // Calendar grid + GridView.count( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + crossAxisCount: 7, + children: dayWidgets, + ), + ], + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/leaderboard_table.dart b/leaderboard_app/lib/components/leaderboard_table.dart new file mode 100644 index 0000000..ae58fcf --- /dev/null +++ b/leaderboard_app/lib/components/leaderboard_table.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; + +class LeaderboardTable extends StatelessWidget { + const LeaderboardTable({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + width: double.infinity, // matches parent width + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + child: DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: WidgetStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn( + label: Text( + "Place", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Player", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Streak", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Solved", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Badge", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + rows: List.generate( + 5, + (index) => DataRow( + cells: [ + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Text( + "Player ${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "12", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "1324", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/problem_table.dart b/leaderboard_app/lib/components/problem_table.dart new file mode 100644 index 0000000..b067bc8 --- /dev/null +++ b/leaderboard_app/lib/components/problem_table.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; + +class ProblemTable extends StatelessWidget { + const ProblemTable({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + width: double.infinity, // match parent width + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + child: DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: WidgetStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn( + label: Text( + "No.", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Title", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Acc.", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Lvl", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Prog", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + rows: List.generate( + 4, + (index) => DataRow( + cells: [ + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "Problem", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "56%", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Text( + "Easy", + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ), + const DataCell( + Icon( + Icons.circle, + color: Colors.green, + size: 10, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/leaderboard_app/lib/components/resume_activity.dart b/leaderboard_app/lib/components/resume_activity.dart new file mode 100644 index 0000000..1c9e4f1 --- /dev/null +++ b/leaderboard_app/lib/components/resume_activity.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class ResumeActivity extends StatelessWidget { + const ResumeActivity({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(14), + width: double.infinity, // ensures it stretches to match parent width + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "10. Regular Expression Matching", + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 12), + LinearProgressIndicator( + value: 0.4, + color: Colors.amber, + minHeight: 12, + borderRadius: BorderRadius.circular(8), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[700], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + // TODO: Implement resume logic + }, + child: const Text( + "Resume >", + style: TextStyle(color: Colors.black), + ), + ), + ), + ], + ), + ); + } +} diff --git a/leaderboard_app/lib/components/week_view.dart b/leaderboard_app/lib/components/week_view.dart new file mode 100644 index 0000000..a9f8bec --- /dev/null +++ b/leaderboard_app/lib/components/week_view.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class WeekView extends StatelessWidget { + const WeekView({super.key}); + + @override + Widget build(BuildContext context) { + const days = [ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', + ]; + + return Container( + padding: const EdgeInsets.all(14), + width: double.infinity, // Ensures it stretches to parent width + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + const Center( + child: Text( + 'June 5, 2025', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 80, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + width: 60, + decoration: BoxDecoration( + color: index == 4 + ? Colors.amber + : Colors.grey[900], + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(height: 4), + Text( + days[index], + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/weekly_stats.dart b/leaderboard_app/lib/components/weekly_stats.dart new file mode 100644 index 0000000..08925d2 --- /dev/null +++ b/leaderboard_app/lib/components/weekly_stats.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class WeeklyStats extends StatelessWidget { + const WeeklyStats({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + const Center( + child: Text( + "This Week", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + const SizedBox(height: 10), + _buildBar("Easy", 0.8, Colors.green), + _buildBar("Medium", 0.8, Colors.amber), + _buildBar("Hard", 0.8, Colors.red), + ], + ), + ); + } + + static Widget _buildBar(String label, double value, Color color) { + return Row( + children: [ + SizedBox( + width: 60, + child: Text(label, style: const TextStyle(color: Colors.white)), + ), + Expanded( + child: LinearProgressIndicator( + value: value, + color: color, + backgroundColor: Colors.white24, + ), + ), + ], + ); + } +} diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 5ff0f08..4e47dce 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -1,4 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/components/compact_calendar.dart'; +import 'package:leaderboard_app/components/leaderboard_table.dart'; +import 'package:leaderboard_app/components/problem_table.dart'; +import 'package:leaderboard_app/components/resume_activity.dart'; +import 'package:leaderboard_app/components/week_view.dart'; +import 'package:leaderboard_app/components/weekly_stats.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; @@ -7,8 +13,6 @@ class DashboardPage extends StatelessWidget { @override Widget build(BuildContext context) { - final user = Provider.of(context); - return Scaffold( backgroundColor: Colors.black, body: SafeArea( @@ -23,423 +27,86 @@ class DashboardPage extends StatelessWidget { constraints: BoxConstraints(maxWidth: maxWidth), child: Column( children: [ - // Header - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - color: Colors.grey[900], - child: Row( - children: [ - const CircleAvatar( - radius: 20, - backgroundColor: Colors.white, - child: Icon(Icons.person, color: Colors.black), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.name, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - Text( - user.email, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - ], - ), - ), - _buildHeaderButton( - Icons.local_fire_department, - "${user.streak}", - Colors.amber, - ), - const SizedBox(width: 8), - _buildHeaderButton( - Icons.person_add, - "Invite", - Colors.amber, - ), - ], - ), - ), - - // Scrollable Content - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + // Header (now reactive using Consumer) + Consumer( + builder: (context, user, _) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + color: Colors.grey[900], + child: Row( children: [ - _buildCard( - child: Column( - children: [ - const Center( - child: Text( - 'June 5, 2025', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - height: 80, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 7, - itemBuilder: (context, index) { - const days = [ - 'Sun', - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat', - ]; - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 6, - ), - width: 60, - decoration: BoxDecoration( - color: index == 4 - ? Colors.amber - : Colors.grey[900], - borderRadius: BorderRadius.circular( - 10, - ), - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons.check_circle, - color: Colors.white, - ), - const SizedBox(height: 4), - Text( - days[index], - style: const TextStyle( - color: Colors.white, - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), + const CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + child: Icon(Icons.person, color: Colors.black), ), - - const SizedBox(height: 10), - - _buildCard( + const SizedBox(width: 12), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "10. Regular Expression Matching", - style: TextStyle( + Text( + user.name, + style: const TextStyle( color: Colors.white, fontSize: 14, + fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 12), - LinearProgressIndicator( - value: 0.4, - color: Colors.amber, - minHeight: 12, - borderRadius: BorderRadius.circular(8), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[700], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 8, - ), - ), - ), - onPressed: () {}, - child: const Text( - "Resume >", - style: TextStyle(color: Colors.black), - ), + Text( + user.email, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, ), ), ], ), ), - - const SizedBox(height: 10), - - _buildCard( - child: DataTable( - columnSpacing: 10, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: WidgetStateProperty.all( - Colors.grey[900], - ), - columns: const [ - DataColumn( - label: Text( - "Place", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Player", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Streak", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Solved", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Badge", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - ], - rows: List.generate( - 5, - (index) => DataRow( - cells: [ - DataCell( - Text( - "${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Text( - "Player ${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "12", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "1324", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Icon( - Icons.star, - color: Colors.amber, - size: 16, - ), - ), - ], - ), - ), - ), + _buildHeaderButton( + Icons.local_fire_department, + "${user.streak}", + Colors.amber, ), - - const SizedBox(height: 10), - - _buildCard( - child: DataTable( - columnSpacing: 10, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: WidgetStateProperty.all( - Colors.grey[900], - ), - columns: const [ - DataColumn( - label: Text( - "No.", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Title", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Acc.", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Lvl", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Prog", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - ], - rows: List.generate( - 4, - (index) => DataRow( - cells: [ - DataCell( - Text( - "${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "Problem", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "56%", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "Easy", - style: TextStyle( - color: Colors.green, - fontSize: 12, - ), - ), - ), - const DataCell( - Icon( - Icons.circle, - color: Colors.green, - size: 10, - ), - ), - ], - ), - ), - ), + const SizedBox(width: 8), + _buildHeaderButton( + Icons.person_add, + "Invite", + Colors.amber, ), + ], + ), + ), + ), + // Scrollable Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WeekView(), const SizedBox(height: 10), - - _buildCard( - child: Column( - children: [ - const Center( - child: Text( - "This Week", - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - const SizedBox(height: 10), - _buildBar("Easy", 0.8, Colors.green), - const SizedBox(height: 6), - _buildBar("Medium", 0.4, Colors.amber), - const SizedBox(height: 6), - _buildBar("Hard", 0.2, Colors.red), - ], + ResumeActivity(), + const SizedBox(height: 10), + LeaderboardTable(), + const SizedBox(height: 10), + ProblemTable(), + const SizedBox(height: 10), + WeeklyStats(), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(14), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), ), + child: const CompactCalendar(), ), - - const SizedBox(height: 10), - - _buildCard(child: _CompactCalendar()), ], ), ), @@ -470,179 +137,4 @@ class DashboardPage extends StatelessWidget { ), ); } - - static Widget _buildBar(String label, double value, Color color) { - return Row( - children: [ - SizedBox( - width: 60, - child: Text(label, style: const TextStyle(color: Colors.white)), - ), - Expanded( - child: LinearProgressIndicator( - value: value, - color: color, - backgroundColor: Colors.white24, - ), - ), - ], - ); - } - - static Widget _buildCard({required Widget child}) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(8), - ), - child: child, - ); - } -} - -class _CompactCalendar extends StatefulWidget { - @override - State<_CompactCalendar> createState() => _CompactCalendarState(); -} - -class _CompactCalendarState extends State<_CompactCalendar> { - DateTime _selectedDate = DateTime.now(); - - List _months = const [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - - List _weekdays = const [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - ]; - - List _years = List.generate(50, (i) => 2000 + i); // 2000–2049 - - @override - Widget build(BuildContext context) { - int year = _selectedDate.year; - int month = _selectedDate.month; - - DateTime firstOfMonth = DateTime(year, month, 1); - int weekdayOffset = firstOfMonth.weekday == 7 - ? 0 - : firstOfMonth.weekday; // Monday = 1 - int daysInMonth = DateTime(year, month + 1, 0).day; - - List dayWidgets = []; - - // Blank slots before month start - for (int i = 1; i < weekdayOffset; i++) { - dayWidgets.add(Container()); - } - - // Days of month - for (int i = 1; i <= daysInMonth; i++) { - dayWidgets.add( - Container( - margin: const EdgeInsets.all(2), - alignment: Alignment.center, - decoration: BoxDecoration( - color: i == _selectedDate.day ? Colors.amber : Colors.transparent, - shape: BoxShape.circle, - ), - child: Text("$i", style: const TextStyle(color: Colors.white)), - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Month + Year dropdown row - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DropdownButton( - dropdownColor: Colors.grey[850], - value: month, - style: const TextStyle(color: Colors.white), - underline: Container(), - items: List.generate(12, (index) { - return DropdownMenuItem( - value: index + 1, - child: Text(_months[index]), - ); - }), - onChanged: (val) { - if (val != null) { - setState(() { - _selectedDate = DateTime(year, val, 1); - }); - } - }, - ), - const SizedBox(width: 16), - DropdownButton( - dropdownColor: Colors.grey[850], - value: year, - style: const TextStyle(color: Colors.white), - underline: Container(), - items: _years.map((yr) { - return DropdownMenuItem(value: yr, child: Text(yr.toString())); - }).toList(), - onChanged: (val) { - if (val != null) { - setState(() { - _selectedDate = DateTime(val, month, 1); - }); - } - }, - ), - ], - ), - - const SizedBox(height: 10), - - // Weekday headers - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: _weekdays.map((day) { - return Expanded( - child: Center( - child: Text( - day, - style: const TextStyle(color: Colors.grey, fontSize: 12), - ), - ), - ); - }).toList(), - ), - - const SizedBox(height: 6), - - // Calendar grid - GridView.count( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - crossAxisCount: 7, - children: dayWidgets, - ), - ], - ); - } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/home_page.dart b/leaderboard_app/lib/pages/home_page.dart index 8078e4b..6e7a0d1 100644 --- a/leaderboard_app/lib/pages/home_page.dart +++ b/leaderboard_app/lib/pages/home_page.dart @@ -24,7 +24,7 @@ class _HomePageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, - body: _pages[_selectedIndex], // <-- Not using IndexedStack anymore + body: _pages[_selectedIndex], bottomNavigationBar: Theme( data: Theme.of(context).copyWith( canvasColor: Colors.grey[900], diff --git a/leaderboard_app/lib/provider/user_provider.dart b/leaderboard_app/lib/provider/user_provider.dart index 63e727b..ec0b06b 100644 --- a/leaderboard_app/lib/provider/user_provider.dart +++ b/leaderboard_app/lib/provider/user_provider.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class UserProvider extends ChangeNotifier { String _name = 'First Name Last Name'; String _email = 'username@email.com'; - int _streak = 9; + int _streak = 8; String get name => _name; String get email => _email; From 109d518e1466af4db9b91697ddda92e3dbdea128 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 11:04:13 +0530 Subject: [PATCH 06/53] =?UTF-8?q?[=E2=9C=85]=20removed=20everything=20but?= =?UTF-8?q?=20text=20based=20input=20[=E2=9C=85]=20removed=20duel=20[?= =?UTF-8?q?=E2=9C=85]=20removed=20google=20auth=20[sign=20in=20with=20gmai?= =?UTF-8?q?l]=20[=E2=9C=85]=20replaced=2010.=20reg....=20with=20daily=20qu?= =?UTF-8?q?estion=20and=20redirect=20[=E2=9C=85]=20removed=20appearance=20?= =?UTF-8?q?customization=20from=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...sume_activity.dart => daily_activity.dart} | 24 +- leaderboard_app/lib/main.dart | 4 +- leaderboard_app/lib/pages/chat_page.dart | 397 +++++++++--------- leaderboard_app/lib/pages/dashboard_page.dart | 4 +- leaderboard_app/lib/pages/profile_page.dart | 314 +++++++------- leaderboard_app/lib/pages/settings_page.dart | 38 -- leaderboard_app/lib/pages/signup_page.dart | 42 -- 7 files changed, 378 insertions(+), 445 deletions(-) rename leaderboard_app/lib/components/{resume_activity.dart => daily_activity.dart} (66%) diff --git a/leaderboard_app/lib/components/resume_activity.dart b/leaderboard_app/lib/components/daily_activity.dart similarity index 66% rename from leaderboard_app/lib/components/resume_activity.dart rename to leaderboard_app/lib/components/daily_activity.dart index 1c9e4f1..a3a7621 100644 --- a/leaderboard_app/lib/components/resume_activity.dart +++ b/leaderboard_app/lib/components/daily_activity.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -class ResumeActivity extends StatelessWidget { - const ResumeActivity({super.key}); +class LeetCodeDailyCard extends StatelessWidget { + const LeetCodeDailyCard({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), - width: double.infinity, // ensures it stretches to match parent width + width: double.infinity, decoration: BoxDecoration( color: Colors.grey[850], borderRadius: BorderRadius.circular(8), @@ -16,20 +16,14 @@ class ResumeActivity extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "10. Regular Expression Matching", + "'Two Sum' (Easy)\n" + "This will later be replaced with the actual user's daily challenge.", style: TextStyle( - color: Colors.white, + color: Colors.white70, fontSize: 14, ), ), const SizedBox(height: 12), - LinearProgressIndicator( - value: 0.4, - color: Colors.amber, - minHeight: 12, - borderRadius: BorderRadius.circular(8), - ), - const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( @@ -40,10 +34,10 @@ class ResumeActivity extends StatelessWidget { ), ), onPressed: () { - // TODO: Implement resume logic + // TODO: Replace with redirect to LeetCode daily question }, child: const Text( - "Resume >", + "Go to Question >", style: TextStyle(color: Colors.black), ), ), @@ -52,4 +46,4 @@ class ResumeActivity extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index a02aad8..04b9551 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -12,7 +12,9 @@ void main() { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider(create: (_) => ChatListProvider()..loadDummyChats(),), + ChangeNotifierProvider( + create: (_) => ChatListProvider()..loadDummyChats(), + ), ChangeNotifierProvider(create: (_) => UserProvider()), ], child: const MainApp(), diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index bef1b83..04cfce1 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -84,33 +84,30 @@ class _ChatViewState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Penny Valeria", style: TextStyle(color: theme.primary, fontWeight: FontWeight.bold, fontSize: 16)), - Text("Online", style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12)), + Text( + "Penny Valeria", + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + "Online", + style: TextStyle( + color: theme.primary.withOpacity(0.6), + fontSize: 12, + ), + ), ], ), ], ), ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: theme.inversePrimary, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ), - child: const Text("Duel Now!"), - ), - ), - ], ), body: Column( children: [ Expanded(child: _buildMessageList(provider)), - if (provider.showAttachmentOptions) _buildAttachmentDropdown(), _buildUserInput(provider), ], ), @@ -147,100 +144,137 @@ class _ChatViewState extends State { } Widget _systemMessage(Map msg) => Center( - child: Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration(color: Colors.grey.shade800, borderRadius: BorderRadius.circular(8)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), - const SizedBox(width: 6), - Text(msg["message"] ?? "", style: const TextStyle(color: Colors.white, fontSize: 12)), - const SizedBox(width: 6), - Text(msg["timestamp"] ?? "", style: const TextStyle(color: Colors.white54, fontSize: 10)), - ], + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), + const SizedBox(width: 6), + Text( + msg["message"] ?? "", + style: const TextStyle(color: Colors.white, fontSize: 12), ), - ), - ); + const SizedBox(width: 6), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + ], + ), + ), + ); Widget _imageMessage(Map msg) => Align( - alignment: Alignment.centerRight, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - borderRadius: BorderRadius.circular(12), + alignment: Alignment.centerRight, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon(Pixel.image, size: 64, color: Colors.grey), + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - width: 180, - height: 180, - decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12)), - child: const Center(child: Icon(Pixel.image, size: 64, color: Colors.grey)), - ), - const SizedBox(height: 4), - Text(msg["timestamp"] ?? "", style: const TextStyle(fontSize: 10, color: Colors.black54)), - ], + const SizedBox(height: 4), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(fontSize: 10, color: Colors.black54), ), - ), - ); + ], + ), + ), + ); - Widget _textMessage(Map msg, bool isMe, ChatProvider provider) => Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isMe ? Theme.of(context).colorScheme.inversePrimary : Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - constraints: const BoxConstraints(maxWidth: 280), - child: Column( - crossAxisAlignment: isMe ? CrossAxisAlignment.start : CrossAxisAlignment.end, - children: [ - if (msg["replyTo"] != null) - Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.black.withOpacity(0.1), borderRadius: BorderRadius.circular(6)), - child: Text( - msg["replyTo"], - style: TextStyle(fontSize: 11, color: isMe ? Colors.black87 : Colors.white60), - ), - ), - Text( - msg["message"] ?? "", - style: TextStyle(color: isMe ? Colors.black : Colors.white, fontSize: 14), + Widget _textMessage( + Map msg, + bool isMe, + ChatProvider provider, + ) => Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isMe + ? Theme.of(context).colorScheme.inversePrimary + : Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: isMe + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [ + if (msg["replyTo"] != null) + Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), ), - const SizedBox(height: 4), - Align( - alignment: isMe ? Alignment.centerLeft : Alignment.centerRight, - child: Text( - msg["timestamp"] ?? "", - style: TextStyle(color: isMe ? Colors.black54 : Colors.white54, fontSize: 10), + child: Text( + msg["replyTo"], + style: TextStyle( + fontSize: 11, + color: isMe ? Colors.black87 : Colors.white60, ), ), - ], + ), + Text( + msg["message"] ?? "", + style: TextStyle( + color: isMe ? Colors.black : Colors.white, + fontSize: 14, + ), ), - ), - ); + const SizedBox(height: 4), + Align( + alignment: isMe ? Alignment.centerLeft : Alignment.centerRight, + child: Text( + msg["timestamp"] ?? "", + style: TextStyle( + color: isMe ? Colors.black54 : Colors.white54, + fontSize: 10, + ), + ), + ), + ], + ), + ), + ); Widget _buildUserInput(ChatProvider provider) { - final theme = Theme.of(context).colorScheme; + final theme = Theme.of(context).colorScheme; - return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 6, 12, 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Reply section (above input) - if (provider.replyTo != null) + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Reply section (above input) + if (provider.replyTo != null) Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), @@ -256,13 +290,20 @@ class _ChatViewState extends State { Expanded( child: Text( "Replying to: ${provider.replyTo}", - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), overflow: TextOverflow.ellipsis, ), ), GestureDetector( onTap: provider.clearReplyTo, - child: const Icon(Icons.close, size: 16, color: Colors.white54), + child: const Icon( + Icons.close, + size: 16, + color: Colors.white54, + ), ), ], ), @@ -270,122 +311,72 @@ class _ChatViewState extends State { ), ), - // Input + Buttons row - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Add / Close Button - Padding( - padding: const EdgeInsets.only(bottom: 6), - child: IconButton( - icon: Icon( - provider.showAttachmentOptions ? Icons.close : Icons.add, - color: Colors.white, - ), - onPressed: provider.toggleAttachmentOptions, - ), - ), - - // Expanding TextField - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(24), - ), - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: 40, - maxHeight: 150, + // Input + Buttons row + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Expanding TextField + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, ), - child: Scrollbar( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _messageController, - focusNode: myFocusNode, - maxLines: null, - keyboardType: TextInputType.multiline, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: "Type a message...", - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(24), + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + maxHeight: 150, + ), + child: Scrollbar( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _messageController, + focusNode: myFocusNode, + maxLines: null, + keyboardType: TextInputType.multiline, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: "Type a message...", + hintStyle: TextStyle(color: Colors.white54), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), ), ), ), ), ), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), - // Send Button - Padding( - padding: const EdgeInsets.only(bottom: 6), - child: CircleAvatar( - backgroundColor: theme.primary, - radius: 22, - child: IconButton( - onPressed: () { - provider.sendMessage(_messageController.text.trim()); - _messageController.clear(); - scrollDown(); - }, - icon: const Icon(Pixel.arrowup, color: Colors.black), + // Send Button + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: CircleAvatar( + backgroundColor: theme.primary, + radius: 22, + child: IconButton( + onPressed: () { + provider.sendMessage(_messageController.text.trim()); + _messageController.clear(); + scrollDown(); + }, + icon: const Icon(Pixel.arrowup, color: Colors.black), + ), ), ), - ), - ], - ), - ], - ), - ), - ); -} - - Widget _buildAttachmentDropdown() { - return Align( - alignment: Alignment.bottomLeft, - child: Container( - margin: const EdgeInsets.only(left: 20, bottom: 8), - padding: const EdgeInsets.all(12), - width: 180, - decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(10)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: const [ - AttachmentOption(icon: Icons.mic, label: "Audio"), - SizedBox(height: 12), - AttachmentOption(icon: Icons.photo, label: "Photos & Videos"), - SizedBox(height: 12), - AttachmentOption(icon: Icons.attach_file, label: "File"), + ], + ), ], ), ), ); } -} - -class AttachmentOption extends StatelessWidget { - final IconData icon; - final String label; - - const AttachmentOption({super.key, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Icon(icon, color: Colors.amber, size: 22), - const SizedBox(width: 10), - Text(label, style: const TextStyle(color: Colors.white, fontSize: 14)), - ], - ); - } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 4e47dce..f9f2573 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/components/compact_calendar.dart'; import 'package:leaderboard_app/components/leaderboard_table.dart'; import 'package:leaderboard_app/components/problem_table.dart'; -import 'package:leaderboard_app/components/resume_activity.dart'; +import 'package:leaderboard_app/components/daily_activity.dart'; import 'package:leaderboard_app/components/week_view.dart'; import 'package:leaderboard_app/components/weekly_stats.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; @@ -90,7 +90,7 @@ class DashboardPage extends StatelessWidget { children: [ WeekView(), const SizedBox(height: 10), - ResumeActivity(), + LeetCodeDailyCard(), const SizedBox(height: 10), LeaderboardTable(), const SizedBox(height: 10), diff --git a/leaderboard_app/lib/pages/profile_page.dart b/leaderboard_app/lib/pages/profile_page.dart index 1896f41..55cd77b 100644 --- a/leaderboard_app/lib/pages/profile_page.dart +++ b/leaderboard_app/lib/pages/profile_page.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/pages/files_page.dart'; -import 'package:leaderboard_app/pages/media_page.dart'; -import 'package:pie_chart/pie_chart.dart'; class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); + final List> members = const [ + {"name": "Alice", "avatar": "A"}, + {"name": "Bob", "avatar": "B"}, + {"name": "Charlie", "avatar": "C"}, + {"name": "Diana", "avatar": "D"}, + ]; + @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; @@ -16,26 +20,6 @@ class ProfilePage extends StatelessWidget { backgroundColor: Colors.transparent, leading: const BackButton(), elevation: 0, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: theme.inversePrimary, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: const Text("Duel Now!"), - ), - ), - ], ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -43,7 +27,7 @@ class ProfilePage extends StatelessWidget { children: [ const SizedBox(height: 10), - // Avatar + // Avatar & Name const CircleAvatar(radius: 50, backgroundColor: Colors.grey), const SizedBox(height: 8), const Text( @@ -53,64 +37,13 @@ class ProfilePage extends StatelessWidget { const SizedBox(height: 16), - // Info Tiles - _infoTile(Icons.people, "Friends for:", "3 Months"), - _infoTile( - Icons.star, - "Rank:", - "Gold", - trailingIcon: Icons.star, - trailingColor: Colors.amber, - ), - _infoTile(Icons.send, "Currently on:", "Title 1"), - - const SizedBox(height: 16), - - // Duel Stats - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(16), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Number of Duels:", - style: TextStyle(color: Colors.white, fontSize: 14), - ), - const SizedBox(height: 8), - PieChart( - dataMap: const {"Penny": 6, "You": 10}, - animationDuration: const Duration(milliseconds: 800), - chartLegendSpacing: 16, - chartRadius: 120, - colorList: [Colors.grey.shade800, Colors.amber], - chartType: ChartType.ring, - ringStrokeWidth: 28, - legendOptions: const LegendOptions( - showLegends: true, - legendTextStyle: TextStyle( - color: Colors.white, - fontSize: 12, - ), - legendPosition: LegendPosition.left, - ), - chartValuesOptions: const ChartValuesOptions( - showChartValues: false, - ), - ), - ], - ), - ), + // Members Card + _membersCard(), const SizedBox(height: 16), - // Bottom Tiles - _bottomTile(context, "Media", "192"), - _bottomTile(context, "Files", "193"), + // Leaderboard Card with DataTable + _leaderboardCard(), const SizedBox(height: 20), ], @@ -119,91 +52,184 @@ class ProfilePage extends StatelessWidget { ); } - Widget _infoTile( - IconData icon, - String label, - String value, { - IconData? trailingIcon, - Color? trailingColor, - }) { + Widget _membersCard() { return Container( - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + width: double.infinity, + padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade900, borderRadius: BorderRadius.circular(12), ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, color: Colors.white, size: 20), - const SizedBox(width: 10), - Expanded( - child: Text( - label, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), + const Text( + "Members", + style: TextStyle(fontSize: 18), ), - if (trailingIcon != null) - Icon(trailingIcon, color: trailingColor ?? Colors.white, size: 18), - if (trailingIcon == null) - Text( - value, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), + const SizedBox(height: 12), + ...members.map((member) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: Colors.grey.shade700, + child: Text( + member["avatar"] ?? "?", + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + Text( + member["name"] ?? "Unknown", + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + ], + ), + ); + }).toList(), ], ), ); } - Widget _bottomTile(BuildContext context, String title, String count) { + Widget _leaderboardCard() { return Container( - margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + width: double.infinity, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), color: Colors.grey.shade900, - ), - child: Material( - color: - Colors.transparent, // So the original container color shows through borderRadius: BorderRadius.circular(12), - child: InkWell( - borderRadius: BorderRadius.circular(12), // Same radius to clip ripple - onTap: () { - if (title == "Media") { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const MediaPage()), - ); - } else if (title == "Files") { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const FilesPage()), - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - child: Row( - children: [ - Expanded( - child: Text( - title, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, // Align to left + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Leaderboard", + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, // Stretch table to max width + child: const LeaderboardTable(), + ), + ], + ), + ); + } +} + +class LeaderboardTable extends StatelessWidget { + const LeaderboardTable({super.key}); + + @override + Widget build(BuildContext context) { + return DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: MaterialStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn( + label: Text( + "Place", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Player", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Streak", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Solved", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Badge", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + rows: List.generate( + 5, + (index) => DataRow( + cells: [ + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, ), - Text( - count, - style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + DataCell( + Text( + "Player ${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, ), - const SizedBox(width: 6), - const Icon( - Icons.arrow_forward_ios_rounded, - size: 16, - color: Colors.white54, + ), + ), + const DataCell( + Text( + "12", + style: TextStyle( + color: Colors.white, + fontSize: 12, ), - ], + ), ), - ), + const DataCell( + Text( + "1324", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const DataCell( + Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), + ], ), ), ); diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index ca4254e..d114911 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -181,44 +181,6 @@ class SettingsPage extends StatelessWidget { ), ), - const SizedBox(height: 25), - - // ====== Container 3 ====== - const Text( - 'Appearance', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(10), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Choose a preferred theme for the website', - style: TextStyle(color: Colors.white54, fontSize: 13), - ), - const SizedBox(height: 12), - Wrap( - spacing: 16, - children: [ - _buildThemeDot(Colors.pink), - _buildThemeDot(Colors.red), - _buildThemeDot(Colors.green), - _buildThemeDot(Colors.teal), - _buildThemeDot(Colors.yellow), - _buildThemeDot(Colors.blueAccent), - _buildThemeDot(Colors.white), - ], - ), - ], - ), - ), - const SizedBox(height: 100), // Extra space above the bottom ], ), diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index 0202648..de3b855 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -44,48 +44,6 @@ class SignUpPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 5), - SizedBox( - width: double.infinity, - height: 45, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF141316), - foregroundColor: Colors.white, - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(vertical: 14), - ), - icon: Image.asset( - 'assets/icons/google.png', - height: 15, - ), - label: const Text('Sign in with Google'), - onPressed: () { - // Handle Google sign-in logic - }, - ), - ), - const SizedBox(height: 10), - Row( - children: [ - const Expanded( - child: Divider(color: Colors.white24, thickness: 1), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Text( - 'Or', - style: TextStyle(color: Colors.white70, fontSize: 10), - ), - ), - const Expanded( - child: Divider(color: Colors.white24, thickness: 1), - ), - ], - ), - const SizedBox(height: 10), TextField( style: const TextStyle(color: Colors.white), decoration: InputDecoration( From 243989c68d076571f6fadeaabfcce0e6967af5cf Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 11:42:38 +0530 Subject: [PATCH 07/53] added support for groups and messages from different users --- leaderboard_app/lib/main.dart | 2 +- leaderboard_app/lib/pages/chat_page.dart | 248 ++++++++++-------- leaderboard_app/lib/pages/chatlists_page.dart | 146 ++++++----- .../lib/provider/chat_provider.dart | 157 +++++++---- .../lib/provider/chatlists_provider.dart | 49 ++-- 5 files changed, 360 insertions(+), 242 deletions(-) diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 04b9551..5885509 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -13,7 +13,7 @@ void main() { providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider( - create: (_) => ChatListProvider()..loadDummyChats(), + create: (_) => ChatListProvider()..loadDummyGroups(), ), ChangeNotifierProvider(create: (_) => UserProvider()), ], diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index 04cfce1..8be6781 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -5,26 +5,32 @@ import 'package:provider/provider.dart'; import 'profile_page.dart'; class ChatPage extends StatelessWidget { - final String receiverEmail; - final String receiverID; + final String groupId; + final String groupName; - const ChatPage({ - super.key, - required this.receiverEmail, - required this.receiverID, - }); + const ChatPage({super.key, required this.groupId, required this.groupName}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => ChatProvider(receiverID: receiverID), - child: const ChatView(), + create: (_) { + final provider = ChatProvider(); + // Ensure group data is initialized by calling methods + // that internally call _initGroupIfNeeded(...) + provider.getReplyTo(groupId); + provider.getAttachmentOptionsVisibility(groupId); + return provider; + }, + child: ChatView(groupId: groupId, groupName: groupName), ); } } class ChatView extends StatefulWidget { - const ChatView({super.key}); + final String groupId; + final String groupName; + + const ChatView({super.key, required this.groupId, required this.groupName}); @override State createState() => _ChatViewState(); @@ -43,6 +49,7 @@ class _ChatViewState extends State { Future.delayed(const Duration(milliseconds: 300), scrollDown); } }); + // initial scroll after build Future.delayed(const Duration(milliseconds: 500), scrollDown); } @@ -85,7 +92,7 @@ class _ChatViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Penny Valeria", + widget.groupName, style: TextStyle( color: theme.primary, fontWeight: FontWeight.bold, @@ -116,11 +123,22 @@ class _ChatViewState extends State { } Widget _buildMessageList(ChatProvider provider) { + final messages = provider.getMessages(widget.groupId); + + if (messages.isEmpty) { + return Center( + child: Text( + "No messages yet", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ); + } + return ListView.builder( controller: _scrollController, - itemCount: provider.messages.length, + itemCount: messages.length, itemBuilder: (context, index) { - final msg = provider.messages[index]; + final msg = messages[index]; final isMe = msg["senderID"] == provider.currentUserID; final isSystem = msg["senderID"] == "system"; final isImage = msg["type"] == "image"; @@ -130,14 +148,14 @@ class _ChatViewState extends State { } if (isImage) { - return _imageMessage(msg); + return _imageMessage(msg, isMe); } return GestureDetector( onDoubleTap: () { - provider.setReplyTo(msg["message"]); + provider.setReplyTo(widget.groupId, msg["message"]); }, - child: _textMessage(msg, isMe, provider), + child: _textMessage(msg, isMe), ); }, ); @@ -170,8 +188,8 @@ class _ChatViewState extends State { ), ); - Widget _imageMessage(Map msg) => Align( - alignment: Alignment.centerRight, + Widget _imageMessage(Map msg, bool isMe) => Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.all(12), @@ -180,7 +198,9 @@ class _ChatViewState extends State { borderRadius: BorderRadius.circular(12), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: isMe + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ Container( width: 180, @@ -203,68 +223,82 @@ class _ChatViewState extends State { ), ); - Widget _textMessage( - Map msg, - bool isMe, - ChatProvider provider, - ) => Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isMe - ? Theme.of(context).colorScheme.inversePrimary - : Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - constraints: const BoxConstraints(maxWidth: 280), - child: Column( - crossAxisAlignment: isMe - ? CrossAxisAlignment.start - : CrossAxisAlignment.end, - children: [ - if (msg["replyTo"] != null) - Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), + Widget _textMessage(Map msg, bool isMe) { + return Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isMe + ? Theme.of(context).colorScheme.inversePrimary + : Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, // Always align left inside bubble + children: [ + // Name (always left-aligned) + Text( + msg["senderName"] ?? "", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: msg["senderColor"] ?? Colors.white, ), - child: Text( - msg["replyTo"], - style: TextStyle( - fontSize: 11, - color: isMe ? Colors.black87 : Colors.white60, + ), + const SizedBox(height: 4), + + // Reply preview + if (msg["replyTo"] != null) + Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + msg["replyTo"], + style: TextStyle( + fontSize: 11, + color: isMe ? Colors.black87 : Colors.white60, + ), ), ), - ), - Text( - msg["message"] ?? "", - style: TextStyle( - color: isMe ? Colors.black : Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - Align( - alignment: isMe ? Alignment.centerLeft : Alignment.centerRight, - child: Text( - msg["timestamp"] ?? "", + + // Message text + Text( + msg["message"] ?? "", style: TextStyle( - color: isMe ? Colors.black54 : Colors.white54, - fontSize: 10, + color: isMe ? Colors.black : Colors.white, + fontSize: 14, ), ), - ), - ], + const SizedBox(height: 4), + + // Timestamp + Align( + alignment: Alignment.bottomRight, + child: Text( + msg["timestamp"] ?? "", + style: TextStyle( + color: isMe ? Colors.black54 : Colors.white54, + fontSize: 10, + ), + ), + ), + ], + ), ), - ), - ); + ); + } Widget _buildUserInput(ChatProvider provider) { final theme = Theme.of(context).colorScheme; + final replyTo = provider.getReplyTo(widget.groupId); return SafeArea( child: Padding( @@ -273,49 +307,47 @@ class _ChatViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Reply section (above input) - if (provider.replyTo != null) - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(24), - ), - child: Row( - children: [ - Expanded( - child: Text( - "Replying to: ${provider.replyTo}", - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, + // Reply section + if (replyTo != null) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 340), + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + Expanded( + child: Text( + "Replying to: $replyTo", + style: const TextStyle( + color: Colors.white70, + fontSize: 12, ), + overflow: TextOverflow.ellipsis, ), - GestureDetector( - onTap: provider.clearReplyTo, - child: const Icon( - Icons.close, - size: 16, - color: Colors.white54, - ), + ), + GestureDetector( + onTap: () => provider.clearReplyTo(widget.groupId), + child: const Icon( + Icons.close, + size: 16, + color: Colors.white54, ), - ], - ), + ), + ], ), ), ), - // Input + Buttons row + // Input row Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - // Expanding TextField + // Text field Expanded( child: Container( padding: const EdgeInsets.symmetric( @@ -353,10 +385,9 @@ class _ChatViewState extends State { ), ), ), - const SizedBox(width: 8), - // Send Button + // Send button Padding( padding: const EdgeInsets.only(bottom: 6), child: CircleAvatar( @@ -364,7 +395,10 @@ class _ChatViewState extends State { radius: 22, child: IconButton( onPressed: () { - provider.sendMessage(_messageController.text.trim()); + provider.sendMessage( + widget.groupId, + _messageController.text.trim(), + ); _messageController.clear(); scrollDown(); }, diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 299b9f0..d05d452 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -6,13 +6,13 @@ import 'chat_page.dart'; class ChatlistsPage extends StatelessWidget { const ChatlistsPage({super.key}); - final List filters = const ["All", "Unread", "Favourites", "Groups"]; + final List filters = const ["All", "Unread", "Favourites"]; @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); - final chatUsers = chatProvider.chatUsers; + final chatGroups = chatProvider.chatGroups; return Scaffold( backgroundColor: theme.surface, @@ -23,19 +23,21 @@ class ChatlistsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Chats title + // Title Padding( padding: const EdgeInsets.only(left: 16, top: 16), - child: Text('Chats', - style: TextStyle( - color: theme.primary, - fontSize: 24, - fontWeight: FontWeight.bold, - )), + child: Text( + 'Group Chats', + style: TextStyle( + color: theme.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), ), const SizedBox(height: 10), - // Search + Add Button + // Search + Add Row( children: [ Expanded( @@ -45,12 +47,15 @@ class ChatlistsPage extends StatelessWidget { style: TextStyle(color: theme.primary), decoration: InputDecoration( hintText: "Search", - hintStyle: - TextStyle(color: theme.primary.withOpacity(0.5)), + hintStyle: TextStyle( + color: theme.primary.withOpacity(0.5), + ), filled: true, fillColor: Colors.grey.shade900, contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), + horizontal: 16, + vertical: 12, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -72,54 +77,69 @@ class ChatlistsPage extends StatelessWidget { ), const SizedBox(height: 16), - // Filter Buttons + // Filters SizedBox( height: 40, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: filters.map((label) { - final isSelected = label == "All"; // static for now - return ElevatedButton( - onPressed: () { - // Add filtering logic later - }, - style: ElevatedButton.styleFrom( - backgroundColor: isSelected - ? theme.secondary - : Colors.grey.shade900, - foregroundColor: - isSelected ? Colors.black : theme.primary, - padding: const EdgeInsets.symmetric(horizontal: 20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: filters.map((label) { + final isSelected = label == "All"; // static for now + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + onPressed: () { + // filtering logic later + }, + style: ElevatedButton.styleFrom( + backgroundColor: isSelected + ? theme.secondary + : Colors.grey.shade900, + foregroundColor: + isSelected ? Colors.black : theme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + elevation: 0, + ), + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), ), - elevation: 0, - ), - child: Text(label, - style: const TextStyle(fontWeight: FontWeight.w600)), - ); - }).toList(), + ); + }).toList(), + ), ), ), + const SizedBox(height: 16), - // Chat List + // Group List Expanded( child: ListView.builder( - itemCount: chatUsers.length, + itemCount: chatGroups.length, itemBuilder: (context, index) { - final user = chatUsers[index]; + final group = chatGroups[index]; + final groupId = group["groupId"]?.toString() ?? ""; + final groupName = + group["name"]?.toString() ?? "Unnamed Group"; + return Column( children: [ InkWell( onTap: () { - chatProvider.markAsRead(user["email"]); + chatProvider.markGroupAsRead(groupId); Navigator.push( context, MaterialPageRoute( builder: (context) => ChatPage( - receiverEmail: user["email"], - receiverID: user["uid"], + groupId: groupId, + groupName: groupName, ), ), ); @@ -131,37 +151,25 @@ class ChatlistsPage extends StatelessWidget { ), child: Row( children: [ - // Profile picture - Stack( - children: [ - const CircleAvatar( - radius: 24, - backgroundColor: Colors.grey, - ), - Positioned( - bottom: 0, - right: 0, - child: CircleAvatar( - radius: 6, - backgroundColor: Colors.black, - child: CircleAvatar( - radius: 4, - backgroundColor: Colors.green, - ), - ), - ), - ], + // Group icon + const CircleAvatar( + radius: 24, + backgroundColor: Colors.grey, + child: Icon( + Icons.group, + color: Colors.white, + ), ), const SizedBox(width: 12), - // Name and message + // Group name + last message Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - user["name"], + groupName, style: TextStyle( color: theme.primary, fontWeight: FontWeight.bold, @@ -170,7 +178,7 @@ class ChatlistsPage extends StatelessWidget { ), const SizedBox(height: 4), Text( - user["message"], + group["lastMessage"] ?? "", style: TextStyle( color: theme.primary.withOpacity(0.7), fontSize: 13, @@ -181,19 +189,19 @@ class ChatlistsPage extends StatelessWidget { ), ), - // Time and unread dot + // Time + unread dot Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - user["time"], + group["time"]?.toString() ?? "", style: TextStyle( color: theme.primary.withOpacity(0.6), fontSize: 12, ), ), const SizedBox(height: 8), - if (user["unread"] == true) + if (group["unread"] == true) const CircleAvatar( radius: 6, backgroundColor: Colors.amber, diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 78a69bc..741f5b1 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -2,79 +2,136 @@ import 'package:flutter/material.dart'; import 'package:pixelarticons/pixel.dart'; class ChatProvider extends ChangeNotifier { - final String receiverID; final String currentUserID = "uid_me"; - ChatProvider({required this.receiverID}); - - final List> _messages = [ - {"senderID": "uid_me", "type": "image", "timestamp": "12:34 pm"}, - { - "senderID": "uid_me", - "message": "text text text text text text text text text text...", - "timestamp": "12:34 pm", - }, - { - "senderID": "system", - "message": "Duelled", - "timestamp": "12:34 pm", - "icon": Pixel.bullseye, - }, - { - "senderID": "uid_1", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - { - "senderID": "uid_me", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - { - "senderID": "uid_1", - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, + /// Stores messages per group: { groupId: [messageMap, ...] } + final Map>> _groupMessages = {}; + + /// Stores replyTo per group + final Map _groupReplyTo = {}; + + /// Stores attachment options visibility per group + final Map _groupAttachmentVisibility = {}; + + /// Define some users with colors + final List> _dummyUsers = [ + {"id": "uid_me", "name": "You", "color": Colors.black}, + {"id": "uid_1", "name": "Person 1", "color": Colors.purple}, + {"id": "uid_2", "name": "Person 2", "color": Colors.red}, + {"id": "uid_3", "name": "Person 3", "color": Colors.green}, + {"id": "uid_4", "name": "Person 4", "color": Colors.blue}, ]; - List> get messages => _messages; + /// Get messages for a specific group + List> getMessages(String groupId) => + _groupMessages[groupId] ?? []; + + /// Helper to get user info + Map _getUser(String id) => + _dummyUsers.firstWhere((u) => u["id"] == id); + + /// Initialize a group with dummy messages + void _initGroupIfNeeded(String groupId) { + _groupMessages.putIfAbsent(groupId, () => [ + { + "senderID": "uid_1", + "senderName": _getUser("uid_1")["name"], + "senderColor": _getUser("uid_1")["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:30 pm", + }, + { + "senderID": "uid_1", + "senderName": _getUser("uid_1")["name"], + "senderColor": _getUser("uid_1")["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:31 pm", + }, + { + "senderID": "uid_2", + "senderName": _getUser("uid_2")["name"], + "senderColor": _getUser("uid_2")["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:33 pm", + }, + { + "senderID": currentUserID, + "senderName": _getUser(currentUserID)["name"], + "senderColor": _getUser(currentUserID)["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:34 pm", + }, + { + "senderID": "uid_3", + "senderName": _getUser("uid_3")["name"], + "senderColor": _getUser("uid_3")["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:35 pm", + }, + { + "senderID": "uid_4", + "senderName": _getUser("uid_4")["name"], + "senderColor": _getUser("uid_4")["color"], + "message": "text text text text text text text text text text...", + "timestamp": "12:36 pm", + }, + ]); - String? _replyTo; - String? get replyTo => _replyTo; + _groupReplyTo.putIfAbsent(groupId, () => null); + _groupAttachmentVisibility.putIfAbsent(groupId, () => false); + } - bool _showAttachmentOptions = false; - bool get showAttachmentOptions => _showAttachmentOptions; + /// Get reply-to for a specific group + String? getReplyTo(String groupId) { + _initGroupIfNeeded(groupId); + return _groupReplyTo[groupId]; + } - /// Send a new message - void sendMessage(String text) { + /// Get attachment options state for a specific group + bool getAttachmentOptionsVisibility(String groupId) { + _initGroupIfNeeded(groupId); + return _groupAttachmentVisibility[groupId] ?? false; + } + + /// Send a new message in a group + void sendMessage(String groupId, String text) { if (text.trim().isEmpty) return; - _messages.add({ + _initGroupIfNeeded(groupId); + final user = _getUser(currentUserID); + + _groupMessages[groupId]!.add({ "senderID": currentUserID, + "senderName": user["name"], + "senderColor": user["color"], "message": text.trim(), "timestamp": "now", - if (_replyTo != null) "replyTo": _replyTo, + if (_groupReplyTo[groupId] != null) "replyTo": _groupReplyTo[groupId], }); - _replyTo = null; + _groupReplyTo[groupId] = null; notifyListeners(); } - /// Set reply-to target - void setReplyTo(String? message) { - _replyTo = message; + /// Set reply-to target for a group + void setReplyTo(String groupId, String? message) { + _initGroupIfNeeded(groupId); + _groupReplyTo[groupId] = message; notifyListeners(); } - /// Clear reply-to state - void clearReplyTo() { - _replyTo = null; + /// Clear reply-to state for a group + void clearReplyTo(String groupId) { + _initGroupIfNeeded(groupId); + _groupReplyTo[groupId] = null; notifyListeners(); } - /// Toggle attachment options visibility - void toggleAttachmentOptions() { - _showAttachmentOptions = !_showAttachmentOptions; + /// Toggle attachment options for a group + void toggleAttachmentOptions(String groupId) { + _initGroupIfNeeded(groupId); + _groupAttachmentVisibility[groupId] = + !(_groupAttachmentVisibility[groupId] ?? false); notifyListeners(); } } \ No newline at end of file diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart index 8df5933..1d7b110 100644 --- a/leaderboard_app/lib/provider/chatlists_provider.dart +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -1,30 +1,49 @@ import 'package:flutter/material.dart'; class ChatListProvider extends ChangeNotifier { - List> _chatUsers = []; + /// List of group chats + List> _chatGroups = []; - List> get chatUsers => _chatUsers; + List> get chatGroups => _chatGroups; - void loadDummyChats() { - _chatUsers = List.generate( - 10, + /// Load dummy group chats + void loadDummyGroups() { + _chatGroups = List.generate( + 5, (index) => { - "name": "Penny Valeria", - "message": "Text text text text....", - "time": "12:35 pm", - "email": "user$index@example.com", - "uid": "uid_$index", - "unread": index != 0, + "groupId": "group_$index", + "name": "Group ${index + 1}", + "lastMessage": "This is the latest message in Group ${index + 1}", + "time": "12:${30 + index} pm", + "members": List.generate( + 4, + (mIndex) => { + "uid": "uid_${index}_${mIndex}", + "name": "Member ${mIndex + 1}", + }, + ), + "unread": index % 2 == 0, // alternate unread status }, ); notifyListeners(); } - void markAsRead(String email) { - final index = _chatUsers.indexWhere((user) => user["email"] == email); + /// Mark a group as read + void markGroupAsRead(String groupId) { + final index = _chatGroups.indexWhere((group) => group["groupId"] == groupId); if (index != -1) { - _chatUsers[index]["unread"] = false; + _chatGroups[index]["unread"] = false; notifyListeners(); } } -} + + /// Update last message for a group + void updateLastMessage(String groupId, String message, String time) { + final index = _chatGroups.indexWhere((group) => group["groupId"] == groupId); + if (index != -1) { + _chatGroups[index]["lastMessage"] = message; + _chatGroups[index]["time"] = time; + notifyListeners(); + } + } +} \ No newline at end of file From 8b85d5bdfbb74b11b5df4d4c01cfb392a7903fcc Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 11:56:42 +0530 Subject: [PATCH 08/53] profile gets data from each group --- leaderboard_app/lib/main.dart | 4 + leaderboard_app/lib/pages/chat_page.dart | 23 +- leaderboard_app/lib/pages/groupinfo_page.dart | 254 ++++++++++++++++++ leaderboard_app/lib/pages/profile_page.dart | 237 ---------------- .../lib/provider/chat_provider.dart | 4 +- 5 files changed, 273 insertions(+), 249 deletions(-) create mode 100644 leaderboard_app/lib/pages/groupinfo_page.dart delete mode 100644 leaderboard_app/lib/pages/profile_page.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 5885509..45f2e0d 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -3,6 +3,7 @@ import 'package:leaderboard_app/pages/home_page.dart'; import 'package:leaderboard_app/pages/signup_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; +import 'package:leaderboard_app/provider/chat_provider.dart'; // <- import ChatProvider import 'package:leaderboard_app/provider/theme_provider.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; @@ -16,6 +17,9 @@ void main() { create: (_) => ChatListProvider()..loadDummyGroups(), ), ChangeNotifierProvider(create: (_) => UserProvider()), + + // Add ChatProvider here + ChangeNotifierProvider(create: (_) => ChatProvider()), ], child: const MainApp(), ), diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index 8be6781..e70c906 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/provider/chat_provider.dart'; import 'package:pixelarticons/pixel.dart'; import 'package:provider/provider.dart'; -import 'profile_page.dart'; +import 'groupinfo_page.dart'; class ChatPage extends StatelessWidget { final String groupId; @@ -81,13 +81,21 @@ class _ChatViewState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute(builder: (_) => const ProfilePage()), + MaterialPageRoute(builder: (_) => GroupInfoPage(groupName: widget.groupName)), ); }, child: Row( children: [ - const CircleAvatar(radius: 20, backgroundColor: Colors.grey), - const SizedBox(width: 8), + // Group icon + const CircleAvatar( + radius: 20, + backgroundColor: Colors.grey, + child: Icon( + Icons.group, + color: Colors.white, + ), + ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -99,13 +107,6 @@ class _ChatViewState extends State { fontSize: 16, ), ), - Text( - "Online", - style: TextStyle( - color: theme.primary.withOpacity(0.6), - fontSize: 12, - ), - ), ], ), ], diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart new file mode 100644 index 0000000..8678ae4 --- /dev/null +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/provider/chat_provider.dart'; +import 'package:provider/provider.dart'; + +class GroupInfoPage extends StatelessWidget { + final String groupName; + + const GroupInfoPage({super.key, required this.groupName}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + + // Get ChatProvider instance + final chatProvider = Provider.of(context); + + // Use the dummy users from ChatProvider as members + final members = chatProvider.dummyUsers; + + // For leaderboard, create sample data based on dummy users + final leaderboard = List.generate( + members.length, + (index) => { + "place": index + 1, + "player": members[index]["name"], + "streak": (12 + index).toString(), + "solved": (1324 + index * 10).toString(), + "badge": Icons.star, + }, + ); + + return Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: const BackButton(), + elevation: 0, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 10), + + // Group icon + const CircleAvatar( + radius: 50, + backgroundColor: Colors.grey, + child: Icon( + Icons.group, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + + // Use dynamic groupName here + Text( + groupName, + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + + const SizedBox(height: 16), + + // Members Card + _membersCard(members), + + const SizedBox(height: 16), + + // Leaderboard Card + _leaderboardCard(leaderboard), + + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _membersCard(List> members) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Members", + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 12), + ...members.map((member) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: Colors.grey.shade700, + child: Text( + (member["name"] != null && member["name"].isNotEmpty) + ? member["name"][0] + : "?", + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + Text( + member["name"] ?? "Unknown", + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + ], + ), + ); + }).toList(), + ], + ), + ); + } + + Widget _leaderboardCard(List> leaderboard) { + return Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, // Align to left + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Leaderboard", + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, // Stretch table to max width + child: DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: MaterialStateProperty.all( + Colors.grey[900], + ), + columns: const [ + DataColumn( + label: Text( + "Place", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Player", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Streak", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Solved", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Badge", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + rows: leaderboard.map((row) { + return DataRow( + cells: [ + DataCell( + Text( + "${row["place"]}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Text( + row["player"] ?? "Player", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Text( + row["streak"] ?? "0", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Text( + row["solved"] ?? "0", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + Icon( + row["badge"] ?? Icons.star, + color: Colors.amber, + size: 16, + ), + ), + ], + ); + }).toList(), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/profile_page.dart b/leaderboard_app/lib/pages/profile_page.dart deleted file mode 100644 index 55cd77b..0000000 --- a/leaderboard_app/lib/pages/profile_page.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter/material.dart'; - -class ProfilePage extends StatelessWidget { - const ProfilePage({super.key}); - - final List> members = const [ - {"name": "Alice", "avatar": "A"}, - {"name": "Bob", "avatar": "B"}, - {"name": "Charlie", "avatar": "C"}, - {"name": "Diana", "avatar": "D"}, - ]; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - - return Scaffold( - backgroundColor: theme.surface, - appBar: AppBar( - backgroundColor: Colors.transparent, - leading: const BackButton(), - elevation: 0, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - const SizedBox(height: 10), - - // Avatar & Name - const CircleAvatar(radius: 50, backgroundColor: Colors.grey), - const SizedBox(height: 8), - const Text( - "Penny Valeria", - style: TextStyle(color: Colors.white, fontSize: 16), - ), - - const SizedBox(height: 16), - - // Members Card - _membersCard(), - - const SizedBox(height: 16), - - // Leaderboard Card with DataTable - _leaderboardCard(), - - const SizedBox(height: 20), - ], - ), - ), - ); - } - - Widget _membersCard() { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Members", - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 12), - ...members.map((member) { - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - CircleAvatar( - radius: 18, - backgroundColor: Colors.grey.shade700, - child: Text( - member["avatar"] ?? "?", - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - const SizedBox(width: 12), - Text( - member["name"] ?? "Unknown", - style: const TextStyle(color: Colors.white70, fontSize: 16), - ), - ], - ), - ); - }).toList(), - ], - ), - ); - } - - Widget _leaderboardCard() { - return Container( - padding: const EdgeInsets.all(12), - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, // Align to left - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - "Leaderboard", - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, // Stretch table to max width - child: const LeaderboardTable(), - ), - ], - ), - ); - } -} - -class LeaderboardTable extends StatelessWidget { - const LeaderboardTable({super.key}); - - @override - Widget build(BuildContext context) { - return DataTable( - columnSpacing: 10, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: MaterialStateProperty.all( - Colors.grey[900], - ), - columns: const [ - DataColumn( - label: Text( - "Place", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Player", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Streak", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Solved", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Badge", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - ], - rows: List.generate( - 5, - (index) => DataRow( - cells: [ - DataCell( - Text( - "${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Text( - "Player ${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "12", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "1324", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Icon( - Icons.star, - color: Colors.amber, - size: 16, - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 741f5b1..63e5168 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:pixelarticons/pixel.dart'; class ChatProvider extends ChangeNotifier { final String currentUserID = "uid_me"; @@ -22,6 +21,9 @@ class ChatProvider extends ChangeNotifier { {"id": "uid_4", "name": "Person 4", "color": Colors.blue}, ]; + // Expose dummyUsers publicly for other widgets to read + List> get dummyUsers => _dummyUsers; + /// Get messages for a specific group List> getMessages(String groupId) => _groupMessages[groupId] ?? []; From 7f4a7a39ddeb48afce9aa6bc27fb83a39127db9a Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 12:07:18 +0530 Subject: [PATCH 09/53] refactored code to be more modular --- .../lib/chatpage-components/chat_view.dart | 120 ++++++ .../lib/chatpage-components/message_list.dart | 202 +++++++++ .../lib/chatpage-components/user_input.dart | 124 ++++++ leaderboard_app/lib/components/week_view.dart | 65 --- .../compact_calendar.dart | 0 .../daily_activity.dart | 0 .../leaderboard_table.dart | 0 .../problem_table.dart | 0 .../lib/dashboard-components/week_view.dart | 105 +++++ .../weekly_stats.dart | 0 leaderboard_app/lib/main.dart | 2 +- leaderboard_app/lib/pages/chat_page.dart | 397 +----------------- leaderboard_app/lib/pages/dashboard_page.dart | 12 +- 13 files changed, 560 insertions(+), 467 deletions(-) create mode 100644 leaderboard_app/lib/chatpage-components/chat_view.dart create mode 100644 leaderboard_app/lib/chatpage-components/message_list.dart create mode 100644 leaderboard_app/lib/chatpage-components/user_input.dart delete mode 100644 leaderboard_app/lib/components/week_view.dart rename leaderboard_app/lib/{components => dashboard-components}/compact_calendar.dart (100%) rename leaderboard_app/lib/{components => dashboard-components}/daily_activity.dart (100%) rename leaderboard_app/lib/{components => dashboard-components}/leaderboard_table.dart (100%) rename leaderboard_app/lib/{components => dashboard-components}/problem_table.dart (100%) create mode 100644 leaderboard_app/lib/dashboard-components/week_view.dart rename leaderboard_app/lib/{components => dashboard-components}/weekly_stats.dart (100%) diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart new file mode 100644 index 0000000..d720826 --- /dev/null +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/pages/groupinfo_page.dart'; +import 'package:provider/provider.dart'; +import '../provider/chat_provider.dart'; +import 'message_list.dart'; +import 'user_input.dart'; + +class ChatView extends StatefulWidget { + final String groupId; + final String groupName; + + const ChatView({super.key, required this.groupId, required this.groupName}); + + @override + State createState() => _ChatViewState(); +} + +class _ChatViewState extends State { + final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final FocusNode myFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + myFocusNode.addListener(() { + if (myFocusNode.hasFocus) { + Future.delayed(const Duration(milliseconds: 300), scrollDown); + } + }); + Future.delayed(const Duration(milliseconds: 500), scrollDown); + } + + void scrollDown() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent + 80, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + + @override + void dispose() { + _messageController.dispose(); + _scrollController.dispose(); + myFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + Provider.of(context); + + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: theme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: const BackButton(), + titleSpacing: 0, + title: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupInfoPage(groupName: widget.groupName), + ), + ); + }, + child: Row( + children: [ + const CircleAvatar( + radius: 20, + backgroundColor: Colors.grey, + child: Icon(Icons.group, color: Colors.white), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.groupName, + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ], + ), + ), + ), + body: Column( + children: [ + Expanded( + child: MessageList( + groupId: widget.groupId, + scrollController: _scrollController, + scrollDownCallback: scrollDown, + ), + ), + UserInput( + groupId: widget.groupId, + messageController: _messageController, + focusNode: myFocusNode, + scrollDownCallback: scrollDown, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart new file mode 100644 index 0000000..0fe670d --- /dev/null +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:pixelarticons/pixel.dart'; +import 'package:provider/provider.dart'; +import '../provider/chat_provider.dart'; + +class MessageList extends StatelessWidget { + final String groupId; + final ScrollController scrollController; + final VoidCallback scrollDownCallback; + + const MessageList({ + super.key, + required this.groupId, + required this.scrollController, + required this.scrollDownCallback, + }); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final messages = provider.getMessages(groupId); + + if (messages.isEmpty) { + return Center( + child: Text( + "No messages yet", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ); + } + + return ListView.builder( + controller: scrollController, + itemCount: messages.length, + itemBuilder: (context, index) { + final msg = messages[index]; + final isMe = msg["senderID"] == provider.currentUserID; + final isSystem = msg["senderID"] == "system"; + final isImage = msg["type"] == "image"; + + if (isSystem) return _SystemMessage(msg: msg); + + if (isImage) return _ImageMessage(msg: msg, isMe: isMe); + + return GestureDetector( + onDoubleTap: () { + provider.setReplyTo(groupId, msg["message"]); + }, + child: _TextMessage(msg: msg, isMe: isMe), + ); + }, + ); + } +} + +class _SystemMessage extends StatelessWidget { + final Map msg; + const _SystemMessage({required this.msg}); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), + const SizedBox(width: 6), + Text( + msg["message"] ?? "", + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + const SizedBox(width: 6), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + ], + ), + ), + ); + } +} + +class _ImageMessage extends StatelessWidget { + final Map msg; + final bool isMe; + const _ImageMessage({required this.msg, required this.isMe}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: + isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon(Pixel.image, size: 64, color: Colors.grey), + ), + ), + const SizedBox(height: 4), + Text( + msg["timestamp"] ?? "", + style: const TextStyle(fontSize: 10, color: Colors.black54), + ), + ], + ), + ), + ); + } +} + +class _TextMessage extends StatelessWidget { + final Map msg; + final bool isMe; + const _TextMessage({required this.msg, required this.isMe}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isMe ? Theme.of(context).colorScheme.inversePrimary : Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + msg["senderName"] ?? "", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: msg["senderColor"] ?? Colors.white, + ), + ), + const SizedBox(height: 4), + if (msg["replyTo"] != null) + Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + msg["replyTo"], + style: TextStyle( + fontSize: 11, + color: isMe ? Colors.black87 : Colors.white60, + ), + ), + ), + Text( + msg["message"] ?? "", + style: TextStyle( + color: isMe ? Colors.black : Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.bottomRight, + child: Text( + msg["timestamp"] ?? "", + style: TextStyle( + color: isMe ? Colors.black54 : Colors.white54, + fontSize: 10, + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/chatpage-components/user_input.dart b/leaderboard_app/lib/chatpage-components/user_input.dart new file mode 100644 index 0000000..d03fc44 --- /dev/null +++ b/leaderboard_app/lib/chatpage-components/user_input.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:pixelarticons/pixel.dart'; +import 'package:provider/provider.dart'; +import '../provider/chat_provider.dart'; + +class UserInput extends StatelessWidget { + final String groupId; + final TextEditingController messageController; + final FocusNode focusNode; + final VoidCallback scrollDownCallback; + + const UserInput({ + super.key, + required this.groupId, + required this.messageController, + required this.focusNode, + required this.scrollDownCallback, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + final provider = Provider.of(context); + final replyTo = provider.getReplyTo(groupId); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (replyTo != null) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 340), + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + Expanded( + child: Text( + "Replying to: $replyTo", + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () => provider.clearReplyTo(groupId), + child: const Icon( + Icons.close, + size: 16, + color: Colors.white54, + ), + ), + ], + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(24), + ), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 40, maxHeight: 150), + child: Scrollbar( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: messageController, + focusNode: focusNode, + maxLines: null, + keyboardType: TextInputType.multiline, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: "Type a message...", + hintStyle: TextStyle(color: Colors.white54), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: CircleAvatar( + backgroundColor: theme.primary, + radius: 22, + child: IconButton( + onPressed: () { + provider.sendMessage(groupId, messageController.text.trim()); + messageController.clear(); + scrollDownCallback(); + }, + icon: const Icon(Pixel.arrowup, color: Colors.black), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/week_view.dart b/leaderboard_app/lib/components/week_view.dart deleted file mode 100644 index a9f8bec..0000000 --- a/leaderboard_app/lib/components/week_view.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -class WeekView extends StatelessWidget { - const WeekView({super.key}); - - @override - Widget build(BuildContext context) { - const days = [ - 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', - ]; - - return Container( - padding: const EdgeInsets.all(14), - width: double.infinity, // Ensures it stretches to parent width - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - const Center( - child: Text( - 'June 5, 2025', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - height: 80, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: days.length, - itemBuilder: (context, index) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 6), - width: 60, - decoration: BoxDecoration( - color: index == 4 - ? Colors.amber - : Colors.grey[900], - borderRadius: BorderRadius.circular(10), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.check_circle, color: Colors.white), - const SizedBox(height: 4), - Text( - days[index], - style: const TextStyle(color: Colors.white), - ), - ], - ), - ); - }, - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/components/compact_calendar.dart b/leaderboard_app/lib/dashboard-components/compact_calendar.dart similarity index 100% rename from leaderboard_app/lib/components/compact_calendar.dart rename to leaderboard_app/lib/dashboard-components/compact_calendar.dart diff --git a/leaderboard_app/lib/components/daily_activity.dart b/leaderboard_app/lib/dashboard-components/daily_activity.dart similarity index 100% rename from leaderboard_app/lib/components/daily_activity.dart rename to leaderboard_app/lib/dashboard-components/daily_activity.dart diff --git a/leaderboard_app/lib/components/leaderboard_table.dart b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart similarity index 100% rename from leaderboard_app/lib/components/leaderboard_table.dart rename to leaderboard_app/lib/dashboard-components/leaderboard_table.dart diff --git a/leaderboard_app/lib/components/problem_table.dart b/leaderboard_app/lib/dashboard-components/problem_table.dart similarity index 100% rename from leaderboard_app/lib/components/problem_table.dart rename to leaderboard_app/lib/dashboard-components/problem_table.dart diff --git a/leaderboard_app/lib/dashboard-components/week_view.dart b/leaderboard_app/lib/dashboard-components/week_view.dart new file mode 100644 index 0000000..0b47f41 --- /dev/null +++ b/leaderboard_app/lib/dashboard-components/week_view.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +class WeekView extends StatefulWidget { + const WeekView({super.key}); + + @override + State createState() => _WeekViewState(); +} + +class _WeekViewState extends State { + final List days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + late final int todayIndex; + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + DateTime now = DateTime.now(); + todayIndex = now.weekday % 7; // DateTime.weekday: Mon=1..Sun=7, so mod 7 gives Sun=0..Sat=6 + _scrollController = ScrollController(); + + // Wait for the first frame then scroll to the current day + WidgetsBinding.instance.addPostFrameCallback((_) { + double position = todayIndex * 72.0; // approx item width (60 + margin 12) + _scrollController.animateTo( + position, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + DateTime now = DateTime.now(); + String dateText = "${_monthName(now.month)} ${now.day}, ${now.year}"; + + return Container( + padding: const EdgeInsets.all(14), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Center( + child: Text( + dateText, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 80, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + bool isToday = index == todayIndex; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + width: 60, + decoration: BoxDecoration( + color: isToday ? Colors.amber : Colors.grey[900], + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check_circle, color: Colors.white), + const SizedBox(height: 4), + Text( + days[index], + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } + + String _monthName(int month) { + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + return months[month - 1]; + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/components/weekly_stats.dart b/leaderboard_app/lib/dashboard-components/weekly_stats.dart similarity index 100% rename from leaderboard_app/lib/components/weekly_stats.dart rename to leaderboard_app/lib/dashboard-components/weekly_stats.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 45f2e0d..232e56a 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -3,7 +3,7 @@ import 'package:leaderboard_app/pages/home_page.dart'; import 'package:leaderboard_app/pages/signup_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; -import 'package:leaderboard_app/provider/chat_provider.dart'; // <- import ChatProvider +import 'package:leaderboard_app/provider/chat_provider.dart'; import 'package:leaderboard_app/provider/theme_provider.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index e70c906..5fe8e55 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/provider/chat_provider.dart'; -import 'package:pixelarticons/pixel.dart'; +import 'package:leaderboard_app/chatpage-components/chat_view.dart'; import 'package:provider/provider.dart'; -import 'groupinfo_page.dart'; +import '../provider/chat_provider.dart'; class ChatPage extends StatelessWidget { final String groupId; @@ -15,8 +14,6 @@ class ChatPage extends StatelessWidget { return ChangeNotifierProvider( create: (_) { final provider = ChatProvider(); - // Ensure group data is initialized by calling methods - // that internally call _initGroupIfNeeded(...) provider.getReplyTo(groupId); provider.getAttachmentOptionsVisibility(groupId); return provider; @@ -24,394 +21,4 @@ class ChatPage extends StatelessWidget { child: ChatView(groupId: groupId, groupName: groupName), ); } -} - -class ChatView extends StatefulWidget { - final String groupId; - final String groupName; - - const ChatView({super.key, required this.groupId, required this.groupName}); - - @override - State createState() => _ChatViewState(); -} - -class _ChatViewState extends State { - final TextEditingController _messageController = TextEditingController(); - final ScrollController _scrollController = ScrollController(); - final FocusNode myFocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - myFocusNode.addListener(() { - if (myFocusNode.hasFocus) { - Future.delayed(const Duration(milliseconds: 300), scrollDown); - } - }); - // initial scroll after build - Future.delayed(const Duration(milliseconds: 500), scrollDown); - } - - void scrollDown() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent + 80, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - final provider = Provider.of(context); - - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Scaffold( - backgroundColor: theme.surface, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: const BackButton(), - titleSpacing: 0, - title: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => GroupInfoPage(groupName: widget.groupName)), - ); - }, - child: Row( - children: [ - // Group icon - const CircleAvatar( - radius: 20, - backgroundColor: Colors.grey, - child: Icon( - Icons.group, - color: Colors.white, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.groupName, - style: TextStyle( - color: theme.primary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - ], - ), - ), - ), - body: Column( - children: [ - Expanded(child: _buildMessageList(provider)), - _buildUserInput(provider), - ], - ), - ), - ); - } - - Widget _buildMessageList(ChatProvider provider) { - final messages = provider.getMessages(widget.groupId); - - if (messages.isEmpty) { - return Center( - child: Text( - "No messages yet", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ); - } - - return ListView.builder( - controller: _scrollController, - itemCount: messages.length, - itemBuilder: (context, index) { - final msg = messages[index]; - final isMe = msg["senderID"] == provider.currentUserID; - final isSystem = msg["senderID"] == "system"; - final isImage = msg["type"] == "image"; - - if (isSystem) { - return _systemMessage(msg); - } - - if (isImage) { - return _imageMessage(msg, isMe); - } - - return GestureDetector( - onDoubleTap: () { - provider.setReplyTo(widget.groupId, msg["message"]); - }, - child: _textMessage(msg, isMe), - ); - }, - ); - } - - Widget _systemMessage(Map msg) => Center( - child: Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), - const SizedBox(width: 6), - Text( - msg["message"] ?? "", - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - const SizedBox(width: 6), - Text( - msg["timestamp"] ?? "", - style: const TextStyle(color: Colors.white54, fontSize: 10), - ), - ], - ), - ), - ); - - Widget _imageMessage(Map msg, bool isMe) => Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: isMe - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Container( - width: 180, - height: 180, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon(Pixel.image, size: 64, color: Colors.grey), - ), - ), - const SizedBox(height: 4), - Text( - msg["timestamp"] ?? "", - style: const TextStyle(fontSize: 10, color: Colors.black54), - ), - ], - ), - ), - ); - - Widget _textMessage(Map msg, bool isMe) { - return Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isMe - ? Theme.of(context).colorScheme.inversePrimary - : Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - constraints: const BoxConstraints(maxWidth: 280), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, // Always align left inside bubble - children: [ - // Name (always left-aligned) - Text( - msg["senderName"] ?? "", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: msg["senderColor"] ?? Colors.white, - ), - ), - const SizedBox(height: 4), - - // Reply preview - if (msg["replyTo"] != null) - Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - msg["replyTo"], - style: TextStyle( - fontSize: 11, - color: isMe ? Colors.black87 : Colors.white60, - ), - ), - ), - - // Message text - Text( - msg["message"] ?? "", - style: TextStyle( - color: isMe ? Colors.black : Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - - // Timestamp - Align( - alignment: Alignment.bottomRight, - child: Text( - msg["timestamp"] ?? "", - style: TextStyle( - color: isMe ? Colors.black54 : Colors.white54, - fontSize: 10, - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildUserInput(ChatProvider provider) { - final theme = Theme.of(context).colorScheme; - final replyTo = provider.getReplyTo(widget.groupId); - - return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 6, 12, 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Reply section - if (replyTo != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 340), - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(24), - ), - child: Row( - children: [ - Expanded( - child: Text( - "Replying to: $replyTo", - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - ), - ), - GestureDetector( - onTap: () => provider.clearReplyTo(widget.groupId), - child: const Icon( - Icons.close, - size: 16, - color: Colors.white54, - ), - ), - ], - ), - ), - ), - - // Input row - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Text field - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(24), - ), - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: 40, - maxHeight: 150, - ), - child: Scrollbar( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _messageController, - focusNode: myFocusNode, - maxLines: null, - keyboardType: TextInputType.multiline, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: "Type a message...", - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - ), - ), - ), - ), - ), - ), - const SizedBox(width: 8), - - // Send button - Padding( - padding: const EdgeInsets.only(bottom: 6), - child: CircleAvatar( - backgroundColor: theme.primary, - radius: 22, - child: IconButton( - onPressed: () { - provider.sendMessage( - widget.groupId, - _messageController.text.trim(), - ); - _messageController.clear(); - scrollDown(); - }, - icon: const Icon(Pixel.arrowup, color: Colors.black), - ), - ), - ), - ], - ), - ], - ), - ), - ); - } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index f9f2573..9dc374b 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/components/compact_calendar.dart'; -import 'package:leaderboard_app/components/leaderboard_table.dart'; -import 'package:leaderboard_app/components/problem_table.dart'; -import 'package:leaderboard_app/components/daily_activity.dart'; -import 'package:leaderboard_app/components/week_view.dart'; -import 'package:leaderboard_app/components/weekly_stats.dart'; +import 'package:leaderboard_app/dashboard-components/compact_calendar.dart'; +import 'package:leaderboard_app/dashboard-components/leaderboard_table.dart'; +import 'package:leaderboard_app/dashboard-components/problem_table.dart'; +import 'package:leaderboard_app/dashboard-components/daily_activity.dart'; +import 'package:leaderboard_app/dashboard-components/week_view.dart'; +import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; From ec2c28a80d6a61f2d8ce2f4a233e2bf5f4cccc49 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 12:12:08 +0530 Subject: [PATCH 10/53] filters now work on group page --- leaderboard_app/lib/pages/chatlists_page.dart | 219 ++++++++++-------- 1 file changed, 124 insertions(+), 95 deletions(-) diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index d05d452..b09d7f7 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -3,16 +3,35 @@ import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:provider/provider.dart'; import 'chat_page.dart'; -class ChatlistsPage extends StatelessWidget { +class ChatlistsPage extends StatefulWidget { const ChatlistsPage({super.key}); + @override + State createState() => _ChatlistsPageState(); +} + +class _ChatlistsPageState extends State { final List filters = const ["All", "Unread", "Favourites"]; + String selectedFilter = "All"; + + List> _applyFilter(List> chatGroups) { + switch (selectedFilter) { + case "Unread": + return chatGroups.where((group) => group["unread"] == true).toList(); + case "Favourites": + // Assuming each group has a "favourite" bool property. If not, update your data model or remove this filter. + return chatGroups.where((group) => group["favourite"] == true).toList(); + case "All": + default: + return chatGroups; + } + } @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); - final chatGroups = chatProvider.chatGroups; + final filteredGroups = _applyFilter(chatProvider.chatGroups); return Scaffold( backgroundColor: theme.surface, @@ -37,7 +56,7 @@ class ChatlistsPage extends StatelessWidget { ), const SizedBox(height: 10), - // Search + Add + // Search + Add (You can later wire search functionality here) Row( children: [ Expanded( @@ -84,13 +103,15 @@ class ChatlistsPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: filters.map((label) { - final isSelected = label == "All"; // static for now + final isSelected = label == selectedFilter; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( onPressed: () { - // filtering logic later + setState(() { + selectedFilter = label; + }); }, style: ElevatedButton.styleFrom( backgroundColor: isSelected @@ -121,106 +142,114 @@ class ChatlistsPage extends StatelessWidget { // Group List Expanded( - child: ListView.builder( - itemCount: chatGroups.length, - itemBuilder: (context, index) { - final group = chatGroups[index]; - final groupId = group["groupId"]?.toString() ?? ""; - final groupName = - group["name"]?.toString() ?? "Unnamed Group"; + child: filteredGroups.isEmpty + ? Center( + child: Text( + "No groups found", + style: TextStyle(color: theme.primary), + ), + ) + : ListView.builder( + itemCount: filteredGroups.length, + itemBuilder: (context, index) { + final group = filteredGroups[index]; + final groupId = group["groupId"]?.toString() ?? ""; + final groupName = + group["name"]?.toString() ?? "Unnamed Group"; - return Column( - children: [ - InkWell( - onTap: () { - chatProvider.markGroupAsRead(groupId); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatPage( - groupId: groupId, - groupName: groupName, - ), - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: Row( - children: [ - // Group icon - const CircleAvatar( - radius: 24, - backgroundColor: Colors.grey, - child: Icon( - Icons.group, - color: Colors.white, + return Column( + children: [ + InkWell( + onTap: () { + chatProvider.markGroupAsRead(groupId); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatPage( + groupId: groupId, + groupName: groupName, + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - ), - const SizedBox(width: 12), - - // Group name + last message - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, + child: Row( children: [ - Text( - groupName, - style: TextStyle( - color: theme.primary, - fontWeight: FontWeight.bold, - fontSize: 16, + // Group icon + const CircleAvatar( + radius: 24, + backgroundColor: Colors.grey, + child: Icon( + Icons.group, + color: Colors.white, ), ), - const SizedBox(height: 4), - Text( - group["lastMessage"] ?? "", - style: TextStyle( - color: theme.primary.withOpacity(0.7), - fontSize: 13, - overflow: TextOverflow.ellipsis, + const SizedBox(width: 12), + + // Group name + last message + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + groupName, + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + group["lastMessage"] ?? "", + style: TextStyle( + color: + theme.primary.withOpacity(0.7), + fontSize: 13, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), - ], - ), - ), - // Time + unread dot - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - group["time"]?.toString() ?? "", - style: TextStyle( - color: theme.primary.withOpacity(0.6), - fontSize: 12, - ), - ), - const SizedBox(height: 8), - if (group["unread"] == true) - const CircleAvatar( - radius: 6, - backgroundColor: Colors.amber, + // Time + unread dot + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + group["time"]?.toString() ?? "", + style: TextStyle( + color: theme.primary.withOpacity(0.6), + fontSize: 12, + ), + ), + const SizedBox(height: 8), + if (group["unread"] == true) + const CircleAvatar( + radius: 6, + backgroundColor: Colors.amber, + ), + ], ), - ], + ], + ), ), - ], - ), - ), - ), - Divider( - height: 1, - thickness: 0.6, - color: Colors.grey.shade800, - ), - ], - ); - }, - ), + ), + Divider( + height: 1, + thickness: 0.6, + color: Colors.grey.shade800, + ), + ], + ); + }, + ), ), ], ), From cf691bb9df465e480256ec82b070c5b7e3d71cc4 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 12:13:16 +0530 Subject: [PATCH 11/53] redundant stats page removed --- leaderboard_app/lib/pages/home_page.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/leaderboard_app/lib/pages/home_page.dart b/leaderboard_app/lib/pages/home_page.dart index 6e7a0d1..48d53a1 100644 --- a/leaderboard_app/lib/pages/home_page.dart +++ b/leaderboard_app/lib/pages/home_page.dart @@ -14,9 +14,8 @@ class _HomePageState extends State { int _selectedIndex = 0; List get _pages => [ - const DashboardPage(), // Rebuilds on each access + const DashboardPage(), ChatlistsPage(), - const Center(child: Text("Stats Page", style: TextStyle(color: Colors.white))), SettingsPage(), ]; @@ -54,10 +53,6 @@ class _HomePageState extends State { icon: Icon(Icons.chat, size: 28), label: 'Chat', ), - BottomNavigationBarItem( - icon: Icon(Icons.bar_chart, size: 28), - label: 'Stats', - ), BottomNavigationBarItem( icon: Icon(Icons.settings, size: 28), label: 'Settings', From b12f7da00ac945f54146fee9269cab619b7487ab Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 12:34:52 +0530 Subject: [PATCH 12/53] theme colors are consistent --- .../compact_calendar.dart | 153 +++++++++--------- .../lib/dashboard-components/week_view.dart | 3 +- leaderboard_app/lib/pages/chatlists_page.dart | 4 +- leaderboard_app/lib/pages/dashboard_page.dart | 45 +++--- leaderboard_app/lib/pages/settings_page.dart | 98 ++++++----- .../lib/provider/theme_provider.dart | 4 +- 6 files changed, 153 insertions(+), 154 deletions(-) diff --git a/leaderboard_app/lib/dashboard-components/compact_calendar.dart b/leaderboard_app/lib/dashboard-components/compact_calendar.dart index 3f95d5f..eed6031 100644 --- a/leaderboard_app/lib/dashboard-components/compact_calendar.dart +++ b/leaderboard_app/lib/dashboard-components/compact_calendar.dart @@ -39,6 +39,7 @@ class _CompactCalendarState extends State { @override Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; int year = _selectedDate.year; int month = _selectedDate.month; @@ -60,7 +61,7 @@ class _CompactCalendarState extends State { margin: const EdgeInsets.all(2), alignment: Alignment.center, decoration: BoxDecoration( - color: i == _selectedDate.day ? Colors.amber : Colors.transparent, + color: i == _selectedDate.day ? colors.secondary : Colors.transparent, shape: BoxShape.circle, ), child: Text( @@ -71,78 +72,86 @@ class _CompactCalendarState extends State { ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Month + Year dropdowns - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DropdownButton( - dropdownColor: Colors.grey[850], - value: month, - style: const TextStyle(color: Colors.white), - underline: Container(), - items: List.generate(12, (index) { - return DropdownMenuItem( - value: index + 1, - child: Text(_months[index]), - ); - }), - onChanged: (val) { - if (val != null) { - setState(() { - _selectedDate = DateTime(year, val, 1); - }); - } - }, - ), - const SizedBox(width: 16), - DropdownButton( - dropdownColor: Colors.grey[850], - value: year, - style: const TextStyle(color: Colors.white), - underline: Container(), - items: _years.map((yr) { - return DropdownMenuItem(value: yr, child: Text(yr.toString())); - }).toList(), - onChanged: (val) { - if (val != null) { - setState(() { - _selectedDate = DateTime(val, month, 1); - }); - } - }, - ), - ], - ), - const SizedBox(height: 10), - - // Weekday headers - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: _weekdays.map((day) { - return Expanded( - child: Center( - child: Text( - day, - style: const TextStyle(color: Colors.grey, fontSize: 12), - ), + return Container( + padding: const EdgeInsets.all(14), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Month + Year dropdowns + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + dropdownColor: Colors.grey[850], + value: month, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: List.generate(12, (index) { + return DropdownMenuItem( + value: index + 1, + child: Text(_months[index]), + ); + }), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(year, val, 1); + }); + } + }, ), - ); - }).toList(), - ), - - const SizedBox(height: 6), - - // Calendar grid - GridView.count( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - crossAxisCount: 7, - children: dayWidgets, - ), - ], + const SizedBox(width: 16), + DropdownButton( + dropdownColor: Colors.grey[850], + value: year, + style: const TextStyle(color: Colors.white), + underline: Container(), + items: _years.map((yr) { + return DropdownMenuItem(value: yr, child: Text(yr.toString())); + }).toList(), + onChanged: (val) { + if (val != null) { + setState(() { + _selectedDate = DateTime(val, month, 1); + }); + } + }, + ), + ], + ), + const SizedBox(height: 10), + + // Weekday headers + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _weekdays.map((day) { + return Expanded( + child: Center( + child: Text( + day, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ); + }).toList(), + ), + + const SizedBox(height: 6), + + // Calendar grid + GridView.count( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + crossAxisCount: 7, + children: dayWidgets, + ), + ], + ), ); } } \ No newline at end of file diff --git a/leaderboard_app/lib/dashboard-components/week_view.dart b/leaderboard_app/lib/dashboard-components/week_view.dart index 0b47f41..704a1c5 100644 --- a/leaderboard_app/lib/dashboard-components/week_view.dart +++ b/leaderboard_app/lib/dashboard-components/week_view.dart @@ -8,6 +8,7 @@ class WeekView extends StatefulWidget { } class _WeekViewState extends State { + ColorScheme get colors => Theme.of(context).colorScheme; final List days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; late final int todayIndex; late final ScrollController _scrollController; @@ -72,7 +73,7 @@ class _WeekViewState extends State { margin: const EdgeInsets.symmetric(horizontal: 6), width: 60, decoration: BoxDecoration( - color: isToday ? Colors.amber : Colors.grey[900], + color: isToday ? colors.secondary : Colors.grey[900], borderRadius: BorderRadius.circular(10), ), child: Column( diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index b09d7f7..70c97f1 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -231,9 +231,9 @@ class _ChatlistsPageState extends State { ), const SizedBox(height: 8), if (group["unread"] == true) - const CircleAvatar( + CircleAvatar( radius: 6, - backgroundColor: Colors.amber, + backgroundColor: theme.secondary, ), ], ), diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 9dc374b..76225eb 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -13,8 +13,11 @@ class DashboardPage extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Scaffold( - backgroundColor: Colors.black, + backgroundColor: colors.surface, body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { @@ -34,13 +37,13 @@ class DashboardPage extends StatelessWidget { horizontal: 16, vertical: 10, ), - color: Colors.grey[900], + color: colors.tertiary.withOpacity(0.15), child: Row( children: [ - const CircleAvatar( + CircleAvatar( radius: 20, - backgroundColor: Colors.white, - child: Icon(Icons.person, color: Colors.black), + backgroundColor: colors.surface, + child: Icon(Icons.person, color: colors.primary), ), const SizedBox(width: 12), Expanded( @@ -49,16 +52,16 @@ class DashboardPage extends StatelessWidget { children: [ Text( user.name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colors.primary, fontSize: 14, fontWeight: FontWeight.bold, ), ), Text( user.email, - style: const TextStyle( - color: Colors.grey, + style: TextStyle( + color: colors.primary.withOpacity(0.7), fontSize: 12, ), ), @@ -68,13 +71,13 @@ class DashboardPage extends StatelessWidget { _buildHeaderButton( Icons.local_fire_department, "${user.streak}", - Colors.amber, + colors.secondary, ), const SizedBox(width: 8), _buildHeaderButton( Icons.person_add, "Invite", - Colors.amber, + colors.secondary, ), ], ), @@ -88,25 +91,17 @@ class DashboardPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - WeekView(), + const WeekView(), const SizedBox(height: 10), - LeetCodeDailyCard(), + const LeetCodeDailyCard(), const SizedBox(height: 10), - LeaderboardTable(), + const LeaderboardTable(), const SizedBox(height: 10), - ProblemTable(), + const ProblemTable(), const SizedBox(height: 10), - WeeklyStats(), + const WeeklyStats(), const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(14), - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(8), - ), - child: const CompactCalendar(), - ), + const CompactCalendar(), ], ), ), diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index d114911..232819c 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -5,30 +5,33 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Scaffold( - backgroundColor: Colors.black, + backgroundColor: colors.surface, appBar: AppBar( title: const Text('Settings'), centerTitle: true, - backgroundColor: Colors.black, + backgroundColor: colors.surface, elevation: 0, - foregroundColor: Colors.white, + foregroundColor: colors.primary, ), body: ListView( padding: const EdgeInsets.all(16), children: [ // ====== Personal Details ====== - const Text( + Text( 'My Account', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colors.primary, fontSize: 16), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey.shade900, + color: colors.tertiary.withOpacity(0.15), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -37,21 +40,21 @@ class SettingsPage extends StatelessWidget { Center( child: Column( children: [ - const CircleAvatar( + CircleAvatar( radius: 40, - backgroundColor: Colors.white24, + backgroundColor: colors.tertiary.withOpacity(0.3), child: Icon( Icons.person, size: 32, - color: Colors.white, + color: colors.primary, ), ), const SizedBox(height: 6), TextButton( onPressed: () {}, - child: const Text( + child: Text( "Edit", - style: TextStyle(color: Colors.amber), + style: TextStyle(color: colors.secondary), ), ), ], @@ -59,44 +62,42 @@ class SettingsPage extends StatelessWidget { ), const SizedBox(height: 10), - const Divider( + Divider( height: 1, thickness: 0.6, - indent: 0, - endIndent: 0, - color: Color.fromARGB(179, 158, 158, 158), + color: colors.primary.withOpacity(0.3), ), // First & Last Name side-by-side Row( children: [ - Expanded(child: _buildDisplayTile('First Name', 'Penny')), + Expanded(child: _buildDisplayTile('First Name', 'Penny', colors)), const SizedBox(width: 10), - Expanded(child: _buildDisplayTile('Last Name', 'Valeria')), + Expanded(child: _buildDisplayTile('Last Name', 'Valeria', colors)), ], ), - const Divider( + Divider( height: 1, thickness: 0.6, - color: Color.fromARGB(179, 158, 158, 158), + color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Username', '@pennyval'), - const Divider( + _buildDisplayTile('Username', '@pennyval', colors), + Divider( height: 1, thickness: 0.6, - color: Color.fromARGB(179, 158, 158, 158), + color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Email', 'penny@example.com'), - const Divider( + _buildDisplayTile('Email', 'penny@example.com', colors), + Divider( height: 1, thickness: 0.6, - color: Color.fromARGB(179, 158, 158, 158), + color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Phone Number', '+91 1234567890'), - const Divider( + _buildDisplayTile('Phone Number', '+91 1234567890', colors), + Divider( height: 1, thickness: 0.6, - color: Color.fromARGB(179, 158, 158, 158), + color: colors.primary.withOpacity(0.3), ), ], ), @@ -105,28 +106,28 @@ class SettingsPage extends StatelessWidget { const SizedBox(height: 25), // ====== Container 2 ====== - const Text( + Text( 'Password and Authentication', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colors.primary, fontSize: 16), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey.shade900, + color: colors.tertiary.withOpacity(0.15), borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Row( children: [ - Expanded(child: _buildDisplayTile('Password', '••••••••')), + Expanded(child: _buildDisplayTile('Password', '••••••••', colors)), const SizedBox(width: 10), ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( - backgroundColor: Colors.amber, - foregroundColor: Colors.black, + backgroundColor: colors.secondary, + foregroundColor: colors.surface, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 14, @@ -140,11 +141,11 @@ class SettingsPage extends StatelessWidget { ], ), const SizedBox(height: 20), - const Align( + Align( alignment: Alignment.centerLeft, child: Text( 'Account removal', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colors.primary, fontSize: 16), ), ), const SizedBox(height: 12), @@ -153,8 +154,8 @@ class SettingsPage extends StatelessWidget { ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[700], - foregroundColor: Colors.white, + backgroundColor: colors.tertiary.withOpacity(0.5), + foregroundColor: colors.primary, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, @@ -166,8 +167,8 @@ class SettingsPage extends StatelessWidget { ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + backgroundColor: Colors.red, // Keep red for danger + foregroundColor: colors.surface, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, @@ -188,14 +189,14 @@ class SettingsPage extends StatelessWidget { } // Non-editable display tile - Widget _buildDisplayTile(String title, String value) { + Widget _buildDisplayTile(String title, String value, ColorScheme colors) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Container( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.grey.shade800, + color: colors.tertiary.withOpacity(0.3), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -203,23 +204,16 @@ class SettingsPage extends StatelessWidget { children: [ Text( title, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle(color: colors.primary.withOpacity(0.7), fontSize: 12), ), const SizedBox(height: 4), Text( value, - style: const TextStyle(color: Colors.white, fontSize: 14), + style: TextStyle(color: colors.primary, fontSize: 14), ), ], ), ), ); } - - Widget _buildThemeDot(Color color) { - return GestureDetector( - onTap: () {}, - child: CircleAvatar(radius: 14, backgroundColor: color), - ); - } -} +} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/theme_provider.dart b/leaderboard_app/lib/provider/theme_provider.dart index c07b8c4..125da80 100644 --- a/leaderboard_app/lib/provider/theme_provider.dart +++ b/leaderboard_app/lib/provider/theme_provider.dart @@ -6,9 +6,9 @@ class ThemeProvider extends ChangeNotifier { colorScheme: const ColorScheme.dark( surface: Colors.black, // background containers primary: Colors.grey, // text & icons - secondary: Colors.amber, // buttons, highlights + secondary:Color(0xFFF6C156), // buttons, highlights tertiary: Colors.grey, // progress bar track, muted UI - inversePrimary: Colors.amber, // badge/gold accent + inversePrimary: Color(0xFFF6C156), // badge/gold accent ), fontFamily: 'PixelifySans', ); From 50325c4edb9f015e2306ddf9986fc9f6a77e476f Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sat, 9 Aug 2025 12:37:23 +0530 Subject: [PATCH 13/53] removed redundnt pages --- leaderboard_app/lib/pages/files_page.dart | 73 ----------------------- leaderboard_app/lib/pages/media_page.dart | 55 ----------------- 2 files changed, 128 deletions(-) delete mode 100644 leaderboard_app/lib/pages/files_page.dart delete mode 100644 leaderboard_app/lib/pages/media_page.dart diff --git a/leaderboard_app/lib/pages/files_page.dart b/leaderboard_app/lib/pages/files_page.dart deleted file mode 100644 index 6c6d50b..0000000 --- a/leaderboard_app/lib/pages/files_page.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; - -class FilesPage extends StatelessWidget { - const FilesPage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - final List months = ["This month", "May", "April", "March", "February", "January"]; - - return Scaffold( - backgroundColor: theme.surface, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: const BackButton(), - title: const Text("Files", style: TextStyle(fontSize: 16)), - centerTitle: true, - ), - body: ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: months.length, - itemBuilder: (context, index) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(months[index], style: const TextStyle(color: Colors.white)), - const SizedBox(height: 8), - ListView.builder( - itemCount: 2, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, _) { - return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Colors.grey.shade700, - ), - alignment: Alignment.center, - child: const Text("PNG", style: TextStyle(color: Colors.white, fontSize: 10)), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text("blahblah.png", style: TextStyle(color: Colors.white, fontSize: 13)), - Text("4 KB | .png", style: TextStyle(color: Colors.white54, fontSize: 10)), - ], - ), - ], - ), - ); - }, - ), - const SizedBox(height: 20), - ], - ); - }, - ), - ); - } -} \ No newline at end of file diff --git a/leaderboard_app/lib/pages/media_page.dart b/leaderboard_app/lib/pages/media_page.dart deleted file mode 100644 index 8e71e94..0000000 --- a/leaderboard_app/lib/pages/media_page.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -class MediaPage extends StatelessWidget { - const MediaPage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - final List months = ["This month", "May", "April"]; - - return Scaffold( - backgroundColor: theme.surface, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: const BackButton(), - title: const Text("Media", style: TextStyle(fontSize: 16)), - centerTitle: true, - ), - body: ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: months.length, - itemBuilder: (context, index) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(months[index], style: const TextStyle(color: Colors.white)), - const SizedBox(height: 8), - GridView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: 10, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - childAspectRatio: 1, - ), - itemBuilder: (context, _) { - return Container( - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(6), - ), - ); - }, - ), - const SizedBox(height: 20), - ], - ); - }, - ), - ); - } -} \ No newline at end of file From aca4f9ea6af470c8e7e6f76067c90889cdad1274 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 3 Sep 2025 00:08:12 +0530 Subject: [PATCH 14/53] chore: minor fixes --- leaderboard_app/devtools_options.yaml | 3 + leaderboard_app/lib/main.dart | 2 +- leaderboard_app/lib/pages/signin_page.dart | 223 ++++++------- leaderboard_app/lib/pages/signup_page.dart | 301 +++++++++--------- .../lib/services/auth/auth_state.dart | 10 + 5 files changed, 281 insertions(+), 258 deletions(-) create mode 100644 leaderboard_app/devtools_options.yaml create mode 100644 leaderboard_app/lib/services/auth/auth_state.dart diff --git a/leaderboard_app/devtools_options.yaml b/leaderboard_app/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/leaderboard_app/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 232e56a..fd048c8 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -36,7 +36,7 @@ class MainApp extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: themeProvider.themeData, - home: const HomePage(), + home: const SignInPage(), ); } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index 19b2850..cbd57bb 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -8,135 +8,140 @@ class SignInPage extends StatelessWidget { Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; - return Scaffold( - backgroundColor: const Color(0xFF141316), - body: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // Top title - Padding( - padding: const EdgeInsets.only(bottom: 20,), - child: Container( - child: const Center( - child: Text( - 'Sign In', - style: TextStyle( - fontSize: 36, - color: Colors.white, - fontWeight: FontWeight.bold, + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: const Color(0xFF141316), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Top title + Padding( + padding: const EdgeInsets.only(bottom: 20,), + child: Container( + child: const Center( + child: Text( + 'Sign In', + style: TextStyle( + fontSize: 36, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: screenHeight * 0.80, - padding: const EdgeInsets.all(35), - decoration: BoxDecoration( - color: const Color(0xff11b1a1d), - borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 5), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Email or username', - hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: screenHeight * 0.80, + padding: const EdgeInsets.all(35), + decoration: BoxDecoration( + color: const Color(0xff11b1a1d), + borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Email or username', + hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 16), - TextField( - obscureText: true, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Password', - hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + const SizedBox(height: 16), + TextField( + obscureText: true, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Password', + hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () {}, - child: const Text( - 'Forgot Password?', - style: TextStyle(color: Color(0xFFD7FE66)), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text( + 'Forgot Password?', + style: TextStyle(color: Color(0xFFD7FE66)), + ), ), ), - ), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - height: 45, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD7FE66), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD7FE66), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), - ), - onPressed: () {}, - child: const Text( - 'Sign In', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, + onPressed: () {}, + child: const Text( + 'Sign In', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "New here? ", - style: TextStyle(color: Colors.white), - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SignUpPage(), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "New here? ", + style: TextStyle(color: Colors.white), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SignUpPage(), + ), + ); + }, + child: const Text( + "Sign up", + style: TextStyle( + color: Color(0xFFD7FE66), + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, ), - ); - }, - child: const Text( - "Sign up", - style: TextStyle( - color: Color(0xFFD7FE66), - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, ), ), - ), - ], + ], + ), ), - ), - const Spacer(), - ], + const Spacer(), + ], + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index de3b855..a819027 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -8,179 +8,184 @@ class SignUpPage extends StatelessWidget { Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; - return Scaffold( - backgroundColor: const Color(0xFF141316), - body: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // Top title - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Container( - child: const Center( - child: Text( - 'Sign Up', - style: TextStyle( - fontSize: 36, - color: Colors.white, - fontWeight: FontWeight.bold, + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: const Color(0xFF141316), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Top title + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + child: const Center( + child: Text( + 'Sign Up', + style: TextStyle( + fontSize: 36, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: screenHeight * 0.80, - padding: const EdgeInsets.all(35), - decoration: BoxDecoration( - color: const Color(0xff11b1a1d), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(30), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 5), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'First Name', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: screenHeight * 0.80, + padding: const EdgeInsets.all(35), + decoration: BoxDecoration( + color: const Color(0xff11b1a1d), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), ), - const SizedBox(height: 10), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Last Name', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'First Name', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 10), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Username', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Last Name', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 10), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Email', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Username', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 10), - TextField( - obscureText: true, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Password', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + const SizedBox(height: 10), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Email', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), ), ), - ), - const SizedBox(height: 40), - SizedBox( - width: double.infinity, - height: 45, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD7FE66), - shape: RoundedRectangleBorder( + const SizedBox(height: 10), + TextField( + obscureText: true, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'Password', + hintStyle: TextStyle( + color: Colors.grey.withOpacity(0.28), + ), + border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, ), ), - onPressed: () {}, - child: const Text( - 'Get Started', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, + ), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD7FE66), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () {}, + child: const Text( + 'Get Started', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "Already have an account? ", - style: TextStyle(color: Colors.white), - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SignInPage(), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Already have an account? ", + style: TextStyle(color: Colors.white), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SignInPage(), + ), + ); + }, + child: const Text( + "Sign in", + style: TextStyle( + color: Color(0xFFD7FE66), + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, ), - ); - }, - child: const Text( - "Sign in", - style: TextStyle( - color: Color(0xFFD7FE66), - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, ), ), - ), - ], + ], + ), ), - ), - const Spacer(), - ], + const Spacer(), + ], + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/leaderboard_app/lib/services/auth/auth_state.dart b/leaderboard_app/lib/services/auth/auth_state.dart new file mode 100644 index 0000000..08c7cea --- /dev/null +++ b/leaderboard_app/lib/services/auth/auth_state.dart @@ -0,0 +1,10 @@ +enum AuthStatus { loggedIn, loggedOut } + +class AuthState { + final AuthStatus status; + + AuthState({required this.status}); + + bool get isLoggedIn => status == AuthStatus.loggedIn; + bool get isLoggedOut => status == AuthStatus.loggedOut; +} \ No newline at end of file From 31c86ea5c4beba6d83f1fc2737e04a78e676c290 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 3 Sep 2025 01:56:32 +0530 Subject: [PATCH 15/53] feat: sign in and register user works --- .../android/app/src/main/AndroidManifest.xml | 4 +- .../main/res/xml/network_security_config.xml | 5 + leaderboard_app/ios/Runner/Info.plist | 16 ++ .../dashboard-components/daily_activity.dart | 31 ++- .../leaderboard_table.dart | 36 +-- .../dashboard-components/problem_table.dart | 66 +++-- leaderboard_app/lib/main.dart | 66 +++-- leaderboard_app/lib/models/auth_models.dart | 54 ++++ .../lib/models/dashboard_models.dart | 67 +++++ leaderboard_app/lib/pages/dashboard_page.dart | 85 +++++- .../lib/pages/leetcode_verification_page.dart | 0 leaderboard_app/lib/pages/settings_page.dart | 57 +++- leaderboard_app/lib/pages/signin_page.dart | 94 +++++-- leaderboard_app/lib/pages/signup_page.dart | 125 +++++++-- .../lib/provider/user_provider.dart | 2 +- leaderboard_app/lib/router/app_router.dart | 48 ++++ .../lib/services/auth/auth_service.dart | 68 +++++ .../lib/services/core/api_client.dart | 61 +++++ .../lib/services/core/error_utils.dart | 28 ++ .../services/dashboard/dashboard_service.dart | 52 ++++ .../lib/services/groups/group_service.dart | 0 .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 + leaderboard_app/pubspec.lock | 255 +++++++++++++++++- leaderboard_app/pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 28 files changed, 1101 insertions(+), 136 deletions(-) create mode 100644 leaderboard_app/android/app/src/main/res/xml/network_security_config.xml create mode 100644 leaderboard_app/lib/models/auth_models.dart create mode 100644 leaderboard_app/lib/models/dashboard_models.dart create mode 100644 leaderboard_app/lib/pages/leetcode_verification_page.dart create mode 100644 leaderboard_app/lib/router/app_router.dart create mode 100644 leaderboard_app/lib/services/auth/auth_service.dart create mode 100644 leaderboard_app/lib/services/core/api_client.dart create mode 100644 leaderboard_app/lib/services/core/error_utils.dart create mode 100644 leaderboard_app/lib/services/dashboard/dashboard_service.dart create mode 100644 leaderboard_app/lib/services/groups/group_service.dart diff --git a/leaderboard_app/android/app/src/main/AndroidManifest.xml b/leaderboard_app/android/app/src/main/AndroidManifest.xml index d94c1c3..9756e11 100644 --- a/leaderboard_app/android/app/src/main/AndroidManifest.xml +++ b/leaderboard_app/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,9 @@ + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true" + android:networkSecurityConfig="@xml/network_security_config"> + + + + diff --git a/leaderboard_app/ios/Runner/Info.plist b/leaderboard_app/ios/Runner/Info.plist index a01f442..94610ba 100644 --- a/leaderboard_app/ios/Runner/Info.plist +++ b/leaderboard_app/ios/Runner/Info.plist @@ -45,5 +45,21 @@ UIApplicationSupportsIndirectInputEvents + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + diff --git a/leaderboard_app/lib/dashboard-components/daily_activity.dart b/leaderboard_app/lib/dashboard-components/daily_activity.dart index a3a7621..9daaa17 100644 --- a/leaderboard_app/lib/dashboard-components/daily_activity.dart +++ b/leaderboard_app/lib/dashboard-components/daily_activity.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; +import 'package:url_launcher/url_launcher.dart'; class LeetCodeDailyCard extends StatelessWidget { - const LeetCodeDailyCard({super.key}); + final DailyQuestion? daily; + const LeetCodeDailyCard({super.key, required this.daily}); @override Widget build(BuildContext context) { @@ -15,10 +18,11 @@ class LeetCodeDailyCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "'Two Sum' (Easy)\n" - "This will later be replaced with the actual user's daily challenge.", - style: TextStyle( + Text( + daily == null + ? 'Daily question unavailable' + : "${daily!.questionTitle} (${daily!.difficulty})\n${daily!.questionLink}", + style: const TextStyle( color: Colors.white70, fontSize: 14, ), @@ -33,13 +37,16 @@ class LeetCodeDailyCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - onPressed: () { - // TODO: Replace with redirect to LeetCode daily question - }, - child: const Text( - "Go to Question >", - style: TextStyle(color: Colors.black), - ), + onPressed: daily?.questionLink == null + ? null + : () async { + final url = Uri.parse(daily!.questionLink); + if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { + // ignore: avoid_print + print('Could not launch $url'); + } + }, + child: const Text("Go to Question >", style: TextStyle(color: Colors.black)), ), ), ], diff --git a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart index ae58fcf..d4ebd35 100644 --- a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart +++ b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; class LeaderboardTable extends StatelessWidget { - const LeaderboardTable({super.key}); + final List users; + const LeaderboardTable({super.key, required this.users}); @override Widget build(BuildContext context) { @@ -12,7 +14,7 @@ class LeaderboardTable extends StatelessWidget { color: Colors.grey[850], borderRadius: BorderRadius.circular(12), ), - child: DataTable( + child: DataTable( columnSpacing: 10, dataRowMinHeight: 32, dataRowMaxHeight: 36, @@ -68,7 +70,7 @@ class LeaderboardTable extends StatelessWidget { ), ], rows: List.generate( - 5, + users.length, (index) => DataRow( cells: [ DataCell( @@ -82,31 +84,21 @@ class LeaderboardTable extends StatelessWidget { ), DataCell( Text( - "Player ${index + 1}", + users[index].username, style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), - const DataCell( - Text( - "12", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "1324", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), + DataCell(Text( + "${users[index].streak}", + style: const TextStyle(color: Colors.white, fontSize: 12), + )), + DataCell(Text( + "${users[index].totalSolved}", + style: const TextStyle(color: Colors.white, fontSize: 12), + )), const DataCell( Icon( Icons.star, diff --git a/leaderboard_app/lib/dashboard-components/problem_table.dart b/leaderboard_app/lib/dashboard-components/problem_table.dart index b067bc8..349294a 100644 --- a/leaderboard_app/lib/dashboard-components/problem_table.dart +++ b/leaderboard_app/lib/dashboard-components/problem_table.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; class ProblemTable extends StatelessWidget { - const ProblemTable({super.key}); + final List submissions; + const ProblemTable({super.key, required this.submissions}); @override Widget build(BuildContext context) { @@ -12,7 +14,7 @@ class ProblemTable extends StatelessWidget { color: Colors.grey[850], borderRadius: BorderRadius.circular(12), ), - child: DataTable( + child: DataTable( columnSpacing: 10, dataRowMinHeight: 32, dataRowMaxHeight: 36, @@ -68,7 +70,7 @@ class ProblemTable extends StatelessWidget { ), ], rows: List.generate( - 4, + submissions.length, (index) => DataRow( cells: [ DataCell( @@ -80,40 +82,32 @@ class ProblemTable extends StatelessWidget { ), ), ), - const DataCell( - Text( - "Problem", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - const DataCell( - Text( - "56%", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), + DataCell(Text( + submissions[index].title, + style: const TextStyle(color: Colors.white, fontSize: 12), + )), + DataCell(Text( + "${submissions[index].acRate.toStringAsFixed(0)}%", + style: const TextStyle(color: Colors.white, fontSize: 12), + )), + DataCell(Text( + submissions[index].difficulty, + style: TextStyle( + color: submissions[index].difficulty.toLowerCase() == 'easy' + ? Colors.green + : submissions[index].difficulty.toLowerCase() == 'medium' + ? Colors.orange + : Colors.red, + fontSize: 12, ), - ), - const DataCell( - Text( - "Easy", - style: TextStyle( - color: Colors.green, - fontSize: 12, - ), - ), - ), - const DataCell( - Icon( - Icons.circle, - color: Colors.green, - size: 10, - ), - ), + )), + DataCell(Icon( + Icons.circle, + color: submissions[index].statusDisplay.toLowerCase() == 'accepted' + ? Colors.green + : Colors.grey, + size: 10, + )), ], ), ), diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index fd048c8..0d0177c 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -1,42 +1,68 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/pages/home_page.dart'; -import 'package:leaderboard_app/pages/signup_page.dart'; -import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/provider/chat_provider.dart'; import 'package:leaderboard_app/provider/theme_provider.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; +import 'package:leaderboard_app/router/app_router.dart'; +import 'package:leaderboard_app/services/auth/auth_service.dart'; +import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; +import 'package:go_router/go_router.dart'; void main() { - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider( - create: (_) => ChatListProvider()..loadDummyGroups(), - ), - ChangeNotifierProvider(create: (_) => UserProvider()), + WidgetsFlutterBinding.ensureInitialized(); + runApp(const Bootstrap()); +} + +class Bootstrap extends StatelessWidget { + const Bootstrap({super.key}); - // Add ChatProvider here - ChangeNotifierProvider(create: (_) => ChatProvider()), - ], - child: const MainApp(), - ), - ); + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Future.wait([ + AuthService.create(), + DashboardService.create(), + ]), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: CircularProgressIndicator())), + ); + } + final authService = snapshot.data![0] as AuthService; + final dashboardService = snapshot.data![1] as DashboardService; + final router = createRouter(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => ThemeProvider()), + ChangeNotifierProvider(create: (_) => ChatListProvider()..loadDummyGroups()), + ChangeNotifierProvider(create: (_) => UserProvider()), + ChangeNotifierProvider(create: (_) => ChatProvider()), + Provider.value(value: authService), + Provider.value(value: dashboardService), + ], + child: MainApp(router: router), + ); + }, + ); + } } class MainApp extends StatelessWidget { - const MainApp({super.key}); + final GoRouter router; + const MainApp({super.key, required this.router}); @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); - return MaterialApp( + return MaterialApp.router( debugShowCheckedModeBanner: false, theme: themeProvider.themeData, - home: const SignInPage(), + routerConfig: router, ); } } \ No newline at end of file diff --git a/leaderboard_app/lib/models/auth_models.dart b/leaderboard_app/lib/models/auth_models.dart new file mode 100644 index 0000000..95474f9 --- /dev/null +++ b/leaderboard_app/lib/models/auth_models.dart @@ -0,0 +1,54 @@ +class AuthResponse { + final bool success; + final String message; + final String token; + final User user; + + AuthResponse({ + required this.success, + required this.message, + required this.token, + required this.user, + }); + + factory AuthResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + final userJson = (data['user'] ?? json['user'] ?? {}) as Map; + final token = (data['token'] ?? json['token'] ?? '') as String; + return AuthResponse( + success: json['success'] == true || json['ok'] == true, + message: (json['message'] ?? json['msg'] ?? '') as String, + token: token, + user: User.fromJson(userJson), + ); + } +} + +class User { + final String id; + final String username; + final String? email; + final String? leetcodeHandle; + final bool leetcodeVerified; + final int streak; + + User({ + required this.id, + required this.username, + required this.email, + required this.leetcodeHandle, + required this.leetcodeVerified, + required this.streak, + }); + + factory User.fromJson(Map json) { + return User( + id: (json['id'] ?? json['_id'] ?? '').toString(), + username: json['username'] ?? '', + email: json['email'], + leetcodeHandle: json['leetcodeHandle'], + leetcodeVerified: json['leetcodeVerified'] == true, + streak: (json['streak'] is int) ? json['streak'] as int : int.tryParse('${json['streak'] ?? 0}') ?? 0, + ); + } +} diff --git a/leaderboard_app/lib/models/dashboard_models.dart b/leaderboard_app/lib/models/dashboard_models.dart new file mode 100644 index 0000000..9fd83d3 --- /dev/null +++ b/leaderboard_app/lib/models/dashboard_models.dart @@ -0,0 +1,67 @@ +class SubmissionItem { + final String title; + final String titleSlug; + final String statusDisplay; + final String lang; + final double acRate; + final String difficulty; + final DateTime timestamp; + + SubmissionItem({ + required this.title, + required this.titleSlug, + required this.statusDisplay, + required this.lang, + required this.acRate, + required this.difficulty, + required this.timestamp, + }); + + factory SubmissionItem.fromJson(Map json) => SubmissionItem( + title: json['title'] ?? '', + titleSlug: json['titleSlug'] ?? '', + statusDisplay: json['statusDisplay'] ?? '', + lang: json['lang'] ?? '', + acRate: (json['acRate'] ?? 0).toDouble(), + difficulty: json['difficulty'] ?? '', + timestamp: DateTime.fromMillisecondsSinceEpoch( + int.tryParse(json['timestamp']?.toString() ?? '0')! * 1000, + isUtc: true, + ), + ); +} + +class DailyQuestion { + final String questionLink; + final String questionTitle; + final String difficulty; + final DateTime date; + + DailyQuestion({ + required this.questionLink, + required this.questionTitle, + required this.difficulty, + required this.date, + }); + + factory DailyQuestion.fromJson(Map json) => DailyQuestion( + questionLink: json['questionLink'] ?? '', + questionTitle: json['questionTitle'] ?? '', + difficulty: json['difficulty'] ?? '', + date: DateTime.tryParse(json['date'] ?? '') ?? DateTime.now(), + ); +} + +class TopUser { + final String username; + final int streak; + final int totalSolved; + + TopUser({required this.username, required this.streak, required this.totalSolved}); + + factory TopUser.fromJson(Map json) => TopUser( + username: json['username'] ?? '', + streak: (json['streak'] ?? 0) as int, + totalSolved: (json['totalSolved'] ?? 0) as int, + ); +} diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 76225eb..7ea991e 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -5,12 +5,59 @@ import 'package:leaderboard_app/dashboard-components/problem_table.dart'; import 'package:leaderboard_app/dashboard-components/daily_activity.dart'; import 'package:leaderboard_app/dashboard-components/week_view.dart'; import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; import 'package:provider/provider.dart'; -class DashboardPage extends StatelessWidget { +class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); + @override + State createState() => _DashboardPageState(); +} + +class _DashboardPageState extends State { + bool _loading = true; + List _submissions = const []; + List _topUsers = const []; + DailyQuestion? _daily; + String? _error; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + setState(() { + _loading = true; + _error = null; + }); + try { + final service = context.read(); + final results = await Future.wait([ + service.getUserSubmissions(), + service.getTopUsers(), + service.getDailyQuestion(), + ]); + if (!mounted) return; + setState(() { + _submissions = results[0] as List; + _topUsers = results[1] as List; + _daily = results[2] as DailyQuestion?; + }); + } catch (e) { + if (!mounted) return; + setState(() => _error = 'Failed to load dashboard'); + } finally { + if (mounted) { + setState(() => _loading = false); + } + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -93,15 +140,28 @@ class DashboardPage extends StatelessWidget { children: [ const WeekView(), const SizedBox(height: 10), - const LeetCodeDailyCard(), + if (_loading) + _loadingCard(height: 90) + else + LeetCodeDailyCard(daily: _daily), const SizedBox(height: 10), - const LeaderboardTable(), + if (_loading) + _loadingCard(height: 180) + else + LeaderboardTable(users: _topUsers), const SizedBox(height: 10), - const ProblemTable(), + if (_loading) + _loadingCard(height: 180) + else + ProblemTable(submissions: _submissions), const SizedBox(height: 10), const WeeklyStats(), const SizedBox(height: 10), const CompactCalendar(), + if (_error != null) ...[ + const SizedBox(height: 10), + Text(_error!, style: const TextStyle(color: Colors.redAccent)), + ], ], ), ), @@ -132,4 +192,21 @@ class DashboardPage extends StatelessWidget { ), ); } + + Widget _loadingCard({double height = 120}) { + return Container( + width: double.infinity, + height: height, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/leetcode_verification_page.dart b/leaderboard_app/lib/pages/leetcode_verification_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 232819c..8f5eb21 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:leaderboard_app/services/auth/auth_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -17,7 +21,7 @@ class SettingsPage extends StatelessWidget { elevation: 0, foregroundColor: colors.primary, ), - body: ListView( + body: ListView( padding: const EdgeInsets.all(16), children: [ @@ -28,7 +32,17 @@ class SettingsPage extends StatelessWidget { ), const SizedBox(height: 10), - Container( + Consumer( + builder: (context, user, _) { + final name = (user.name).trim(); + final parts = name.split(RegExp(r"\s+")); + final firstName = parts.isNotEmpty && parts.first.isNotEmpty ? parts.first : '-'; + final lastName = parts.length > 1 ? parts.sublist(1).join(' ') : ''; + final username = name.isNotEmpty ? name : '-'; + final email = (user.email).isNotEmpty ? user.email : '-'; + final streak = user.streak; + + return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.tertiary.withOpacity(0.15), @@ -71,9 +85,9 @@ class SettingsPage extends StatelessWidget { // First & Last Name side-by-side Row( children: [ - Expanded(child: _buildDisplayTile('First Name', 'Penny', colors)), + Expanded(child: _buildDisplayTile('First Name', firstName, colors)), const SizedBox(width: 10), - Expanded(child: _buildDisplayTile('Last Name', 'Valeria', colors)), + Expanded(child: _buildDisplayTile('Last Name', lastName.isEmpty ? '-' : lastName, colors)), ], ), Divider( @@ -81,19 +95,19 @@ class SettingsPage extends StatelessWidget { thickness: 0.6, color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Username', '@pennyval', colors), + _buildDisplayTile('Username', '@$username', colors), Divider( height: 1, thickness: 0.6, color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Email', 'penny@example.com', colors), + _buildDisplayTile('Email', email, colors), Divider( height: 1, thickness: 0.6, color: colors.primary.withOpacity(0.3), ), - _buildDisplayTile('Phone Number', '+91 1234567890', colors), + _buildDisplayTile('Streak', streak.toString(), colors), Divider( height: 1, thickness: 0.6, @@ -101,6 +115,8 @@ class SettingsPage extends StatelessWidget { ), ], ), + ); + }, ), const SizedBox(height: 25), @@ -182,7 +198,32 @@ class SettingsPage extends StatelessWidget { ), ), - const SizedBox(height: 100), // Extra space above the bottom + const SizedBox(height: 20), // Extra space above the bottom + + // ====== Logout button (full-width) ====== + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.logout), + label: const Text( + 'Log out', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + onPressed: () async { + await context.read().logout(); + if (!context.mounted) return; + context.go('/signin'); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(52), + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), ], ), ); diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index cbd57bb..d9682e6 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -1,9 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/pages/signup_page.dart'; +import 'package:dio/dio.dart'; +import 'package:go_router/go_router.dart'; +import 'package:leaderboard_app/services/auth/auth_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:leaderboard_app/services/core/error_utils.dart'; -class SignInPage extends StatelessWidget { +class SignInPage extends StatefulWidget { const SignInPage({super.key}); + @override + State createState() => _SignInPageState(); +} + +class _SignInPageState extends State { + final _emailCtrl = TextEditingController(); + final _passwordCtrl = TextEditingController(); + bool _loading = false; + String? _error; + + @override + void dispose() { + _emailCtrl.dispose(); + _passwordCtrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; @@ -47,6 +69,7 @@ class SignInPage extends StatelessWidget { children: [ const SizedBox(height: 5), TextField( + controller: _emailCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -62,6 +85,7 @@ class SignInPage extends StatelessWidget { const SizedBox(height: 16), TextField( obscureText: true, + controller: _passwordCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -86,6 +110,11 @@ class SignInPage extends StatelessWidget { ), ), const SizedBox(height: 10), + if (_error != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text(_error!, style: const TextStyle(color: Colors.redAccent)), + ), SizedBox( width: double.infinity, height: 45, @@ -96,14 +125,20 @@ class SignInPage extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - onPressed: () {}, - child: const Text( - 'Sign In', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), + onPressed: _loading ? null : _onSignIn, + child: _loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), + ) + : const Text( + 'Sign In', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), ), ), Padding( @@ -117,11 +152,7 @@ class SignInPage extends StatelessWidget { ), GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SignUpPage(), - ), - ); + context.go('/signup'); }, child: const Text( "Sign up", @@ -145,4 +176,37 @@ class SignInPage extends StatelessWidget { ), ); } + + Future _onSignIn() async { + setState(() { + _loading = true; + _error = null; + }); + try { + final authService = context.read(); + final res = await authService.signIn(email: _emailCtrl.text.trim(), password: _passwordCtrl.text); + // Update user provider + context.read().updateUser( + name: res.user.username, + email: res.user.email ?? '', + streak: res.user.streak, + ); + if (!mounted) return; + context.go('/'); + } on DioException catch (e) { + setState(() { + _error = ErrorUtils.fromDio(e); + }); + } catch (e) { + setState(() { + _error = 'Something went wrong'; + }); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index a819027..0a783d3 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -1,9 +1,37 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/pages/signin_page.dart'; +import 'package:dio/dio.dart'; +import 'package:go_router/go_router.dart'; +import 'package:leaderboard_app/services/auth/auth_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:leaderboard_app/services/core/error_utils.dart'; -class SignUpPage extends StatelessWidget { +class SignUpPage extends StatefulWidget { const SignUpPage({super.key}); + @override + State createState() => _SignUpPageState(); +} + +class _SignUpPageState extends State { + final _firstNameCtrl = TextEditingController(); + final _lastNameCtrl = TextEditingController(); + final _usernameCtrl = TextEditingController(); + final _emailCtrl = TextEditingController(); + final _passwordCtrl = TextEditingController(); + bool _loading = false; + String? _error; + + @override + void dispose() { + _firstNameCtrl.dispose(); + _lastNameCtrl.dispose(); + _usernameCtrl.dispose(); + _emailCtrl.dispose(); + _passwordCtrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; @@ -49,6 +77,7 @@ class SignUpPage extends StatelessWidget { children: [ const SizedBox(height: 5), TextField( + controller: _firstNameCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -65,6 +94,7 @@ class SignUpPage extends StatelessWidget { ), const SizedBox(height: 10), TextField( + controller: _lastNameCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -81,6 +111,7 @@ class SignUpPage extends StatelessWidget { ), const SizedBox(height: 10), TextField( + controller: _usernameCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -97,6 +128,7 @@ class SignUpPage extends StatelessWidget { ), const SizedBox(height: 10), TextField( + controller: _emailCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -114,6 +146,7 @@ class SignUpPage extends StatelessWidget { const SizedBox(height: 10), TextField( obscureText: true, + controller: _passwordCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, @@ -129,6 +162,11 @@ class SignUpPage extends StatelessWidget { ), ), const SizedBox(height: 40), + if (_error != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text(_error!, style: const TextStyle(color: Colors.redAccent)), + ), SizedBox( width: double.infinity, height: 45, @@ -139,14 +177,20 @@ class SignUpPage extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - onPressed: () {}, - child: const Text( - 'Get Started', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), + onPressed: _loading ? null : _onSignUp, + child: _loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), + ) + : const Text( + 'Get Started', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), ), ), Padding( @@ -160,12 +204,7 @@ class SignUpPage extends StatelessWidget { ), GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SignInPage(), - ), - ); + context.go('/signin'); }, child: const Text( "Sign in", @@ -189,4 +228,58 @@ class SignUpPage extends StatelessWidget { ), ); } + + bool _validate() { + final username = _usernameCtrl.text.trim(); + final email = _emailCtrl.text.trim(); + final password = _passwordCtrl.text; + if (username.isEmpty || email.isEmpty || password.isEmpty) { + setState(() => _error = 'Username, email and password are required'); + return false; + } + if (!RegExp(r"^[^@\s]+@[^@\s]+\.[^@\s]+$").hasMatch(email)) { + setState(() => _error = 'Enter a valid email'); + return false; + } + if (password.length < 6) { + setState(() => _error = 'Password must be at least 6 characters'); + return false; + } + return true; + } + + Future _onSignUp() async { + if (!_validate()) return; + setState(() { + _loading = true; + _error = null; + }); + try { + final authService = context.read(); + final res = await authService.signUp( + username: _usernameCtrl.text.trim(), + email: _emailCtrl.text.trim(), + password: _passwordCtrl.text, + ); + context.read().updateUser( + name: res.user.username, + email: res.user.email ?? '', + streak: res.user.streak, + ); + if (!mounted) return; + context.go('/'); + } on DioException catch (e) { + setState(() { + _error = ErrorUtils.fromDio(e); + }); + } catch (_) { + setState(() { + _error = 'Something went wrong'; + }); + } finally { + if (mounted) { + setState(() => _loading = false); + } + } + } } diff --git a/leaderboard_app/lib/provider/user_provider.dart b/leaderboard_app/lib/provider/user_provider.dart index ec0b06b..c1855ec 100644 --- a/leaderboard_app/lib/provider/user_provider.dart +++ b/leaderboard_app/lib/provider/user_provider.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class UserProvider extends ChangeNotifier { String _name = 'First Name Last Name'; String _email = 'username@email.com'; - int _streak = 8; + int _streak = 0; String get name => _name; String get email => _email; diff --git a/leaderboard_app/lib/router/app_router.dart b/leaderboard_app/lib/router/app_router.dart new file mode 100644 index 0000000..d3f2355 --- /dev/null +++ b/leaderboard_app/lib/router/app_router.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:leaderboard_app/pages/home_page.dart'; +import 'package:leaderboard_app/pages/signin_page.dart'; +import 'package:leaderboard_app/pages/signup_page.dart'; + +Future _isLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('authToken'); + return token != null && token.isNotEmpty; +} + +GoRouter createRouter() { + return GoRouter( + initialLocation: '/signin', + refreshListenable: _RouterRefresh(), + redirect: (context, state) async { + final loggedIn = await _isLoggedIn(); + final atAuth = state.matchedLocation == '/signin' || state.matchedLocation == '/signup'; + if (!loggedIn && !atAuth) return '/signin'; + if (loggedIn && atAuth) return '/'; + return null; + }, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomePage(), + ), + GoRoute( + path: '/signin', + builder: (context, state) => const SignInPage(), + ), + GoRoute( + path: '/signup', + builder: (context, state) => const SignUpPage(), + ), + ], + ); +} + +// A simple ChangeNotifier to trigger router refresh on auth changes +class _RouterRefresh extends ChangeNotifier { + _RouterRefresh() { + // no-op. In a real app you could listen to auth provider. + } +} diff --git a/leaderboard_app/lib/services/auth/auth_service.dart b/leaderboard_app/lib/services/auth/auth_service.dart new file mode 100644 index 0000000..f054d56 --- /dev/null +++ b/leaderboard_app/lib/services/auth/auth_service.dart @@ -0,0 +1,68 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/models/auth_models.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AuthService { + final Dio _dio; + AuthService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return AuthService(client.dio); + } + + Future signUp({required String username, required String email, required String password}) async { + final res = await _dio.post('/auth/signup', data: { + 'username': username, + 'email': email, + 'password': password, + }); + final response = AuthResponse.fromJson(res.data as Map); + if (response.token.isEmpty) { + // Some backends do not return JWT on signup; try to login immediately. + final login = await signIn(email: email, password: password); + return login; + } else { + await _saveAuth(response.token); + return response; + } + } + + Future signIn({required String email, required String password}) async { + final res = await _dio.post('/auth/login', data: { + 'email': email, + 'password': password, + }); + final response = AuthResponse.fromJson(res.data as Map); + if (response.token.isEmpty) { + throw DioException(requestOptions: res.requestOptions, response: res, message: 'Token missing in response'); + } + await _saveAuth(response.token); + return response; + } + + Future logout() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('authToken'); + } + + Future> getProfile() async { + final res = await _dio.get('/user/profile'); + return (res.data as Map)['data'] as Map; + } + + // New: typed user profile fetcher. + Future getUserProfile() async { + final res = await _dio.get('/user/profile'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final userJson = (data['user'] ?? data) as Map; + return User.fromJson(userJson); + } + + Future _saveAuth(String token) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('authToken', token); + } +} diff --git a/leaderboard_app/lib/services/core/api_client.dart b/leaderboard_app/lib/services/core/api_client.dart new file mode 100644 index 0000000..288576e --- /dev/null +++ b/leaderboard_app/lib/services/core/api_client.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; + +class ApiClient { + // Choose a sensible default base URL for each platform. Can be overridden via --dart-define=BASE_URL=... + static String _defaultBaseUrl() { + const fromEnv = String.fromEnvironment('BASE_URL'); + if (fromEnv.isNotEmpty) return fromEnv; + + if (kIsWeb) { + // Web runs in the browser on the host, so localhost maps to the dev machine. + return 'http://localhost:3000/api'; + } + + if (defaultTargetPlatform == TargetPlatform.android) { + // Android emulator can't reach host via localhost; use the special alias. + return 'http://10.0.2.2:3000/api'; + } + + // iOS simulator, desktop, etc. can usually use localhost. + return 'http://localhost:3000/api'; + } + + static final String kBaseUrl = _defaultBaseUrl(); + final Dio dio; + + ApiClient._internal(this.dio); + + static Future create({String? baseUrl}) async { + final prefs = await SharedPreferences.getInstance(); + final dio = Dio( + BaseOptions( + baseUrl: baseUrl ?? kBaseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 20), + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + + dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + final token = prefs.getString('authToken'); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + handler.next(options); + }, + onError: (e, handler) { + // You can add logging or global error handling here + handler.next(e); + }, + ), + ); + + return ApiClient._internal(dio); + } +} diff --git a/leaderboard_app/lib/services/core/error_utils.dart b/leaderboard_app/lib/services/core/error_utils.dart new file mode 100644 index 0000000..7587969 --- /dev/null +++ b/leaderboard_app/lib/services/core/error_utils.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; + +class ErrorUtils { + static String fromDio(Object error) { + if (error is DioException) { + final code = error.response?.statusCode; + final data = error.response?.data; + if (data is Map) { + final msg = data['message'] ?? data['error'] ?? data['msg']; + if (msg is String && msg.isNotEmpty) { + return code != null ? '($code) $msg' : msg; + } + } else if (data is String && data.isNotEmpty) { + return code != null ? '($code) $data' : data; + } + if (error.type == DioExceptionType.connectionTimeout || + error.type == DioExceptionType.receiveTimeout || + error.type == DioExceptionType.sendTimeout) { + return 'Network timeout. Please try again.'; + } + if (error.type == DioExceptionType.connectionError) { + return 'Cannot reach server. Check BASE_URL and that the backend is running.'; + } + return code != null ? 'Request failed ($code)' : 'Request failed'; + } + return 'Unexpected error'; + } +} diff --git a/leaderboard_app/lib/services/dashboard/dashboard_service.dart b/leaderboard_app/lib/services/dashboard/dashboard_service.dart new file mode 100644 index 0000000..2a6b589 --- /dev/null +++ b/leaderboard_app/lib/services/dashboard/dashboard_service.dart @@ -0,0 +1,52 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; + +class DashboardService { + final Dio _dio; + DashboardService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return DashboardService(client.dio); + } + + Future> getUserSubmissions() async { + final res = await _dio.get('/dashboard/submissions'); + final data = (res.data as Map)['data'] as Map; + final list = (data['submissions'] as List).cast>(); + return list.map(SubmissionItem.fromJson).toList(); + } + + Future getDailyQuestion() async { + final res = await _dio.get('/dashboard/daily'); + final data = (res.data as Map)['data'] as Map; + final dq = data['dailyQuestion']; + if (dq == null) return null; + return DailyQuestion.fromJson(dq as Map); + } + + Future> getTopUsers() async { + final res = await _dio.get('/dashboard/leaderboard'); + final data = (res.data as Map)['data'] as Map; + final list = (data['leaderboard'] as List).cast>(); + return list.map(TopUser.fromJson).toList(); + } + + // New: explicit getLeaderboard support. Tries /leaderboard first, falls back to /dashboard/leaderboard. + Future> getLeaderboard() async { + try { + final res = await _dio.get('/leaderboard'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final raw = (data['leaderboard'] ?? data['users'] ?? []) as List; + return raw.cast>().map(TopUser.fromJson).toList(); + } on DioException catch (e) { + // Fallback to the existing dashboard endpoint for compatibility + if (e.response?.statusCode == 404 || e.type == DioExceptionType.unknown) { + return getTopUsers(); + } + rethrow; + } + } +} diff --git a/leaderboard_app/lib/services/groups/group_service.dart b/leaderboard_app/lib/services/groups/group_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/leaderboard_app/linux/flutter/generated_plugin_registrant.cc b/leaderboard_app/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/leaderboard_app/linux/flutter/generated_plugin_registrant.cc +++ b/leaderboard_app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/leaderboard_app/linux/flutter/generated_plugins.cmake b/leaderboard_app/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/leaderboard_app/linux/flutter/generated_plugins.cmake +++ b/leaderboard_app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..997e35d 100644 --- a/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index 51bd14c..207a799 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -49,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +107,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" highlight: dependency: transitive description: @@ -83,6 +128,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -115,6 +168,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -139,6 +200,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -155,6 +224,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" pie_chart: dependency: "direct main" description: @@ -171,6 +264,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" provider: dependency: "direct main" description: @@ -179,6 +288,62 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -232,6 +397,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + url: "https://pub.dev" + source: hosted + version: "6.3.18" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: @@ -248,6 +485,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.9.0-288.0.dev <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index a67ad7a..91817a6 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -13,6 +13,10 @@ dependencies: pie_chart: ^5.4.0 pixelarticons: ^0.4.0 provider: ^6.1.5 + dio: ^5.6.0 + go_router: ^14.2.7 + shared_preferences: ^2.3.2 + url_launcher: ^6.3.0 dev_dependencies: flutter_test: diff --git a/leaderboard_app/windows/flutter/generated_plugin_registrant.cc b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/leaderboard_app/windows/flutter/generated_plugin_registrant.cc +++ b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/leaderboard_app/windows/flutter/generated_plugins.cmake b/leaderboard_app/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/leaderboard_app/windows/flutter/generated_plugins.cmake +++ b/leaderboard_app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 9f822c157e032a3339088f18212ff54c38ef5afc Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 3 Sep 2025 02:51:04 +0530 Subject: [PATCH 16/53] feat: verification page added --- leaderboard_app/lib/main.dart | 8 +- .../lib/pages/leetcode_verification_page.dart | 223 ++++++++++++++++++ leaderboard_app/lib/pages/signin_page.dart | 8 +- leaderboard_app/lib/pages/signup_page.dart | 8 +- leaderboard_app/lib/router/app_router.dart | 9 +- .../services/leetcode/leetcode_service.dart | 74 ++++++ 6 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 leaderboard_app/lib/services/leetcode/leetcode_service.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 0d0177c..82b3d82 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:leaderboard_app/router/app_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; +import 'package:leaderboard_app/services/leetcode/leetcode_service.dart'; import 'package:go_router/go_router.dart'; void main() { @@ -23,6 +24,7 @@ class Bootstrap extends StatelessWidget { future: Future.wait([ AuthService.create(), DashboardService.create(), + LeetCodeService.create(), ]), builder: (context, snapshot) { if (!snapshot.hasData) { @@ -31,8 +33,9 @@ class Bootstrap extends StatelessWidget { home: Scaffold(body: Center(child: CircularProgressIndicator())), ); } - final authService = snapshot.data![0] as AuthService; - final dashboardService = snapshot.data![1] as DashboardService; + final authService = snapshot.data![0] as AuthService; + final dashboardService = snapshot.data![1] as DashboardService; + final leetCodeService = snapshot.data![2] as LeetCodeService; final router = createRouter(); return MultiProvider( @@ -43,6 +46,7 @@ class Bootstrap extends StatelessWidget { ChangeNotifierProvider(create: (_) => ChatProvider()), Provider.value(value: authService), Provider.value(value: dashboardService), + Provider.value(value: leetCodeService), ], child: MainApp(router: router), ); diff --git a/leaderboard_app/lib/pages/leetcode_verification_page.dart b/leaderboard_app/lib/pages/leetcode_verification_page.dart index e69de29..326d5f4 100644 --- a/leaderboard_app/lib/pages/leetcode_verification_page.dart +++ b/leaderboard_app/lib/pages/leetcode_verification_page.dart @@ -0,0 +1,223 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:leaderboard_app/services/core/error_utils.dart'; +import 'package:leaderboard_app/services/leetcode/leetcode_service.dart'; +import 'package:provider/provider.dart'; + +class LeetCodeVerificationPage extends StatefulWidget { + const LeetCodeVerificationPage({super.key}); + + @override + State createState() => _LeetCodeVerificationPageState(); +} + +class _LeetCodeVerificationPageState extends State { + final _usernameCtrl = TextEditingController(); + bool _loading = false; + String? _error; + String? _verificationCode; + String? _instructions; + Timer? _pollTimer; + int _secondsLeft = 0; + + @override + void dispose() { + _pollTimer?.cancel(); + _usernameCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: const Color(0xFF141316), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + child: const Center( + child: Text( + 'Verify LeetCode', + style: TextStyle( + fontSize: 36, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: screenHeight * 0.80, + padding: const EdgeInsets.all(35), + decoration: const BoxDecoration( + color: Color(0xff11b1a1d), + borderRadius: BorderRadius.vertical(top: Radius.circular(30)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + TextField( + controller: _usernameCtrl, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFF141316), + hintText: 'LeetCode username', + hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 12), + if (_error != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text(_error!, style: const TextStyle(color: Colors.redAccent)), + ), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD7FE66), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: _loading ? null : _startVerification, + child: _loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), + ) + : const Text( + 'Start Verification', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 16), + if (_verificationCode != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF141316), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Verification Code', + style: TextStyle(color: Colors.grey[300], fontWeight: FontWeight.bold), + ), + const SizedBox(height: 6), + SelectableText( + _verificationCode!, + style: const TextStyle(color: Colors.white, fontSize: 18), + ), + const SizedBox(height: 12), + Text( + _instructions ?? 'Set this as your LeetCode Real Name, then wait for verification.', + style: TextStyle(color: Colors.grey[400]), + ), + const SizedBox(height: 8), + if (_secondsLeft > 0) + Text( + 'Auto-checking... $_secondsLeft s left', + style: const TextStyle(color: Color(0xFFD7FE66)), + ), + ], + ), + ), + ], + const Spacer(), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Skip for now', style: TextStyle(color: Color(0xFFD7FE66))), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Future _startVerification() async { + final username = _usernameCtrl.text.trim(); + if (username.isEmpty) { + setState(() => _error = 'Enter your LeetCode username'); + return; + } + setState(() { + _loading = true; + _error = null; + }); + try { + final service = context.read(); + final resp = await service.startVerification(username); + final status = await service.getStatus(); + if (status.isVerified) { + if (!mounted) return; + context.go('/'); + return; + } + setState(() { + _verificationCode = resp.verificationCode; + _instructions = resp.instructions; + _secondsLeft = (resp.timeoutInSeconds ?? 120); + }); + _startPolling(); + } on DioException catch (e) { + setState(() => _error = ErrorUtils.fromDio(e)); + } catch (_) { + setState(() => _error = 'Something went wrong'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + void _startPolling() { + _pollTimer?.cancel(); + // Poll every 5s until verified or timeout + _pollTimer = Timer.periodic(const Duration(seconds: 5), (t) async { + setState(() { + _secondsLeft = (_secondsLeft - 5).clamp(0, 9999); + }); + try { + final status = await context.read().getStatus(); + if (status.isVerified) { + t.cancel(); + if (!mounted) return; + context.go('/'); + } else if (_secondsLeft <= 0) { + t.cancel(); + if (mounted) { + setState(() => _error = 'Verification timed out. Try again.'); + } + } + } catch (e) { + // Ignore transient errors and keep polling until timeout + } + }); + } +} + diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index d9682e6..1701c6b 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -191,8 +191,14 @@ class _SignInPageState extends State { email: res.user.email ?? '', streak: res.user.streak, ); + // Fetch current profile to check verification + final profile = await authService.getUserProfile(); if (!mounted) return; - context.go('/'); + if (!profile.leetcodeVerified) { + context.go('/verify'); + } else { + context.go('/'); + } } on DioException catch (e) { setState(() { _error = ErrorUtils.fromDio(e); diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index 0a783d3..1d09bc6 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -266,8 +266,14 @@ class _SignUpPageState extends State { email: res.user.email ?? '', streak: res.user.streak, ); + // Check verification status + final profile = await authService.getUserProfile(); if (!mounted) return; - context.go('/'); + if (!profile.leetcodeVerified) { + context.go('/verify'); + } else { + context.go('/'); + } } on DioException catch (e) { setState(() { _error = ErrorUtils.fromDio(e); diff --git a/leaderboard_app/lib/router/app_router.dart b/leaderboard_app/lib/router/app_router.dart index d3f2355..2a85070 100644 --- a/leaderboard_app/lib/router/app_router.dart +++ b/leaderboard_app/lib/router/app_router.dart @@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:leaderboard_app/pages/home_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/pages/signup_page.dart'; +import 'package:leaderboard_app/pages/leetcode_verification_page.dart'; Future _isLoggedIn() async { final prefs = await SharedPreferences.getInstance(); @@ -19,8 +20,8 @@ GoRouter createRouter() { redirect: (context, state) async { final loggedIn = await _isLoggedIn(); final atAuth = state.matchedLocation == '/signin' || state.matchedLocation == '/signup'; - if (!loggedIn && !atAuth) return '/signin'; - if (loggedIn && atAuth) return '/'; + if (!loggedIn && !atAuth) return '/signin'; + if (loggedIn && atAuth) return '/'; return null; }, routes: [ @@ -36,6 +37,10 @@ GoRouter createRouter() { path: '/signup', builder: (context, state) => const SignUpPage(), ), + GoRoute( + path: '/verify', + builder: (context, state) => const LeetCodeVerificationPage(), + ), ], ); } diff --git a/leaderboard_app/lib/services/leetcode/leetcode_service.dart b/leaderboard_app/lib/services/leetcode/leetcode_service.dart new file mode 100644 index 0000000..845cef5 --- /dev/null +++ b/leaderboard_app/lib/services/leetcode/leetcode_service.dart @@ -0,0 +1,74 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; + +class LeetCodeService { + final Dio _dio; + LeetCodeService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return LeetCodeService(client.dio); + } + + Future startVerification(String username) async { + final res = await _dio.post('/leetcode/connect', data: { + 'leetcodeUsername': username, + }); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return StartVerificationResponse.fromJson(data); + } + + Future getStatus() async { + final res = await _dio.get('/leetcode/status'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return LeetCodeVerificationStatus.fromJson(data); + } +} + +class LeetCodeVerificationStatus { + final bool isVerified; + final bool isInProgress; + final String? leetcodeHandle; + + LeetCodeVerificationStatus({ + required this.isVerified, + required this.isInProgress, + required this.leetcodeHandle, + }); + + factory LeetCodeVerificationStatus.fromJson(Map json) { + return LeetCodeVerificationStatus( + isVerified: json['isVerified'] == true, + isInProgress: json['isInProgress'] == true, + leetcodeHandle: json['leetcodeHandle'] as String?, + ); + } +} + +class StartVerificationResponse { + final String verificationCode; + final String leetcodeUsername; + final int? timeoutInSeconds; + final int? pollIntervalInSeconds; + final String? instructions; + + StartVerificationResponse({ + required this.verificationCode, + required this.leetcodeUsername, + this.timeoutInSeconds, + this.pollIntervalInSeconds, + this.instructions, + }); + + factory StartVerificationResponse.fromJson(Map json) { + return StartVerificationResponse( + verificationCode: (json['verificationCode'] ?? '') as String, + leetcodeUsername: (json['leetcodeUsername'] ?? '') as String, + timeoutInSeconds: (json['timeoutInSeconds'] as num?)?.toInt(), + pollIntervalInSeconds: (json['pollIntervalInSeconds'] as num?)?.toInt(), + instructions: json['instructions'] as String?, + ); + } +} From fee0ae69b72f71173ae0fd92c99f4aee436e5023 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 3 Sep 2025 03:12:02 +0530 Subject: [PATCH 17/53] feat: leetcode verification works, you can join public groupchats? --- .../lib/chatpage-components/chat_view.dart | 2 +- leaderboard_app/lib/main.dart | 12 +- leaderboard_app/lib/models/group_models.dart | 155 +++++ leaderboard_app/lib/pages/chatlists_page.dart | 34 +- leaderboard_app/lib/pages/dashboard_page.dart | 9 + leaderboard_app/lib/pages/groupinfo_page.dart | 598 ++++++++++++------ leaderboard_app/lib/pages/settings_page.dart | 9 + leaderboard_app/lib/pages/signin_page.dart | 7 +- leaderboard_app/lib/pages/signup_page.dart | 6 +- .../lib/provider/chatlists_provider.dart | 34 + .../lib/provider/group_provider.dart | 43 ++ .../lib/provider/user_provider.dart | 61 +- .../lib/services/core/health_service.dart | 21 + .../lib/services/groups/group_service.dart | 116 ++++ .../lib/services/user/user_service.dart | 34 + 15 files changed, 909 insertions(+), 232 deletions(-) create mode 100644 leaderboard_app/lib/models/group_models.dart create mode 100644 leaderboard_app/lib/provider/group_provider.dart create mode 100644 leaderboard_app/lib/services/core/health_service.dart create mode 100644 leaderboard_app/lib/services/user/user_service.dart diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index d720826..5a4176f 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -68,7 +68,7 @@ class _ChatViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (_) => GroupInfoPage(groupName: widget.groupName), + builder: (_) => GroupInfoPage(groupId: widget.groupId, initialName: widget.groupName), ), ); }, diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 82b3d82..5deaaee 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -8,7 +8,10 @@ import 'package:leaderboard_app/router/app_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; import 'package:leaderboard_app/services/leetcode/leetcode_service.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:go_router/go_router.dart'; +import 'package:leaderboard_app/provider/group_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -25,6 +28,8 @@ class Bootstrap extends StatelessWidget { AuthService.create(), DashboardService.create(), LeetCodeService.create(), + GroupService.create(), + UserService.create(), ]), builder: (context, snapshot) { if (!snapshot.hasData) { @@ -36,17 +41,22 @@ class Bootstrap extends StatelessWidget { final authService = snapshot.data![0] as AuthService; final dashboardService = snapshot.data![1] as DashboardService; final leetCodeService = snapshot.data![2] as LeetCodeService; + final groupService = snapshot.data![3] as GroupService; + final userService = snapshot.data![4] as UserService; final router = createRouter(); return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider(create: (_) => ChatListProvider()..loadDummyGroups()), + ChangeNotifierProvider(create: (_) => ChatListProvider()), ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => ChatProvider()), + ChangeNotifierProvider(create: (ctx) => GroupProvider(groupService)), Provider.value(value: authService), Provider.value(value: dashboardService), Provider.value(value: leetCodeService), + Provider.value(value: groupService), + Provider.value(value: userService), ], child: MainApp(router: router), ); diff --git a/leaderboard_app/lib/models/group_models.dart b/leaderboard_app/lib/models/group_models.dart new file mode 100644 index 0000000..821c669 --- /dev/null +++ b/leaderboard_app/lib/models/group_models.dart @@ -0,0 +1,155 @@ +// Model classes for Group APIs + +class Group { + final String id; + final String name; + final String? description; + final bool isPrivate; + final int? maxMembers; + final String? createdBy; + final DateTime? createdAt; + final DateTime? updatedAt; + final List members; + final GroupCreator? creator; + + Group({ + required this.id, + required this.name, + this.description, + required this.isPrivate, + this.maxMembers, + this.createdBy, + this.createdAt, + this.updatedAt, + this.members = const [], + this.creator, + }); + + factory Group.fromJson(Map json) { + final membersRaw = (json['members'] as List?)?.cast>() ?? const []; + return Group( + id: (json['id'] ?? json['_id'] ?? '').toString(), + name: (json['name'] ?? '').toString(), + description: json['description'] as String?, + isPrivate: json['isPrivate'] == true, + maxMembers: (json['maxMembers'] as num?)?.toInt(), + createdBy: (json['createdBy'] ?? json['ownerId'])?.toString(), + createdAt: _tryDate(json['createdAt']), + updatedAt: _tryDate(json['updatedAt']), + members: membersRaw.map(GroupMember.fromJson).toList(growable: false), + creator: json['creator'] != null ? GroupCreator.fromJson(json['creator'] as Map) : null, + ); + } + + static DateTime? _tryDate(dynamic v) { + if (v == null) return null; + if (v is DateTime) return v; + return DateTime.tryParse(v.toString()); + } +} + +class GroupMember { + final String id; + final String userId; + final String groupId; + final String role; // OWNER, ADMIN, MODERATOR, MEMBER + final int xp; + final DateTime? joinedAt; + final GroupMemberUser? user; + + GroupMember({ + required this.id, + required this.userId, + required this.groupId, + required this.role, + required this.xp, + this.joinedAt, + this.user, + }); + + factory GroupMember.fromJson(Map json) { + return GroupMember( + id: (json['id'] ?? json['_id'] ?? '').toString(), + userId: (json['userId'] ?? '').toString(), + groupId: (json['groupId'] ?? '').toString(), + role: (json['role'] ?? 'MEMBER').toString(), + xp: (json['xp'] as num?)?.toInt() ?? 0, + joinedAt: Group._tryDate(json['joinedAt']), + user: json['user'] != null ? GroupMemberUser.fromJson(json['user'] as Map) : null, + ); + } +} + +class GroupCreator { + final String id; + final String username; + final String? email; + + GroupCreator({required this.id, required this.username, this.email}); + + factory GroupCreator.fromJson(Map json) => GroupCreator( + id: (json['id'] ?? json['_id'] ?? '').toString(), + username: (json['username'] ?? '').toString(), + email: json['email'] as String?, + ); +} + +class GroupMemberUser { + final String id; + final String username; + final String? leetcodeHandle; + final bool leetcodeVerified; + + GroupMemberUser({ + required this.id, + required this.username, + this.leetcodeHandle, + required this.leetcodeVerified, + }); + + factory GroupMemberUser.fromJson(Map json) => GroupMemberUser( + id: (json['id'] ?? json['_id'] ?? '').toString(), + username: (json['username'] ?? '').toString(), + leetcodeHandle: json['leetcodeHandle'] as String?, + leetcodeVerified: json['leetcodeVerified'] == true, + ); +} + +class Pagination { + final int currentPage; + final int totalPages; + final int totalCount; + final bool hasNext; + final bool hasPrev; + + Pagination({ + required this.currentPage, + required this.totalPages, + required this.totalCount, + required this.hasNext, + required this.hasPrev, + }); + + factory Pagination.fromJson(Map json) => Pagination( + currentPage: (json['currentPage'] as num?)?.toInt() ?? 1, + totalPages: (json['totalPages'] as num?)?.toInt() ?? 1, + totalCount: (json['totalCount'] as num?)?.toInt() ?? 0, + hasNext: json['hasNext'] == true, + hasPrev: json['hasPrev'] == true, + ); +} + +class PagedGroups { + final List groups; + final Pagination? pagination; + + PagedGroups({required this.groups, this.pagination}); + + factory PagedGroups.fromJson(Map json) { + final groupsRaw = (json['groups'] as List?)?.cast>() ?? const []; + return PagedGroups( + groups: groupsRaw.map(Group.fromJson).toList(growable: false), + pagination: json['pagination'] != null ? Pagination.fromJson(json['pagination'] as Map) : null, + ); + } +} diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 70c97f1..e9e1c7f 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; import 'package:provider/provider.dart'; -import 'chat_page.dart'; +import 'groupinfo_page.dart'; class ChatlistsPage extends StatefulWidget { const ChatlistsPage({super.key}); @@ -31,6 +32,12 @@ class _ChatlistsPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); + // Kick off group load once + if (!chatProvider.isLoading && chatProvider.chatGroups.isEmpty && chatProvider.error == null) { + final svc = context.read(); + // fire and forget + chatProvider.loadPublicGroups(svc); + } final filteredGroups = _applyFilter(chatProvider.chatGroups); return Scaffold( @@ -142,14 +149,18 @@ class _ChatlistsPageState extends State { // Group List Expanded( - child: filteredGroups.isEmpty - ? Center( - child: Text( - "No groups found", - style: TextStyle(color: theme.primary), - ), - ) - : ListView.builder( + child: chatProvider.isLoading + ? const Center(child: CircularProgressIndicator()) + : (chatProvider.error != null) + ? Center(child: Text(chatProvider.error!, style: TextStyle(color: theme.primary))) + : filteredGroups.isEmpty + ? Center( + child: Text( + "No groups found", + style: TextStyle(color: theme.primary), + ), + ) + : ListView.builder( itemCount: filteredGroups.length, itemBuilder: (context, index) { final group = filteredGroups[index]; @@ -165,10 +176,7 @@ class _ChatlistsPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChatPage( - groupId: groupId, - groupName: groupName, - ), + builder: (context) => GroupInfoPage(groupId: groupId, initialName: groupName), ), ); }, diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 7ea991e..4a6c3ca 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -8,6 +8,7 @@ import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:provider/provider.dart'; class DashboardPage extends StatefulWidget { @@ -28,6 +29,14 @@ class _DashboardPageState extends State { void initState() { super.initState(); _loadData(); + // Also load user profile once if not available + WidgetsBinding.instance.addPostFrameCallback((_) { + final up = context.read(); + if (up.user == null) { + final us = context.read(); + up.fetchProfile(us); + } + }); } Future _loadData() async { diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 8678ae4..ce0e779 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -1,33 +1,95 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/provider/chat_provider.dart'; +import 'package:leaderboard_app/models/group_models.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:provider/provider.dart'; -class GroupInfoPage extends StatelessWidget { - final String groupName; +class GroupInfoPage extends StatefulWidget { + final String groupId; + final String? initialName; - const GroupInfoPage({super.key, required this.groupName}); + const GroupInfoPage({super.key, required this.groupId, this.initialName}); + + @override + State createState() => _GroupInfoPageState(); +} + +class _GroupInfoPageState extends State { + Group? _group; + bool _loading = true; + String? _error; + bool _mutating = false; + String? _currentUserId; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() { + _loading = true; + _error = null; + }); + try { + _currentUserId = context.read().user?.id; + final svc = context.read(); + final g = await svc.getGroupById(widget.groupId); + if (!mounted) return; + setState(() => _group = g); + } catch (e) { + if (!mounted) return; + setState(() => _error = 'Failed to load group'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + bool get _isMember { + final uid = _currentUserId; + if (uid == null || _group == null) return false; + return _group!.members.any((m) => m.userId == uid); + } + + bool get _isOwner { + final uid = _currentUserId; + final g = _group; + if (uid == null || g == null) return false; + if (g.creator?.id == uid || g.createdBy == uid) return true; + return g.members.any((m) => m.userId == uid && m.role.toUpperCase() == 'OWNER'); + } + + bool get _isAdminOrOwner { + final uid = _currentUserId; + final g = _group; + if (uid == null || g == null) return false; + if (_isOwner) return true; + return g.members.any((m) => m.userId == uid && (m.role.toUpperCase() == 'ADMIN' || m.role.toUpperCase() == 'MODERATOR')); + } + + Future _joinLeave() async { + if (_group == null) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + if (_isMember) { + await svc.leaveGroup(_group!.id); + } else { + await svc.joinGroup(_group!.id); + } + await _load(); + } catch (e) { + setState(() => _error = 'Operation failed'); + } finally { + setState(() => _mutating = false); + } + } @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; - - // Get ChatProvider instance - final chatProvider = Provider.of(context); - - // Use the dummy users from ChatProvider as members - final members = chatProvider.dummyUsers; - - // For leaderboard, create sample data based on dummy users - final leaderboard = List.generate( - members.length, - (index) => { - "place": index + 1, - "player": members[index]["name"], - "streak": (12 + index).toString(), - "solved": (1324 + index * 10).toString(), - "badge": Icons.star, - }, - ); + final name = _group?.name ?? widget.initialName ?? 'Group'; return Scaffold( backgroundColor: theme.surface, @@ -35,48 +97,83 @@ class GroupInfoPage extends StatelessWidget { backgroundColor: Colors.transparent, leading: const BackButton(), elevation: 0, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - const SizedBox(height: 10), - - // Group icon - const CircleAvatar( - radius: 50, - backgroundColor: Colors.grey, - child: Icon( - Icons.group, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - - // Use dynamic groupName here - Text( - groupName, - style: const TextStyle(color: Colors.white, fontSize: 16), + actions: [ + if (_loading) + const Padding( + padding: EdgeInsets.only(right: 12), + child: Center(child: SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2))), ), - - const SizedBox(height: 16), - - // Members Card - _membersCard(members), - - const SizedBox(height: 16), - - // Leaderboard Card - _leaderboardCard(leaderboard), - - const SizedBox(height: 20), - ], - ), + if (!_loading && _group != null && _isAdminOrOwner) + PopupMenuButton( + onSelected: (value) async { + switch (value) { + case 'edit': + await _showEditGroupDialog(); + break; + case 'delete': + await _confirmDelete(); + break; + case 'transfer': + await _promptTransferOwnership(); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'edit', child: Text('Edit Group')), + const PopupMenuItem(value: 'delete', child: Text('Delete Group')), + if (_isOwner) const PopupMenuItem(value: 'transfer', child: Text('Transfer Ownership')), + ], ), + ], + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center(child: Text(_error!, style: const TextStyle(color: Colors.redAccent))) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 10), + + const CircleAvatar( + radius: 50, + backgroundColor: Colors.grey, + child: Icon( + Icons.group, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text(name, style: const TextStyle(color: Colors.white, fontSize: 16)), + const SizedBox(height: 8), + if (_group?.description != null) + Text( + _group!.description!, + style: const TextStyle(color: Colors.white70), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _mutating ? null : _joinLeave, + style: ElevatedButton.styleFrom(backgroundColor: theme.secondary), + child: Text(_isMember ? 'Leave Group' : 'Join Group', style: const TextStyle(color: Colors.black)), + ), + ), + const SizedBox(height: 16), + _membersCard(_group?.members ?? const []), + const SizedBox(height: 16), + _xpTable(_group?.members ?? const []), + const SizedBox(height: 20), + ], + ), + ), ); } - Widget _membersCard(List> members) { + Widget _membersCard(List members) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -87,168 +184,261 @@ class GroupInfoPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Members", - style: TextStyle(fontSize: 18), - ), + const Text('Members', style: TextStyle(fontSize: 18)), const SizedBox(height: 12), - ...members.map((member) { - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - CircleAvatar( - radius: 18, - backgroundColor: Colors.grey.shade700, - child: Text( - (member["name"] != null && member["name"].isNotEmpty) - ? member["name"][0] - : "?", - style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold), + ...members.map((m) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: Colors.grey.shade700, + child: Text( + (m.user?.username.isNotEmpty == true) ? m.user!.username[0] : '?', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), ), - ), - const SizedBox(width: 12), - Text( - member["name"] ?? "Unknown", - style: const TextStyle(color: Colors.white70, fontSize: 16), - ), - ], - ), - ); - }).toList(), + const SizedBox(width: 12), + Text( + m.user?.username ?? m.userId, + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.blueGrey.shade700, borderRadius: BorderRadius.circular(8)), + child: Text(m.role, style: const TextStyle(fontSize: 12)), + ), + if (_isAdminOrOwner) + PopupMenuButton( + onSelected: (value) async { + switch (value) { + case 'remove': + await _removeMember(m); + break; + case 'promote': + await _changeRole(m, 'ADMIN'); + break; + case 'demote': + await _changeRole(m, 'MEMBER'); + break; + case 'makeOwner': + await _transferOwnershipTo(m); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'remove', child: Text('Remove')), + const PopupMenuItem(value: 'promote', child: Text('Promote to Admin')), + const PopupMenuItem(value: 'demote', child: Text('Demote to Member')), + if (_isOwner) const PopupMenuItem(value: 'makeOwner', child: Text('Make Owner')), + ], + ), + ], + ), + )), ], ), ); } - Widget _leaderboardCard(List> leaderboard) { + Widget _xpTable(List members) { + final sorted = [...members]..sort((a, b) => (b.xp).compareTo(a.xp)); return Container( padding: const EdgeInsets.all(12), width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(12)), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, // Align to left - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Leaderboard", - style: TextStyle(fontSize: 18), - ), + const Text('XP Leaderboard', style: TextStyle(fontSize: 18)), const SizedBox(height: 12), - SizedBox( - width: double.infinity, // Stretch table to max width - child: DataTable( - columnSpacing: 10, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: MaterialStateProperty.all( - Colors.grey[900], + DataTable( + columnSpacing: 10, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: MaterialStateProperty.all(Colors.grey[900]), + columns: const [ + DataColumn(label: Text('Place', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Player', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Role', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('XP', style: TextStyle(color: Colors.white, fontSize: 12))), + ], + rows: List.generate(sorted.length, (i) { + final m = sorted[i]; + return DataRow(cells: [ + DataCell(Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(m.user?.username ?? m.userId, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(m.role, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text('${m.xp}', style: const TextStyle(color: Colors.white, fontSize: 12))), + ]); + }), + ), + ], + ), + ); + } + + Future _showEditGroupDialog() async { + if (_group == null) return; + final nameCtrl = TextEditingController(text: _group!.name); + final descCtrl = TextEditingController(text: _group!.description ?? ''); + bool isPrivate = _group!.isPrivate; + final result = await showDialog( + context: context, + builder: (context) { + final colors = Theme.of(context).colorScheme; + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Edit Group'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameCtrl, + decoration: const InputDecoration(labelText: 'Name'), ), - columns: const [ - DataColumn( - label: Text( - "Place", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Player", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Streak", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Solved", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Badge", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - ], - rows: leaderboard.map((row) { - return DataRow( - cells: [ - DataCell( - Text( - "${row["place"]}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Text( - row["player"] ?? "Player", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Text( - row["streak"] ?? "0", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Text( - row["solved"] ?? "0", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataCell( - Icon( - row["badge"] ?? Icons.star, - color: Colors.amber, - size: 16, - ), - ), - ], - ); - }).toList(), - ), + const SizedBox(height: 8), + TextField( + controller: descCtrl, + decoration: const InputDecoration(labelText: 'Description'), + ), + const SizedBox(height: 8), + SwitchListTile( + title: const Text('Private'), + value: isPrivate, + onChanged: (v) => isPrivate = v, + ), + ], ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), + child: const Text('Save'), + ), + ], + ); + }, + ); + if (result != true) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + await svc.updateGroup(_group!.id, name: nameCtrl.text.trim(), description: descCtrl.text.trim().isEmpty ? null : descCtrl.text.trim(), isPrivate: isPrivate); + await _load(); + } catch (_) { + setState(() => _error = 'Failed to update group'); + } finally { + setState(() => _mutating = false); + } + } + + Future _confirmDelete() async { + if (_group == null) return; + final ok = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Delete Group'), + content: const Text('Are you sure you want to delete this group? This cannot be undone.'), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')), ], ), ); + if (ok != true) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + await svc.deleteGroup(_group!.id); + if (!mounted) return; + Navigator.of(context).pop(); + } catch (_) { + setState(() => _error = 'Failed to delete group'); + } finally { + setState(() => _mutating = false); + } + } + + Future _removeMember(GroupMember m) async { + if (_group == null) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + await svc.removeMember(_group!.id, m.userId); + await _load(); + } catch (_) { + setState(() => _error = 'Failed to remove member'); + } finally { + setState(() => _mutating = false); + } + } + + Future _changeRole(GroupMember m, String role) async { + if (_group == null) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + await svc.updateMemberRole(_group!.id, m.userId, role); + await _load(); + } catch (_) { + setState(() => _error = 'Failed to update role'); + } finally { + setState(() => _mutating = false); + } + } + + Future _promptTransferOwnership() async { + if (_group == null) return; + final members = _group!.members.where((m) => m.userId != _currentUserId).toList(); + String? selectedUserId = members.isNotEmpty ? members.first.userId : null; + final ok = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Transfer Ownership'), + content: DropdownButton( + value: selectedUserId, + items: members + .map((m) => DropdownMenuItem( + value: m.userId, + child: Text(m.user?.username ?? m.userId), + )) + .toList(), + onChanged: (v) { + selectedUserId = v; + }, + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Transfer')), + ], + ), + ); + if (ok == true && selectedUserId != null) { + await _transferOwnershipToUserId(selectedUserId!); + } + } + + Future _transferOwnershipTo(GroupMember m) async { + await _transferOwnershipToUserId(m.userId); + } + + Future _transferOwnershipToUserId(String userId) async { + if (_group == null) return; + setState(() => _mutating = true); + try { + final svc = context.read(); + await svc.transferOwnership(_group!.id, userId); + await _load(); + } catch (_) { + setState(() => _error = 'Failed to transfer ownership'); + } finally { + setState(() => _mutating = false); + } } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 8f5eb21..d50d386 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { @@ -12,6 +13,14 @@ class SettingsPage extends StatelessWidget { final theme = Theme.of(context); final colors = theme.colorScheme; + // trigger profile fetch if not yet + final up = context.watch(); + if (up.user == null && !up.isLoading && up.error == null) { + // fire and forget + final svc = context.read(); + up.fetchProfile(svc); + } + return Scaffold( backgroundColor: colors.surface, appBar: AppBar( diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index 1701c6b..d532fbd 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:provider/provider.dart'; import 'package:leaderboard_app/services/core/error_utils.dart'; @@ -191,8 +192,10 @@ class _SignInPageState extends State { email: res.user.email ?? '', streak: res.user.streak, ); - // Fetch current profile to check verification - final profile = await authService.getUserProfile(); + // Fetch current profile to check verification + final profile = await authService.getUserProfile(); + // Also refresh provider from UserService for consistency + try { await context.read().fetchProfile(context.read()); } catch (_) {} if (!mounted) return; if (!profile.leetcodeVerified) { context.go('/verify'); diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index 1d09bc6..ca3bfad 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:provider/provider.dart'; import 'package:leaderboard_app/services/core/error_utils.dart'; @@ -266,8 +267,9 @@ class _SignUpPageState extends State { email: res.user.email ?? '', streak: res.user.streak, ); - // Check verification status - final profile = await authService.getUserProfile(); + // Check verification status + final profile = await authService.getUserProfile(); + try { await context.read().fetchProfile(context.read()); } catch (_) {} if (!mounted) return; if (!profile.leetcodeVerified) { context.go('/verify'); diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart index 1d7b110..a438b78 100644 --- a/leaderboard_app/lib/provider/chatlists_provider.dart +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; class ChatListProvider extends ChangeNotifier { /// List of group chats List> _chatGroups = []; + bool _isLoading = false; + String? _error; List> get chatGroups => _chatGroups; + bool get isLoading => _isLoading; + String? get error => _error; /// Load dummy group chats void loadDummyGroups() { @@ -28,6 +33,35 @@ class ChatListProvider extends ChangeNotifier { notifyListeners(); } + /// Load groups from backend (public groups) + Future loadPublicGroups(GroupService service, {int page = 1, int limit = 10, String? search}) async { + _isLoading = true; + _error = null; + notifyListeners(); + try { + final paged = await service.getAllGroups(page: page, limit: limit, search: search); + _chatGroups = paged.groups.map((g) { + return { + 'groupId': g.id, + 'name': g.name, + 'lastMessage': '', + 'time': '', + 'members': g.members.map((m) => { + 'uid': m.userId, + 'name': m.user?.username ?? m.userId, + }).toList(), + 'unread': false, + 'favourite': false, + }; + }).toList(); + } catch (e) { + _error = 'Failed to load groups'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + /// Mark a group as read void markGroupAsRead(String groupId) { final index = _chatGroups.indexWhere((group) => group["groupId"] == groupId); diff --git a/leaderboard_app/lib/provider/group_provider.dart b/leaderboard_app/lib/provider/group_provider.dart new file mode 100644 index 0000000..e24d333 --- /dev/null +++ b/leaderboard_app/lib/provider/group_provider.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/group_models.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; + +class GroupProvider extends ChangeNotifier { + final GroupService service; + GroupProvider(this.service); + + bool _loading = false; + String? _error; + List _myGroups = const []; + + bool get isLoading => _loading; + String? get error => _error; + List get myGroups => _myGroups; + + Future loadMyGroups() async { + _loading = true; + _error = null; + notifyListeners(); + try { + _myGroups = await service.getMyGroups(); + } catch (e) { + _error = 'Failed to load my groups'; + } finally { + _loading = false; + notifyListeners(); + } + } + + Future createGroup({required String name, String? description, bool isPrivate = false, int? maxMembers}) async { + try { + final g = await service.createGroup(name: name, description: description, isPrivate: isPrivate, maxMembers: maxMembers); + _myGroups = [..._myGroups, g]; + notifyListeners(); + return g; + } catch (e) { + _error = 'Failed to create group'; + notifyListeners(); + return null; + } + } +} diff --git a/leaderboard_app/lib/provider/user_provider.dart b/leaderboard_app/lib/provider/user_provider.dart index c1855ec..b074336 100644 --- a/leaderboard_app/lib/provider/user_provider.dart +++ b/leaderboard_app/lib/provider/user_provider.dart @@ -1,18 +1,61 @@ import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/auth_models.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; class UserProvider extends ChangeNotifier { - String _name = 'First Name Last Name'; - String _email = 'username@email.com'; - int _streak = 0; + User? _user; + bool _loading = false; + String? _error; - String get name => _name; - String get email => _email; - int get streak => _streak; + String get name => _user?.username ?? 'First Last'; + String get email => _user?.email ?? 'username@email.com'; + int get streak => _user?.streak ?? 0; + User? get user => _user; + bool get isLoading => _loading; + String? get error => _error; void updateUser({required String name, required String email, required int streak}) { - _name = name; - _email = email; - _streak = streak; + _user = User( + id: _user?.id ?? '', + username: name, + email: email, + leetcodeHandle: _user?.leetcodeHandle, + leetcodeVerified: _user?.leetcodeVerified ?? false, + streak: streak, + ); notifyListeners(); } + + Future fetchProfile(UserService service) async { + _loading = true; + _error = null; + notifyListeners(); + try { + _user = await service.getProfile(); + } catch (e) { + _error = 'Failed to load profile'; + } finally { + _loading = false; + notifyListeners(); + } + } + + Future updateStreakRemote(UserService service, int newStreak) async { + try { + await service.updateStreak(newStreak); + if (_user != null) { + _user = User( + id: _user!.id, + username: _user!.username, + email: _user!.email, + leetcodeHandle: _user!.leetcodeHandle, + leetcodeVerified: _user!.leetcodeVerified, + streak: newStreak, + ); + notifyListeners(); + } + } catch (e) { + // swallow error for now + } + } } diff --git a/leaderboard_app/lib/services/core/health_service.dart b/leaderboard_app/lib/services/core/health_service.dart new file mode 100644 index 0000000..40174db --- /dev/null +++ b/leaderboard_app/lib/services/core/health_service.dart @@ -0,0 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; + +class HealthService { + final Dio _dio; + HealthService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return HealthService(client.dio); + } + + Future isHealthy() async { + try { + final res = await _dio.get('/health'); + return (res.statusCode ?? 200) >= 200 && (res.statusCode ?? 200) < 300; + } catch (_) { + return false; + } + } +} diff --git a/leaderboard_app/lib/services/groups/group_service.dart b/leaderboard_app/lib/services/groups/group_service.dart index e69de29..9a55d33 100644 --- a/leaderboard_app/lib/services/groups/group_service.dart +++ b/leaderboard_app/lib/services/groups/group_service.dart @@ -0,0 +1,116 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/models/group_models.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; + +class GroupService { + final Dio _dio; + GroupService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return GroupService(client.dio); + } + + // Public: Get all groups with pagination and optional search + Future getAllGroups({int page = 1, int limit = 10, String? search}) async { + final res = await _dio.get('/groups', queryParameters: { + 'page': page, + 'limit': limit, + if (search != null && search.isNotEmpty) 'search': search, + }); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return PagedGroups.fromJson(data); + } + + // Public: Get group by ID + Future getGroupById(String groupId) async { + final res = await _dio.get('/groups/$groupId'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return Group.fromJson(data); + } + + // Public: Get group members + Future> getGroupMembers(String groupId) async { + final res = await _dio.get('/groups/$groupId/members'); + final body = res.data as Map; + final data = (body['data'] ?? body) as List; + return data.cast>().map(GroupMember.fromJson).toList(); + } + + // Protected: Create group + Future createGroup({required String name, String? description, bool isPrivate = false, int? maxMembers}) async { + final res = await _dio.post('/groups', data: { + 'name': name, + if (description != null) 'description': description, + 'isPrivate': isPrivate, + if (maxMembers != null) 'maxMembers': maxMembers, + }); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return Group.fromJson(data); + } + + // Protected: Get user's groups + Future> getMyGroups() async { + final res = await _dio.get('/groups/user/my-groups'); + final body = res.data as Map; + final data = (body['data'] ?? body); + if (data is List) { + return data.cast>().map(Group.fromJson).toList(); + } + if (data is Map && data['groups'] is List) { + return (data['groups'] as List).cast>().map(Group.fromJson).toList(); + } + return const []; + } + + // Protected: Update group + Future updateGroup(String groupId, {required String name, String? description, required bool isPrivate, int? maxMembers}) async { + final res = await _dio.put('/groups/$groupId', data: { + 'name': name, + if (description != null) 'description': description, + 'isPrivate': isPrivate, + if (maxMembers != null) 'maxMembers': maxMembers, + }); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + return Group.fromJson(data); + } + + // Protected: Delete group + Future deleteGroup(String groupId) async { + await _dio.delete('/groups/$groupId'); + } + + // Membership: Join group + Future joinGroup(String groupId) async { + await _dio.post('/groups/$groupId/join'); + } + + // Membership: Leave group + Future leaveGroup(String groupId) async { + await _dio.delete('/groups/$groupId/leave'); + } + + // Management: Remove member + Future removeMember(String groupId, String userId) async { + await _dio.delete('/groups/$groupId/members/$userId'); + } + + // Management: Update member role + Future updateMemberRole(String groupId, String userId, String role) async { + await _dio.put('/groups/$groupId/members/$userId/role', data: { + 'role': role, + }); + } + + // Management: Transfer ownership + Future transferOwnership(String groupId, String newOwnerId) async { + await _dio.post('/groups/$groupId/transfer-ownership', data: { + 'newOwnerId': newOwnerId, + }); + } +} + diff --git a/leaderboard_app/lib/services/user/user_service.dart b/leaderboard_app/lib/services/user/user_service.dart new file mode 100644 index 0000000..7103f46 --- /dev/null +++ b/leaderboard_app/lib/services/user/user_service.dart @@ -0,0 +1,34 @@ +import 'package:dio/dio.dart'; +import 'package:leaderboard_app/models/auth_models.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; +import 'package:leaderboard_app/services/core/api_client.dart'; + +class UserService { + final Dio _dio; + UserService(this._dio); + + static Future create() async { + final client = await ApiClient.create(); + return UserService(client.dio); + } + + Future getProfile() async { + final res = await _dio.get('/user/profile'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final userJson = (data['user'] ?? data) as Map; + return User.fromJson(userJson); + } + + Future> getPublicLeaderboard() async { + final res = await _dio.get('/user/leaderboard'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final list = (data['leaderboard'] ?? data['users'] ?? []) as List; + return list.cast>().map(TopUser.fromJson).toList(); + } + + Future updateStreak(int streak) async { + await _dio.patch('/user/streak', data: {'streak': streak}); + } +} From 7b07bcea309b32b851fd0fe0e3a3f146ea16466b Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 3 Sep 2025 03:32:24 +0530 Subject: [PATCH 18/53] feat: integrated dashboard partially --- leaderboard_app/lib/main.dart | 2 + leaderboard_app/lib/pages/chatlists_page.dart | 23 +++-- leaderboard_app/lib/pages/dashboard_page.dart | 97 ++++++++++--------- .../lib/provider/dashboard_provider.dart | 79 +++++++++++++++ .../services/dashboard/dashboard_service.dart | 39 +++++--- 5 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 leaderboard_app/lib/provider/dashboard_provider.dart diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 5deaaee..06fde56 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -12,6 +12,7 @@ import 'package:leaderboard_app/services/groups/group_service.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/provider/group_provider.dart'; +import 'package:leaderboard_app/provider/dashboard_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -52,6 +53,7 @@ class Bootstrap extends StatelessWidget { ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => ChatProvider()), ChangeNotifierProvider(create: (ctx) => GroupProvider(groupService)), + ChangeNotifierProvider(create: (ctx) => DashboardProvider(service: dashboardService, userProvider: ctx.read())), Provider.value(value: authService), Provider.value(value: dashboardService), Provider.value(value: leetCodeService), diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index e9e1c7f..1c39034 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -14,6 +14,21 @@ class ChatlistsPage extends StatefulWidget { class _ChatlistsPageState extends State { final List filters = const ["All", "Unread", "Favourites"]; String selectedFilter = "All"; + bool _loadedOnce = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _loadedOnce) return; + _loadedOnce = true; + final chatProvider = context.read(); + if (!chatProvider.isLoading && chatProvider.chatGroups.isEmpty && chatProvider.error == null) { + final svc = context.read(); + chatProvider.loadPublicGroups(svc); + } + }); + } List> _applyFilter(List> chatGroups) { switch (selectedFilter) { @@ -31,13 +46,7 @@ class _ChatlistsPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; - final chatProvider = Provider.of(context); - // Kick off group load once - if (!chatProvider.isLoading && chatProvider.chatGroups.isEmpty && chatProvider.error == null) { - final svc = context.read(); - // fire and forget - chatProvider.loadPublicGroups(svc); - } + final chatProvider = Provider.of(context); final filteredGroups = _applyFilter(chatProvider.chatGroups); return Scaffold( diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 4a6c3ca..05802ad 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -5,10 +5,11 @@ import 'package:leaderboard_app/dashboard-components/problem_table.dart'; import 'package:leaderboard_app/dashboard-components/daily_activity.dart'; import 'package:leaderboard_app/dashboard-components/week_view.dart'; import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; -import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; -import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; +// models via provider components +// import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; +import 'package:leaderboard_app/provider/dashboard_provider.dart'; import 'package:provider/provider.dart'; class DashboardPage extends StatefulWidget { @@ -19,16 +20,16 @@ class DashboardPage extends StatefulWidget { } class _DashboardPageState extends State { - bool _loading = true; - List _submissions = const []; - List _topUsers = const []; - DailyQuestion? _daily; - String? _error; + String? _error; // page-level error @override void initState() { super.initState(); - _loadData(); + // Start loading all dashboard data via provider + WidgetsBinding.instance.addPostFrameCallback((_) { + final dp = context.read(); + dp.loadAll(); + }); // Also load user profile once if not available WidgetsBinding.instance.addPostFrameCallback((_) { final up = context.read(); @@ -39,33 +40,7 @@ class _DashboardPageState extends State { }); } - Future _loadData() async { - setState(() { - _loading = true; - _error = null; - }); - try { - final service = context.read(); - final results = await Future.wait([ - service.getUserSubmissions(), - service.getTopUsers(), - service.getDailyQuestion(), - ]); - if (!mounted) return; - setState(() { - _submissions = results[0] as List; - _topUsers = results[1] as List; - _daily = results[2] as DailyQuestion?; - }); - } catch (e) { - if (!mounted) return; - setState(() => _error = 'Failed to load dashboard'); - } finally { - if (mounted) { - setState(() => _loading = false); - } - } - } + // legacy loader removed; using DashboardProvider @override Widget build(BuildContext context) { @@ -149,20 +124,27 @@ class _DashboardPageState extends State { children: [ const WeekView(), const SizedBox(height: 10), - if (_loading) - _loadingCard(height: 90) - else - LeetCodeDailyCard(daily: _daily), + Consumer( + builder: (_, dp, __) => dp.loadingDaily + ? _loadingCard(height: 90) + : LeetCodeDailyCard(daily: dp.daily), + ), const SizedBox(height: 10), - if (_loading) - _loadingCard(height: 180) - else - LeaderboardTable(users: _topUsers), + Consumer( + builder: (_, dp, __) => dp.loadingLeaders + ? _loadingCard(height: 180) + : LeaderboardTable(users: dp.leaderboard), + ), const SizedBox(height: 10), - if (_loading) - _loadingCard(height: 180) - else - ProblemTable(submissions: _submissions), + Consumer( + builder: (_, dp, __) { + if (dp.loadingSubs) return _loadingCard(height: 180); + if (!dp.isVerified) { + return _verifyCard(); + } + return ProblemTable(submissions: dp.submissions); + }, + ), const SizedBox(height: 10), const WeeklyStats(), const SizedBox(height: 10), @@ -218,4 +200,25 @@ class _DashboardPageState extends State { ), ); } + + Widget _verifyCard() { + return Container( + width: double.infinity, + height: 160, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text('Connect your LeetCode account to see recent submissions and streaks', + style: TextStyle(color: Colors.white70)), + SizedBox(height: 8), + Text('Go to Settings > Verify LeetCode', style: TextStyle(color: Colors.white54, fontSize: 12)), + ], + ), + ); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/provider/dashboard_provider.dart b/leaderboard_app/lib/provider/dashboard_provider.dart new file mode 100644 index 0000000..6b5be18 --- /dev/null +++ b/leaderboard_app/lib/provider/dashboard_provider.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/dashboard_models.dart'; +import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; + +class DashboardProvider extends ChangeNotifier { + final DashboardService service; + final UserProvider userProvider; + + DashboardProvider({required this.service, required this.userProvider}); + + bool loadingDaily = false; + bool loadingSubs = false; + bool loadingLeaders = false; + + String? errorDaily; + String? errorSubs; + String? errorLeaders; + + DailyQuestion? daily; + List submissions = const []; + List leaderboard = const []; + + bool get isVerified => userProvider.user?.leetcodeVerified == true; + + Future loadAll() async { + await Future.wait([ + loadDaily(), + loadSubmissions(), + loadLeaderboard(), + ]); + } + + Future loadDaily() async { + loadingDaily = true; + errorDaily = null; + notifyListeners(); + try { + daily = await service.getDailyQuestion(); + } catch (_) { + errorDaily = 'Failed to load today\'s question'; + } finally { + loadingDaily = false; + notifyListeners(); + } + } + + Future loadSubmissions() async { + loadingSubs = true; + errorSubs = null; + notifyListeners(); + try { + if (!isVerified) { + submissions = const []; + } else { + submissions = await service.getUserSubmissions(); + } + } catch (_) { + errorSubs = 'Failed to load recent submissions'; + } finally { + loadingSubs = false; + notifyListeners(); + } + } + + Future loadLeaderboard() async { + loadingLeaders = true; + errorLeaders = null; + notifyListeners(); + try { + leaderboard = await service.getTopUsers(); + } catch (_) { + errorLeaders = 'Failed to load leaderboard'; + } finally { + loadingLeaders = false; + notifyListeners(); + } + } +} diff --git a/leaderboard_app/lib/services/dashboard/dashboard_service.dart b/leaderboard_app/lib/services/dashboard/dashboard_service.dart index 2a6b589..edc4c0c 100644 --- a/leaderboard_app/lib/services/dashboard/dashboard_service.dart +++ b/leaderboard_app/lib/services/dashboard/dashboard_service.dart @@ -12,25 +12,38 @@ class DashboardService { } Future> getUserSubmissions() async { - final res = await _dio.get('/dashboard/submissions'); - final data = (res.data as Map)['data'] as Map; - final list = (data['submissions'] as List).cast>(); - return list.map(SubmissionItem.fromJson).toList(); + final res = await _dio.get('/dashboard/submissions'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final raw = (data['submissions'] ?? data['items'] ?? data['results'] ?? data) as dynamic; + final list = raw is List + ? raw.cast>() + : (raw is Map && raw['submissions'] is List) + ? (raw['submissions'] as List).cast>() + : >[]; + return list.map(SubmissionItem.fromJson).toList(); } Future getDailyQuestion() async { - final res = await _dio.get('/dashboard/daily'); - final data = (res.data as Map)['data'] as Map; - final dq = data['dailyQuestion']; - if (dq == null) return null; - return DailyQuestion.fromJson(dq as Map); + final res = await _dio.get('/dashboard/daily'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final dq = (data['dailyQuestion'] ?? data['daily'] ?? data['question'] ?? data) as dynamic; + if (dq is Map) return DailyQuestion.fromJson(dq); + return null; } Future> getTopUsers() async { - final res = await _dio.get('/dashboard/leaderboard'); - final data = (res.data as Map)['data'] as Map; - final list = (data['leaderboard'] as List).cast>(); - return list.map(TopUser.fromJson).toList(); + final res = await _dio.get('/dashboard/leaderboard'); + final body = res.data as Map; + final data = (body['data'] ?? body) as Map; + final raw = (data['leaderboard'] ?? data['users'] ?? data['results'] ?? data) as dynamic; + final list = raw is List + ? raw.cast>() + : (raw is Map && raw['leaderboard'] is List) + ? (raw['leaderboard'] as List).cast>() + : >[]; + return list.map(TopUser.fromJson).toList(); } // New: explicit getLeaderboard support. Tries /leaderboard first, falls back to /dashboard/leaderboard. From af37a4cd48fa1f04dd5521180b391c083de590d5 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sun, 28 Sep 2025 23:12:50 +0530 Subject: [PATCH 19/53] feat: settings page integration complete --- leaderboard_app/README.md | 61 +++- leaderboard_app/lib/models/api_wrappers.dart | 267 ++++++++++++++ leaderboard_app/lib/models/group_models.dart | 33 ++ leaderboard_app/lib/models/models.dart | 8 + .../lib/models/verification_models.dart | 43 +++ leaderboard_app/lib/pages/settings_page.dart | 117 +++--- .../lib/services/core/dio_provider.dart | 104 ++++++ .../lib/services/core/rest_client.dart | 28 ++ .../lib/services/core/rest_client.g.dart | 184 ++++++++++ leaderboard_app/pubspec.lock | 344 ++++++++++++++++++ leaderboard_app/pubspec.yaml | 5 + 11 files changed, 1123 insertions(+), 71 deletions(-) create mode 100644 leaderboard_app/lib/models/api_wrappers.dart create mode 100644 leaderboard_app/lib/models/models.dart create mode 100644 leaderboard_app/lib/models/verification_models.dart create mode 100644 leaderboard_app/lib/services/core/dio_provider.dart create mode 100644 leaderboard_app/lib/services/core/rest_client.dart create mode 100644 leaderboard_app/lib/services/core/rest_client.g.dart diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md index cbfad68..82a60a1 100644 --- a/leaderboard_app/README.md +++ b/leaderboard_app/README.md @@ -1,3 +1,62 @@ # leaderboard_app -A new Flutter project. +Flutter application with backend integration using `dio` + `retrofit` for typed HTTP APIs. + +## Backend Integration + +We use: + +* `dio` for HTTP transport, interceptors, timeouts. +* `retrofit` for declarative REST interface generation (`lib/services/core/rest_client.dart`). +* `build_runner` + `retrofit_generator` (and `json_serializable` if/when model code generation is added). + +### Generating code + +Run code generation after updating API interface annotations: + +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +### Using the REST client + +```dart +import 'package:leaderboard_app/services/core/dio_provider.dart'; +import 'package:leaderboard_app/services/core/rest_client.dart'; + +final dio = await DioProvider.getInstance(); +final api = RestClient(dio); + +final start = await api.startVerification({'leetcodeUsername': 'someUser'}); +final status = await api.getVerificationStatus('someUser'); +``` + +Auth tokens (JWT) are automatically attached from `SharedPreferences` via an interceptor in `DioProvider`. + +### Environment / Base URL + +Override API base URL at build time: + +```bash +flutter run --dart-define=BASE_URL=https://your.api.host/api +``` + +Default development URLs: + +* Web / Desktop / iOS simulator: `http://localhost:3000/api` +* Android emulator: `http://10.0.2.2:3000/api` + +### Adding new endpoints + +1. Edit `lib/services/core/rest_client.dart` – add a method with appropriate HTTP verb annotation. +2. Run the build command above to regenerate `rest_client.g.dart`. +3. Consume the new method from services or providers. + +### Logging & Retry + +`DioProvider` adds a lightweight log interceptor and simple retry (only once) for idempotent GET requests on connection errors. + +--- + +Generated code (`rest_client.g.dart`) should not be manually edited. + diff --git a/leaderboard_app/lib/models/api_wrappers.dart b/leaderboard_app/lib/models/api_wrappers.dart new file mode 100644 index 0000000..fac8847 --- /dev/null +++ b/leaderboard_app/lib/models/api_wrappers.dart @@ -0,0 +1,267 @@ +// Generic-ish API response helpers and specific wrapper models for +// various backend endpoints. These map the `data` object of the backend +// responses so UI/widgets can depend on strongly typed structures. +// +// Each backend response uses the envelope: +// { +// "success": bool, +// "message": String, +// "timestamp": String (ISO)?, +// "data": { ... } // shape varies per endpoint +// } +// +// We provide a light-weight [ApiEnvelope] plus concrete data wrappers. + + +import 'auth_models.dart'; +import 'dashboard_models.dart'; +import 'group_models.dart'; +import 'verification_models.dart'; + +DateTime? _parseTime(dynamic v) { + if (v == null) return null; + if (v is DateTime) return v; + return DateTime.tryParse(v.toString()); +} + +/// Base envelope for an API response. The [data] field is left dynamic; most +/// callers should prefer using the concrete `XYZResponse` classes below. +class ApiEnvelope { + final bool success; + final String message; + final DateTime? timestamp; + final T? data; + + ApiEnvelope({ + required this.success, + required this.message, + required this.timestamp, + required this.data, + }); + + factory ApiEnvelope.fromJson( + Map json, { + T Function(Object? json)? parse, + }) { + final rawData = json['data']; + return ApiEnvelope( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + data: parse != null ? parse(rawData) : rawData as T?, + ); + } +} + +/// User profile (`GET /user/profile`) +class UserProfileResponse { + final bool success; + final String message; + final DateTime? timestamp; + final User user; + + UserProfileResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.user, + }); + + factory UserProfileResponse.fromJson(Map json) { + final data = (json['data'] ?? json) as Map; + final userJson = (data['user'] ?? data) as Map; + return UserProfileResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + user: User.fromJson(userJson), + ); + } +} + +/// Group list (`GET /groups`) +class GroupsResponse { + final bool success; + final String message; + final DateTime? timestamp; + final PagedGroups groups; + + GroupsResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.groups, + }); + + factory GroupsResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + return GroupsResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + groups: PagedGroups.fromJson(data), + ); + } +} + +/// Single group (`GET /groups/:id`, create, update) +class GroupResponse { + final bool success; + final String message; + final DateTime? timestamp; + final Group group; + + GroupResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.group, + }); + + factory GroupResponse.fromJson(Map json) { + final data = (json['data'] ?? json) as Map; + final groupJson = (data['group'] ?? data) as Map; + return GroupResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + group: Group.fromJson(groupJson), + ); + } +} + +/// LeetCode verification start (`POST /leetcode/connect`) +class VerificationStartResponse { + final bool success; + final String message; + final DateTime? timestamp; + final VerificationStart data; + + VerificationStartResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.data, + }); + + factory VerificationStartResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + return VerificationStartResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + data: VerificationStart.fromJson(data), + ); + } +} + +/// LeetCode verification status (`GET /leetcode/status`) +class VerificationStatusResponse { + final bool success; + final String message; + final DateTime? timestamp; + final VerificationStatus data; + + VerificationStatusResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.data, + }); + + factory VerificationStatusResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + return VerificationStatusResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + data: VerificationStatus.fromJson(data), + ); + } +} + +/// Dashboard submissions (`GET /dashboard/submissions`) +class SubmissionsResponse { + final bool success; + final String message; + final DateTime? timestamp; + final List submissions; + final int count; + final int limit; + + SubmissionsResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.submissions, + required this.count, + required this.limit, + }); + + factory SubmissionsResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + final list = (data['submissions'] as List?)?.cast>() ?? const []; + return SubmissionsResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + submissions: list.map(SubmissionItem.fromJson).toList(growable: false), + count: (data['count'] as num?)?.toInt() ?? list.length, + limit: (data['limit'] as num?)?.toInt() ?? list.length, + ); + } +} + +/// Daily question (`GET /dashboard/daily`) +class DailyQuestionResponse { + final bool success; + final String message; + final DateTime? timestamp; + final DailyQuestion dailyQuestion; + + DailyQuestionResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.dailyQuestion, + }); + + factory DailyQuestionResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + return DailyQuestionResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + dailyQuestion: DailyQuestion.fromJson((data['dailyQuestion'] ?? {}) as Map), + ); + } +} + +/// Top users leaderboard (`GET /dashboard/leaderboard` or `/user/leaderboard`) +class TopUsersResponse { + final bool success; + final String message; + final DateTime? timestamp; + final List leaderboard; + final int count; + + TopUsersResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.leaderboard, + required this.count, + }); + + factory TopUsersResponse.fromJson(Map json) { + final data = (json['data'] ?? {}) as Map; + final list = (data['leaderboard'] as List?)?.cast>() ?? const []; + return TopUsersResponse( + success: json['success'] == true, + message: (json['message'] ?? '') as String, + timestamp: _parseTime(json['timestamp']), + leaderboard: list.map(TopUser.fromJson).toList(growable: false), + count: (data['count'] as num?)?.toInt() ?? list.length, + ); + } +} diff --git a/leaderboard_app/lib/models/group_models.dart b/leaderboard_app/lib/models/group_models.dart index 821c669..0695fce 100644 --- a/leaderboard_app/lib/models/group_models.dart +++ b/leaderboard_app/lib/models/group_models.dart @@ -80,6 +80,39 @@ class GroupMember { } } +/// Optional strongly typed role enum. Use [GroupRoleExt.parse] to convert +/// backend string values without throwing. +enum GroupRole { owner, admin, moderator, member } + +extension GroupRoleExt on GroupRole { + static GroupRole parse(String? raw) { + switch (raw?.toUpperCase()) { + case 'OWNER': + return GroupRole.owner; + case 'ADMIN': + return GroupRole.admin; + case 'MODERATOR': + return GroupRole.moderator; + default: + return GroupRole.member; + } + } + + String get asApiValue { + switch (this) { + case GroupRole.owner: + return 'OWNER'; + case GroupRole.admin: + return 'ADMIN'; + case GroupRole.moderator: + return 'MODERATOR'; + case GroupRole.member: + return 'MEMBER'; + } + } +} + + class GroupCreator { final String id; final String username; diff --git a/leaderboard_app/lib/models/models.dart b/leaderboard_app/lib/models/models.dart new file mode 100644 index 0000000..66b242c --- /dev/null +++ b/leaderboard_app/lib/models/models.dart @@ -0,0 +1,8 @@ +// Barrel export for all model files. Import this file when you need access +// to multiple backend models in a feature/widget. + +export 'auth_models.dart'; +export 'dashboard_models.dart'; +export 'group_models.dart'; +export 'verification_models.dart'; +export 'api_wrappers.dart'; diff --git a/leaderboard_app/lib/models/verification_models.dart b/leaderboard_app/lib/models/verification_models.dart new file mode 100644 index 0000000..3975b0d --- /dev/null +++ b/leaderboard_app/lib/models/verification_models.dart @@ -0,0 +1,43 @@ +// Models specifically for LeetCode verification endpoints. + +class VerificationStart { + final String verificationCode; + final String leetcodeUsername; + final String instructions; + final int timeoutInSeconds; + final int pollIntervalInSeconds; + + VerificationStart({ + required this.verificationCode, + required this.leetcodeUsername, + required this.instructions, + required this.timeoutInSeconds, + required this.pollIntervalInSeconds, + }); + + factory VerificationStart.fromJson(Map json) => VerificationStart( + verificationCode: (json['verificationCode'] ?? '') as String, + leetcodeUsername: (json['leetcodeUsername'] ?? '') as String, + instructions: (json['instructions'] ?? '') as String, + timeoutInSeconds: (json['timeoutInSeconds'] as num?)?.toInt() ?? 0, + pollIntervalInSeconds: (json['pollIntervalInSeconds'] as num?)?.toInt() ?? 0, + ); +} + +class VerificationStatus { + final bool isVerified; + final bool isInProgress; + final String? leetcodeHandle; + + VerificationStatus({ + required this.isVerified, + required this.isInProgress, + required this.leetcodeHandle, + }); + + factory VerificationStatus.fromJson(Map json) => VerificationStatus( + isVerified: json['isVerified'] == true, + isInProgress: json['isInProgress'] == true, + leetcodeHandle: json['leetcodeHandle'] as String?, + ); +} diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index d50d386..b74d2a6 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -24,7 +24,7 @@ class SettingsPage extends StatelessWidget { return Scaffold( backgroundColor: colors.surface, appBar: AppBar( - title: const Text('Settings'), + title: const Text('Settings', style: TextStyle(color: Colors.white),), centerTitle: true, backgroundColor: colors.surface, elevation: 0, @@ -37,16 +37,14 @@ class SettingsPage extends StatelessWidget { // ====== Personal Details ====== Text( 'My Account', - style: TextStyle(color: colors.primary, fontSize: 16), + style: TextStyle(color: Colors.white, fontSize: 16), ), const SizedBox(height: 10), Consumer( builder: (context, user, _) { + // We no longer display separate first/last name fields – just show the full username. final name = (user.name).trim(); - final parts = name.split(RegExp(r"\s+")); - final firstName = parts.isNotEmpty && parts.first.isNotEmpty ? parts.first : '-'; - final lastName = parts.length > 1 ? parts.sublist(1).join(' ') : ''; final username = name.isNotEmpty ? name : '-'; final email = (user.email).isNotEmpty ? user.email : '-'; final streak = user.streak; @@ -61,26 +59,14 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( - child: Column( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: colors.tertiary.withOpacity(0.3), - child: Icon( - Icons.person, - size: 32, - color: colors.primary, - ), - ), - const SizedBox(height: 6), - TextButton( - onPressed: () {}, - child: Text( - "Edit", - style: TextStyle(color: colors.secondary), - ), - ), - ], + child: CircleAvatar( + radius: 40, + backgroundColor: colors.tertiary.withOpacity(0.3), + child: Icon( + Icons.person, + size: 32, + color: colors.primary, + ), ), ), const SizedBox(height: 10), @@ -91,19 +77,7 @@ class SettingsPage extends StatelessWidget { color: colors.primary.withOpacity(0.3), ), - // First & Last Name side-by-side - Row( - children: [ - Expanded(child: _buildDisplayTile('First Name', firstName, colors)), - const SizedBox(width: 10), - Expanded(child: _buildDisplayTile('Last Name', lastName.isEmpty ? '-' : lastName, colors)), - ], - ), - Divider( - height: 1, - thickness: 0.6, - color: colors.primary.withOpacity(0.3), - ), + // Username _buildDisplayTile('Username', '@$username', colors), Divider( height: 1, @@ -133,7 +107,7 @@ class SettingsPage extends StatelessWidget { // ====== Container 2 ====== Text( 'Password and Authentication', - style: TextStyle(color: colors.primary, fontSize: 16), + style: TextStyle(color: Colors.white, fontSize: 16), ), const SizedBox(height: 10), Container( @@ -148,19 +122,22 @@ class SettingsPage extends StatelessWidget { children: [ Expanded(child: _buildDisplayTile('Password', '••••••••', colors)), const SizedBox(width: 10), - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: colors.secondary, - foregroundColor: colors.surface, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 14, + Align( + alignment: Alignment.bottomCenter, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: colors.secondary, + foregroundColor: colors.surface, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), + ), + child: const Text( + 'Change password', + style: TextStyle(fontSize: 12), ), - ), - child: const Text( - 'Change password', - style: TextStyle(fontSize: 12), ), ), ], @@ -238,31 +215,31 @@ class SettingsPage extends StatelessWidget { ); } - // Non-editable display tile + // Label outside, grey pill only around value Widget _buildDisplayTile(String title, String value, ColorScheme colors) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: colors.tertiary.withOpacity(0.3), - borderRadius: BorderRadius.circular(10), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle(color: colors.primary.withOpacity(0.7), fontSize: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(color: colors.onSurface, fontSize: 12, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: colors.tertiary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), ), - const SizedBox(height: 4), - Text( + child: Text( value, - style: TextStyle(color: colors.primary, fontSize: 14), + style: TextStyle(color: colors.primary, fontSize: 14, fontWeight: FontWeight.w500), ), - ], - ), + ), + ], ), ); } diff --git a/leaderboard_app/lib/services/core/dio_provider.dart b/leaderboard_app/lib/services/core/dio_provider.dart new file mode 100644 index 0000000..eeef422 --- /dev/null +++ b/leaderboard_app/lib/services/core/dio_provider.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Provides a configured singleton Dio instance with auth header + logging. +class DioProvider { + static Dio? _dio; + + static String _defaultBaseUrl() { + const fromEnv = String.fromEnvironment('BASE_URL'); + if (fromEnv.isNotEmpty) return fromEnv; + if (kIsWeb) return 'http://localhost:3000/api'; + if (defaultTargetPlatform == TargetPlatform.android) return 'http://10.0.2.2:3000/api'; + return 'http://localhost:3000/api'; + } + + static Future getInstance({String? baseUrl}) async { + if (_dio != null) return _dio!; + final prefs = await SharedPreferences.getInstance(); + final dio = Dio( + BaseOptions( + baseUrl: baseUrl ?? _defaultBaseUrl(), + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 20), + headers: {'Content-Type': 'application/json'}, + ), + ); + + dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) async { + final token = prefs.getString('authToken'); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + handler.next(options); + }, onError: (e, handler) { + // Simple retry for idempotent GETs on network issues + if (_shouldRetry(e)) { + _retry(dio, e.requestOptions).then(handler.resolve).catchError((_) => handler.next(e)); + } else { + handler.next(e); + } + })); + + // Basic log interceptor (custom to avoid extra dependency) + dio.interceptors.add(_LogInterceptor()); + + _dio = dio; + return dio; + } + + static bool _shouldRetry(DioException e) { + return e.type == DioExceptionType.connectionError && e.requestOptions.method == 'GET'; + } + + static Future> _retry(Dio dio, RequestOptions requestOptions) async { + final opts = Options( + method: requestOptions.method, + headers: requestOptions.headers, + responseType: requestOptions.responseType, + contentType: requestOptions.contentType, + sendTimeout: requestOptions.sendTimeout, + receiveTimeout: requestOptions.receiveTimeout, + ); + return dio.request( + requestOptions.path, + data: requestOptions.data, + queryParameters: requestOptions.queryParameters, + options: opts, + ); + } +} + +class _LogInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (kIsWeb) { + // Avoid verbose logs in web release; adjust as needed. + } + debugPrint('[HTTP] => ${options.method} ${options.baseUrl}${options.path}'); + if (options.data != null) debugPrint(' Body: ${options.data}'); + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + debugPrint('[HTTP] <= ${response.statusCode} ${response.requestOptions.path}'); + handler.next(response); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + debugPrint('[HTTP] !! ${err.type} ${err.message} path=${err.requestOptions.path}'); + handler.next(err); + } +} + +void debugPrint(String message) { + // Lightweight wrapper to silence in release if desired. + if (kIsWeb || true) { + // ignore: avoid_print + print(message); + } +} diff --git a/leaderboard_app/lib/services/core/rest_client.dart b/leaderboard_app/lib/services/core/rest_client.dart new file mode 100644 index 0000000..141cac1 --- /dev/null +++ b/leaderboard_app/lib/services/core/rest_client.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:leaderboard_app/models/verification_models.dart'; +import 'package:leaderboard_app/models/auth_models.dart'; + +part 'rest_client.g.dart'; + +@RestApi() +abstract class RestClient { + factory RestClient(Dio dio, {String baseUrl}) = _RestClient; + + // AUTH + @POST('/auth/signup') + Future signUp(@Body() Map body); + + @POST('/auth/login') + Future signIn(@Body() Map body); + + @GET('/user/profile') + Future> getProfile(); + + // VERIFICATION + @POST('/verification/start') + Future startVerification(@Body() Map body); + + @GET('/verification/status/{username}') + Future getVerificationStatus(@Path('username') String username); +} diff --git a/leaderboard_app/lib/services/core/rest_client.g.dart b/leaderboard_app/lib/services/core/rest_client.g.dart new file mode 100644 index 0000000..8c6c160 --- /dev/null +++ b/leaderboard_app/lib/services/core/rest_client.g.dart @@ -0,0 +1,184 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rest_client.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter + +class _RestClient implements RestClient { + _RestClient(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future signUp(Map body) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/auth/signup', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late AuthResponse _value; + try { + _value = AuthResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future signIn(Map body) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/auth/login', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late AuthResponse _value; + try { + _value = AuthResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future> getProfile() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/user/profile', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late Map _value; + try { + _value = _result.data!; + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future startVerification(Map body) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/verification/start', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late VerificationStart _value; + try { + _value = VerificationStart.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future getVerificationStatus(String username) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/verification/status/${username}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late VerificationStatus _value; + try { + _value = VerificationStatus.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index 207a799..e7a6fac 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + url: "https://pub.dev" + source: hosted + version: "7.7.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -17,6 +41,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" characters: dependency: transitive description: @@ -25,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" clock: dependency: transitive description: @@ -33,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" collection: dependency: transitive description: @@ -41,6 +145,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" dio: dependency: "direct main" description: @@ -81,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -112,6 +248,22 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" go_router: dependency: "direct main" description: @@ -120,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.8.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" highlight: dependency: transitive description: @@ -128,6 +288,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -136,6 +312,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" leak_tracker: dependency: transitive description: @@ -216,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -280,6 +496,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + url: "https://pub.dev" + source: hosted + version: "4.2.0" provider: dependency: "direct main" description: @@ -288,6 +520,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + retrofit: + dependency: "direct main" + description: + name: retrofit + sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25" + url: "https://pub.dev" + source: hosted + version: "4.7.2" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "9abcf21acb95bf7040546eafff87f60cf0aee20b05101d71f99876fc4df1f522" + url: "https://pub.dev" + source: hosted + version: "9.7.0" shared_preferences: dependency: "direct main" description: @@ -344,11 +608,43 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" source_span: dependency: transitive description: @@ -373,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -397,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -485,6 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" web: dependency: transitive description: @@ -493,6 +813,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -501,6 +837,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.9.0-288.0.dev <4.0.0" flutter: ">=3.29.0" diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index 91817a6..3adcb7c 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: pixelarticons: ^0.4.0 provider: ^6.1.5 dio: ^5.6.0 + retrofit: ^4.4.1 + json_annotation: ^4.9.0 go_router: ^14.2.7 shared_preferences: ^2.3.2 url_launcher: ^6.3.0 @@ -22,6 +24,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + build_runner: ^2.4.13 + retrofit_generator: ^9.1.5 + json_serializable: ^6.8.0 flutter: uses-material-design: true From 795cf2c7ed903f83dc86fe35784a1d8d6bc0f406 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Sun, 28 Sep 2025 23:34:51 +0530 Subject: [PATCH 20/53] feat: cleaned up the dashboard --- .../leaderboard_table.dart | 16 --- .../dashboard-components/problem_table.dart | 17 +++ .../lib/models/submissions_models.dart | 126 ++++++++++++++++++ leaderboard_app/lib/pages/dashboard_page.dart | 53 ++++++-- .../services/dashboard/dashboard_service.dart | 28 ++-- 5 files changed, 200 insertions(+), 40 deletions(-) create mode 100644 leaderboard_app/lib/models/submissions_models.dart diff --git a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart index d4ebd35..337f9be 100644 --- a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart +++ b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart @@ -59,15 +59,6 @@ class LeaderboardTable extends StatelessWidget { ), ), ), - DataColumn( - label: Text( - "Badge", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), ], rows: List.generate( users.length, @@ -99,13 +90,6 @@ class LeaderboardTable extends StatelessWidget { "${users[index].totalSolved}", style: const TextStyle(color: Colors.white, fontSize: 12), )), - const DataCell( - Icon( - Icons.star, - color: Colors.amber, - size: 16, - ), - ), ], ), ), diff --git a/leaderboard_app/lib/dashboard-components/problem_table.dart b/leaderboard_app/lib/dashboard-components/problem_table.dart index 349294a..592436b 100644 --- a/leaderboard_app/lib/dashboard-components/problem_table.dart +++ b/leaderboard_app/lib/dashboard-components/problem_table.dart @@ -7,6 +7,23 @@ class ProblemTable extends StatelessWidget { @override Widget build(BuildContext context) { + if (submissions.isEmpty) { + return Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'No recent accepted submissions', + style: TextStyle(color: Colors.white54, fontSize: 12), + ), + ), + ); + } + return Container( padding: const EdgeInsets.all(12), width: double.infinity, // match parent width diff --git a/leaderboard_app/lib/models/submissions_models.dart b/leaderboard_app/lib/models/submissions_models.dart new file mode 100644 index 0000000..cd2a99c --- /dev/null +++ b/leaderboard_app/lib/models/submissions_models.dart @@ -0,0 +1,126 @@ +import 'dashboard_models.dart'; + +class SubmissionsResponse { + final bool success; + final String message; + final DateTime timestamp; + final SubmissionData data; + + SubmissionsResponse({ + required this.success, + required this.message, + required this.timestamp, + required this.data, + }); + + factory SubmissionsResponse.fromJson(Map json) => SubmissionsResponse( + success: json['success'] == true || json['ok'] == true, + message: json['message']?.toString() ?? '', + timestamp: _parseTimestamp(json['timestamp']), + data: SubmissionData.fromJson((json['data'] ?? json) as Map), + ); + + static DateTime _parseTimestamp(dynamic ts) { + if (ts == null) return DateTime.now(); + if (ts is int) { + // assume ms if large, else seconds + if (ts > 2000000000) return DateTime.fromMillisecondsSinceEpoch(ts, isUtc: true); + return DateTime.fromMillisecondsSinceEpoch(ts * 1000, isUtc: true); + } + if (ts is String) { + final i = int.tryParse(ts); + if (i != null) return _parseTimestamp(i); + return DateTime.tryParse(ts) ?? DateTime.now(); + } + return DateTime.now(); + } +} + +class SubmissionData { + final List submissions; + final int count; + final int limit; + + SubmissionData({ + required this.submissions, + required this.count, + required this.limit, + }); + + factory SubmissionData.fromJson(Map json) { + final raw = json['submissions'] ?? json['items'] ?? json['results']; + final list = raw is List ? raw : []; + return SubmissionData( + submissions: list.map((e) => SubmissionEntry.fromJson(e as Map)).toList(), + count: (json['count'] ?? list.length) as int, + limit: (json['limit'] ?? list.length) as int, + ); + } +} + +class SubmissionEntry { + final String title; + final String titleSlug; + final String timestamp; // keep raw for reference + final StatusDisplay statusDisplay; + final Lang lang; + final double acRate; + final Difficulty difficulty; + + SubmissionEntry({ + required this.title, + required this.titleSlug, + required this.timestamp, + required this.statusDisplay, + required this.lang, + required this.acRate, + required this.difficulty, + }); + + factory SubmissionEntry.fromJson(Map json) => SubmissionEntry( + title: json['title']?.toString() ?? '', + titleSlug: json['titleSlug']?.toString() ?? '', + timestamp: json['timestamp']?.toString() ?? '0', + statusDisplay: _parseStatus(json['statusDisplay']), + lang: _parseLang(json['lang']), + acRate: (json['acRate'] ?? 0).toDouble(), + difficulty: _parseDifficulty(json['difficulty']), + ); + + SubmissionItem toSubmissionItem() => SubmissionItem( + title: title, + titleSlug: titleSlug, + statusDisplay: statusDisplay.name.toLowerCase(), + lang: lang.name.toLowerCase(), + acRate: acRate, + difficulty: difficulty.name.toLowerCase(), + timestamp: DateTime.fromMillisecondsSinceEpoch( + int.tryParse(timestamp) != null ? int.parse(timestamp) * 1000 : 0, + isUtc: true, + ), + ); + + static StatusDisplay _parseStatus(dynamic v) { + final s = v?.toString().toLowerCase(); + if (s == 'accepted' || s == 'ac') return StatusDisplay.ACCEPTED; + return StatusDisplay.ACCEPTED; // only one variant currently + } + + static Lang _parseLang(dynamic v) { + final s = v?.toString().toLowerCase(); + if (s == 'python3' || s == 'python') return Lang.PYTHON3; + return Lang.PYTHON3; // default + } + + static Difficulty _parseDifficulty(dynamic v) { + final s = v?.toString().toLowerCase(); + if (s == 'easy') return Difficulty.EASY; + if (s == 'medium') return Difficulty.MEDIUM; + if (s == 'hard') return Difficulty.HARD; + return Difficulty.EASY; + } +} + +enum Difficulty { EASY, HARD, MEDIUM } +enum Lang { PYTHON3 } +enum StatusDisplay { ACCEPTED } diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index 05802ad..ffd5a37 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:leaderboard_app/dashboard-components/compact_calendar.dart'; +// import 'package:leaderboard_app/dashboard-components/compact_calendar.dart'; // removed widget import 'package:leaderboard_app/dashboard-components/leaderboard_table.dart'; import 'package:leaderboard_app/dashboard-components/problem_table.dart'; import 'package:leaderboard_app/dashboard-components/daily_activity.dart'; -import 'package:leaderboard_app/dashboard-components/week_view.dart'; -import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; +// import 'package:leaderboard_app/dashboard-components/week_view.dart'; // removed widget +// import 'package:leaderboard_app/dashboard-components/weekly_stats.dart'; // removed widget import 'package:leaderboard_app/provider/user_provider.dart'; // models via provider components // import 'package:leaderboard_app/models/dashboard_models.dart'; @@ -104,12 +104,6 @@ class _DashboardPageState extends State { "${user.streak}", colors.secondary, ), - const SizedBox(width: 8), - _buildHeaderButton( - Icons.person_add, - "Invite", - colors.secondary, - ), ], ), ), @@ -122,8 +116,6 @@ class _DashboardPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const WeekView(), - const SizedBox(height: 10), Consumer( builder: (_, dp, __) => dp.loadingDaily ? _loadingCard(height: 90) @@ -142,13 +134,14 @@ class _DashboardPageState extends State { if (!dp.isVerified) { return _verifyCard(); } + if (dp.errorSubs != null) { + return _errorCard(dp.errorSubs!); + } return ProblemTable(submissions: dp.submissions); }, ), const SizedBox(height: 10), - const WeeklyStats(), - const SizedBox(height: 10), - const CompactCalendar(), + // Removed WeeklyStats and CompactCalendar per request if (_error != null) ...[ const SizedBox(height: 10), Text(_error!, style: const TextStyle(color: Colors.redAccent)), @@ -221,4 +214,36 @@ class _DashboardPageState extends State { ), ); } + + Widget _errorCard(String message) { + return Container( + width: double.infinity, + height: 120, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.redAccent.withOpacity(0.4)), + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.error_outline, color: Colors.redAccent), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Unable to load submissions', + style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold, fontSize: 13)), + const SizedBox(height: 6), + Text(message, style: const TextStyle(color: Colors.white70, fontSize: 12)), + ], + ), + ), + ], + ), + ); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/services/dashboard/dashboard_service.dart b/leaderboard_app/lib/services/dashboard/dashboard_service.dart index edc4c0c..4b628d0 100644 --- a/leaderboard_app/lib/services/dashboard/dashboard_service.dart +++ b/leaderboard_app/lib/services/dashboard/dashboard_service.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:leaderboard_app/models/dashboard_models.dart'; +import 'package:leaderboard_app/models/submissions_models.dart'; import 'package:leaderboard_app/services/core/api_client.dart'; class DashboardService { @@ -12,16 +13,23 @@ class DashboardService { } Future> getUserSubmissions() async { - final res = await _dio.get('/dashboard/submissions'); - final body = res.data as Map; - final data = (body['data'] ?? body) as Map; - final raw = (data['submissions'] ?? data['items'] ?? data['results'] ?? data) as dynamic; - final list = raw is List - ? raw.cast>() - : (raw is Map && raw['submissions'] is List) - ? (raw['submissions'] as List).cast>() - : >[]; - return list.map(SubmissionItem.fromJson).toList(); + final res = await _dio.get('/dashboard/submissions'); + final body = res.data as Map; + // Attempt structured parsing + try { + final parsed = SubmissionsResponse.fromJson(body); + return parsed.data.submissions.map((e) => e.toSubmissionItem()).toList(); + } catch (_) { + // Fallback to previous loose parsing strategy + final data = (body['data'] ?? body) as Map; + final raw = (data['submissions'] ?? data['items'] ?? data['results'] ?? data) as dynamic; + final list = raw is List + ? raw.cast>() + : (raw is Map && raw['submissions'] is List) + ? (raw['submissions'] as List).cast>() + : >[]; + return list.map(SubmissionItem.fromJson).toList(); + } } Future getDailyQuestion() async { From db89db6132832c20094e1f5adbe37a34d99599f0 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Mon, 29 Sep 2025 00:30:58 +0530 Subject: [PATCH 21/53] feat: refresh on dashboard page --- leaderboard_app/README.md | 21 +++++++--- leaderboard_app/lib/config/api_config.dart | 28 ++++++++++++++ leaderboard_app/lib/main.dart | 6 ++- leaderboard_app/lib/pages/dashboard_page.dart | 19 +++++++--- leaderboard_app/lib/pages/signup_page.dart | 38 ------------------- .../lib/provider/dashboard_provider.dart | 38 ++++++++++++++++++- .../lib/services/core/api_client.dart | 23 +---------- .../lib/services/core/dio_provider.dart | 13 ++----- 8 files changed, 104 insertions(+), 82 deletions(-) create mode 100644 leaderboard_app/lib/config/api_config.dart diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md index 82a60a1..f7910de 100644 --- a/leaderboard_app/README.md +++ b/leaderboard_app/README.md @@ -35,16 +35,27 @@ Auth tokens (JWT) are automatically attached from `SharedPreferences` via an int ### Environment / Base URL -Override API base URL at build time: +Centralized in `lib/config/api_config.dart`. + +Default baked-in base URL (when no override is supplied): + +``` +http://140.238.213.170:3002/api +``` + +Override at build/run time: ```bash -flutter run --dart-define=BASE_URL=https://your.api.host/api +flutter run --dart-define=API_BASE_URL=https://your.api.host/api ``` -Default development URLs: +Release / CI example: + +```bash +flutter build apk --dart-define=API_BASE_URL=https://prod.api.host/api +``` -* Web / Desktop / iOS simulator: `http://localhost:3000/api` -* Android emulator: `http://10.0.2.2:3000/api` +Trailing slashes are trimmed automatically. Keep `/api` if your backend routes are under that prefix. ### Adding new endpoints diff --git a/leaderboard_app/lib/config/api_config.dart b/leaderboard_app/lib/config/api_config.dart new file mode 100644 index 0000000..bbe7779 --- /dev/null +++ b/leaderboard_app/lib/config/api_config.dart @@ -0,0 +1,28 @@ +// Centralized API configuration for backend base URL. + +/// Priority order: +/// 1. Compile-time override via --dart-define=API_BASE_URL=... +/// 2. Baked-in default (production/dev fallback): http://140.238.213.170:3002/api +/// +/// If you need per-platform localhost behavior again, reintroduce the previous +/// logic or create an Environment enum. +class ApiConfig { + static const String _dartDefineBaseUrl = String.fromEnvironment('API_BASE_URL'); + + /// Returns the base URL (without trailing slash) to use for HTTP requests. + static String get baseUrl { + if (_dartDefineBaseUrl.isNotEmpty) return _dartDefineBaseUrl.rstrip('/'); + return 'http://140.238.213.170:3002/api'; + } +} + +extension _StringTrim on String { + String rstrip([String pattern = '/']) { + if (isEmpty) return this; + var result = this; + while (result.endsWith(pattern)) { + result = result.substring(0, result.length - pattern.length); + } + return result; + } +} diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 06fde56..9b94290 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -53,7 +53,11 @@ class Bootstrap extends StatelessWidget { ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => ChatProvider()), ChangeNotifierProvider(create: (ctx) => GroupProvider(groupService)), - ChangeNotifierProvider(create: (ctx) => DashboardProvider(service: dashboardService, userProvider: ctx.read())), + ChangeNotifierProvider(create: (ctx) => DashboardProvider( + service: dashboardService, + userProvider: ctx.read(), + userService: userService, + )), Provider.value(value: authService), Provider.value(value: dashboardService), Provider.value(value: leetCodeService), diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index ffd5a37..f22e525 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -111,11 +111,17 @@ class _DashboardPageState extends State { // Scrollable Content Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: RefreshIndicator( + onRefresh: () async { + // Refresh all dashboard data (daily question, submissions, leaderboard) + await context.read().loadAll(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Consumer( builder: (_, dp, __) => dp.loadingDaily ? _loadingCard(height: 90) @@ -146,7 +152,8 @@ class _DashboardPageState extends State { const SizedBox(height: 10), Text(_error!, style: const TextStyle(color: Colors.redAccent)), ], - ], + ], + ), ), ), ), diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index ca3bfad..95cb2e4 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -15,8 +15,6 @@ class SignUpPage extends StatefulWidget { } class _SignUpPageState extends State { - final _firstNameCtrl = TextEditingController(); - final _lastNameCtrl = TextEditingController(); final _usernameCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); @@ -25,8 +23,6 @@ class _SignUpPageState extends State { @override void dispose() { - _firstNameCtrl.dispose(); - _lastNameCtrl.dispose(); _usernameCtrl.dispose(); _emailCtrl.dispose(); _passwordCtrl.dispose(); @@ -77,40 +73,6 @@ class _SignUpPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 5), - TextField( - controller: _firstNameCtrl, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'First Name', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - ), - ), - const SizedBox(height: 10), - TextField( - controller: _lastNameCtrl, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFF141316), - hintText: 'Last Name', - hintStyle: TextStyle( - color: Colors.grey.withOpacity(0.28), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - ), - ), - const SizedBox(height: 10), TextField( controller: _usernameCtrl, style: const TextStyle(color: Colors.white), diff --git a/leaderboard_app/lib/provider/dashboard_provider.dart b/leaderboard_app/lib/provider/dashboard_provider.dart index 6b5be18..28f2750 100644 --- a/leaderboard_app/lib/provider/dashboard_provider.dart +++ b/leaderboard_app/lib/provider/dashboard_provider.dart @@ -2,12 +2,18 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/services/user/user_service.dart'; class DashboardProvider extends ChangeNotifier { final DashboardService service; final UserProvider userProvider; + final UserService? userService; // optional for profile refresh - DashboardProvider({required this.service, required this.userProvider}); + DashboardProvider({required this.service, required this.userProvider, this.userService}) { + // Listen for changes to user verification so we can (re)load submissions + // when the user becomes verified after the initial dashboard load. + userProvider.addListener(_handleUserChange); + } bool loadingDaily = false; bool loadingSubs = false; @@ -54,6 +60,14 @@ class DashboardProvider extends ChangeNotifier { submissions = const []; } else { submissions = await service.getUserSubmissions(); + // After fetching submissions, refresh user profile to update streak if service available. + if (userService != null) { + try { + await userProvider.fetchProfile(userService!); + } catch (_) { + // ignore profile refresh errors + } + } } } catch (_) { errorSubs = 'Failed to load recent submissions'; @@ -76,4 +90,26 @@ class DashboardProvider extends ChangeNotifier { notifyListeners(); } } + + void _handleUserChange() { + // If user became verified and we have not loaded submissions yet, load them. + if (isVerified) { + if (submissions.isEmpty && !loadingSubs) { + // Fire and forget; internal method handles notify + errors. + loadSubmissions(); + } + } else { + // User no longer verified (or logged out): clear submissions. + if (submissions.isNotEmpty) { + submissions = const []; + notifyListeners(); + } + } + } + + @override + void dispose() { + userProvider.removeListener(_handleUserChange); + super.dispose(); + } } diff --git a/leaderboard_app/lib/services/core/api_client.dart b/leaderboard_app/lib/services/core/api_client.dart index 288576e..472b26e 100644 --- a/leaderboard_app/lib/services/core/api_client.dart +++ b/leaderboard_app/lib/services/core/api_client.dart @@ -1,28 +1,9 @@ import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; +import 'package:leaderboard_app/config/api_config.dart'; class ApiClient { - // Choose a sensible default base URL for each platform. Can be overridden via --dart-define=BASE_URL=... - static String _defaultBaseUrl() { - const fromEnv = String.fromEnvironment('BASE_URL'); - if (fromEnv.isNotEmpty) return fromEnv; - - if (kIsWeb) { - // Web runs in the browser on the host, so localhost maps to the dev machine. - return 'http://localhost:3000/api'; - } - - if (defaultTargetPlatform == TargetPlatform.android) { - // Android emulator can't reach host via localhost; use the special alias. - return 'http://10.0.2.2:3000/api'; - } - - // iOS simulator, desktop, etc. can usually use localhost. - return 'http://localhost:3000/api'; - } - - static final String kBaseUrl = _defaultBaseUrl(); + static final String kBaseUrl = ApiConfig.baseUrl; final Dio dio; ApiClient._internal(this.dio); diff --git a/leaderboard_app/lib/services/core/dio_provider.dart b/leaderboard_app/lib/services/core/dio_provider.dart index eeef422..35c1b60 100644 --- a/leaderboard_app/lib/services/core/dio_provider.dart +++ b/leaderboard_app/lib/services/core/dio_provider.dart @@ -1,26 +1,19 @@ import 'dart:async'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; +import 'package:flutter/foundation.dart' show kIsWeb; // narrow imports import 'package:shared_preferences/shared_preferences.dart'; +import 'package:leaderboard_app/config/api_config.dart'; /// Provides a configured singleton Dio instance with auth header + logging. class DioProvider { static Dio? _dio; - static String _defaultBaseUrl() { - const fromEnv = String.fromEnvironment('BASE_URL'); - if (fromEnv.isNotEmpty) return fromEnv; - if (kIsWeb) return 'http://localhost:3000/api'; - if (defaultTargetPlatform == TargetPlatform.android) return 'http://10.0.2.2:3000/api'; - return 'http://localhost:3000/api'; - } - static Future getInstance({String? baseUrl}) async { if (_dio != null) return _dio!; final prefs = await SharedPreferences.getInstance(); final dio = Dio( BaseOptions( - baseUrl: baseUrl ?? _defaultBaseUrl(), + baseUrl: baseUrl ?? ApiConfig.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 20), headers: {'Content-Type': 'application/json'}, From 6fdce0cfec3c6d6a0651e6c0cc55adbd68203405 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Mon, 29 Sep 2025 00:50:56 +0530 Subject: [PATCH 22/53] feat: can create update and dleet groups --- .../dashboard-components/problem_table.dart | 24 +-- leaderboard_app/lib/pages/chatlists_page.dart | 182 +++++++++++++++++- leaderboard_app/lib/pages/groupinfo_page.dart | 70 ++++--- .../lib/provider/chatlists_provider.dart | 62 ++++++ 4 files changed, 285 insertions(+), 53 deletions(-) diff --git a/leaderboard_app/lib/dashboard-components/problem_table.dart b/leaderboard_app/lib/dashboard-components/problem_table.dart index 592436b..c4f9186 100644 --- a/leaderboard_app/lib/dashboard-components/problem_table.dart +++ b/leaderboard_app/lib/dashboard-components/problem_table.dart @@ -31,8 +31,8 @@ class ProblemTable extends StatelessWidget { color: Colors.grey[850], borderRadius: BorderRadius.circular(12), ), - child: DataTable( - columnSpacing: 10, + child: DataTable( + columnSpacing: 12, dataRowMinHeight: 32, dataRowMaxHeight: 36, headingRowHeight: 32, @@ -60,7 +60,7 @@ class ProblemTable extends StatelessWidget { ), DataColumn( label: Text( - "Acc.", + "Accuracy", style: TextStyle( color: Colors.white, fontSize: 12, @@ -69,16 +69,7 @@ class ProblemTable extends StatelessWidget { ), DataColumn( label: Text( - "Lvl", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Prog", + "Level", style: TextStyle( color: Colors.white, fontSize: 12, @@ -118,13 +109,6 @@ class ProblemTable extends StatelessWidget { fontSize: 12, ), )), - DataCell(Icon( - Icons.circle, - color: submissions[index].statusDisplay.toLowerCase() == 'accepted' - ? Colors.green - : Colors.grey, - size: 10, - )), ], ), ), diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 1c39034..72fc13f 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -43,10 +43,181 @@ class _ChatlistsPageState extends State { } } + void _showCreateGroupSheet() { + final nameController = TextEditingController(); + final descController = TextEditingController(); + final maxMembersController = TextEditingController(); + bool isPrivate = false; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) { + return StatefulBuilder( + builder: (context, setSheetState) { + final theme = Theme.of(context).colorScheme; + final provider = context.watch(); + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + left: 16, + right: 16, + top: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Create Group', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: theme.primary, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + color: theme.primary, + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: nameController, + textInputAction: TextInputAction.next, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Name *', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: descController, + maxLines: 3, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Description', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: maxMembersController, + keyboardType: TextInputType.number, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Max Members (optional)', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Private', style: TextStyle(color: theme.primary.withOpacity(0.7))), + Switch( + value: isPrivate, + onChanged: (v) => setSheetState(() => isPrivate = v), + activeColor: theme.secondary, + ), + ], + ), + ], + ), + if (provider.createError != null) ...[ + const SizedBox(height: 4), + Text(provider.createError!, style: const TextStyle(color: Colors.redAccent, fontSize: 12)), + ], + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: provider.isCreating + ? null + : () async { + final name = nameController.text.trim(); + if (name.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Name is required'))); + return; + } + final maxMembers = int.tryParse(maxMembersController.text.trim()); + final svc = context.read(); + final created = await provider.createNewGroup( + svc, + name: name, + description: descController.text.trim().isEmpty ? null : descController.text.trim(), + isPrivate: isPrivate, + maxMembers: maxMembers, + ); + if (created != null && mounted) { + Navigator.pop(context); // close sheet + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Group created')), + ); + } + }, + icon: provider.isCreating + ? SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Theme.of(context).colorScheme.onSecondary), + ) + : const Icon(Icons.check), + label: Text(provider.isCreating ? 'Creating...' : 'Create'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + ), + const SizedBox(height: 4), + ], + ), + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; - final chatProvider = Provider.of(context); + final chatProvider = Provider.of(context); final filteredGroups = _applyFilter(chatProvider.chatGroups); return Scaffold( @@ -103,9 +274,12 @@ class _ChatlistsPageState extends State { const SizedBox(width: 10), Padding( padding: const EdgeInsets.only(right: 16), - child: CircleAvatar( - backgroundColor: theme.secondary, - child: Icon(Icons.add, color: Colors.black), + child: GestureDetector( + onTap: _showCreateGroupSheet, + child: CircleAvatar( + backgroundColor: theme.secondary, + child: const Icon(Icons.add, color: Colors.black), + ), ), ), ], diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index ce0e779..cea6fe0 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/models/group_models.dart'; import 'package:leaderboard_app/services/groups/group_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; +import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:provider/provider.dart'; class GroupInfoPage extends StatefulWidget { @@ -289,37 +290,39 @@ class _GroupInfoPageState extends State { context: context, builder: (context) { final colors = Theme.of(context).colorScheme; - return AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Edit Group'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameCtrl, - decoration: const InputDecoration(labelText: 'Name'), - ), - const SizedBox(height: 8), - TextField( - controller: descCtrl, - decoration: const InputDecoration(labelText: 'Description'), - ), - const SizedBox(height: 8), - SwitchListTile( - title: const Text('Private'), - value: isPrivate, - onChanged: (v) => isPrivate = v, + return StatefulBuilder( + builder: (context, setLocalState) => AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Edit Group'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameCtrl, + decoration: const InputDecoration(labelText: 'Name'), + ), + const SizedBox(height: 8), + TextField( + controller: descCtrl, + decoration: const InputDecoration(labelText: 'Description'), + ), + const SizedBox(height: 8), + SwitchListTile( + title: const Text('Private'), + value: isPrivate, + onChanged: (v) => setLocalState(() => isPrivate = v), + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), + child: const Text('Save'), ), ], ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - ElevatedButton( - onPressed: () => Navigator.pop(context, true), - style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), - child: const Text('Save'), - ), - ], ); }, ); @@ -327,7 +330,11 @@ class _GroupInfoPageState extends State { setState(() => _mutating = true); try { final svc = context.read(); - await svc.updateGroup(_group!.id, name: nameCtrl.text.trim(), description: descCtrl.text.trim().isEmpty ? null : descCtrl.text.trim(), isPrivate: isPrivate); + final updated = await svc.updateGroup(_group!.id, name: nameCtrl.text.trim(), description: descCtrl.text.trim().isEmpty ? null : descCtrl.text.trim(), isPrivate: isPrivate); + // Optimistically update chat list provider + if (mounted) { + context.read()?.updateGroupMeta(groupId: updated.id, name: updated.name, isPrivate: updated.isPrivate); + } await _load(); } catch (_) { setState(() => _error = 'Failed to update group'); @@ -355,6 +362,11 @@ class _GroupInfoPageState extends State { try { final svc = context.read(); await svc.deleteGroup(_group!.id); + // Update chat list provider so list reflects deletion + if (mounted) { + final chatListProv = context.read(); + chatListProv?.removeGroup(_group!.id); + } if (!mounted) return; Navigator.of(context).pop(); } catch (_) { diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart index a438b78..db251e2 100644 --- a/leaderboard_app/lib/provider/chatlists_provider.dart +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -7,9 +7,15 @@ class ChatListProvider extends ChangeNotifier { bool _isLoading = false; String? _error; + // Creation state + bool _isCreating = false; + String? _createError; + List> get chatGroups => _chatGroups; bool get isLoading => _isLoading; String? get error => _error; + bool get isCreating => _isCreating; + String? get createError => _createError; /// Load dummy group chats void loadDummyGroups() { @@ -62,6 +68,37 @@ class ChatListProvider extends ChangeNotifier { } } + /// Create a new group and add to list (optimistically inserts at top) + Future?> createNewGroup(GroupService service, {required String name, String? description, bool isPrivate = false, int? maxMembers}) async { + if (_isCreating) return null; // prevent duplicate taps + _isCreating = true; + _createError = null; + notifyListeners(); + try { + final group = await service.createGroup(name: name, description: description, isPrivate: isPrivate, maxMembers: maxMembers); + final map = { + 'groupId': group.id, + 'name': group.name, + 'lastMessage': '', + 'time': '', + 'members': group.members.map((m) => { + 'uid': m.userId, + 'name': m.user?.username ?? m.userId, + }).toList(), + 'unread': false, + 'favourite': false, + }; + _chatGroups = [map, ..._chatGroups]; + return map; + } catch (e) { + _createError = 'Failed to create group'; + return null; + } finally { + _isCreating = false; + notifyListeners(); + } + } + /// Mark a group as read void markGroupAsRead(String groupId) { final index = _chatGroups.indexWhere((group) => group["groupId"] == groupId); @@ -80,4 +117,29 @@ class ChatListProvider extends ChangeNotifier { notifyListeners(); } } + + /// Remove a group from the list (e.g., after deletion) + void removeGroup(String groupId) { + final beforeLen = _chatGroups.length; + _chatGroups.removeWhere((g) => g['groupId'] == groupId); + if (beforeLen != _chatGroups.length) { + notifyListeners(); + } + } + + /// Update group metadata (e.g., after editing name / privacy) + void updateGroupMeta({required String groupId, String? name, bool? isPrivate}) { + final index = _chatGroups.indexWhere((g) => g['groupId'] == groupId); + if (index == -1) return; + bool changed = false; + if (name != null && name.isNotEmpty && _chatGroups[index]['name'] != name) { + _chatGroups[index]['name'] = name; + changed = true; + } + if (isPrivate != null && _chatGroups[index]['isPrivate'] != isPrivate) { + _chatGroups[index]['isPrivate'] = isPrivate; + changed = true; + } + if (changed) notifyListeners(); + } } \ No newline at end of file From ec09f84b1e35f917532ae992d63221519ebf9c51 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Mon, 29 Sep 2025 01:22:57 +0530 Subject: [PATCH 23/53] feat: verification skip works + tweaks to groupinfo pages --- leaderboard_app/lib/models/group_models.dart | 11 ++ leaderboard_app/lib/pages/groupinfo_page.dart | 72 +++++--- leaderboard_app/lib/pages/settings_page.dart | 157 ++++++++++++++++++ .../lib/provider/user_provider.dart | 13 ++ 4 files changed, 230 insertions(+), 23 deletions(-) diff --git a/leaderboard_app/lib/models/group_models.dart b/leaderboard_app/lib/models/group_models.dart index 0695fce..2a54a43 100644 --- a/leaderboard_app/lib/models/group_models.dart +++ b/leaderboard_app/lib/models/group_models.dart @@ -132,12 +132,16 @@ class GroupMemberUser { final String username; final String? leetcodeHandle; final bool leetcodeVerified; + final int streak; // daily streak count + final int totalSolved; // total solved problems GroupMemberUser({ required this.id, required this.username, this.leetcodeHandle, required this.leetcodeVerified, + this.streak = 0, + this.totalSolved = 0, }); factory GroupMemberUser.fromJson(Map json) => GroupMemberUser( @@ -145,6 +149,13 @@ class GroupMemberUser { username: (json['username'] ?? '').toString(), leetcodeHandle: json['leetcodeHandle'] as String?, leetcodeVerified: json['leetcodeVerified'] == true, + // Support multiple possible key names from different endpoints + streak: (json['streak'] ?? json['currentStreak'] ?? json['dailyStreak']) is num + ? ((json['streak'] ?? json['currentStreak'] ?? json['dailyStreak']) as num).toInt() + : 0, + totalSolved: (json['totalSolved'] ?? json['solved'] ?? json['problemsSolved']) is num + ? ((json['totalSolved'] ?? json['solved'] ?? json['problemsSolved']) as num).toInt() + : 0, ); } diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index cea6fe0..f165c26 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_app/models/group_models.dart'; import 'package:leaderboard_app/services/groups/group_service.dart'; +import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:provider/provider.dart'; @@ -21,6 +22,8 @@ class _GroupInfoPageState extends State { String? _error; bool _mutating = false; String? _currentUserId; + // Hydrated user stats from global leaderboard (username -> (streak, solved)) + final Map _userStats = {}; @override void initState() { @@ -34,9 +37,19 @@ class _GroupInfoPageState extends State { _error = null; }); try { - _currentUserId = context.read().user?.id; + _currentUserId = context.read().user?.id; final svc = context.read(); final g = await svc.getGroupById(widget.groupId); + // Hydrate streak / solved from dashboard leaderboard (best-effort) + try { + final dash = context.read(); + final lb = await dash.getLeaderboard(); + _userStats + ..clear() + ..addEntries(lb.map((u) => MapEntry(u.username.toLowerCase(), (u.streak, u.totalSolved)))); + } catch (_) { + // ignore hydration errors silently + } if (!mounted) return; setState(() => _group = g); } catch (e) { @@ -244,6 +257,7 @@ class _GroupInfoPageState extends State { } Widget _xpTable(List members) { + // Renamed section (no XP) - still sorts by XP to keep ordering if needed final sorted = [...members]..sort((a, b) => (b.xp).compareTo(a.xp)); return Container( padding: const EdgeInsets.all(12), @@ -252,29 +266,41 @@ class _GroupInfoPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('XP Leaderboard', style: TextStyle(fontSize: 18)), + const Text('Group Members', style: TextStyle(fontSize: 18)), const SizedBox(height: 12), - DataTable( - columnSpacing: 10, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: MaterialStateProperty.all(Colors.grey[900]), - columns: const [ - DataColumn(label: Text('Place', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('Player', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('Role', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('XP', style: TextStyle(color: Colors.white, fontSize: 12))), - ], - rows: List.generate(sorted.length, (i) { - final m = sorted[i]; - return DataRow(cells: [ - DataCell(Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text(m.user?.username ?? m.userId, style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text(m.role, style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text('${m.xp}', style: const TextStyle(color: Colors.white, fontSize: 12))), - ]); - }), + // Wrap DataTable to enforce full-width usage and allow overflow if needed + LayoutBuilder( + builder: (context, constraints) => ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: DataTable( + columnSpacing: 16, + dataRowMinHeight: 32, + dataRowMaxHeight: 40, + headingRowHeight: 32, + headingRowColor: MaterialStateProperty.all(Colors.grey[850]), + columns: const [ + DataColumn(label: Text('Place', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Player', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Streak', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Solved', style: TextStyle(color: Colors.white, fontSize: 12))), + ], + rows: List.generate(sorted.length, (i) { + final m = sorted[i]; + final uname = (m.user?.username ?? m.userId).toLowerCase(); + final hydrated = _userStats[uname]; + final streak = hydrated != null ? hydrated.$1 : (m.user?.streak ?? 0); + final solved = hydrated != null ? hydrated.$2 : (m.user?.totalSolved ?? 0); + final streakDisplay = streak == 0 ? '—' : '$streak'; + final solvedDisplay = solved == 0 ? '—' : '$solved'; + return DataRow(cells: [ + DataCell(Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(m.user?.username ?? m.userId, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(streakDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(solvedDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), + ]); + }), + ), + ), ), ], ), diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index b74d2a6..3678e41 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; +import 'package:leaderboard_app/services/leetcode/leetcode_service.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { @@ -48,6 +49,8 @@ class SettingsPage extends StatelessWidget { final username = name.isNotEmpty ? name : '-'; final email = (user.email).isNotEmpty ? user.email : '-'; final streak = user.streak; + final handle = user.user?.leetcodeHandle; + final verified = user.user?.leetcodeVerified == true; return Container( padding: const EdgeInsets.all(16), @@ -96,6 +99,49 @@ class SettingsPage extends StatelessWidget { thickness: 0.6, color: colors.primary.withOpacity(0.3), ), + // LeetCode handle & verify section + if (verified) + _buildDisplayTile('LeetCode', handle ?? '-', colors) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LeetCode', + style: TextStyle(color: colors.onSurface, fontSize: 12, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: colors.tertiary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text(handle ?? 'Not linked', style: TextStyle(color: colors.primary, fontSize: 14, fontWeight: FontWeight.w500)), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => _showLeetCodeVerifyDialog(context), + style: ElevatedButton.styleFrom( + backgroundColor: colors.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ), + child: const Text('Verify'), + ), + ], + ), + ], + ), + Divider( + height: 1, + thickness: 0.6, + color: colors.primary.withOpacity(0.3), + ), ], ), ); @@ -215,6 +261,117 @@ class SettingsPage extends StatelessWidget { ); } + Future _showLeetCodeVerifyDialog(BuildContext context) async { + final colors = Theme.of(context).colorScheme; + final handleCtrl = TextEditingController(); + String? code; + String? instructions; + bool loading = false; + bool started = false; + bool polling = false; + String? error; + + await showDialog( + context: context, + barrierDismissible: !loading, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setState) => AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Verify LeetCode'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!started) ...[ + TextField( + controller: handleCtrl, + decoration: const InputDecoration(labelText: 'LeetCode Username'), + ), + const SizedBox(height: 12), + if (error != null) Text(error!, style: const TextStyle(color: Colors.redAccent, fontSize: 12)), + ] else ...[ + if (instructions != null) Text(instructions!, style: const TextStyle(fontSize: 13)), + if (code != null) ...[ + const SizedBox(height: 12), + SelectableText('Verification Code: $code', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text('Add this code to your LeetCode profile bio then keep this dialog open.'), + ], + if (polling) ...[ + const SizedBox(height: 16), + Row(children: const [SizedBox(width:16,height:16, child: CircularProgressIndicator(strokeWidth:2)), SizedBox(width:8), Text('Checking status...')]), + ], + ], + ], + ), + actions: [ + if (!loading) + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Close'), + ), + if (!started) + ElevatedButton( + onPressed: loading + ? null + : () async { + final handle = handleCtrl.text.trim(); + if (handle.isEmpty) { + setState(() => error = 'Enter a username'); + return; + } + setState(() { + loading = true; + error = null; + }); + try { + final svc = ctx.read(); + final res = await svc.startVerification(handle); + code = res.verificationCode; + instructions = res.instructions ?? 'Place the code in your LeetCode bio.'; + started = true; + // begin polling + polling = true; + setState(() {}); + _pollLeetCodeStatus(ctx, setState); + } catch (e) { + error = 'Failed to start verification'; + } finally { + loading = false; + setState(() {}); + } + }, + style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), + child: const Text('Start'), + ), + ], + ), + ), + ); + } + + Future _pollLeetCodeStatus(BuildContext dialogContext, void Function(void Function()) setState) async { + final svc = dialogContext.read(); + for (int i = 0; i < 30; i++) { // up to ~30 polls + await Future.delayed(const Duration(seconds: 4)); + try { + final status = await svc.getStatus(); + if (status.isVerified) { + // update user provider + dialogContext.read().setLeetCodeStatus(handle: status.leetcodeHandle, verified: true); + if (Navigator.of(dialogContext).canPop()) { + Navigator.of(dialogContext).pop(); + } + return; + } + } catch (_) { + // ignore transient errors + } + // refresh UI each loop + setState(() {}); + } + } + // Label outside, grey pill only around value Widget _buildDisplayTile(String title, String value, ColorScheme colors) { return Padding( diff --git a/leaderboard_app/lib/provider/user_provider.dart b/leaderboard_app/lib/provider/user_provider.dart index b074336..d85f655 100644 --- a/leaderboard_app/lib/provider/user_provider.dart +++ b/leaderboard_app/lib/provider/user_provider.dart @@ -14,6 +14,19 @@ class UserProvider extends ChangeNotifier { bool get isLoading => _loading; String? get error => _error; + void setLeetCodeStatus({String? handle, bool? verified}) { + if (_user == null) return; + _user = User( + id: _user!.id, + username: _user!.username, + email: _user!.email, + leetcodeHandle: handle ?? _user!.leetcodeHandle, + leetcodeVerified: verified ?? _user!.leetcodeVerified, + streak: _user!.streak, + ); + notifyListeners(); + } + void updateUser({required String name, required String email, required int streak}) { _user = User( id: _user?.id ?? '', From c7a96f8e17371dff341c97318e5b16a18f097055 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Mon, 29 Sep 2025 01:25:58 +0530 Subject: [PATCH 24/53] feat: enhance XP table sorting by streak, solved, and username --- leaderboard_app/lib/pages/groupinfo_page.dart | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index f165c26..15ad68d 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -257,8 +257,22 @@ class _GroupInfoPageState extends State { } Widget _xpTable(List members) { - // Renamed section (no XP) - still sorts by XP to keep ordering if needed - final sorted = [...members]..sort((a, b) => (b.xp).compareTo(a.xp)); + // Sort by hydrated streak desc, then solved desc, then xp desc, then username asc as final tie-breaker + final sorted = [...members]; + sorted.sort((a, b) { + String aName = (a.user?.username ?? a.userId).toLowerCase(); + String bName = (b.user?.username ?? b.userId).toLowerCase(); + final aStats = _userStats[aName]; + final bStats = _userStats[bName]; + final aStreak = aStats?.$1 ?? a.user?.streak ?? 0; + final bStreak = bStats?.$1 ?? b.user?.streak ?? 0; + if (bStreak != aStreak) return bStreak.compareTo(aStreak); + final aSolved = aStats?.$2 ?? a.user?.totalSolved ?? 0; + final bSolved = bStats?.$2 ?? b.user?.totalSolved ?? 0; + if (bSolved != aSolved) return bSolved.compareTo(aSolved); + if (b.xp != a.xp) return b.xp.compareTo(a.xp); + return aName.compareTo(bName); + }); return Container( padding: const EdgeInsets.all(12), width: double.infinity, @@ -266,8 +280,7 @@ class _GroupInfoPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Group Members', style: TextStyle(fontSize: 18)), - const SizedBox(height: 12), + // Header removed per request // Wrap DataTable to enforce full-width usage and allow overflow if needed LayoutBuilder( builder: (context, constraints) => ConstrainedBox( From b4a0c745d5fc3b85e5fa97160f1528ff7fe7f231 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Mon, 29 Sep 2025 02:04:04 +0530 Subject: [PATCH 25/53] cleanup: UI tweaks --- leaderboard_app/README.md | 76 +++++++ .../android/app/src/main/AndroidManifest.xml | 3 + leaderboard_app/lib/pages/chatlists_page.dart | 213 ++++++++---------- leaderboard_app/lib/pages/settings_page.dart | 29 ++- leaderboard_app/lib/pages/signin_page.dart | 2 +- 5 files changed, 192 insertions(+), 131 deletions(-) diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md index f7910de..8f0d7af 100644 --- a/leaderboard_app/README.md +++ b/leaderboard_app/README.md @@ -71,3 +71,79 @@ Trailing slashes are trimmed automatically. Keep `/api` if your backend routes a Generated code (`rest_client.g.dart`) should not be manually edited. +## Building a Release APK / Sharing the App + +1. (Optional) Override the API base URL at build time (recommended for different envs): + +```bash +flutter build apk --release --dart-define=API_BASE_URL=https://prod.api.host/api +``` + +If you omit `--dart-define` the baked-in default from `ApiConfig` is used. + +2. The unsigned release APK will be at: + +``` +build\app\outputs\flutter-apk\app-release.apk +``` + +3. (Recommended) Create a keystore and configure signing in `android/key.properties` + `build.gradle` to avoid Play Store rejection and to allow in-place upgrades. + +### Example keystore creation (run once) + +```bash +keytool -genkey -v -keystore my-release-key.keystore -alias upload -keyalg RSA -keysize 2048 -validity 10000 +``` + +Place the keystore under `android/` (never commit to VCS) and add a `key.properties`: + +``` +storePassword=YOUR_STORE_PASSWORD +keyPassword=YOUR_KEY_PASSWORD +keyAlias=upload +storeFile=../my-release-key.keystore +``` + +Then update `android/app/build.gradle` signingConfigs + buildTypes (if not already present). + +### Distributing for quick tests + +You can directly share `app-release.apk` with testers (they must enable install from unknown sources). For Play Store publishing prefer an AAB: + +```bash +flutter build appbundle --dart-define=API_BASE_URL=https://prod.api.host/api +``` + +## Troubleshooting: "Cannot reach server. Check BASE_URL..." + +This message originates from `ErrorUtils.fromDio` when the `DioExceptionType.connectionError` occurs. Common causes: + +| Cause | Fix | +|-------|-----| +| Device has no internet | Ensure Wi‑Fi/data works (open a website) | +| Backend URL wrong or down | Open the URL in mobile Chrome to verify response | +| Using `localhost` / private IP not reachable externally | Use a public/stable host or expose via tunneling (ngrok, Cloudflare) | +| HTTP blocked (if you switch to HTTPS only) | Ensure correct scheme in `API_BASE_URL` | +| Missing INTERNET permission | Manifest now includes `` | + +To quickly verify the URL the app is using, add a temporary log: + +```dart +print('API base URL: ' + ApiConfig.baseUrl); +``` + +Or run with an override: + +```bash +flutter run --release --dart-define=API_BASE_URL=https://your-temp-api/api +``` + +If the backend uses a self-signed certificate, Android may reject it—use a valid cert (Let's Encrypt) for production. + +## Future Enhancements (Optional) + +* Add build flavors: dev / staging / prod with per-flavor `--dart-define` presets. +* Add environment banner in-app for non-prod. +* Implement exponential backoff retries for transient network errors. +* Add Sentry or similar for error monitoring. + diff --git a/leaderboard_app/android/app/src/main/AndroidManifest.xml b/leaderboard_app/android/app/src/main/AndroidManifest.xml index 9756e11..512b0bf 100644 --- a/leaderboard_app/android/app/src/main/AndroidManifest.xml +++ b/leaderboard_app/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + { - final List filters = const ["All", "Unread", "Favourites"]; - String selectedFilter = "All"; bool _loadedOnce = false; + final TextEditingController _searchController = TextEditingController(); + Timer? _debounce; + String _searchQuery = ''; @override void initState() { @@ -30,17 +32,27 @@ class _ChatlistsPageState extends State { }); } - List> _applyFilter(List> chatGroups) { - switch (selectedFilter) { - case "Unread": - return chatGroups.where((group) => group["unread"] == true).toList(); - case "Favourites": - // Assuming each group has a "favourite" bool property. If not, update your data model or remove this filter. - return chatGroups.where((group) => group["favourite"] == true).toList(); - case "All": - default: - return chatGroups; - } + @override + void dispose() { + _debounce?.cancel(); + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged(String value) { + setState(() => _searchQuery = value.trim()); + // Debounce network search to avoid spamming backend + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), () { + if (!mounted) return; + final svc = context.read(); + context.read().loadPublicGroups(svc, search: _searchQuery.isEmpty ? null : _searchQuery); + }); + } + + Future _refresh() async { + final svc = context.read(); + await context.read().loadPublicGroups(svc, search: _searchQuery.isEmpty ? null : _searchQuery); } void _showCreateGroupSheet() { @@ -218,7 +230,7 @@ class _ChatlistsPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); - final filteredGroups = _applyFilter(chatProvider.chatGroups); + final groups = chatProvider.chatGroups; return Scaffold( backgroundColor: theme.surface, @@ -243,13 +255,14 @@ class _ChatlistsPageState extends State { ), const SizedBox(height: 10), - // Search + Add (You can later wire search functionality here) + // Search + Add Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(left: 16), child: TextField( + controller: _searchController, style: TextStyle(color: theme.primary), decoration: InputDecoration( hintText: "Search", @@ -267,7 +280,20 @@ class _ChatlistsPageState extends State { borderSide: BorderSide.none, ), prefixIcon: Icon(Icons.search, color: theme.primary), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + tooltip: 'Clear', + icon: Icon(Icons.close, color: theme.primary.withOpacity(0.7)), + onPressed: () { + _searchController.clear(); + _onSearchChanged(''); + }, + ) + : null, ), + onChanged: _onSearchChanged, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _onSearchChanged(_searchController.text), ), ), ), @@ -286,71 +312,50 @@ class _ChatlistsPageState extends State { ), const SizedBox(height: 16), - // Filters - SizedBox( - height: 40, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Row( - children: filters.map((label) { - final isSelected = label == selectedFilter; - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ElevatedButton( - onPressed: () { - setState(() { - selectedFilter = label; - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: isSelected - ? theme.secondary - : Colors.grey.shade900, - foregroundColor: - isSelected ? Colors.black : theme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + // Group List + Expanded( + child: RefreshIndicator( + onRefresh: _refresh, + child: Builder( + builder: (context) { + if (chatProvider.isLoading && groups.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + if (chatProvider.error != null) { + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + const SizedBox(height: 120), + Center( + child: Text( + chatProvider.error!, + style: TextStyle(color: theme.primary), ), - elevation: 0, ), - child: Text( - label, - style: const TextStyle( - fontWeight: FontWeight.w600, + ], + ); + } + if (groups.isEmpty) { + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + const SizedBox(height: 120), + Center( + child: Text( + 'No groups found', + style: TextStyle(color: theme.primary), ), ), - ), - ), - ); - }).toList(), - ), - ), - ), - - const SizedBox(height: 16), - - // Group List - Expanded( - child: chatProvider.isLoading - ? const Center(child: CircularProgressIndicator()) - : (chatProvider.error != null) - ? Center(child: Text(chatProvider.error!, style: TextStyle(color: theme.primary))) - : filteredGroups.isEmpty - ? Center( - child: Text( - "No groups found", - style: TextStyle(color: theme.primary), - ), - ) - : ListView.builder( - itemCount: filteredGroups.length, + ], + ); + } + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: groups.length, itemBuilder: (context, index) { - final group = filteredGroups[index]; - final groupId = group["groupId"]?.toString() ?? ""; - final groupName = - group["name"]?.toString() ?? "Unnamed Group"; - + final group = groups[index]; + final groupId = group['groupId']?.toString() ?? ''; + final groupName = group['name']?.toString() ?? 'Unnamed Group'; return Column( children: [ InkWell( @@ -364,83 +369,55 @@ class _ChatlistsPageState extends State { ); }, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Group icon const CircleAvatar( radius: 24, backgroundColor: Colors.grey, - child: Icon( - Icons.group, - color: Colors.white, - ), + child: Icon(Icons.group, color: Colors.white), ), const SizedBox(width: 12), - - // Group name + last message Expanded( child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( groupName, - style: TextStyle( - color: theme.primary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), + style: TextStyle(color: theme.primary, fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 4), Text( - group["lastMessage"] ?? "", - style: TextStyle( - color: - theme.primary.withOpacity(0.7), - fontSize: 13, - overflow: TextOverflow.ellipsis, - ), + group['lastMessage'] ?? '', + style: TextStyle(color: theme.primary.withOpacity(0.7), fontSize: 13, overflow: TextOverflow.ellipsis), ), ], ), ), - - // Time + unread dot Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - group["time"]?.toString() ?? "", - style: TextStyle( - color: theme.primary.withOpacity(0.6), - fontSize: 12, - ), + group['time']?.toString() ?? '', + style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12), ), const SizedBox(height: 8), - if (group["unread"] == true) - CircleAvatar( - radius: 6, - backgroundColor: theme.secondary, - ), + if (group['unread'] == true) + CircleAvatar(radius: 6, backgroundColor: theme.secondary), ], ), ], ), ), ), - Divider( - height: 1, - thickness: 0.6, - color: Colors.grey.shade800, - ), + Divider(height: 1, thickness: 0.6, color: Colors.grey.shade800), ], ); }, - ), + ); + }, + ), + ), ), ], ), diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 3678e41..7e30731 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -24,21 +24,25 @@ class SettingsPage extends StatelessWidget { return Scaffold( backgroundColor: colors.surface, - appBar: AppBar( - title: const Text('Settings', style: TextStyle(color: Colors.white),), - centerTitle: true, - backgroundColor: colors.surface, - elevation: 0, - foregroundColor: colors.primary, - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(left:16, right:16, top:16, bottom:24), + children: [ + // Title (match Group Chats styling) + Text( + 'Settings', + style: TextStyle( + color: colors.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), // ====== Personal Details ====== Text( 'My Account', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colors.primary, fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), @@ -153,7 +157,7 @@ class SettingsPage extends StatelessWidget { // ====== Container 2 ====== Text( 'Password and Authentication', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colors.primary, fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), Container( @@ -257,6 +261,7 @@ class SettingsPage extends StatelessWidget { ), ), ], + ), ), ); } diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index d532fbd..b98fbfd 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -75,7 +75,7 @@ class _SignInPageState extends State { decoration: InputDecoration( filled: true, fillColor: const Color(0xFF141316), - hintText: 'Email or username', + hintText: 'Email', hintStyle: TextStyle(color: Colors.grey.withOpacity(0.28)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), From dabf17f66cb098e60483ea45a069687fa268d8ba Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Wed, 1 Oct 2025 02:15:34 +0530 Subject: [PATCH 26/53] feat: chat flow --- leaderboard_app/README.md | 1 + .../lib/chatpage-components/chat_view.dart | 8 +- leaderboard_app/lib/main.dart | 3 + leaderboard_app/lib/models/chat_message.dart | 39 +++++ leaderboard_app/lib/pages/chat_gate.dart | 56 +++++++ leaderboard_app/lib/pages/chat_page.dart | 12 +- leaderboard_app/lib/pages/chatlists_page.dart | 38 ++++- leaderboard_app/lib/pages/groupinfo_page.dart | 7 + .../lib/provider/chat_provider.dart | 140 +++++------------- .../provider/group_membership_provider.dart | 50 +++++++ leaderboard_app/lib/router/app_router.dart | 9 ++ leaderboard_app/pubspec.lock | 4 +- 12 files changed, 241 insertions(+), 126 deletions(-) create mode 100644 leaderboard_app/lib/models/chat_message.dart create mode 100644 leaderboard_app/lib/pages/chat_gate.dart create mode 100644 leaderboard_app/lib/provider/group_membership_provider.dart diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md index 8f0d7af..f8bd805 100644 --- a/leaderboard_app/README.md +++ b/leaderboard_app/README.md @@ -71,6 +71,7 @@ Trailing slashes are trimmed automatically. Keep `/api` if your backend routes a Generated code (`rest_client.g.dart`) should not be manually edited. + ## Building a Release APK / Sharing the App 1. (Optional) Override the API base URL at build time (recommended for different envs): diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index 5a4176f..158294b 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -52,7 +52,7 @@ class _ChatViewState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; - Provider.of(context); + Provider.of(context); // keep provider watch for message updates return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), @@ -70,7 +70,11 @@ class _ChatViewState extends State { MaterialPageRoute( builder: (_) => GroupInfoPage(groupId: widget.groupId, initialName: widget.groupName), ), - ); + ).then((result) { + if (result is Map && result['leftGroup'] == true) { + if (mounted) Navigator.of(context).pop(); // leave chat view + } + }); }, child: Row( children: [ diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index 9b94290..c192a64 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -13,6 +13,7 @@ import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/provider/group_provider.dart'; import 'package:leaderboard_app/provider/dashboard_provider.dart'; +import 'package:leaderboard_app/provider/group_membership_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -58,6 +59,8 @@ class Bootstrap extends StatelessWidget { userProvider: ctx.read(), userService: userService, )), + // Group membership status (scoped per group via proxy widgets later) + ChangeNotifierProvider(create: (ctx) => GroupMembershipProvider(service: groupService, userProvider: ctx.read())), Provider.value(value: authService), Provider.value(value: dashboardService), Provider.value(value: leetCodeService), diff --git a/leaderboard_app/lib/models/chat_message.dart b/leaderboard_app/lib/models/chat_message.dart new file mode 100644 index 0000000..2918ba1 --- /dev/null +++ b/leaderboard_app/lib/models/chat_message.dart @@ -0,0 +1,39 @@ +class ChatMessage { + final String id; + final String groupId; + final String senderId; + final String senderName; + final String message; + final DateTime timestamp; + final String? replyTo; + + ChatMessage({ + required this.id, + required this.groupId, + required this.senderId, + required this.senderName, + required this.message, + required this.timestamp, + this.replyTo, + }); + + factory ChatMessage.fromSocket(Map raw) { + final sender = raw['sender']; + return ChatMessage( + id: (raw['id'] ?? raw['_id'] ?? DateTime.now().millisecondsSinceEpoch.toString()).toString(), + groupId: (raw['groupId'] ?? '').toString(), + senderId: sender is Map ? (sender['id'] ?? sender['_id'] ?? '').toString() : '', + senderName: sender is Map ? (sender['username'] ?? 'User').toString() : 'User', + message: (raw['message'] ?? '').toString(), + timestamp: _parseTs(raw['timestamp']), + replyTo: raw['replyTo'] as String?, + ); + } + + static DateTime _parseTs(dynamic v) { + if (v == null) return DateTime.now(); + if (v is int) return DateTime.fromMillisecondsSinceEpoch(v); + if (v is String) return DateTime.tryParse(v) ?? DateTime.now(); + return DateTime.now(); + } +} diff --git a/leaderboard_app/lib/pages/chat_gate.dart b/leaderboard_app/lib/pages/chat_gate.dart new file mode 100644 index 0000000..b00cd3c --- /dev/null +++ b/leaderboard_app/lib/pages/chat_gate.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:leaderboard_app/provider/group_membership_provider.dart'; +import 'package:leaderboard_app/pages/chat_page.dart'; +import 'package:leaderboard_app/pages/groupinfo_page.dart'; + +/// Decides whether to show chat directly or group info depending on membership. +class ChatGate extends StatefulWidget { + final String groupId; + final String? groupName; + const ChatGate({super.key, required this.groupId, this.groupName}); + + @override + State createState() => _ChatGateState(); +} + +class _ChatGateState extends State { + @override + void initState() { + super.initState(); + // Kick off membership check + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + context.read().check(widget.groupId); + }); + } + + @override + Widget build(BuildContext context) { + final membership = context.watch(); + switch (membership.status) { + case GroupMembershipStatus.loading: + return const Scaffold(body: Center(child: CircularProgressIndicator())); + case GroupMembershipStatus.error: + return Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(membership.error ?? 'Error'), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () => membership.check(widget.groupId), + child: const Text('Retry'), + ), + ], + ), + ), + ); + case GroupMembershipStatus.member: + return ChatPage(groupId: widget.groupId, groupName: widget.groupName ?? membership.group?.name ?? 'Group'); + case GroupMembershipStatus.notMember: + return GroupInfoPage(groupId: widget.groupId, initialName: widget.groupName ?? membership.group?.name); + } + } +} diff --git a/leaderboard_app/lib/pages/chat_page.dart b/leaderboard_app/lib/pages/chat_page.dart index 5fe8e55..c5d44ae 100644 --- a/leaderboard_app/lib/pages/chat_page.dart +++ b/leaderboard_app/lib/pages/chat_page.dart @@ -11,14 +11,8 @@ class ChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) { - final provider = ChatProvider(); - provider.getReplyTo(groupId); - provider.getAttachmentOptionsVisibility(groupId); - return provider; - }, - child: ChatView(groupId: groupId, groupName: groupName), - ); + final chatProv = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) => chatProv.joinGroup(context, groupId)); + return ChatView(groupId: groupId, groupName: groupName); } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 1d7bd17..8d3399c 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -4,6 +4,8 @@ import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/services/groups/group_service.dart'; import 'package:provider/provider.dart'; import 'groupinfo_page.dart'; +import 'chat_page.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; class ChatlistsPage extends StatefulWidget { const ChatlistsPage({super.key}); @@ -359,14 +361,36 @@ class _ChatlistsPageState extends State { return Column( children: [ InkWell( - onTap: () { + onTap: () async { chatProvider.markGroupAsRead(groupId); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupInfoPage(groupId: groupId, initialName: groupName), - ), - ); + // Determine if user already member; best effort by fetching group + final groupSvc = context.read(); + final userId = context.read()?.user?.id; + bool isMember = false; + try { + final g = await groupSvc.getGroupById(groupId); + if (userId != null) { + isMember = g.members.any((m) => m.userId == userId); + } + } catch (_) { + // fallback: show info page + } + if (!mounted) return; + if (isMember) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatPage(groupId: groupId, groupName: groupName), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupInfoPage(groupId: groupId, initialName: groupName), + ), + ); + } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 15ad68d..75744df 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -5,6 +5,7 @@ import 'package:leaderboard_app/services/dashboard/dashboard_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:provider/provider.dart'; +import 'package:leaderboard_app/provider/group_membership_provider.dart'; class GroupInfoPage extends StatefulWidget { final String groupId; @@ -89,8 +90,14 @@ class _GroupInfoPageState extends State { final svc = context.read(); if (_isMember) { await svc.leaveGroup(_group!.id); + // Pop immediately with result so upstream (e.g., ChatPage) can react + if (mounted) Navigator.of(context).pop({'leftGroup': true, 'groupId': _group!.id}); + return; // skip reloading after leave } else { await svc.joinGroup(_group!.id); + // Inform membership provider (if in tree) so gate can switch to chat + final membershipProv = context.read(); + membershipProv?.markJoined(); } await _load(); } catch (e) { diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 63e5168..3f0ae37 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -1,139 +1,67 @@ import 'package:flutter/material.dart'; +/// Local-only ChatProvider: keeps in-memory messages per group. Navigation +/// logic (direct-to-chat if member) remains intact, but no realtime backend. class ChatProvider extends ChangeNotifier { - final String currentUserID = "uid_me"; + final String currentUserID = 'local_me'; - /// Stores messages per group: { groupId: [messageMap, ...] } final Map>> _groupMessages = {}; - - /// Stores replyTo per group final Map _groupReplyTo = {}; - - /// Stores attachment options visibility per group final Map _groupAttachmentVisibility = {}; + final Set _joinedGroups = {}; - /// Define some users with colors - final List> _dummyUsers = [ - {"id": "uid_me", "name": "You", "color": Colors.black}, - {"id": "uid_1", "name": "Person 1", "color": Colors.purple}, - {"id": "uid_2", "name": "Person 2", "color": Colors.red}, - {"id": "uid_3", "name": "Person 3", "color": Colors.green}, - {"id": "uid_4", "name": "Person 4", "color": Colors.blue}, - ]; - - // Expose dummyUsers publicly for other widgets to read - List> get dummyUsers => _dummyUsers; - - /// Get messages for a specific group - List> getMessages(String groupId) => - _groupMessages[groupId] ?? []; + // Exposed connection flags (kept for UI compatibility; always "connected"). + bool get isConnecting => false; + bool get isConnected => true; + String? get connectionError => null; - /// Helper to get user info - Map _getUser(String id) => - _dummyUsers.firstWhere((u) => u["id"] == id); - - /// Initialize a group with dummy messages - void _initGroupIfNeeded(String groupId) { - _groupMessages.putIfAbsent(groupId, () => [ - { - "senderID": "uid_1", - "senderName": _getUser("uid_1")["name"], - "senderColor": _getUser("uid_1")["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:30 pm", - }, - { - "senderID": "uid_1", - "senderName": _getUser("uid_1")["name"], - "senderColor": _getUser("uid_1")["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:31 pm", - }, - { - "senderID": "uid_2", - "senderName": _getUser("uid_2")["name"], - "senderColor": _getUser("uid_2")["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:33 pm", - }, - { - "senderID": currentUserID, - "senderName": _getUser(currentUserID)["name"], - "senderColor": _getUser(currentUserID)["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:34 pm", - }, - { - "senderID": "uid_3", - "senderName": _getUser("uid_3")["name"], - "senderColor": _getUser("uid_3")["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:35 pm", - }, - { - "senderID": "uid_4", - "senderName": _getUser("uid_4")["name"], - "senderColor": _getUser("uid_4")["color"], - "message": "text text text text text text text text text text...", - "timestamp": "12:36 pm", - }, - ]); + List> getMessages(String groupId) => _groupMessages[groupId] ?? const []; + String? getReplyTo(String groupId) => _groupReplyTo[groupId]; + bool getAttachmentOptionsVisibility(String groupId) => _groupAttachmentVisibility[groupId] ?? false; + Future joinGroup(BuildContext context, String groupId) async { + if (_joinedGroups.contains(groupId)) return; + _joinedGroups.add(groupId); + _groupMessages.putIfAbsent(groupId, () => []); _groupReplyTo.putIfAbsent(groupId, () => null); _groupAttachmentVisibility.putIfAbsent(groupId, () => false); } - /// Get reply-to for a specific group - String? getReplyTo(String groupId) { - _initGroupIfNeeded(groupId); - return _groupReplyTo[groupId]; - } - - /// Get attachment options state for a specific group - bool getAttachmentOptionsVisibility(String groupId) { - _initGroupIfNeeded(groupId); - return _groupAttachmentVisibility[groupId] ?? false; - } - - /// Send a new message in a group void sendMessage(String groupId, String text) { if (text.trim().isEmpty) return; - - _initGroupIfNeeded(groupId); - final user = _getUser(currentUserID); - - _groupMessages[groupId]!.add({ - "senderID": currentUserID, - "senderName": user["name"], - "senderColor": user["color"], - "message": text.trim(), - "timestamp": "now", - if (_groupReplyTo[groupId] != null) "replyTo": _groupReplyTo[groupId], + if (!_joinedGroups.contains(groupId)) joinGroup(null as dynamic, groupId); // ensure initialized + final list = (_groupMessages[groupId] ??= []); + list.add({ + 'id': 'local-${DateTime.now().millisecondsSinceEpoch}', + 'groupId': groupId, + 'message': text.trim(), + 'timestamp': _formatTimestamp(DateTime.now()), + 'senderID': currentUserID, + 'senderName': 'You', + 'senderColor': Colors.black, + if (_groupReplyTo[groupId] != null) 'replyTo': _groupReplyTo[groupId], }); - _groupReplyTo[groupId] = null; notifyListeners(); } - /// Set reply-to target for a group void setReplyTo(String groupId, String? message) { - _initGroupIfNeeded(groupId); _groupReplyTo[groupId] = message; notifyListeners(); } - - /// Clear reply-to state for a group void clearReplyTo(String groupId) { - _initGroupIfNeeded(groupId); _groupReplyTo[groupId] = null; notifyListeners(); } - - /// Toggle attachment options for a group void toggleAttachmentOptions(String groupId) { - _initGroupIfNeeded(groupId); - _groupAttachmentVisibility[groupId] = - !(_groupAttachmentVisibility[groupId] ?? false); + _groupAttachmentVisibility[groupId] = !(_groupAttachmentVisibility[groupId] ?? false); notifyListeners(); } + + String _formatTimestamp(DateTime dt) { + final h = dt.hour > 12 ? dt.hour - 12 : dt.hour; + final m = dt.minute.toString().padLeft(2, '0'); + final ampm = dt.hour >= 12 ? 'pm' : 'am'; + return '$h:$m $ampm'; + } } \ No newline at end of file diff --git a/leaderboard_app/lib/provider/group_membership_provider.dart b/leaderboard_app/lib/provider/group_membership_provider.dart new file mode 100644 index 0000000..139621d --- /dev/null +++ b/leaderboard_app/lib/provider/group_membership_provider.dart @@ -0,0 +1,50 @@ +import 'package:flutter/foundation.dart'; +import 'package:leaderboard_app/models/group_models.dart'; +import 'package:leaderboard_app/services/groups/group_service.dart'; +import 'package:leaderboard_app/provider/user_provider.dart'; + +enum GroupMembershipStatus { loading, member, notMember, error } + +class GroupMembershipProvider extends ChangeNotifier { + final GroupService service; + final UserProvider userProvider; + + GroupMembershipProvider({required this.service, required this.userProvider}); + + GroupMembershipStatus _status = GroupMembershipStatus.loading; + GroupMembershipStatus get status => _status; + + Group? _group; + Group? get group => _group; + + String? _error; + String? get error => _error; + + Future check(String groupId) async { + _status = GroupMembershipStatus.loading; + _error = null; + notifyListeners(); + try { + final g = await service.getGroupById(groupId); + _group = g; + final uid = userProvider.user?.id; + if (uid != null && g.members.any((m) => m.userId == uid)) { + _status = GroupMembershipStatus.member; + } else { + _status = GroupMembershipStatus.notMember; + } + } catch (e) { + _error = 'Failed to load group'; + _status = GroupMembershipStatus.error; + } finally { + notifyListeners(); + } + } + + void markJoined() { + if (_status != GroupMembershipStatus.member) { + _status = GroupMembershipStatus.member; + notifyListeners(); + } + } +} diff --git a/leaderboard_app/lib/router/app_router.dart b/leaderboard_app/lib/router/app_router.dart index 2a85070..f40b10a 100644 --- a/leaderboard_app/lib/router/app_router.dart +++ b/leaderboard_app/lib/router/app_router.dart @@ -6,6 +6,7 @@ import 'package:leaderboard_app/pages/home_page.dart'; import 'package:leaderboard_app/pages/signin_page.dart'; import 'package:leaderboard_app/pages/signup_page.dart'; import 'package:leaderboard_app/pages/leetcode_verification_page.dart'; +import 'package:leaderboard_app/pages/chat_gate.dart'; Future _isLoggedIn() async { final prefs = await SharedPreferences.getInstance(); @@ -41,6 +42,14 @@ GoRouter createRouter() { path: '/verify', builder: (context, state) => const LeetCodeVerificationPage(), ), + GoRoute( + path: '/chat/:groupId', + builder: (context, state) { + final groupId = state.pathParameters['groupId'] ?? ''; + final name = state.uri.queryParameters['name']; + return ChatGate(groupId: groupId, groupName: name); + }, + ), ], ); } diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index e7a6fac..4a7795a 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.6.7" json_annotation: dependency: "direct main" description: From d3fbef0d0f4daa88c68d14976e956650a3e92f67 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 00:03:56 +0530 Subject: [PATCH 27/53] =?UTF-8?q?feat:=20complete=20backedn=20integration?= =?UTF-8?q?=20with=20messaging=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/chatpage-components/chat_view.dart | 42 +++- .../lib/chatpage-components/message_list.dart | 6 +- .../lib/models/chat_message_dto.dart | 41 ++++ .../lib/provider/chat_provider.dart | 198 ++++++++++++++++-- .../lib/services/chat/chat_service.dart | 140 +++++++++++++ leaderboard_app/pubspec.lock | 16 ++ leaderboard_app/pubspec.yaml | 1 + 7 files changed, 409 insertions(+), 35 deletions(-) create mode 100644 leaderboard_app/lib/models/chat_message_dto.dart create mode 100644 leaderboard_app/lib/services/chat/chat_service.dart diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index 158294b..bc3231f 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -19,6 +19,7 @@ class _ChatViewState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final FocusNode myFocusNode = FocusNode(); + int _lastMessageCount = 0; // retained for possible future usage @override void initState() { @@ -29,6 +30,16 @@ class _ChatViewState extends State { } }); Future.delayed(const Duration(milliseconds: 500), scrollDown); + // Hook into provider after first frame to attach incoming message callback + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final chat = context.read(); + chat.onIncomingMessage = (gid) { + if (gid == widget.groupId) { + WidgetsBinding.instance.addPostFrameCallback((_) => scrollDown()); + } + }; + }); } void scrollDown() { @@ -43,6 +54,10 @@ class _ChatViewState extends State { @override void dispose() { + try { + final chat = context.read(); + if (chat.onIncomingMessage != null) chat.onIncomingMessage = null; + } catch (_) {} _messageController.dispose(); _scrollController.dispose(); myFocusNode.dispose(); @@ -52,7 +67,7 @@ class _ChatViewState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; - Provider.of(context); // keep provider watch for message updates + final chat = Provider.of(context); // watch return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), @@ -72,7 +87,7 @@ class _ChatViewState extends State { ), ).then((result) { if (result is Map && result['leftGroup'] == true) { - if (mounted) Navigator.of(context).pop(); // leave chat view + if (mounted) Navigator.of(context).pop(); } }); }, @@ -87,14 +102,21 @@ class _ChatViewState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.groupName, - style: TextStyle( - color: theme.primary, - fontWeight: FontWeight.bold, - fontSize: 16, + Row(children: [ + Text( + widget.groupName, + style: TextStyle( + color: theme.primary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), + const SizedBox(width: 8), + if (chat.isConnecting) + const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)), + if (!chat.isConnecting && !chat.isConnected) + const Icon(Icons.cloud_off, color: Colors.red, size: 16), + ]), ], ), ], @@ -121,4 +143,4 @@ class _ChatViewState extends State { ), ); } -} \ No newline at end of file +} diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 0fe670d..baaedc9 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -34,7 +34,7 @@ class MessageList extends StatelessWidget { itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[index]; - final isMe = msg["senderID"] == provider.currentUserID; + final isMe = msg["isMe"] == true; final isSystem = msg["senderID"] == "system"; final isImage = msg["type"] == "image"; @@ -152,11 +152,11 @@ class _TextMessage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - msg["senderName"] ?? "", + (isMe ? 'You' : (msg["senderName"] ?? "")), style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, - color: msg["senderColor"] ?? Colors.white, + color: isMe ? Colors.black : (msg["senderColor"] ?? Colors.white), ), ), const SizedBox(height: 4), diff --git a/leaderboard_app/lib/models/chat_message_dto.dart b/leaderboard_app/lib/models/chat_message_dto.dart new file mode 100644 index 0000000..a07c0c0 --- /dev/null +++ b/leaderboard_app/lib/models/chat_message_dto.dart @@ -0,0 +1,41 @@ +import 'chat_message.dart'; + +/// DTO for messages retrieved via REST (history endpoint) +class ChatMessageDto { + final String id; + final String content; + final String senderId; + final String groupId; + final DateTime createdAt; + final String? senderName; + + ChatMessageDto({ + required this.id, + required this.content, + required this.senderId, + required this.groupId, + required this.createdAt, + this.senderName, + }); + + factory ChatMessageDto.fromJson(Map json) { + final sender = json['sender']; + return ChatMessageDto( + id: (json['id'] ?? json['_id']).toString(), + content: (json['content'] ?? json['message'] ?? '').toString(), + senderId: (json['senderId'] ?? (sender is Map ? sender['id'] ?? sender['_id'] : '')).toString(), + groupId: (json['groupId'] ?? '').toString(), + createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(), + senderName: sender is Map ? (sender['username'] ?? sender['name'])?.toString() : null, + ); + } + + ChatMessage toDomain() => ChatMessage( + id: id, + groupId: groupId, + senderId: senderId, + senderName: senderName ?? 'User', + message: content, + timestamp: createdAt, + ); +} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 3f0ae37..69ecca5 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -1,48 +1,195 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:leaderboard_app/models/chat_message.dart'; +import 'package:leaderboard_app/services/chat/chat_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; +import 'user_provider.dart'; -/// Local-only ChatProvider: keeps in-memory messages per group. Navigation -/// logic (direct-to-chat if member) remains intact, but no realtime backend. +/// ChatProvider integrates REST history + Socket.IO realtime events. class ChatProvider extends ChangeNotifier { - final String currentUserID = 'local_me'; - final Map>> _groupMessages = {}; final Map _groupReplyTo = {}; final Map _groupAttachmentVisibility = {}; final Set _joinedGroups = {}; + final Map _groupCurrentPage = {}; // page loaded so far (starts at 1) + final Map _groupHasMore = {}; // whether more pages available + bool isLoadingMore(String groupId) => _loadingMoreGroups.contains(groupId); + final Set _loadingMoreGroups = {}; + + StreamSubscription? _sub; + + bool _connecting = false; + bool _connected = false; + String? _connError; + String? _currentUserId; + String? _currentUsername; + void Function(String groupId)? onIncomingMessage; // UI hook for auto-scroll - // Exposed connection flags (kept for UI compatibility; always "connected"). - bool get isConnecting => false; - bool get isConnected => true; - String? get connectionError => null; + // Public getters + bool get isConnecting => _connecting; + bool get isConnected => _connected; + String? get connectionError => _connError; + String get currentUserID => _currentUserId ?? ''; + String get currentUsername => _currentUsername ?? ''; List> getMessages(String groupId) => _groupMessages[groupId] ?? const []; String? getReplyTo(String groupId) => _groupReplyTo[groupId]; bool getAttachmentOptionsVisibility(String groupId) => _groupAttachmentVisibility[groupId] ?? false; + Future initIfNeeded([BuildContext? context]) async { + if (_currentUserId != null) return; + final prefs = await SharedPreferences.getInstance(); + _currentUserId = prefs.getString('userId'); + _currentUsername = prefs.getString('username') ?? prefs.getString('name'); + if ((_currentUserId == null || _currentUsername == null) && context != null) { + try { + final userProv = context.read(); + if (_currentUserId == null) _currentUserId = userProv.user?.id; + if (_currentUsername == null) _currentUsername = userProv.user?.username; + } catch (_) {} + } + _currentUserId ??= 'me'; + _currentUsername ??= 'You'; + // ignore: avoid_print + print('[CHAT] init userId=$_currentUserId username=$_currentUsername'); + } + + Future _ensureSocket() async { + if (_connected || _connecting) return; + _connecting = true; + notifyListeners(); + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('authToken'); + try { + await ChatService.instance.ensureConnected(authToken: token); + _connected = true; + _connError = null; + _sub ??= ChatService.instance.messagesStream.listen(_handleIncomingMessage); + } catch (e) { + _connError = e.toString(); + } finally { + _connecting = false; + notifyListeners(); + } + } + Future joinGroup(BuildContext context, String groupId) async { + await initIfNeeded(context); + await _ensureSocket(); if (_joinedGroups.contains(groupId)) return; _joinedGroups.add(groupId); _groupMessages.putIfAbsent(groupId, () => []); _groupReplyTo.putIfAbsent(groupId, () => null); _groupAttachmentVisibility.putIfAbsent(groupId, () => false); + + // Fetch history (page=1) + try { + final history = await ChatService.instance.fetchHistory(groupId, page: 1); + final mapped = history.map(_toMap).toList(); + _groupMessages[groupId] = mapped; + _groupCurrentPage[groupId] = 1; + _groupHasMore[groupId] = history.length >= 50; // heuristic based on limit + } catch (e) { + // Optionally add a system message + _groupMessages[groupId]?.add({ + 'id': 'err-${DateTime.now().millisecondsSinceEpoch}', + 'groupId': groupId, + 'message': 'Failed to load history: $e', + 'timestamp': _formatTimestamp(DateTime.now()), + 'senderID': 'system', + 'senderName': 'System', + }); + } + // Join via socket after history + ChatService.instance.joinGroup(groupId); + // debug log + // ignore: avoid_print + print('[CHAT] joinGroup requested for $groupId'); + notifyListeners(); } - void sendMessage(String groupId, String text) { + Future sendMessage(String groupId, String text) async { if (text.trim().isEmpty) return; - if (!_joinedGroups.contains(groupId)) joinGroup(null as dynamic, groupId); // ensure initialized - final list = (_groupMessages[groupId] ??= []); - list.add({ - 'id': 'local-${DateTime.now().millisecondsSinceEpoch}', - 'groupId': groupId, - 'message': text.trim(), - 'timestamp': _formatTimestamp(DateTime.now()), - 'senderID': currentUserID, - 'senderName': 'You', - 'senderColor': Colors.black, - if (_groupReplyTo[groupId] != null) 'replyTo': _groupReplyTo[groupId], - }); - _groupReplyTo[groupId] = null; + final trimmed = text.trim(); + final ok = await ChatService.instance.sendMessage( + groupId, + trimmed, + sender: { + 'id': currentUserID, + 'username': 'You', + }, + ); + if (!ok) { + // Append a system error message (optional) + final list = (_groupMessages[groupId] ??= []); + list.add({ + 'id': 'err-${DateTime.now().microsecondsSinceEpoch}', + 'groupId': groupId, + 'message': 'Failed to send message.', + 'timestamp': _formatTimestamp(DateTime.now()), + 'senderID': 'system', + 'senderName': 'System', + 'senderColor': Colors.red, + }); + notifyListeners(); + } + } + + Future loadMore(String groupId) async { + if (!(_groupHasMore[groupId] ?? false)) return; + if (_loadingMoreGroups.contains(groupId)) return; + final next = (_groupCurrentPage[groupId] ?? 1) + 1; + _loadingMoreGroups.add(groupId); + notifyListeners(); + try { + final history = await ChatService.instance.fetchHistory(groupId, page: next); + if (history.isEmpty) { + _groupHasMore[groupId] = false; + } else { + final list = _groupMessages[groupId] ??= []; + final existingIds = list.map((e) => e['id']).toSet(); + final newOnes = history.where((m) => !existingIds.contains(m.id)).map(_toMap); + list.insertAll(0, newOnes); // prepend older messages + _groupCurrentPage[groupId] = next; + _groupHasMore[groupId] = history.length >= 50; + } + } catch (_) { + // swallow or add a system message if desired + } finally { + _loadingMoreGroups.remove(groupId); + notifyListeners(); + } + } + + void _handleIncomingMessage(ChatMessage m) { + final list = (_groupMessages[m.groupId] ??= []); + // Dedupe exact id + if (list.any((e) => e['id'] == m.id)) return; + list.add(_toMap(m)); notifyListeners(); + // Trigger UI callback after listeners update + try { + onIncomingMessage?.call(m.groupId); + } catch (_) {} + } + + Map _toMap(ChatMessage m) { + final bool isMe = (m.senderId.isNotEmpty && m.senderId == currentUserID) || + (m.senderName.toLowerCase() == currentUsername.toLowerCase()); + // ignore: avoid_print + print('[CHAT] map message id=${m.id} senderId=${m.senderId} senderName=${m.senderName} isMe=$isMe currentUser=$currentUserID/$currentUsername'); + return { + 'id': m.id, + 'groupId': m.groupId, + 'message': m.message, + 'timestamp': _formatTimestamp(m.timestamp), + 'senderID': m.senderId, + 'senderName': isMe ? 'You' : m.senderName, + 'senderColor': isMe ? Colors.black : Colors.white, + 'isMe': isMe, + if (m.replyTo != null) 'replyTo': m.replyTo, + }; } void setReplyTo(String groupId, String? message) { @@ -64,4 +211,11 @@ class ChatProvider extends ChangeNotifier { final ampm = dt.hour >= 12 ? 'pm' : 'am'; return '$h:$m $ampm'; } + + @override + void dispose() { + _sub?.cancel(); + ChatService.instance.dispose(); + super.dispose(); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/services/chat/chat_service.dart b/leaderboard_app/lib/services/chat/chat_service.dart new file mode 100644 index 0000000..365ea3f --- /dev/null +++ b/leaderboard_app/lib/services/chat/chat_service.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'package:socket_io_client/socket_io_client.dart' as io; +import 'package:leaderboard_app/config/api_config.dart'; +import 'package:leaderboard_app/services/core/dio_provider.dart'; +import 'package:leaderboard_app/models/chat_message.dart'; +import 'package:leaderboard_app/models/chat_message_dto.dart'; + +typedef MessageHandler = void Function(ChatMessage message); + +class ChatService { + ChatService._(); + static final ChatService instance = ChatService._(); + + io.Socket? _socket; + bool get isConnected => _socket?.connected == true; + bool _connecting = false; + bool get isConnecting => _connecting; + String? lastError; + + final _messageController = StreamController.broadcast(); + Stream get messagesStream => _messageController.stream; + + /// Establish the socket connection (idempotent). Provide [authToken] for + /// backend auth if required. + Future ensureConnected({String? authToken}) async { + if (isConnected || _connecting) return; + _connecting = true; + lastError = null; + final base = ApiConfig.baseUrl; // e.g. http://host:port/api + final wsBase = base.replaceFirst('/api', ''); + try { + final socket = io.io(wsBase, { + 'transports': ['websocket'], + 'autoConnect': false, + // Backend guide: JWT via auth.token + if (authToken != null) 'auth': {'token': authToken}, + }); + final completer = Completer(); + socket.on('connect', (_) { + // Basic event logging hook + socket.onAny((event, data) { + // ignore: avoid_print + print('[SOCKET] event=$event data=${data is Map ? data.keys : data}'); + }); + completer.complete(); + }); + socket.on('connect_error', (err) { + lastError = err.toString(); + if (!completer.isCompleted) completer.completeError(err); + }); + // Server → Client: receive_message + socket.on('receive_message', (data) { + if (data is Map) { + try { + final msg = ChatMessage.fromSocket(_normalizeSocketPayload(Map.from(data))); + _messageController.add(msg); + } catch (e) { + // ignore + } + } + }); + // Joined group ack: add minimal log + socket.on('joined_group', (d) => print('[SOCKET] joined_group: $d')); + socket.on('error', (e) => print('[SOCKET] server_error: $e')); + socket.connect(); + _socket = socket; + await completer.future.timeout(const Duration(seconds: 8)); + } catch (e) { + lastError = e.toString(); + rethrow; + } finally { + _connecting = false; + } + } + + Map _normalizeSocketPayload(Map raw) { + // Backend receive_message: { id, groupId, message, sender, timestamp } + if (raw.containsKey('content') && !raw.containsKey('message')) { + raw['message'] = raw['content']; + } + if (raw.containsKey('createdAt') && !raw.containsKey('timestamp')) { + raw['timestamp'] = raw['createdAt']; + } + // Ensure sender structure + if (raw['sender'] is! Map) { + final id = raw['senderId'] ?? raw['senderID']; + raw['sender'] = { + 'id': id, + 'username': raw['senderName'] ?? 'User', + }; + } + return raw; + } + + /// Ask server to subscribe to a group's room for realtime events. + void joinGroup(String groupId) { + if (!isConnected) return; + // Guide: join_group expects groupId as raw string, not object + _socket?.emit('join_group', groupId); + } + + /// Emit a message to server (server should broadcast back with `message:new`). + Future sendMessage(String groupId, String text, {Map? sender}) async { + if (text.trim().isEmpty) return false; + if (!isConnected) return false; + final payload = { + 'groupId': groupId, + 'message': text.trim(), + if (sender != null) 'sender': sender, + }; + try { + // Backend does not specify ack; emit fire-and-forget. + _socket?.emit('send_message', payload); + return true; + } catch (_) { + return false; + } + } + + /// Retrieve paginated message history using the documented REST endpoint. + Future> fetchHistory(String groupId, {int page = 1, int limit = 50}) async { + final dio = await DioProvider.getInstance(); + final res = await dio.get('/messages/groups/$groupId', queryParameters: { + 'page': page, + 'limit': limit, + }); + final data = res.data; + final list = (data['data']?['messages'] ?? []) as List; + return list + .whereType>() + .map(ChatMessageDto.fromJson) + .map((dto) => dto.toDomain()) + .toList(); + } + + void dispose() { + _messageController.close(); + _socket?.dispose(); + } +} \ No newline at end of file diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index 4a7795a..ef9cebc 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -629,6 +629,22 @@ packages: description: flutter source: sdk version: "0.0.0" + socket_io_client: + dependency: "direct main" + description: + name: socket_io_client + sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b + url: "https://pub.dev" + source: hosted + version: "2.0.3+1" + socket_io_common: + dependency: transitive + description: + name: socket_io_common + sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" + url: "https://pub.dev" + source: hosted + version: "2.0.3" source_gen: dependency: transitive description: diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index 3adcb7c..11afd9b 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: go_router: ^14.2.7 shared_preferences: ^2.3.2 url_launcher: ^6.3.0 + socket_io_client: ^2.0.3 dev_dependencies: flutter_test: From cd1b382359ce355fba6612c82149bd4489abdae6 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 00:32:17 +0530 Subject: [PATCH 28/53] chore: UI cleanup groups and settings page --- .../lib/chatpage-components/chat_view.dart | 10 + leaderboard_app/lib/pages/chatlists_page.dart | 12 +- leaderboard_app/lib/pages/home_page.dart | 4 +- leaderboard_app/lib/pages/settings_page.dart | 199 +----------------- 4 files changed, 25 insertions(+), 200 deletions(-) diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index bc3231f..ea69d30 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -20,6 +20,7 @@ class _ChatViewState extends State { final ScrollController _scrollController = ScrollController(); final FocusNode myFocusNode = FocusNode(); int _lastMessageCount = 0; // retained for possible future usage + bool _didInitialAutoScroll = false; // guard to only auto-scroll once after history loads @override void initState() { @@ -68,6 +69,15 @@ class _ChatViewState extends State { Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chat = Provider.of(context); // watch + // Attempt initial auto-scroll when messages first arrive + if (!_didInitialAutoScroll) { + final msgs = chat.getMessages(widget.groupId); + if (msgs.isNotEmpty) { + // schedule after current frame so ListView has dimensions + WidgetsBinding.instance.addPostFrameCallback((_) => scrollDown()); + _didInitialAutoScroll = true; + } + } return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 8d3399c..6027600 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -273,15 +273,17 @@ class _ChatlistsPageState extends State { ), filled: true, fillColor: Colors.grey.shade900, + // Reduced vertical padding to make the bar slightly shorter contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + horizontal: 14, + vertical: 8, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), - prefixIcon: Icon(Icons.search, color: theme.primary), + // Removed search icon per request + // prefixIcon: Icon(Icons.search, color: theme.primary), suffixIcon: _searchQuery.isNotEmpty ? IconButton( tooltip: 'Clear', @@ -305,7 +307,7 @@ class _ChatlistsPageState extends State { child: GestureDetector( onTap: _showCreateGroupSheet, child: CircleAvatar( - backgroundColor: theme.secondary, + backgroundColor: Colors.white, child: const Icon(Icons.add, color: Colors.black), ), ), @@ -361,6 +363,8 @@ class _ChatlistsPageState extends State { return Column( children: [ InkWell( + splashColor: const Color(0xFF705B37).withOpacity(0.35), + highlightColor: const Color(0xFF705B37).withOpacity(0.25), onTap: () async { chatProvider.markGroupAsRead(groupId); // Determine if user already member; best effort by fetching group diff --git a/leaderboard_app/lib/pages/home_page.dart b/leaderboard_app/lib/pages/home_page.dart index 48d53a1..b1461e7 100644 --- a/leaderboard_app/lib/pages/home_page.dart +++ b/leaderboard_app/lib/pages/home_page.dart @@ -27,13 +27,13 @@ class _HomePageState extends State { bottomNavigationBar: Theme( data: Theme.of(context).copyWith( canvasColor: Colors.grey[900], - primaryColor: Colors.yellow, + primaryColor: Colors.amber, textTheme: Theme.of(context).textTheme.copyWith( bodySmall: const TextStyle(color: Colors.white), ), ), child: BottomNavigationBar( - selectedItemColor: Colors.yellow, + selectedItemColor: Color(0xFFF6C155), unselectedItemColor: Colors.white, showSelectedLabels: false, showUnselectedLabels: false, diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 7e30731..7f57826 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -3,7 +3,6 @@ import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; -import 'package:leaderboard_app/services/leetcode/leetcode_service.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { @@ -129,7 +128,8 @@ class SettingsPage extends StatelessWidget { ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => _showLeetCodeVerifyDialog(context), + // Navigate to the dedicated verification page used in signup/login flow + onPressed: () => context.push('/verify'), style: ElevatedButton.styleFrom( backgroundColor: colors.secondary, foregroundColor: Colors.black, @@ -153,88 +153,8 @@ class SettingsPage extends StatelessWidget { ), const SizedBox(height: 25), - - // ====== Container 2 ====== - Text( - 'Password and Authentication', - style: TextStyle(color: colors.primary, fontSize: 16, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colors.tertiary.withOpacity(0.15), - borderRadius: BorderRadius.circular(10), - ), - child: Column( - children: [ - Row( - children: [ - Expanded(child: _buildDisplayTile('Password', '••••••••', colors)), - const SizedBox(width: 10), - Align( - alignment: Alignment.bottomCenter, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: colors.secondary, - foregroundColor: colors.surface, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 14, - ), - ), - child: const Text( - 'Change password', - style: TextStyle(fontSize: 12), - ), - ), - ), - ], - ), - const SizedBox(height: 20), - Align( - alignment: Alignment.centerLeft, - child: Text( - 'Account removal', - style: TextStyle(color: colors.primary, fontSize: 16), - ), - ), - const SizedBox(height: 12), - Row( - children: [ - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: colors.tertiary.withOpacity(0.5), - foregroundColor: colors.primary, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - ), - child: const Text('Disable Account'), - ), - const SizedBox(width: 10), - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, // Keep red for danger - foregroundColor: colors.surface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - ), - child: const Text('Delete Account'), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 20), // Extra space above the bottom + // Removed password & authentication section per request + const SizedBox(height: 4), // ====== Logout button (full-width) ====== SizedBox( @@ -266,116 +186,7 @@ class SettingsPage extends StatelessWidget { ); } - Future _showLeetCodeVerifyDialog(BuildContext context) async { - final colors = Theme.of(context).colorScheme; - final handleCtrl = TextEditingController(); - String? code; - String? instructions; - bool loading = false; - bool started = false; - bool polling = false; - String? error; - - await showDialog( - context: context, - barrierDismissible: !loading, - builder: (ctx) => StatefulBuilder( - builder: (ctx, setState) => AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Verify LeetCode'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!started) ...[ - TextField( - controller: handleCtrl, - decoration: const InputDecoration(labelText: 'LeetCode Username'), - ), - const SizedBox(height: 12), - if (error != null) Text(error!, style: const TextStyle(color: Colors.redAccent, fontSize: 12)), - ] else ...[ - if (instructions != null) Text(instructions!, style: const TextStyle(fontSize: 13)), - if (code != null) ...[ - const SizedBox(height: 12), - SelectableText('Verification Code: $code', style: const TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - const Text('Add this code to your LeetCode profile bio then keep this dialog open.'), - ], - if (polling) ...[ - const SizedBox(height: 16), - Row(children: const [SizedBox(width:16,height:16, child: CircularProgressIndicator(strokeWidth:2)), SizedBox(width:8), Text('Checking status...')]), - ], - ], - ], - ), - actions: [ - if (!loading) - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('Close'), - ), - if (!started) - ElevatedButton( - onPressed: loading - ? null - : () async { - final handle = handleCtrl.text.trim(); - if (handle.isEmpty) { - setState(() => error = 'Enter a username'); - return; - } - setState(() { - loading = true; - error = null; - }); - try { - final svc = ctx.read(); - final res = await svc.startVerification(handle); - code = res.verificationCode; - instructions = res.instructions ?? 'Place the code in your LeetCode bio.'; - started = true; - // begin polling - polling = true; - setState(() {}); - _pollLeetCodeStatus(ctx, setState); - } catch (e) { - error = 'Failed to start verification'; - } finally { - loading = false; - setState(() {}); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), - child: const Text('Start'), - ), - ], - ), - ), - ); - } - - Future _pollLeetCodeStatus(BuildContext dialogContext, void Function(void Function()) setState) async { - final svc = dialogContext.read(); - for (int i = 0; i < 30; i++) { // up to ~30 polls - await Future.delayed(const Duration(seconds: 4)); - try { - final status = await svc.getStatus(); - if (status.isVerified) { - // update user provider - dialogContext.read().setLeetCodeStatus(handle: status.leetcodeHandle, verified: true); - if (Navigator.of(dialogContext).canPop()) { - Navigator.of(dialogContext).pop(); - } - return; - } - } catch (_) { - // ignore transient errors - } - // refresh UI each loop - setState(() {}); - } - } + // Removed in-dialog verification flow; navigation now uses dedicated page '/verify' // Label outside, grey pill only around value Widget _buildDisplayTile(String title, String value, ColorScheme colors) { From 3f315aed5d1209526f62660d9e9076115d059d85 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 00:52:50 +0530 Subject: [PATCH 29/53] chore: updated dashboard ui --- .../leaderboard_table.dart | 13 +- .../dashboard-components/problem_table.dart | 197 ++++++++++-------- 2 files changed, 121 insertions(+), 89 deletions(-) diff --git a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart index 337f9be..a4b0bf2 100644 --- a/leaderboard_app/lib/dashboard-components/leaderboard_table.dart +++ b/leaderboard_app/lib/dashboard-components/leaderboard_table.dart @@ -7,14 +7,12 @@ class LeaderboardTable extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12), - width: double.infinity, // matches parent width - decoration: BoxDecoration( + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, color: Colors.grey[850], - borderRadius: BorderRadius.circular(12), - ), - child: DataTable( + child: DataTable( columnSpacing: 10, dataRowMinHeight: 32, dataRowMaxHeight: 36, @@ -93,6 +91,7 @@ class LeaderboardTable extends StatelessWidget { ], ), ), + ), ), ); } diff --git a/leaderboard_app/lib/dashboard-components/problem_table.dart b/leaderboard_app/lib/dashboard-components/problem_table.dart index c4f9186..85c81cf 100644 --- a/leaderboard_app/lib/dashboard-components/problem_table.dart +++ b/leaderboard_app/lib/dashboard-components/problem_table.dart @@ -24,93 +24,126 @@ class ProblemTable extends StatelessWidget { ); } - return Container( - padding: const EdgeInsets.all(12), - width: double.infinity, // match parent width - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(12), - ), - child: DataTable( - columnSpacing: 12, - dataRowMinHeight: 32, - dataRowMaxHeight: 36, - headingRowHeight: 32, - headingRowColor: WidgetStateProperty.all( - Colors.grey[900], + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[850], ), - columns: const [ - DataColumn( - label: Text( - "No.", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Title", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Accuracy", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - DataColumn( - label: Text( - "Level", - style: TextStyle( - color: Colors.white, - fontSize: 12, + // Use SingleChildScrollView horizontally if titles overflow available width + child: LayoutBuilder( + builder: (context, constraints) { + final maxTitleWidth = (constraints.maxWidth - 12*4) * 0.45; // heuristic for title column + return DataTable( + columnSpacing: 12, + dataRowMinHeight: 32, + dataRowMaxHeight: 36, + headingRowHeight: 32, + headingRowColor: WidgetStateProperty.all( + Colors.grey[900], ), - ), - ), - ], - rows: List.generate( - submissions.length, - (index) => DataRow( - cells: [ - DataCell( - Text( - "${index + 1}", - style: const TextStyle( - color: Colors.white, - fontSize: 12, + columns: const [ + DataColumn( + label: Text( + "No.", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), ), ), - ), - DataCell(Text( - submissions[index].title, - style: const TextStyle(color: Colors.white, fontSize: 12), - )), - DataCell(Text( - "${submissions[index].acRate.toStringAsFixed(0)}%", - style: const TextStyle(color: Colors.white, fontSize: 12), - )), - DataCell(Text( - submissions[index].difficulty, - style: TextStyle( - color: submissions[index].difficulty.toLowerCase() == 'easy' - ? Colors.green - : submissions[index].difficulty.toLowerCase() == 'medium' - ? Colors.orange - : Colors.red, - fontSize: 12, + DataColumn( + label: Text( + "Title", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), ), - )), - ], - ), + DataColumn( + label: Text( + "Accuracy", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataColumn( + label: Text( + "Level", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + rows: List.generate( + submissions.length, + (index) => DataRow( + cells: [ + DataCell( + Text( + "${index + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + DataCell( + ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxTitleWidth.clamp(60, 260)), + child: Text( + submissions[index].title, + style: const TextStyle(color: Colors.white, fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + ), + DataCell( + Text( + "${submissions[index].acRate.toStringAsFixed(0)}%", + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ), + DataCell( + Builder(builder: (context) { + final diff = submissions[index].difficulty.toLowerCase(); + final Color bg = diff == 'easy' + ? const Color(0xFF6BC864) + : diff == 'medium' + ? const Color(0xFFFFC44E) + : const Color(0xFFFF2727); + final raw = submissions[index].difficulty.trim(); + final label = raw.isEmpty + ? raw + : raw[0].toUpperCase() + raw.substring(1).toLowerCase(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: bg, + // Squircle / boxy-pill: moderate radius + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: const TextStyle( + color: Colors.black, + fontSize: 12, + ), + ), + ); + }), + ), + ], + ), + ), + ); + }, ), ), ); From b4acbd2a5e0ede8caef39772e674eaed3411be6e Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 00:59:01 +0530 Subject: [PATCH 30/53] chore: small tweaks to groupinfo page --- leaderboard_app/lib/pages/groupinfo_page.dart | 96 +++++++++++-------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 75744df..e8a15fe 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -195,6 +195,23 @@ class _GroupInfoPageState extends State { } Widget _membersCard(List members) { + // Sort members: Owner first, then Admin/Moderator, then Member; alphabetically within each tier + int roleRank(String role) { + final r = role.toUpperCase(); + if (r == 'OWNER') return 0; + if (r == 'ADMIN' || r == 'MODERATOR') return 1; + return 2; // MEMBER or anything else + } + final sorted = [...members] + ..sort((a, b) { + final ar = roleRank(a.role); + final br = roleRank(b.role); + if (ar != br) return ar.compareTo(br); + final aName = (a.user?.username ?? a.userId).toLowerCase(); + final bName = (b.user?.username ?? b.userId).toLowerCase(); + return aName.compareTo(bName); + }); + return Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -207,7 +224,7 @@ class _GroupInfoPageState extends State { children: [ const Text('Members', style: TextStyle(fontSize: 18)), const SizedBox(height: 12), - ...members.map((m) => Padding( + ...sorted.map((m) => Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ @@ -280,49 +297,44 @@ class _GroupInfoPageState extends State { if (b.xp != a.xp) return b.xp.compareTo(a.xp); return aName.compareTo(bName); }); - return Container( - padding: const EdgeInsets.all(12), - width: double.infinity, - decoration: BoxDecoration(color: Colors.grey.shade900, borderRadius: BorderRadius.circular(12)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header removed per request - // Wrap DataTable to enforce full-width usage and allow overflow if needed - LayoutBuilder( - builder: (context, constraints) => ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: DataTable( - columnSpacing: 16, - dataRowMinHeight: 32, - dataRowMaxHeight: 40, - headingRowHeight: 32, - headingRowColor: MaterialStateProperty.all(Colors.grey[850]), - columns: const [ - DataColumn(label: Text('Place', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('Player', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('Streak', style: TextStyle(color: Colors.white, fontSize: 12))), - DataColumn(label: Text('Solved', style: TextStyle(color: Colors.white, fontSize: 12))), - ], - rows: List.generate(sorted.length, (i) { - final m = sorted[i]; - final uname = (m.user?.username ?? m.userId).toLowerCase(); - final hydrated = _userStats[uname]; - final streak = hydrated != null ? hydrated.$1 : (m.user?.streak ?? 0); - final solved = hydrated != null ? hydrated.$2 : (m.user?.totalSolved ?? 0); - final streakDisplay = streak == 0 ? '—' : '$streak'; - final solvedDisplay = solved == 0 ? '—' : '$solved'; - return DataRow(cells: [ - DataCell(Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text(m.user?.username ?? m.userId, style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text(streakDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), - DataCell(Text(solvedDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), - ]); - }), - ), + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + color: Colors.grey.shade900, + child: LayoutBuilder( + builder: (context, constraints) => ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: DataTable( + columnSpacing: 16, + dataRowMinHeight: 32, + dataRowMaxHeight: 40, + headingRowHeight: 32, + headingRowColor: MaterialStateProperty.all(Colors.grey[850]), + columns: const [ + DataColumn(label: Text('Place', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Player', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Streak', style: TextStyle(color: Colors.white, fontSize: 12))), + DataColumn(label: Text('Solved', style: TextStyle(color: Colors.white, fontSize: 12))), + ], + rows: List.generate(sorted.length, (i) { + final m = sorted[i]; + final uname = (m.user?.username ?? m.userId).toLowerCase(); + final hydrated = _userStats[uname]; + final streak = hydrated != null ? hydrated.$1 : (m.user?.streak ?? 0); + final solved = hydrated != null ? hydrated.$2 : (m.user?.totalSolved ?? 0); + final streakDisplay = streak == 0 ? '—' : '$streak'; + final solvedDisplay = solved == 0 ? '—' : '$solved'; + return DataRow(cells: [ + DataCell(Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(m.user?.username ?? m.userId, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(streakDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), + DataCell(Text(solvedDisplay, style: const TextStyle(color: Colors.white, fontSize: 12))), + ]); + }), ), ), - ], + ), ), ); } From 63fc91daa649d397912ce857bf03818433db28aa Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 01:12:12 +0530 Subject: [PATCH 31/53] chore: dashboard ui cleanup --- .../dashboard-components/daily_activity.dart | 129 +++++++++++++----- 1 file changed, 96 insertions(+), 33 deletions(-) diff --git a/leaderboard_app/lib/dashboard-components/daily_activity.dart b/leaderboard_app/lib/dashboard-components/daily_activity.dart index 9daaa17..4bedf6a 100644 --- a/leaderboard_app/lib/dashboard-components/daily_activity.dart +++ b/leaderboard_app/lib/dashboard-components/daily_activity.dart @@ -8,6 +8,7 @@ class LeetCodeDailyCard extends StatelessWidget { @override Widget build(BuildContext context) { + final dq = daily; return Container( padding: const EdgeInsets.all(14), width: double.infinity, @@ -15,41 +16,103 @@ class LeetCodeDailyCard extends StatelessWidget { color: Colors.grey[850], borderRadius: BorderRadius.circular(8), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - daily == null - ? 'Daily question unavailable' - : "${daily!.questionTitle} (${daily!.difficulty})\n${daily!.questionLink}", - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[700], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + child: dq == null + ? const Text( + 'Daily question unavailable', + style: TextStyle(color: Colors.white70, fontSize: 14), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 4), // nudge text downward + child: Text( + dq.questionTitle, + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.25, + ), + overflow: TextOverflow.ellipsis, + maxLines: 3, + softWrap: true, + ), + ), + ), + const SizedBox(width: 12), + if (dq.difficulty.trim().isNotEmpty) + Align( + alignment: Alignment.center, + child: _DifficultyPill(dq.difficulty), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), + ), + onPressed: dq.questionLink.isEmpty + ? null + : () async { + final url = Uri.parse(dq.questionLink); + if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { + // ignore: avoid_print + print('Could not launch $url'); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Go to Question', style: TextStyle(fontWeight: FontWeight.w600)), + SizedBox(width: 6), + Icon(Icons.north_east, size: 18, color: Colors.black), + ], + ), + ), ), - ), - onPressed: daily?.questionLink == null - ? null - : () async { - final url = Uri.parse(daily!.questionLink); - if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { - // ignore: avoid_print - print('Could not launch $url'); - } - }, - child: const Text("Go to Question >", style: TextStyle(color: Colors.black)), + ], ), - ), - ], + ); + } +} + +class _DifficultyPill extends StatelessWidget { + final String raw; + const _DifficultyPill(this.raw); + + @override + Widget build(BuildContext context) { + final diff = raw.toLowerCase(); + final Color bg = diff == 'easy' + ? const Color(0xFF6BC864) + : diff == 'medium' + ? const Color(0xFFFFC44E) + : const Color(0xFFFF2727); + final label = raw.isEmpty ? raw : raw[0].toUpperCase() + raw.substring(1).toLowerCase(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: const TextStyle(color: Colors.black, fontSize: 12, fontWeight: FontWeight.w500), ), ); } From d595c560ee21337153eb2546655de7d9011f2c15 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 01:21:41 +0530 Subject: [PATCH 32/53] feat: added page when device is offline --- leaderboard_app/README.md | 20 ++ leaderboard_app/lib/main.dart | 175 +++++++++++++++--- .../lib/pages/no_internet_page.dart | 34 ++++ .../lib/provider/connectivity_provider.dart | 48 +++++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + leaderboard_app/pubspec.lock | 112 +++++++++++ leaderboard_app/pubspec.yaml | 12 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 9 files changed, 382 insertions(+), 25 deletions(-) create mode 100644 leaderboard_app/lib/pages/no_internet_page.dart create mode 100644 leaderboard_app/lib/provider/connectivity_provider.dart diff --git a/leaderboard_app/README.md b/leaderboard_app/README.md index f8bd805..2ae7317 100644 --- a/leaderboard_app/README.md +++ b/leaderboard_app/README.md @@ -148,3 +148,23 @@ If the backend uses a self-signed certificate, Android may reject it—use a val * Implement exponential backoff retries for transient network errors. * Add Sentry or similar for error monitoring. +## Splash Screen & Offline Handling + +The app shows a native splash (configured via `flutter_native_splash`) while core services initialize. For returning users (flag stored in `SharedPreferences` as `returningUser`), dashboard data is preloaded (daily question, submissions if verified, leaderboard) before removing the splash to deliver a populated home view quickly. + +If there's no network connectivity at launch, a dedicated offline screen (`NoInternetPage`) is displayed. Connectivity is monitored with `connectivity_plus` through `ConnectivityProvider`; once a connection becomes available the app automatically proceeds with initialization and dismisses the splash. + +Update splash assets/colors in `pubspec.yaml` under `flutter_native_splash:` then regenerate: + +```bash +flutter pub run flutter_native_splash:create +``` + +Key files: + +* `lib/main.dart` – splash preservation & initialization logic (`_AppInitializer`). +* `lib/provider/connectivity_provider.dart` – connectivity listener. +* `lib/pages/no_internet_page.dart` – offline UI. + +To disable preloading behavior simply remove the `dashboardProvider.loadAll()` call in `_preload()`. + diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index c192a64..dfcb97d 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/provider/chat_provider.dart'; import 'package:leaderboard_app/provider/theme_provider.dart'; @@ -14,9 +15,13 @@ import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/provider/group_provider.dart'; import 'package:leaderboard_app/provider/dashboard_provider.dart'; import 'package:leaderboard_app/provider/group_membership_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:leaderboard_app/provider/connectivity_provider.dart'; +import 'package:leaderboard_app/pages/no_internet_page.dart'; void main() { - WidgetsFlutterBinding.ensureInitialized(); + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); runApp(const Bootstrap()); } @@ -25,14 +30,8 @@ class Bootstrap extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - AuthService.create(), - DashboardService.create(), - LeetCodeService.create(), - GroupService.create(), - UserService.create(), - ]), + return FutureBuilder( + future: _bootstrapServices(), builder: (context, snapshot) { if (!snapshot.hasData) { return const MaterialApp( @@ -40,40 +39,168 @@ class Bootstrap extends StatelessWidget { home: Scaffold(body: Center(child: CircularProgressIndicator())), ); } - final authService = snapshot.data![0] as AuthService; - final dashboardService = snapshot.data![1] as DashboardService; - final leetCodeService = snapshot.data![2] as LeetCodeService; - final groupService = snapshot.data![3] as GroupService; - final userService = snapshot.data![4] as UserService; - final router = createRouter(); - - return MultiProvider( + + final data = snapshot.data!; + final authService = data.authService; + final dashboardService = data.dashboardService; + final leetCodeService = data.leetCodeService; + final groupService = data.groupService; + final userService = data.userService; + final router = createRouter(); + + return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ChatListProvider()), ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => ChatProvider()), + ChangeNotifierProvider(create: (_) => ConnectivityProvider()), ChangeNotifierProvider(create: (ctx) => GroupProvider(groupService)), ChangeNotifierProvider(create: (ctx) => DashboardProvider( - service: dashboardService, - userProvider: ctx.read(), - userService: userService, - )), - // Group membership status (scoped per group via proxy widgets later) - ChangeNotifierProvider(create: (ctx) => GroupMembershipProvider(service: groupService, userProvider: ctx.read())), + service: dashboardService, + userProvider: ctx.read(), + userService: userService, + )), + ChangeNotifierProvider( + create: (ctx) => GroupMembershipProvider( + service: groupService, userProvider: ctx.read())), Provider.value(value: authService), Provider.value(value: dashboardService), Provider.value(value: leetCodeService), Provider.value(value: groupService), Provider.value(value: userService), ], - child: MainApp(router: router), + child: _AppInitializer(router: router), ); }, ); } } +class _BootstrapData { + final AuthService authService; + final DashboardService dashboardService; + final LeetCodeService leetCodeService; + final GroupService groupService; + final UserService userService; + _BootstrapData({ + required this.authService, + required this.dashboardService, + required this.leetCodeService, + required this.groupService, + required this.userService, + }); +} + +Future<_BootstrapData> _bootstrapServices() async { + final results = await Future.wait([ + AuthService.create(), + DashboardService.create(), + LeetCodeService.create(), + GroupService.create(), + UserService.create(), + ]); + return _BootstrapData( + authService: results[0] as AuthService, + dashboardService: results[1] as DashboardService, + leetCodeService: results[2] as LeetCodeService, + groupService: results[3] as GroupService, + userService: results[4] as UserService, + ); +} + +class _AppInitializer extends StatefulWidget { + final GoRouter router; + const _AppInitializer({required this.router}); + + @override + State<_AppInitializer> createState() => _AppInitializerState(); +} + +class _AppInitializerState extends State<_AppInitializer> { + bool _ready = false; + bool _offline = false; + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + final connectivity = context.read(); + // Wait a tick for connectivity to initialize + await Future.delayed(const Duration(milliseconds: 50)); + _offline = !connectivity.isOnline; + if (!_offline) { + await _preload(); + } + if (mounted) { + setState(() { + _ready = true; + }); + FlutterNativeSplash.remove(); + } + // Listen for connectivity changes to leave offline screen automatically + connectivity.addListener(() async { + if (mounted && _offline && connectivity.isOnline) { + setState(() { + _offline = false; + _ready = false; // show loading while we fetch + }); + await _preload(); + if (mounted) { + setState(() { + _ready = true; + }); + } + } + }); + } + + Future _preload() async { + final prefs = await SharedPreferences.getInstance(); + final returning = prefs.getBool('returningUser') ?? false; + final userProvider = context.read(); + final dashboardProvider = context.read(); + final userService = context.read(); + + // Always fetch profile if logged in (token check can happen in router later but we attempt anyway) + try { + await userProvider.fetchProfile(userService); + } catch (_) {} + + if (returning) { + try { + await dashboardProvider.loadAll(); + } catch (_) {} + } + + // Mark as returning after first launch completion + if (!returning) { + await prefs.setBool('returningUser', true); + } + } + + @override + Widget build(BuildContext context) { + final connectivity = context.watch(); + if (!connectivity.isOnline || _offline) { + return const MaterialApp( + debugShowCheckedModeBanner: false, + home: NoInternetPage(), + ); + } + if (!_ready) { + return const MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: CircularProgressIndicator())), + ); + } + return MainApp(router: widget.router); + } +} + class MainApp extends StatelessWidget { final GoRouter router; const MainApp({super.key, required this.router}); diff --git a/leaderboard_app/lib/pages/no_internet_page.dart b/leaderboard_app/lib/pages/no_internet_page.dart new file mode 100644 index 0000000..07deee6 --- /dev/null +++ b/leaderboard_app/lib/pages/no_internet_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class NoInternetPage extends StatelessWidget { + const NoInternetPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.wifi_off, size: 96, color: Colors.grey), + const SizedBox(height: 24), + const Text('No Internet Connection', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + const Text('Please check your connection. The app will continue once you are back online.', textAlign: TextAlign.center), + const SizedBox(height: 32), + FilledButton( + onPressed: () { + // Simply trigger a rebuild; actual status is managed by provider listening. + (context as Element).markNeedsBuild(); + }, + child: const Text('Retry'), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/leaderboard_app/lib/provider/connectivity_provider.dart b/leaderboard_app/lib/provider/connectivity_provider.dart new file mode 100644 index 0000000..6c99dcc --- /dev/null +++ b/leaderboard_app/lib/provider/connectivity_provider.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/foundation.dart'; + +class ConnectivityProvider extends ChangeNotifier { + final Connectivity _connectivity = Connectivity(); + late final StreamSubscription> _sub; + bool _isOnline = true; // assume online until checked + bool get isOnline => _isOnline; + + ConnectivityProvider() { + _init(); + } + + Future _init() async { + await _checkInitial(); + _sub = _connectivity.onConnectivityChanged.listen(_handleChange); + } + + Future _checkInitial() async { + try { + final results = await _connectivity.checkConnectivity(); + _updateStatus(results); + } catch (_) { + // ignore + } + } + + void _handleChange(List results) { + _updateStatus(results); + } + + void _updateStatus(List results) { + final online = results.any((r) => r != ConnectivityResult.none); + if (online != _isOnline) { + _isOnline = online; + notifyListeners(); + } + } + + @override + void dispose() { + try { + _sub.cancel(); + } catch (_) {} + super.dispose(); + } +} \ No newline at end of file diff --git a/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift index 997e35d..bc90613 100644 --- a/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/leaderboard_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import connectivity_plus import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index ef9cebc..215ed6c 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -17,6 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.7.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -145,6 +161,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -161,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -169,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" dio: dependency: "direct main" description: @@ -238,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" + url: "https://pub.dev" + source: hosted + version: "2.4.6" flutter_test: dependency: "direct dev" description: flutter @@ -288,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -312,6 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" io: dependency: transitive description: @@ -424,6 +496,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" package_config: dependency: transitive description: @@ -464,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" pie_chart: dependency: "direct main" description: @@ -504,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" protobuf: dependency: transitive description: @@ -733,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -853,6 +957,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index 11afd9b..fbc47b7 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: shared_preferences: ^2.3.2 url_launcher: ^6.3.0 socket_io_client: ^2.0.3 + connectivity_plus: ^6.0.5 dev_dependencies: flutter_test: @@ -28,6 +29,7 @@ dev_dependencies: build_runner: ^2.4.13 retrofit_generator: ^9.1.5 json_serializable: ^6.8.0 + flutter_native_splash: ^2.4.1 flutter: uses-material-design: true @@ -39,4 +41,12 @@ flutter: - family: AlumniSans fonts: - asset: fonts/AlumniSans-VariableFont_wght.ttf - - asset: fonts/AlumniSans-Italic-VariableFont_wght.ttf \ No newline at end of file + - asset: fonts/AlumniSans-Italic-VariableFont_wght.ttf + +flutter_native_splash: + color: "#FFFFFF" + image: assets/icons/google.png + android_12: + image: assets/icons/google.png + color: "#FFFFFF" + web: false \ No newline at end of file diff --git a/leaderboard_app/windows/flutter/generated_plugin_registrant.cc b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc index 4f78848..5777988 100644 --- a/leaderboard_app/windows/flutter/generated_plugin_registrant.cc +++ b/leaderboard_app/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/leaderboard_app/windows/flutter/generated_plugins.cmake b/leaderboard_app/windows/flutter/generated_plugins.cmake index 88b22e5..3103206 100644 --- a/leaderboard_app/windows/flutter/generated_plugins.cmake +++ b/leaderboard_app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus url_launcher_windows ) From ff47b6c7b80273904b7cba027ca3c608d6daab93 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 01:41:10 +0530 Subject: [PATCH 33/53] chore: UI tweaks dashboard --- leaderboard_app/lib/pages/dashboard_page.dart | 84 +++++++++++++++---- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index f22e525..ebee5b2 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -10,6 +10,7 @@ import 'package:leaderboard_app/provider/user_provider.dart'; // import 'package:leaderboard_app/models/dashboard_models.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; import 'package:leaderboard_app/provider/dashboard_provider.dart'; +import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; class DashboardPage extends StatefulWidget { @@ -138,7 +139,7 @@ class _DashboardPageState extends State { builder: (_, dp, __) { if (dp.loadingSubs) return _loadingCard(height: 180); if (!dp.isVerified) { - return _verifyCard(); + return _verifyCard(context); } if (dp.errorSubs != null) { return _errorCard(dp.errorSubs!); @@ -201,23 +202,70 @@ class _DashboardPageState extends State { ); } - Widget _verifyCard() { - return Container( - width: double.infinity, - height: 160, - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text('Connect your LeetCode account to see recent submissions and streaks', - style: TextStyle(color: Colors.white70)), - SizedBox(height: 8), - Text('Go to Settings > Verify LeetCode', style: TextStyle(color: Colors.white54, fontSize: 12)), - ], + Widget _verifyCard(BuildContext context) { + // Single-line actionable tile styled like the provided screenshot. + return GestureDetector( + onTap: () => context.push('/verify'), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + // Leading icon (generic code icon since no LeetCode asset present) + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.link, color: Colors.white70, size: 18), + ), + const SizedBox(width: 12), + // Texts + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + 'Connect to LeetCode', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + SizedBox(height: 2), + Text( + 'See recent submissions & streaks', + style: TextStyle(color: Colors.white54, fontSize: 11), + ), + ], + ), + ), + // Red error / attention indicator + Container( + width: 22, + height: 22, + decoration: const BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: const Text( + '!', + style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 10), + const Icon(Icons.chevron_right, color: Colors.white38, size: 22), + ], + ), ), ); } From ca7cc5f80dd38638309fd3ef14f52fb242ac3c70 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 01:58:59 +0530 Subject: [PATCH 34/53] chore: fixed options in group info --- leaderboard_app/lib/pages/groupinfo_page.dart | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index e8a15fe..9490026 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -75,14 +75,27 @@ class _GroupInfoPageState extends State { return g.members.any((m) => m.userId == uid && m.role.toUpperCase() == 'OWNER'); } - bool get _isAdminOrOwner { + bool get _isAdmin { final uid = _currentUserId; final g = _group; if (uid == null || g == null) return false; - if (_isOwner) return true; return g.members.any((m) => m.userId == uid && (m.role.toUpperCase() == 'ADMIN' || m.role.toUpperCase() == 'MODERATOR')); } + bool _canManage(GroupMember target) { + // Owner can manage anyone except themselves + if (_isOwner) { + // Prevent self demotion via manage menu (handled elsewhere) by disallowing actions on OWNER role belonging to current user. + return !(target.role.toUpperCase() == 'OWNER' && target.userId == _currentUserId); + } + // Admins can manage only regular members (not owner, not other admins/mods) + if (_isAdmin) { + final role = target.role.toUpperCase(); + return role != 'OWNER' && role != 'ADMIN' && role != 'MODERATOR'; + } + return false; // members cannot manage anyone + } + Future _joinLeave() async { if (_group == null) return; setState(() => _mutating = true); @@ -124,7 +137,8 @@ class _GroupInfoPageState extends State { padding: EdgeInsets.only(right: 12), child: Center(child: SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2))), ), - if (!_loading && _group != null && _isAdminOrOwner) + // Only the owner should see the 3-dot menu (admins no longer see it) + if (!_loading && _group != null && _isOwner) PopupMenuButton( onSelected: (value) async { switch (value) { @@ -247,21 +261,21 @@ class _GroupInfoPageState extends State { decoration: BoxDecoration(color: Colors.blueGrey.shade700, borderRadius: BorderRadius.circular(8)), child: Text(m.role, style: const TextStyle(fontSize: 12)), ), - if (_isAdminOrOwner) + if (_canManage(m)) PopupMenuButton( onSelected: (value) async { switch (value) { case 'remove': - await _removeMember(m); + if (_canManage(m)) await _removeMember(m); break; case 'promote': - await _changeRole(m, 'ADMIN'); + if (_canManage(m)) await _changeRole(m, 'ADMIN'); break; case 'demote': - await _changeRole(m, 'MEMBER'); + if (_canManage(m)) await _changeRole(m, 'MEMBER'); break; case 'makeOwner': - await _transferOwnershipTo(m); + if (_isOwner && m.role.toUpperCase() != 'OWNER') await _transferOwnershipTo(m); break; } }, @@ -269,7 +283,7 @@ class _GroupInfoPageState extends State { const PopupMenuItem(value: 'remove', child: Text('Remove')), const PopupMenuItem(value: 'promote', child: Text('Promote to Admin')), const PopupMenuItem(value: 'demote', child: Text('Demote to Member')), - if (_isOwner) const PopupMenuItem(value: 'makeOwner', child: Text('Make Owner')), + if (_isOwner && m.role.toUpperCase() != 'OWNER') const PopupMenuItem(value: 'makeOwner', child: Text('Make Owner')), ], ), ], From badf7da350ebbcde54e810459390960618a6007e Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 02:17:19 +0530 Subject: [PATCH 35/53] feat: added filter to group chat lists page --- leaderboard_app/lib/pages/chatlists_page.dart | 102 +++++++++++++++++- leaderboard_app/lib/pages/groupinfo_page.dart | 14 +++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 6027600..105793d 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -19,6 +19,8 @@ class _ChatlistsPageState extends State { final TextEditingController _searchController = TextEditingController(); Timer? _debounce; String _searchQuery = ''; + // Filter state: true = Joined, false = Not Joined + bool _showJoined = true; @override void initState() { @@ -232,7 +234,28 @@ class _ChatlistsPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); - final groups = chatProvider.chatGroups; + final rawGroups = chatProvider.chatGroups; + // Determine current user id to evaluate membership + final currentUserId = context.read()?.user?.id; + int joinedCount = 0; + int notJoinedCount = 0; + // Precompute membership flags for performance + final membershipFlags = {}; + for (final g in rawGroups) { + final members = (g['members'] as List?) ?? const []; + final isJoined = currentUserId != null && members.any((m) => m is Map && m['uid'] == currentUserId); + membershipFlags[g['groupId']?.toString() ?? ''] = isJoined; + if (isJoined) { + joinedCount++; + } else { + notJoinedCount++; + } + } + List> groups = rawGroups.where((g) { + final id = g['groupId']?.toString() ?? ''; + final isJoined = membershipFlags[id] ?? false; + return _showJoined ? isJoined : !isJoined; + }).toList(); return Scaffold( backgroundColor: theme.surface, @@ -315,6 +338,32 @@ class _ChatlistsPageState extends State { ], ), const SizedBox(height: 16), + // Joined / Not Joined filter buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + _buildFilterButton( + label: 'Joined', + count: joinedCount, + selected: _showJoined, + onTap: () { + if (!_showJoined) setState(() => _showJoined = true); + }, + ), + const SizedBox(width: 12), + _buildFilterButton( + label: 'Not Joined', + count: notJoinedCount, + selected: !_showJoined, + onTap: () { + if (_showJoined) setState(() => _showJoined = false); + }, + ), + ], + ), + ), + const SizedBox(height: 12), // Group List Expanded( @@ -346,7 +395,7 @@ class _ChatlistsPageState extends State { const SizedBox(height: 120), Center( child: Text( - 'No groups found', + _showJoined ? 'No joined groups' : 'No groups available', style: TextStyle(color: theme.primary), ), ), @@ -453,4 +502,53 @@ class _ChatlistsPageState extends State { ), ); } +} + +// Helper widget builder for filter buttons +extension _ChatFilters on _ChatlistsPageState { + Widget _buildFilterButton({required String label, required int count, required bool selected, required VoidCallback onTap}) { + const activeColor = Color(0xFFF6C155); // #f6c155 + final inactiveBorder = Colors.white.withOpacity(0.1); + return Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + height: 36, + child: Material( + color: selected ? activeColor : Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + side: BorderSide(color: selected ? activeColor : inactiveBorder, width: 1), + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + splashColor: Colors.white24, + child: Center( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + color: selected ? Colors.black : Colors.white70, + ), + children: [ + TextSpan(text: label), + const TextSpan(text: ' '), + TextSpan( + text: count.toString(), + style: TextStyle( + fontWeight: FontWeight.w600, + color: selected ? Colors.black : Colors.white60, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 9490026..2d6120f 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -103,6 +103,13 @@ class _GroupInfoPageState extends State { final svc = context.read(); if (_isMember) { await svc.leaveGroup(_group!.id); + // After leaving, refresh public group listing so counts & membership reflect change + try { + final chatListProv = context.read(); + if (chatListProv != null) { + chatListProv.loadPublicGroups(svc); + } + } catch (_) {} // Pop immediately with result so upstream (e.g., ChatPage) can react if (mounted) Navigator.of(context).pop({'leftGroup': true, 'groupId': _group!.id}); return; // skip reloading after leave @@ -111,6 +118,13 @@ class _GroupInfoPageState extends State { // Inform membership provider (if in tree) so gate can switch to chat final membershipProv = context.read(); membershipProv?.markJoined(); + // Refresh public groups so membership filter updates automatically + try { + final chatListProv = context.read(); + if (chatListProv != null) { + chatListProv.loadPublicGroups(svc); + } + } catch (_) {} } await _load(); } catch (e) { From 815e1db16be04638c7b600eb3e9aa7de34fe137b Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 02:45:30 +0530 Subject: [PATCH 36/53] chore: UI tweak group page + remove reply feature --- .../lib/chatpage-components/message_list.dart | 23 +------- .../lib/chatpage-components/user_input.dart | 35 ----------- leaderboard_app/lib/models/chat_message.dart | 3 - leaderboard_app/lib/pages/chatlists_page.dart | 59 ++++++++++++------- .../lib/provider/chat_provider.dart | 12 ---- .../lib/provider/chatlists_provider.dart | 31 +++++++--- 6 files changed, 64 insertions(+), 99 deletions(-) diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index baaedc9..da02ccd 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -42,12 +42,7 @@ class MessageList extends StatelessWidget { if (isImage) return _ImageMessage(msg: msg, isMe: isMe); - return GestureDetector( - onDoubleTap: () { - provider.setReplyTo(groupId, msg["message"]); - }, - child: _TextMessage(msg: msg, isMe: isMe), - ); + return _TextMessage(msg: msg, isMe: isMe); }, ); } @@ -160,22 +155,6 @@ class _TextMessage extends StatelessWidget { ), ), const SizedBox(height: 4), - if (msg["replyTo"] != null) - Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - msg["replyTo"], - style: TextStyle( - fontSize: 11, - color: isMe ? Colors.black87 : Colors.white60, - ), - ), - ), Text( msg["message"] ?? "", style: TextStyle( diff --git a/leaderboard_app/lib/chatpage-components/user_input.dart b/leaderboard_app/lib/chatpage-components/user_input.dart index d03fc44..59114da 100644 --- a/leaderboard_app/lib/chatpage-components/user_input.dart +++ b/leaderboard_app/lib/chatpage-components/user_input.dart @@ -21,7 +21,6 @@ class UserInput extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context).colorScheme; final provider = Provider.of(context); - final replyTo = provider.getReplyTo(groupId); return SafeArea( child: Padding( @@ -30,40 +29,6 @@ class UserInput extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (replyTo != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 340), - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(24), - ), - child: Row( - children: [ - Expanded( - child: Text( - "Replying to: $replyTo", - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - ), - ), - GestureDetector( - onTap: () => provider.clearReplyTo(groupId), - child: const Icon( - Icons.close, - size: 16, - color: Colors.white54, - ), - ), - ], - ), - ), - ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/leaderboard_app/lib/models/chat_message.dart b/leaderboard_app/lib/models/chat_message.dart index 2918ba1..a57915e 100644 --- a/leaderboard_app/lib/models/chat_message.dart +++ b/leaderboard_app/lib/models/chat_message.dart @@ -5,7 +5,6 @@ class ChatMessage { final String senderName; final String message; final DateTime timestamp; - final String? replyTo; ChatMessage({ required this.id, @@ -14,7 +13,6 @@ class ChatMessage { required this.senderName, required this.message, required this.timestamp, - this.replyTo, }); factory ChatMessage.fromSocket(Map raw) { @@ -26,7 +24,6 @@ class ChatMessage { senderName: sender is Map ? (sender['username'] ?? 'User').toString() : 'User', message: (raw['message'] ?? '').toString(), timestamp: _parseTs(raw['timestamp']), - replyTo: raw['replyTo'] as String?, ); } diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 105793d..f127f0f 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -235,27 +235,14 @@ class _ChatlistsPageState extends State { final theme = Theme.of(context).colorScheme; final chatProvider = Provider.of(context); final rawGroups = chatProvider.chatGroups; - // Determine current user id to evaluate membership - final currentUserId = context.read()?.user?.id; int joinedCount = 0; int notJoinedCount = 0; - // Precompute membership flags for performance - final membershipFlags = {}; for (final g in rawGroups) { - final members = (g['members'] as List?) ?? const []; - final isJoined = currentUserId != null && members.any((m) => m is Map && m['uid'] == currentUserId); - membershipFlags[g['groupId']?.toString() ?? ''] = isJoined; - if (isJoined) { - joinedCount++; - } else { - notJoinedCount++; - } + final isMember = g['isMember'] == true; + if (isMember) { + joinedCount++; } else { notJoinedCount++; } } - List> groups = rawGroups.where((g) { - final id = g['groupId']?.toString() ?? ''; - final isJoined = membershipFlags[id] ?? false; - return _showJoined ? isJoined : !isJoined; - }).toList(); + final groups = rawGroups.where((g) => _showJoined ? g['isMember'] == true : g['isMember'] != true).toList(); return Scaffold( backgroundColor: theme.surface, @@ -474,9 +461,41 @@ class _ChatlistsPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - group['time']?.toString() ?? '', - style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Time text + if ((group['time']?.toString() ?? '').isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 4), + child: Text( + group['time']?.toString() ?? '', + style: TextStyle(color: theme.primary.withOpacity(0.6), fontSize: 12), + ), + ), + // Privacy pill + Builder(builder: (ctx) { + final isPrivate = group['isPrivate'] == true; + final label = isPrivate ? 'Private' : 'Public'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: theme.primary.withOpacity(0.35), width: 1.5), + ), + child: Text( + label, + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w600, + color: theme.primary.withOpacity(0.85), + letterSpacing: 0.2, + ), + ), + ); + }), + ], ), const SizedBox(height: 8), if (group['unread'] == true) diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 69ecca5..b9056bb 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -9,7 +9,6 @@ import 'user_provider.dart'; /// ChatProvider integrates REST history + Socket.IO realtime events. class ChatProvider extends ChangeNotifier { final Map>> _groupMessages = {}; - final Map _groupReplyTo = {}; final Map _groupAttachmentVisibility = {}; final Set _joinedGroups = {}; final Map _groupCurrentPage = {}; // page loaded so far (starts at 1) @@ -34,7 +33,6 @@ class ChatProvider extends ChangeNotifier { String get currentUsername => _currentUsername ?? ''; List> getMessages(String groupId) => _groupMessages[groupId] ?? const []; - String? getReplyTo(String groupId) => _groupReplyTo[groupId]; bool getAttachmentOptionsVisibility(String groupId) => _groupAttachmentVisibility[groupId] ?? false; Future initIfNeeded([BuildContext? context]) async { @@ -80,7 +78,6 @@ class ChatProvider extends ChangeNotifier { if (_joinedGroups.contains(groupId)) return; _joinedGroups.add(groupId); _groupMessages.putIfAbsent(groupId, () => []); - _groupReplyTo.putIfAbsent(groupId, () => null); _groupAttachmentVisibility.putIfAbsent(groupId, () => false); // Fetch history (page=1) @@ -188,18 +185,9 @@ class ChatProvider extends ChangeNotifier { 'senderName': isMe ? 'You' : m.senderName, 'senderColor': isMe ? Colors.black : Colors.white, 'isMe': isMe, - if (m.replyTo != null) 'replyTo': m.replyTo, }; } - void setReplyTo(String groupId, String? message) { - _groupReplyTo[groupId] = message; - notifyListeners(); - } - void clearReplyTo(String groupId) { - _groupReplyTo[groupId] = null; - notifyListeners(); - } void toggleAttachmentOptions(String groupId) { _groupAttachmentVisibility[groupId] = !(_groupAttachmentVisibility[groupId] ?? false); notifyListeners(); diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart index db251e2..e441eda 100644 --- a/leaderboard_app/lib/provider/chatlists_provider.dart +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -39,27 +39,43 @@ class ChatListProvider extends ChangeNotifier { notifyListeners(); } - /// Load groups from backend (public groups) + /// Load groups from backend (public groups) and merge with user's joined groups (including private) Future loadPublicGroups(GroupService service, {int page = 1, int limit = 10, String? search}) async { _isLoading = true; _error = null; notifyListeners(); try { final paged = await service.getAllGroups(page: page, limit: limit, search: search); - _chatGroups = paged.groups.map((g) { - return { - 'groupId': g.id, - 'name': g.name, + final myGroups = await service.getMyGroups(); + + // Map by id to merge (my groups take precedence for member list completeness / privacy visibility) + final Map> merged = {}; + + void addOrUpdate(group, {bool isMember = false}) { + merged[group.id] = { + 'groupId': group.id, + 'name': group.name, 'lastMessage': '', 'time': '', - 'members': g.members.map((m) => { + 'isPrivate': group.isPrivate, + 'members': group.members.map((m) => { 'uid': m.userId, 'name': m.user?.username ?? m.userId, }).toList(), 'unread': false, 'favourite': false, + 'isMember': isMember, }; - }).toList(); + } + + for (final g in paged.groups) { + addOrUpdate(g, isMember: false); // unknown membership until merged with myGroups + } + for (final g in myGroups) { + addOrUpdate(g, isMember: true); // mark membership + } + + _chatGroups = merged.values.toList(); } catch (e) { _error = 'Failed to load groups'; } finally { @@ -81,6 +97,7 @@ class ChatListProvider extends ChangeNotifier { 'name': group.name, 'lastMessage': '', 'time': '', + 'isPrivate': group.isPrivate, 'members': group.members.map((m) => { 'uid': m.userId, 'name': m.user?.username ?? m.userId, From ef17d03cf2551a92f7d9b4da60524a1069448eac Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 03:05:48 +0530 Subject: [PATCH 37/53] chore: fixed time issue with messages --- .../lib/chatpage-components/message_list.dart | 13 +++++++------ leaderboard_app/lib/provider/chat_provider.dart | 10 +++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index da02ccd..1b82f62 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -90,18 +90,18 @@ class _ImageMessage extends StatelessWidget { @override Widget build(BuildContext context) { + final bubbleColor = isMe ? const Color(0xFFE3C17D) : Colors.grey.shade900; return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, + color: bubbleColor, borderRadius: BorderRadius.circular(12), ), child: Column( - crossAxisAlignment: - isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( width: 180, @@ -117,7 +117,7 @@ class _ImageMessage extends StatelessWidget { const SizedBox(height: 4), Text( msg["timestamp"] ?? "", - style: const TextStyle(fontSize: 10, color: Colors.black54), + style: TextStyle(fontSize: 10, color: isMe ? Colors.black54 : Colors.white54), ), ], ), @@ -133,13 +133,14 @@ class _TextMessage extends StatelessWidget { @override Widget build(BuildContext context) { + final bubbleColor = isMe ? const Color(0xFFE3C17D) : Colors.grey.shade900; return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isMe ? Theme.of(context).colorScheme.inversePrimary : Colors.grey.shade900, + color: bubbleColor, borderRadius: BorderRadius.circular(12), ), constraints: const BoxConstraints(maxWidth: 280), @@ -178,4 +179,4 @@ class _TextMessage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index b9056bb..97dc174 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -180,7 +180,8 @@ class ChatProvider extends ChangeNotifier { 'id': m.id, 'groupId': m.groupId, 'message': m.message, - 'timestamp': _formatTimestamp(m.timestamp), + // ensure local time for display + 'timestamp': _formatTimestamp(m.timestamp.toLocal()), 'senderID': m.senderId, 'senderName': isMe ? 'You' : m.senderName, 'senderColor': isMe ? Colors.black : Colors.white, @@ -194,9 +195,12 @@ class ChatProvider extends ChangeNotifier { } String _formatTimestamp(DateTime dt) { - final h = dt.hour > 12 ? dt.hour - 12 : dt.hour; + // Normalize to local just in case caller forgets + dt = dt.toLocal(); + final h24 = dt.hour; + final h = h24 == 0 ? 12 : (h24 > 12 ? h24 - 12 : h24); final m = dt.minute.toString().padLeft(2, '0'); - final ampm = dt.hour >= 12 ? 'pm' : 'am'; + final ampm = h24 >= 12 ? 'pm' : 'am'; return '$h:$m $ampm'; } From fb4d74dae01ffb0f4de85755c00613dc5eea5138 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 03:26:14 +0530 Subject: [PATCH 38/53] chroe: UI tweaks edit groups --- leaderboard_app/lib/pages/groupinfo_page.dart | 213 +++++++++++++----- .../lib/pages/widgets/group_form_dialog.dart | 200 ++++++++++++++++ 2 files changed, 362 insertions(+), 51 deletions(-) create mode 100644 leaderboard_app/lib/pages/widgets/group_form_dialog.dart diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 2d6120f..4134e36 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -369,64 +369,175 @@ class _GroupInfoPageState extends State { Future _showEditGroupDialog() async { if (_group == null) return; - final nameCtrl = TextEditingController(text: _group!.name); - final descCtrl = TextEditingController(text: _group!.description ?? ''); + final nameController = TextEditingController(text: _group!.name); + final descController = TextEditingController(text: _group!.description ?? ''); + final maxMembersController = TextEditingController(text: _group!.maxMembers?.toString() ?? ''); bool isPrivate = _group!.isPrivate; - final result = await showDialog( + + await showModalBottomSheet( context: context, - builder: (context) { - final colors = Theme.of(context).colorScheme; + isScrollControlled: true, + backgroundColor: Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) { return StatefulBuilder( - builder: (context, setLocalState) => AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Edit Group'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameCtrl, - decoration: const InputDecoration(labelText: 'Name'), - ), - const SizedBox(height: 8), - TextField( - controller: descCtrl, - decoration: const InputDecoration(labelText: 'Description'), - ), - const SizedBox(height: 8), - SwitchListTile( - title: const Text('Private'), - value: isPrivate, - onChanged: (v) => setLocalState(() => isPrivate = v), - ), - ], - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - ElevatedButton( - onPressed: () => Navigator.pop(context, true), - style: ElevatedButton.styleFrom(backgroundColor: colors.secondary, foregroundColor: Colors.black), - child: const Text('Save'), + builder: (context, setSheetState) { + final theme = Theme.of(context).colorScheme; + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + left: 16, + right: 16, + top: 16, ), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Edit Group', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: theme.primary, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + color: theme.primary, + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: nameController, + textInputAction: TextInputAction.next, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Name *', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: descController, + maxLines: 3, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Description', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: maxMembersController, + keyboardType: TextInputType.number, + style: TextStyle(color: theme.primary), + decoration: InputDecoration( + labelText: 'Max Members (optional)', + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Private', style: TextStyle(color: theme.primary.withOpacity(0.7))), + Switch( + value: isPrivate, + onChanged: (v) => setSheetState(() => isPrivate = v), + activeColor: theme.secondary, + ), + ], + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _mutating + ? null + : () async { + final name = nameController.text.trim(); + if (name.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Name is required'))); + return; + } + setState(() => _mutating = true); + final maxMembers = int.tryParse(maxMembersController.text.trim()); + try { + final svc = context.read(); + final g = await svc.updateGroup( + _group!.id, + name: name, + description: descController.text.trim().isEmpty ? null : descController.text.trim(), + isPrivate: isPrivate, + maxMembers: maxMembers, + ); + if (mounted) { + context.read()?.updateGroupMeta(groupId: g.id, name: g.name, isPrivate: g.isPrivate); + } + await _load(); + if (mounted) Navigator.pop(context); + } catch (_) { + if (mounted) setState(() => _error = 'Failed to update group'); + } finally { + if (mounted) setState(() => _mutating = false); + } + }, + icon: _mutating + ? SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Theme.of(context).colorScheme.onSecondary), + ) + : const Icon(Icons.check), + label: Text(_mutating ? 'Saving...' : 'Save Changes'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + ), + const SizedBox(height: 4), + ], + ), + ); + }, ); }, ); - if (result != true) return; - setState(() => _mutating = true); - try { - final svc = context.read(); - final updated = await svc.updateGroup(_group!.id, name: nameCtrl.text.trim(), description: descCtrl.text.trim().isEmpty ? null : descCtrl.text.trim(), isPrivate: isPrivate); - // Optimistically update chat list provider - if (mounted) { - context.read()?.updateGroupMeta(groupId: updated.id, name: updated.name, isPrivate: updated.isPrivate); - } - await _load(); - } catch (_) { - setState(() => _error = 'Failed to update group'); - } finally { - setState(() => _mutating = false); - } } Future _confirmDelete() async { diff --git a/leaderboard_app/lib/pages/widgets/group_form_dialog.dart b/leaderboard_app/lib/pages/widgets/group_form_dialog.dart new file mode 100644 index 0000000..58f2333 --- /dev/null +++ b/leaderboard_app/lib/pages/widgets/group_form_dialog.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; + +/// Reusable group form dialog/sheet content used for both create & edit flows. +/// Mirrors styling of the create group bottom sheet (rounded fields on dark bg). +class GroupFormDialog extends StatefulWidget { + final String title; + final String? initialName; + final String? initialDescription; + final bool initialPrivate; + final int? initialMaxMembers; // kept for symmetry (unused in edit currently) + final bool showMaxMembers; + final bool showPrivateToggle; + final Future Function({required String name, String? description, required bool isPrivate}) onSubmit; + + const GroupFormDialog({ + super.key, + required this.title, + this.initialName, + this.initialDescription, + this.initialPrivate = false, + this.initialMaxMembers, + this.showMaxMembers = false, + this.showPrivateToggle = true, + required this.onSubmit, + }); + + @override + State createState() => _GroupFormDialogState(); +} + +class _GroupFormDialogState extends State { + late final TextEditingController _nameCtrl; + late final TextEditingController _descCtrl; + late final TextEditingController _maxCtrl; + bool _isPrivate = false; + bool _submitting = false; + String? _error; + + @override + void initState() { + super.initState(); + _nameCtrl = TextEditingController(text: widget.initialName ?? ''); + _descCtrl = TextEditingController(text: widget.initialDescription ?? ''); + _maxCtrl = TextEditingController(text: widget.initialMaxMembers?.toString() ?? ''); + _isPrivate = widget.initialPrivate; + } + + @override + void dispose() { + _nameCtrl.dispose(); + _descCtrl.dispose(); + _maxCtrl.dispose(); + super.dispose(); + } + + InputDecoration _fieldDecoration(BuildContext context, String label) { + final theme = Theme.of(context).colorScheme; + return InputDecoration( + labelText: label, + labelStyle: TextStyle(color: theme.primary.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey.shade900, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ); + } + + Future _submit() async { + final name = _nameCtrl.text.trim(); + if (name.isEmpty) { + setState(() => _error = 'Name is required'); + return; + } + setState(() { + _submitting = true; + _error = null; + }); + final ok = await widget.onSubmit( + name: name, + description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(), + isPrivate: _isPrivate); + if (!mounted) return; + setState(() => _submitting = false); + if (ok) Navigator.pop(context, true); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + return Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: theme.primary, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + color: theme.primary, + onPressed: () => Navigator.pop(context, false), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: _nameCtrl, + textInputAction: TextInputAction.next, + style: TextStyle(color: theme.primary), + decoration: _fieldDecoration(context, 'Name *'), + ), + const SizedBox(height: 12), + TextField( + controller: _descCtrl, + maxLines: 3, + style: TextStyle(color: theme.primary), + decoration: _fieldDecoration(context, 'Description'), + ), + if (widget.showMaxMembers) ...[ + const SizedBox(height: 12), + TextField( + controller: _maxCtrl, + keyboardType: TextInputType.number, + style: TextStyle(color: theme.primary), + decoration: _fieldDecoration(context, 'Max Members (optional)'), + ), + ], + if (widget.showPrivateToggle) ...[ + const SizedBox(height: 12), + Row( + children: [ + Text('Private', style: TextStyle(color: theme.primary.withOpacity(0.7))), + const SizedBox(width: 8), + Switch( + value: _isPrivate, + activeColor: theme.secondary, + onChanged: (v) => setState(() => _isPrivate = v), + ), + ], + ), + ], + if (_error != null) ...[ + const SizedBox(height: 6), + Text(_error!, style: const TextStyle(color: Colors.redAccent, fontSize: 12)), + ], + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _submitting ? null : _submit, + icon: _submitting + ? SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.onSecondary, + ), + ) + : const Icon(Icons.check), + label: Text(_submitting ? 'Saving...' : 'Save'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file From b773cd817da5488c6fa0dbc5753995dd96cfbad7 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 22:23:19 +0530 Subject: [PATCH 39/53] chore: UI tweaks groupinfo --- leaderboard_app/lib/pages/groupinfo_page.dart | 239 +++++++++--------- 1 file changed, 126 insertions(+), 113 deletions(-) diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 4134e36..1e2d920 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -162,15 +162,11 @@ class _GroupInfoPageState extends State { case 'delete': await _confirmDelete(); break; - case 'transfer': - await _promptTransferOwnership(); - break; } }, itemBuilder: (context) => [ const PopupMenuItem(value: 'edit', child: Text('Edit Group')), const PopupMenuItem(value: 'delete', child: Text('Delete Group')), - if (_isOwner) const PopupMenuItem(value: 'transfer', child: Text('Transfer Ownership')), ], ), ], @@ -203,18 +199,50 @@ class _GroupInfoPageState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _mutating ? null : _joinLeave, - style: ElevatedButton.styleFrom(backgroundColor: theme.secondary), - child: Text(_isMember ? 'Leave Group' : 'Join Group', style: const TextStyle(color: Colors.black)), + // Join button (only when not already a member). Leave button moved below leaderboard. + if (!_isMember) + Center( + child: FractionallySizedBox( + widthFactor: 0.5, // half-width similar to leave button + child: ElevatedButton( + onPressed: _mutating ? null : _joinLeave, + style: ElevatedButton.styleFrom( + backgroundColor: theme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 8), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(0, 36), + ), + child: Text(_mutating ? 'Joining...' : 'Join Group'), + ), + ), ), - ), const SizedBox(height: 16), _membersCard(_group?.members ?? const []), const SizedBox(height: 16), _xpTable(_group?.members ?? const []), + const SizedBox(height: 16), + // Red leave button placed below the top players table as requested. + if (_isMember) + Center( + child: FractionallySizedBox( + widthFactor: 0.5, // half of available width + child: ElevatedButton( + onPressed: _mutating ? null : _joinLeave, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + disabledBackgroundColor: Colors.redAccent.withOpacity(0.5), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 8), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(0, 36), + ), + child: Text(_mutating ? 'Leaving...' : 'Leave Group'), + ), + ), + ), const SizedBox(height: 20), ], ), @@ -242,7 +270,7 @@ class _GroupInfoPageState extends State { return Container( width: double.infinity, - padding: const EdgeInsets.all(16), + padding: EdgeInsets.zero, // Removed padding so divider lines span edge-to-edge decoration: BoxDecoration( color: Colors.grey.shade900, borderRadius: BorderRadius.circular(12), @@ -250,59 +278,94 @@ class _GroupInfoPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Members', style: TextStyle(fontSize: 18)), - const SizedBox(height: 12), - ...sorted.map((m) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - CircleAvatar( - radius: 18, - backgroundColor: Colors.grey.shade700, - child: Text( - (m.user?.username.isNotEmpty == true) ? m.user!.username[0] : '?', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - const SizedBox(width: 12), - Text( - m.user?.username ?? m.userId, - style: const TextStyle(color: Colors.white70, fontSize: 16), - ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.blueGrey.shade700, borderRadius: BorderRadius.circular(8)), - child: Text(m.role, style: const TextStyle(fontSize: 12)), + // Local padding for header only (keeps container itself unpadded for full-width lines) + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Text('Members', style: TextStyle(fontSize: 18)), + ), + // Top divider above the first member row + Divider( + color: Colors.grey.shade500, + height: 1, + thickness: 1, + ), + for (int i = 0; i < sorted.length; i++) ...[ + Padding( + // Maintain horizontal padding for content while allowing dividers to stretch full width + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + child: Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: Colors.grey.shade700, + child: Text( + (sorted[i].user?.username.isNotEmpty == true) ? sorted[i].user!.username[0] : '?', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), - if (_canManage(m)) - PopupMenuButton( - onSelected: (value) async { - switch (value) { - case 'remove': - if (_canManage(m)) await _removeMember(m); - break; - case 'promote': - if (_canManage(m)) await _changeRole(m, 'ADMIN'); - break; - case 'demote': - if (_canManage(m)) await _changeRole(m, 'MEMBER'); - break; - case 'makeOwner': - if (_isOwner && m.role.toUpperCase() != 'OWNER') await _transferOwnershipTo(m); - break; - } - }, - itemBuilder: (context) => [ - const PopupMenuItem(value: 'remove', child: Text('Remove')), - const PopupMenuItem(value: 'promote', child: Text('Promote to Admin')), - const PopupMenuItem(value: 'demote', child: Text('Demote to Member')), - if (_isOwner && m.role.toUpperCase() != 'OWNER') const PopupMenuItem(value: 'makeOwner', child: Text('Make Owner')), - ], + ), + const SizedBox(width: 12), + Text( + sorted[i].user?.username ?? sorted[i].userId, + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + sorted[i].role, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade300, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, ), - ], - ), - )), + ), + ), + PopupMenuButton( + tooltip: 'Member actions', + icon: const Icon(Icons.chevron_right, color: Colors.white70, size: 20), + onSelected: (value) async { + final m = sorted[i]; + switch (value) { + case 'remove': + if (_canManage(m)) await _removeMember(m); + break; + case 'promote': + if (_canManage(m)) await _changeRole(m, 'ADMIN'); + break; + case 'demote': + if (_canManage(m)) await _changeRole(m, 'MEMBER'); + break; + } + }, + itemBuilder: (context) { + final m = sorted[i]; + final can = _canManage(m); + if (!can) { + return const [ + PopupMenuItem( + enabled: false, + child: Text('No actions available'), + ), + ]; + } + return [ + const PopupMenuItem(value: 'remove', child: Text('Remove')), + if (m.role.toUpperCase() == 'MEMBER') const PopupMenuItem(value: 'promote', child: Text('Promote to Admin')), + if (m.role.toUpperCase() == 'ADMIN' || m.role.toUpperCase() == 'MODERATOR') const PopupMenuItem(value: 'demote', child: Text('Demote to Member')), + ]; + }, + ), + ], + ), + ), + if (i < sorted.length - 1) + Divider( + color: Colors.grey.shade800, + height: 1, + thickness: 1, + ), + ], ], ), ); @@ -600,54 +663,4 @@ class _GroupInfoPageState extends State { setState(() => _mutating = false); } } - - Future _promptTransferOwnership() async { - if (_group == null) return; - final members = _group!.members.where((m) => m.userId != _currentUserId).toList(); - String? selectedUserId = members.isNotEmpty ? members.first.userId : null; - final ok = await showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Transfer Ownership'), - content: DropdownButton( - value: selectedUserId, - items: members - .map((m) => DropdownMenuItem( - value: m.userId, - child: Text(m.user?.username ?? m.userId), - )) - .toList(), - onChanged: (v) { - selectedUserId = v; - }, - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Transfer')), - ], - ), - ); - if (ok == true && selectedUserId != null) { - await _transferOwnershipToUserId(selectedUserId!); - } - } - - Future _transferOwnershipTo(GroupMember m) async { - await _transferOwnershipToUserId(m.userId); - } - - Future _transferOwnershipToUserId(String userId) async { - if (_group == null) return; - setState(() => _mutating = true); - try { - final svc = context.read(); - await svc.transferOwnership(_group!.id, userId); - await _load(); - } catch (_) { - setState(() => _error = 'Failed to transfer ownership'); - } finally { - setState(() => _mutating = false); - } - } } \ No newline at end of file From bca4eb78c06d6e5c701dbc3b824735462674f330 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 22:57:21 +0530 Subject: [PATCH 40/53] chore: UI cleanup settings page --- leaderboard_app/lib/pages/settings_page.dart | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 7f57826..96aaa8e 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -66,23 +66,17 @@ class SettingsPage extends StatelessWidget { children: [ Center( child: CircleAvatar( - radius: 40, + radius: 50, backgroundColor: colors.tertiary.withOpacity(0.3), child: Icon( Icons.person, - size: 32, + size: 40, color: colors.primary, ), ), ), const SizedBox(height: 10), - Divider( - height: 1, - thickness: 0.6, - color: colors.primary.withOpacity(0.3), - ), - // Username _buildDisplayTile('Username', '@$username', colors), Divider( @@ -102,6 +96,7 @@ class SettingsPage extends StatelessWidget { thickness: 0.6, color: colors.primary.withOpacity(0.3), ), + SizedBox(height: 12), // LeetCode handle & verify section if (verified) _buildDisplayTile('LeetCode', handle ?? '-', colors) @@ -133,7 +128,7 @@ class SettingsPage extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: colors.secondary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), ), child: const Text('Verify'), ), @@ -141,6 +136,7 @@ class SettingsPage extends StatelessWidget { ), ], ), + SizedBox(height: 12), Divider( height: 1, thickness: 0.6, @@ -154,7 +150,6 @@ class SettingsPage extends StatelessWidget { const SizedBox(height: 25), // Removed password & authentication section per request - const SizedBox(height: 4), // ====== Logout button (full-width) ====== SizedBox( From 24e52b7518729745292eca9d269008e2f4936837 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Thu, 2 Oct 2025 23:49:43 +0530 Subject: [PATCH 41/53] feat: UI overhaul chat page --- .../lib/chatpage-components/message_list.dart | 213 ++++++++++++++---- leaderboard_app/lib/pages/groupinfo_page.dart | 2 +- leaderboard_app/pubspec.lock | 16 ++ leaderboard_app/pubspec.yaml | 1 + 4 files changed, 184 insertions(+), 48 deletions(-) diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 1b82f62..00cf20e 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:pixelarticons/pixel.dart'; import 'package:provider/provider.dart'; +import 'package:chat_bubbles/chat_bubbles.dart'; import '../provider/chat_provider.dart'; -class MessageList extends StatelessWidget { +/// MessageList features: +/// 1. Bubble tails only on the last message in a consecutive block from the same sender. +/// 2. Only other users' names are shown (current user's name omitted) above the first bubble in their consecutive block. +/// 3. Swipe left on any message to reveal timestamps for all messages; swipe right to hide. +class MessageList extends StatefulWidget { final String groupId; final ScrollController scrollController; final VoidCallback scrollDownCallback; @@ -15,10 +20,23 @@ class MessageList extends StatelessWidget { required this.scrollDownCallback, }); + @override + State createState() => _MessageListState(); +} + +class _MessageListState extends State { + /// Global toggle: when true show timestamp for all messages. + bool _showAllTimes = false; + + void _setShowAllTimes(bool value) { + if (_showAllTimes == value) return; + setState(() => _showAllTimes = value); + } + @override Widget build(BuildContext context) { final provider = Provider.of(context); - final messages = provider.getMessages(groupId); + final messages = provider.getMessages(widget.groupId); if (messages.isEmpty) { return Center( @@ -30,19 +48,39 @@ class MessageList extends StatelessWidget { } return ListView.builder( - controller: scrollController, + controller: widget.scrollController, itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[index]; - final isMe = msg["isMe"] == true; + final isMe = msg["isMe"] == true; final isSystem = msg["senderID"] == "system"; final isImage = msg["type"] == "image"; if (isSystem) return _SystemMessage(msg: msg); - if (isImage) return _ImageMessage(msg: msg, isMe: isMe); - return _TextMessage(msg: msg, isMe: isMe); + // Determine tail: only last in consecutive block by same sender. + final currentSender = msg["senderID"]; + bool hasTail = true; + if (index < messages.length - 1) { + final next = messages[index + 1]; + if (next["senderID"] == currentSender) { + hasTail = false; + } + } + + final prevSender = index > 0 ? messages[index - 1]["senderID"] : null; + final showName = prevSender != msg["senderID"]; // only first in block + + return _TextMessage( + msg: msg, + isMe: isMe, + tail: hasTail, + showTime: _showAllTimes, + showName: showName, + onSwipeLeft: () => _setShowAllTimes(true), + onSwipeRight: () => _setShowAllTimes(false), + ); }, ); } @@ -126,55 +164,136 @@ class _ImageMessage extends StatelessWidget { } } -class _TextMessage extends StatelessWidget { +class _TextMessage extends StatefulWidget { final Map msg; final bool isMe; - const _TextMessage({required this.msg, required this.isMe}); + final bool tail; + final bool showTime; // global toggle + final bool showName; + final VoidCallback onSwipeLeft; // show all times + final VoidCallback onSwipeRight; // hide all times + + const _TextMessage({ + required this.msg, + required this.isMe, + required this.tail, + required this.showTime, + required this.showName, + required this.onSwipeLeft, + required this.onSwipeRight, + }); + + @override + State<_TextMessage> createState() => _TextMessageState(); +} + +class _TextMessageState extends State<_TextMessage> { + double _dragX = 0; // negative when swiping left + + static const double _revealThreshold = -30; // pixels + static const double _hideThreshold = 30; // for right swipe (unused mostly) + static const double _minDrag = -90; // allow slight off-screen travel + static const double _maxDrag = 40; + + void _onDragUpdate(DragUpdateDetails d) { + setState(() { + _dragX += d.delta.dx; + if (_dragX < _minDrag) _dragX = _minDrag; + if (_dragX > _maxDrag) _dragX = _maxDrag; + }); + } + + void _onDragEnd(DragEndDetails d) { + final velocity = d.primaryVelocity ?? 0; + if (_dragX <= _revealThreshold || velocity < -600) { + widget.onSwipeLeft(); + } else if (_dragX >= _hideThreshold || velocity > 600) { + widget.onSwipeRight(); + } + // snap back + setState(() => _dragX = 0); + } @override Widget build(BuildContext context) { + final isMe = widget.isMe; final bubbleColor = isMe ? const Color(0xFFE3C17D) : Colors.grey.shade900; - return Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: BorderRadius.circular(12), - ), - constraints: const BoxConstraints(maxWidth: 280), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - (isMe ? 'You' : (msg["senderName"] ?? "")), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: isMe ? Colors.black : (msg["senderColor"] ?? Colors.white), - ), - ), - const SizedBox(height: 4), - Text( - msg["message"] ?? "", - style: TextStyle( - color: isMe ? Colors.black : Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - Align( - alignment: Alignment.bottomRight, - child: Text( - msg["timestamp"] ?? "", - style: TextStyle( - color: isMe ? Colors.black54 : Colors.white54, - fontSize: 10, + final textColor = isMe ? Colors.black : Colors.white; + final nameColor = isMe ? Colors.black : (widget.msg["senderColor"] ?? Colors.white); + + // Base offset when timestamps visible (shift left a bit to emphasize reveal) + final baseShift = widget.showTime ? -12.0 : 0.0; + final effectiveShift = baseShift + _dragX; // _dragX usually 0 except during gesture + + return GestureDetector( + onHorizontalDragUpdate: _onDragUpdate, + onHorizontalDragEnd: _onDragEnd, + child: Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), + child: Column( + crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + if (widget.showName && !isMe) + Transform.translate( + offset: Offset(effectiveShift, 0), + child: Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 20), + Text( + widget.msg["senderName"] ?? '', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: nameColor, + ), + ), + ], + ), + ), ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Bubble slides left/right; timestamp stays put creating a reveal effect. + Transform.translate( + offset: Offset(effectiveShift, 0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: BubbleSpecialThree( + text: widget.msg["message"] ?? '', + color: bubbleColor, + isSender: isMe, + tail: widget.tail, + textStyle: TextStyle( + color: textColor, + fontSize: 14, + ), + ), + ), + ), + if (widget.showTime) ...[ + const SizedBox(width: 6), + Align( + alignment: Alignment.center, + child: Text( + widget.msg["timestamp"] ?? '', + style: TextStyle( + fontSize: 10, + color: Colors.white54, + ), + ), + ), + ], + ], ), - ), - ], + ], + ), ), ), ); diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 1e2d920..3d45d1e 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -243,7 +243,7 @@ class _GroupInfoPageState extends State { ), ), ), - const SizedBox(height: 20), + const SizedBox(height: 200), ], ), ), diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index 215ed6c..2f38196 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + chat_bubbles: + dependency: "direct main" + description: + name: chat_bubbles + sha256: "902bc84e22f9ce6b993d015457067a37d37eb7478bd7b5b185712d5c5329b866" + url: "https://pub.dev" + source: hosted + version: "1.7.0" checked_yaml: dependency: transitive description: @@ -384,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index fbc47b7..ec4701f 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: url_launcher: ^6.3.0 socket_io_client: ^2.0.3 connectivity_plus: ^6.0.5 + chat_bubbles: ^1.7.0 dev_dependencies: flutter_test: From 7f7556917cf60c92499a0f1f42a0a42c4f1c806e Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 00:06:08 +0530 Subject: [PATCH 42/53] =?UTF-8?q?feat:=20logo=20done=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- leaderboard_app/assets/icons/LL_Logo.svg | 17 ++++++++ .../lib/chatpage-components/message_list.dart | 3 +- leaderboard_app/lib/pages/chatlists_page.dart | 32 +++++++++++---- leaderboard_app/lib/pages/settings_page.dart | 31 ++++++++++---- leaderboard_app/pubspec.lock | 40 +++++++++++++++++++ leaderboard_app/pubspec.yaml | 1 + 6 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 leaderboard_app/assets/icons/LL_Logo.svg diff --git a/leaderboard_app/assets/icons/LL_Logo.svg b/leaderboard_app/assets/icons/LL_Logo.svg new file mode 100644 index 0000000..397b49c --- /dev/null +++ b/leaderboard_app/assets/icons/LL_Logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 00cf20e..791fc21 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -260,7 +260,6 @@ class _TextMessageState extends State<_TextMessage> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Bubble slides left/right; timestamp stays put creating a reveal effect. Transform.translate( offset: Offset(effectiveShift, 0), child: ConstrainedBox( @@ -278,7 +277,7 @@ class _TextMessageState extends State<_TextMessage> { ), ), if (widget.showTime) ...[ - const SizedBox(width: 6), + SizedBox(width: isMe ? 6 : 43), // 6 normal; 40 extra for others Align( alignment: Alignment.center, child: Text( diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index f127f0f..0ee6e18 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:leaderboard_app/services/groups/group_service.dart'; import 'package:provider/provider.dart'; @@ -255,14 +256,29 @@ class _ChatlistsPageState extends State { children: [ // Title Padding( - padding: const EdgeInsets.only(left: 16, top: 16), - child: Text( - 'Group Chats', - style: TextStyle( - color: theme.primary, - fontSize: 24, - fontWeight: FontWeight.bold, - ), + padding: const EdgeInsets.only(left: 16, top: 16, right: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // SVG Icon + SizedBox( + width: 28, + height: 28, + child: SvgPicture.asset( + 'assets/icons/LL_Logo.svg', + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 10), + Text( + 'Chats', + style: TextStyle( + color: theme.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], ), ), const SizedBox(height: 10), diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 96aaa8e..fd77a08 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; @@ -27,14 +28,28 @@ class SettingsPage extends StatelessWidget { child: ListView( padding: const EdgeInsets.only(left:16, right:16, top:16, bottom:24), children: [ - // Title (match Group Chats styling) - Text( - 'Settings', - style: TextStyle( - color: colors.primary, - fontSize: 24, - fontWeight: FontWeight.bold, - ), + // Title with SVG icon (match Chats styling) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 28, + height: 28, + child: SvgPicture.asset( + 'assets/icons/LL_Logo.svg', + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 10), + Text( + 'Settings', + style: TextStyle( + color: colors.primary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], ), const SizedBox(height: 20), diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index 2f38196..f426568 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -302,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.6" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + url: "https://pub.dev" + source: hosted + version: "2.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -536,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider_linux: dependency: transitive description: @@ -917,6 +933,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" vector_math: dependency: transitive description: diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index ec4701f..a3f9f06 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: socket_io_client: ^2.0.3 connectivity_plus: ^6.0.5 chat_bubbles: ^1.7.0 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: From 2806ff5a4f12dc4c1132ace75ae2f6089e888453 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 00:54:36 +0530 Subject: [PATCH 43/53] feat: updated logos -> app icon, splash screen, pages in the app --- .../res/drawable-hdpi/android12splash.png | Bin 0 -> 8778 bytes .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 3269 bytes .../app/src/main/res/drawable-hdpi/splash.png | Bin 0 -> 8778 bytes .../res/drawable-mdpi/android12splash.png | Bin 0 -> 5780 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 2138 bytes .../app/src/main/res/drawable-mdpi/splash.png | Bin 0 -> 5780 bytes .../drawable-night-hdpi/android12splash.png | Bin 0 -> 8778 bytes .../drawable-night-mdpi/android12splash.png | Bin 0 -> 5780 bytes .../drawable-night-xhdpi/android12splash.png | Bin 0 -> 12168 bytes .../drawable-night-xxhdpi/android12splash.png | Bin 0 -> 22730 bytes .../android12splash.png | Bin 0 -> 31539 bytes .../src/main/res/drawable-v21/background.png | Bin 0 -> 69 bytes .../res/drawable-v21/launch_background.xml | 15 +-- .../res/drawable-xhdpi/android12splash.png | Bin 0 -> 12168 bytes .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4555 bytes .../src/main/res/drawable-xhdpi/splash.png | Bin 0 -> 12168 bytes .../res/drawable-xxhdpi/android12splash.png | Bin 0 -> 22730 bytes .../ic_launcher_foreground.png | Bin 0 -> 6932 bytes .../src/main/res/drawable-xxhdpi/splash.png | Bin 0 -> 22730 bytes .../res/drawable-xxxhdpi/android12splash.png | Bin 0 -> 31539 bytes .../ic_launcher_foreground.png | Bin 0 -> 8778 bytes .../src/main/res/drawable-xxxhdpi/splash.png | Bin 0 -> 31539 bytes .../app/src/main/res/drawable/background.png | Bin 0 -> 69 bytes .../main/res/drawable/launch_background.xml | 15 +-- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 1322 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 813 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 1825 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 2749 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 3906 bytes .../src/main/res/values-night-v31/styles.xml | 22 ++++ .../app/src/main/res/values-night/styles.xml | 4 + .../app/src/main/res/values-v31/styles.xml | 22 ++++ .../app/src/main/res/values/colors.xml | 4 + .../app/src/main/res/values/styles.xml | 4 + leaderboard_app/assets/icons/LL_Logo.png | Bin 0 -> 67042 bytes .../ios/Runner.xcodeproj/project.pbxproj | 4 +- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 32983 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 324 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 728 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 1102 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 469 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 1018 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 1697 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 728 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 1514 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 2394 bytes .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 938 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 1890 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 1037 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 2208 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 2394 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 3757 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 1322 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 2749 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 1369 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 3012 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 3406 bytes .../LaunchBackground.imageset/Contents.json | 21 ++++ .../LaunchBackground.imageset/background.png | Bin 0 -> 69 bytes .../LaunchImage.imageset/Contents.json | 10 +- .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 5780 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 12168 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 22730 bytes .../Runner/Base.lproj/LaunchScreen.storyboard | 17 ++- leaderboard_app/ios/Runner/Info.plist | 112 +++++++++--------- .../lib/chatpage-components/message_list.dart | 18 +-- leaderboard_app/lib/pages/chatlists_page.dart | 4 +- leaderboard_app/lib/pages/settings_page.dart | 4 +- leaderboard_app/lib/widgets/app_logo.dart | 0 leaderboard_app/pubspec.lock | 16 +++ leaderboard_app/pubspec.yaml | 25 +++- leaderboard_app/tool/pad_logo.dart | 0 73 files changed, 220 insertions(+), 102 deletions(-) create mode 100644 leaderboard_app/android/app/src/main/res/drawable-hdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-hdpi/splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-mdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-mdpi/splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-night-hdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-night-mdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-night-xhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-v21/background.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xhdpi/splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxhdpi/splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/android12splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/splash.png create mode 100644 leaderboard_app/android/app/src/main/res/drawable/background.png create mode 100644 leaderboard_app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 leaderboard_app/android/app/src/main/res/values-night-v31/styles.xml create mode 100644 leaderboard_app/android/app/src/main/res/values-v31/styles.xml create mode 100644 leaderboard_app/android/app/src/main/res/values/colors.xml create mode 100644 leaderboard_app/assets/icons/LL_Logo.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json create mode 100644 leaderboard_app/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png create mode 100644 leaderboard_app/lib/widgets/app_logo.dart create mode 100644 leaderboard_app/tool/pad_logo.dart diff --git a/leaderboard_app/android/app/src/main/res/drawable-hdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..89ce0806fdc27bb9eb94d23204b9e22eff357766 GIT binary patch literal 8778 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 KPWf%ycmD?z)(b5F literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/leaderboard_app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..739120d09f44faf7b721f4568dea04a5e0416bd2 GIT binary patch literal 3269 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-hdpi/splash.png b/leaderboard_app/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..89ce0806fdc27bb9eb94d23204b9e22eff357766 GIT binary patch literal 8778 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 KPWf%ycmD?z)(b5F literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-mdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e958d0190b7ee883ca7e85fddceb51122d1da787 GIT binary patch literal 5780 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 KPWf%ycmD?z)(b5F literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e958d0190b7ee883ca7e85fddceb51122d1da787 GIT binary patch literal 5780 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

;heFA^L-7zu!m@C>fI_*e-i$G0IG*u{r~^~ literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e1dee315044fc1d732b20895f1e5a0b03914cf GIT binary patch literal 22730 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 R8x%C)JnWJ?`OLN7{|hx4Eyn-= literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..d29a8ea437a611a2222fb19708aa4f5060cea170 GIT binary patch literal 31539 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 h>^=$o+yCW - - - - - + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/drawable-xhdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..faa50f8ee1c58a0b02ca168bb6734c3dd1cfe5e9 GIT binary patch literal 12168 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

;heFA^L-7zu!m@C>fI_*e-i$G0IG*u{r~^~ literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/leaderboard_app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..27708dd8fd5a82cc113cfa6d9ce457068a06008e GIT binary patch literal 4555 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 literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xhdpi/splash.png b/leaderboard_app/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..faa50f8ee1c58a0b02ca168bb6734c3dd1cfe5e9 GIT binary patch literal 12168 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

;heFA^L-7zu!m@C>fI_*e-i$G0IG*u{r~^~ literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e1dee315044fc1d732b20895f1e5a0b03914cf GIT binary patch literal 22730 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 R8x%C)JnWJ?`OLN7{|hx4Eyn-= literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e774c13424a082dc84fcac5a3047220ff424b1e9 GIT binary patch literal 6932 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 b$Q;x(jn%_5m-HyNT~r#XIx3|~=0X1jm*PFy literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/splash.png b/leaderboard_app/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e1dee315044fc1d732b20895f1e5a0b03914cf GIT binary patch literal 22730 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 R8x%C)JnWJ?`OLN7{|hx4Eyn-= literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 0000000000000000000000000000000000000000..d29a8ea437a611a2222fb19708aa4f5060cea170 GIT binary patch literal 31539 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 KPWf%ycmD?z)(b5F literal 0 HcmV?d00001 diff --git a/leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/splash.png b/leaderboard_app/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..d29a8ea437a611a2222fb19708aa4f5060cea170 GIT binary patch literal 31539 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 h>^=$o+yCW - - - - - + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/leaderboard_app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5f349f7 --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/leaderboard_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..c460d9248b9177deb6b7961c2d2ad77670b67d5f 100644 GIT binary patch delta 1314 zcma*ndovSISf4j7==uM!|~8`B-jBij)sc;AYZ_fR9Y&oC3-FN zm^`>XIldTskg*=OOjc1Y!bo+USiEg@Zet+JrKWJ;qZ{-msg&h9T1?>>$9uI(Fflo4 z9xdtZ2iP4y`VcPi*mVlmw;8d)UcSiML&IS4b6YV-=%Em5zr1_#bY9d_5VthT@|vag z`tB#y919$ZDx=-lbJ$;SQ)z8~8t2>7M{0-%*ZDJI*&9)@&4AvT;+b-RS5HQ;tinKVAdqY9YiiE1 zoq2v48(F4a{jtxL>y~k;9Ez8t;varAh>owA72+timRg-no`*x=C)1^QNTQ`ywj*Rm z4zvAg?*WFtVz(n$?7ZcbOs0L#>xVH+d!ef~wjV0d|+E_ci+JOxE@ES^2?`>Vmk(91I?eUz*TG&q# zJQbhvaaB|RGp!U^`QhT*6?^{bl9TYQFzY$^=2?%vi<}7@4j>J6Y5xk#+&2?jS9>jc zekFJAQ+OMSqbA1mQ#|ROj}N?x)#{|;Gtg{$+0Kz&zA1JlvOuiiL5jY+u!~k&+&E`$ zlz&urw&ffj2c_@42PFX4Bbb>#yBQ^_AKUEOFE>N>&=*?H5h1@9bJ7MeMS9@@md{pG zcSW>~uNsYjttxLm$f4%jq28`fN*!e9;d}4sOJN2Gc3Je6f^*jAZR_f*hEGsNguxj# z%L6X`t*1RE3NG=1?CHU}i2+MKk43*}xuzLE2HWvDjg702vv}pSH>nk*KJvG+c?s+I zo9gk~ukD61&(z-cY`EP=J?_S5G$&3c7>~;)yw33hvbolCR{ZGZFq)%~Kp^>@%|z)W z*0*tWEGF8`A5FX>(GnHszf+3xAm- z+GTF(YuV3teiK)6G#XhtHDI47s6XL*V>RAg&m;_@ROMK0*s+zFm^iv-fsV;+ij^D` z(4v9S;^n$fa>wQpS2FTYVAkBJko~AOR!7MT{*}?ul^b31g*xDV{$^|C9vjlzS2>J! zi83m0-J38cPEBHny|`G8Qf@7{t9|c_`}kee4>A-Tq!a#T>i<>5 R=|2rY1V=ZAdb@y>e*tLNWqtqv literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/leaderboard_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..d44ee45c0745768112509158a1616b5295b62e55 100644 GIT binary patch delta 801 zcmV++1K#|)1FZ&-8Gi-<00374`G)`i0`y5lK~#90V__KLfMuW^Kq(mYz(9jh4-7OI z^}s-bQ4b6>81=wF1FCyKfR~l8C{m-=(^AU4Y3}8U%}1VXqqZH$fT|uy^ifLEQQ~i# zu;Qk8oR=cQqI%1%2hYFSRyz4Y{_V$~?@-e&7@(>L%(R3iWq)>^;Rdn){QVEc)_S5| zQSS2R8s}W8rKVjlKvfU?{QVEk{`2P_!=Jwp{=a|!sc8)gpsEM{A~_Tk`oI7D#}KDB zpsEM{p?Clh174O=k*BY{JHGqW3#w;mn#KSKg8(N7vvQ!l%+@G3`K_%Bt`^;S^5qt_ z?4+&-{-VXeAAd*;eEI%|A*=fggRzF7_mol-Cy)FCT-36YhDqo@!*8gEU>-Yr<2}Qz zho4!gX(#QH(4YU{Sor+y55woLzp<*J2B4V-e*XT;uzk-nhM61gFmz-aAgTF>LpOPV zW=SZe^%TS7SKq*F4vx^6DGIw6(Y4YN*{ZrFtG{pMLveQWF2tnL`^{TQHh_w z{(W1s|8d>ni?8N~I?45EDf0V+k`qYo3! literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Ul_C$1U(W<4isG_wD ziUy;$+IK=xbv4MuRzXsV*h*aYZPE&2C2IabHH;-E@@eXeWewME;!ILBU|DxAl;(e-+CWdy>3bW|{L& zZ={xPgSOBJQ3$d5B%)K^w`I0mmXzD04hE&r9~Na~ry`{-nZg4W`S3mc2eL}jt+F15 zM)$ZsbdNfl_GS-^+C$p68Ce46|B~kd{Eb3j=pQ0G1%Q`ZHmlRuc>^U4Pb<9ju^(oH?5m{v=B0R$oIZ~=5)s5^RdBw<9-R2aAbsgVd`SuP9rO=V01R=aq{p1jnEw= z-L0%chq)+MFlYxenD^h;x`@k4JlCYjI#KAkh@JeOtL`_nr%Y?l@sv7*R(12Y?okkt!dWD57Jb&4D3(Y>!>-uG4A9 z@-nDHD7#Mx&CK37=wvQNj%(fjT4(O%1kCpX0^ryYDpPEmVN7O}IpP)qEXz(bf5ZxR z>C1aT5_!RMnpQ5RA-)=-4GlX_lZ5TG5kD32IUT&jSsWrVL9yJ2hM^=Dqws;~(h?d+ zpmHuuMD>L&rOC`cOD2l?%!&vL`W^$`gwybcd%Kto@In-58vL<AW39H+0Xdqz+M;e_19!hb?^fvsPgHjiM3W^WgH`T)S7qyLtOe3Ahf1vM~bB zSAh_1qP;(bc8Mo7%s0^O6NxIogQ1tGuk365Nw~~MC2+VwGugrqtsWlN0iwfL(RF`L zh}mmrcF!-VmTlzT4kkR}9WDH#TPLkoi>a41|FaRM-2?1VpHKt-3Xt5I=k(gNZZR*l z)--|JF2^d4-pMjcD|pr!k~p9&fjNCNK)*gzF7t7$TS;*R12=MPrP>a(zuqt!9kbSsimAx7{ZWzxo~ z#pT=w?vt7EzRo>H)^;D&4I8`PfSiKKYr-APHt)z9W!MWkZ>>iW{>z0RtOu@iR#F_~ zd(*X{Eo2`LbP)IBF;-2Q7FW~Y)GSn{6*8T^3jC96M60XBDf#}6st7v z6r@&)ayVDvJ$k&*uUJ>G{Zo@pO!F2d8rO7{yykk#mGbTdxJ}>$5+mhBsN(asMi@(x z4s#~C5-iEP=&b3B6+~a0^Q+*tnDsGB<3RcqQ^P8*o3DA);Cp)B+P<^-?1SjHobLLS z^e7~VyYlXH&(B`Z{e8VBCPgl%n0RRvz8qpqy}8uCP25#kLDmK~>xK8|U<#^#D=yrL zzkqHvsx7X_Sq8tqtalvyizpboo!)Q@n_%yZ=X;Nu7|ZsXzOFL2g)XvWi} r%49kIi*QZwat)F+`VWWVG=2n^fZnOeyipNxI(>lkuXxL9a}xD`?}TuB literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!iOI#yLg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+5i=O3-AeX1=1l$e`s#|#^}+&7(N@w0CIr{$Oe+Uk^K-ZP~83C zcc@hG6rikF&NPT(23>y!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/leaderboard_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..250106fdf0bdf8f19de6a8188056c40cfd892106 100644 GIT binary patch literal 2749 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 a=5}aPOn$8_uSB<{495EAdNn$3G5-UO0TWOF literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/leaderboard_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/leaderboard_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb28e45604e46eeda8dd24651419bc0..b39c836ce744b8cff0038ba69c0ee68d9414a9d4 100644 GIT binary patch literal 3906 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 aGQ_%Eq0#iT-3V|t2F#6ZjPM4YfBhH5P8IzC literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/leaderboard_app/android/app/src/main/res/values-night-v31/styles.xml b/leaderboard_app/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..c23369c --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/leaderboard_app/android/app/src/main/res/values-night/styles.xml b/leaderboard_app/android/app/src/main/res/values-night/styles.xml index 06952be..3c4a1fe 100644 --- a/leaderboard_app/android/app/src/main/res/values-night/styles.xml +++ b/leaderboard_app/android/app/src/main/res/values-night/styles.xml @@ -5,6 +5,10 @@ @drawable/launch_background + false + true + true + shortEdges + + + + diff --git a/leaderboard_app/android/app/src/main/res/values/colors.xml b/leaderboard_app/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..beab31f --- /dev/null +++ b/leaderboard_app/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/leaderboard_app/android/app/src/main/res/values/styles.xml b/leaderboard_app/android/app/src/main/res/values/styles.xml index cb1ef88..847e1be 100644 --- a/leaderboard_app/android/app/src/main/res/values/styles.xml +++ b/leaderboard_app/android/app/src/main/res/values/styles.xml @@ -5,6 +5,10 @@ @drawable/launch_background + false + true + true + shortEdges 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

;heFA^L-7zu!m@C>fI_*e-i$G0IG*u{r~^~ literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/leaderboard_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..a5e1dee315044fc1d732b20895f1e5a0b03914cf 100644 GIT binary patch literal 22730 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 R8x%C)JnWJ?`OLN7{|hx4Eyn-= literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard index f2e259c..5a37630 100644 --- a/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/leaderboard_app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -16,13 +16,19 @@ - - + + - - + + + + + + + + @@ -32,6 +38,7 @@ - + + diff --git a/leaderboard_app/ios/Runner/Info.plist b/leaderboard_app/ios/Runner/Info.plist index 94610ba..849c51a 100644 --- a/leaderboard_app/ios/Runner/Info.plist +++ b/leaderboard_app/ios/Runner/Info.plist @@ -1,65 +1,69 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Leaderboard App - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - leaderboard_app - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - NSAppTransportSecurity - NSAllowsArbitraryLoads + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Leaderboard App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + leaderboard_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS - NSExceptionDomains + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + NSAppTransportSecurity - localhost + NSAllowsArbitraryLoads + + NSExceptionDomains - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - + localhost + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + - diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 791fc21..5c821ea 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -103,16 +103,16 @@ class _SystemMessage extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(msg["icon"] ?? Icons.info, size: 16, color: Colors.white), + Icon(msg["icon"] ?? Icons.info, size: 18, color: Colors.white), const SizedBox(width: 6), Text( msg["message"] ?? "", - style: const TextStyle(color: Colors.white, fontSize: 12), + style: const TextStyle(color: Colors.white, fontSize: 14), ), const SizedBox(width: 6), Text( msg["timestamp"] ?? "", - style: const TextStyle(color: Colors.white54, fontSize: 10), + style: const TextStyle(color: Colors.white54, fontSize: 12), ), ], ), @@ -149,13 +149,13 @@ class _ImageMessage extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), child: const Center( - child: Icon(Pixel.image, size: 64, color: Colors.grey), + child: Icon(Pixel.image, size: 66, color: Colors.grey), ), ), const SizedBox(height: 4), Text( msg["timestamp"] ?? "", - style: TextStyle(fontSize: 10, color: isMe ? Colors.black54 : Colors.white54), + style: TextStyle(fontSize: 12, color: isMe ? Colors.black54 : Colors.white54), ), ], ), @@ -247,7 +247,7 @@ class _TextMessageState extends State<_TextMessage> { Text( widget.msg["senderName"] ?? '', style: TextStyle( - fontSize: 12, + fontSize: 14, fontWeight: FontWeight.bold, color: nameColor, ), @@ -271,19 +271,19 @@ class _TextMessageState extends State<_TextMessage> { tail: widget.tail, textStyle: TextStyle( color: textColor, - fontSize: 14, + fontSize: 16, ), ), ), ), if (widget.showTime) ...[ - SizedBox(width: isMe ? 6 : 43), // 6 normal; 40 extra for others + SizedBox(width: isMe ? 6 : 34), // 6 normal; 40 extra for others Align( alignment: Alignment.center, child: Text( widget.msg["timestamp"] ?? '', style: TextStyle( - fontSize: 10, + fontSize: 12, color: Colors.white54, ), ), diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index 0ee6e18..ff315e6 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -262,8 +262,8 @@ class _ChatlistsPageState extends State { children: [ // SVG Icon SizedBox( - width: 28, - height: 28, + width: 35, + height: 35, child: SvgPicture.asset( 'assets/icons/LL_Logo.svg', fit: BoxFit.contain, diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index fd77a08..6f26634 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -33,8 +33,8 @@ class SettingsPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ SizedBox( - width: 28, - height: 28, + width: 35, + height: 35, child: SvgPicture.asset( 'assets/icons/LL_Logo.svg', fit: BoxFit.contain, diff --git a/leaderboard_app/lib/widgets/app_logo.dart b/leaderboard_app/lib/widgets/app_logo.dart new file mode 100644 index 0000000..e69de29 diff --git a/leaderboard_app/pubspec.lock b/leaderboard_app/pubspec.lock index f426568..de23e1f 100644 --- a/leaderboard_app/pubspec.lock +++ b/leaderboard_app/pubspec.lock @@ -145,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -286,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: diff --git a/leaderboard_app/pubspec.yaml b/leaderboard_app/pubspec.yaml index a3f9f06..cfe067b 100644 --- a/leaderboard_app/pubspec.yaml +++ b/leaderboard_app/pubspec.yaml @@ -32,6 +32,7 @@ dev_dependencies: retrofit_generator: ^9.1.5 json_serializable: ^6.8.0 flutter_native_splash: ^2.4.1 + flutter_launcher_icons: ^0.13.1 flutter: uses-material-design: true @@ -46,9 +47,23 @@ flutter: - asset: fonts/AlumniSans-Italic-VariableFont_wght.ttf flutter_native_splash: - color: "#FFFFFF" - image: assets/icons/google.png + color: "#000000" # Dark backdrop + image: assets/icons/LL_Logo.png # PNG version of logo (exported from SVG) android_12: - image: assets/icons/google.png - color: "#FFFFFF" - web: false \ No newline at end of file + image: assets/icons/LL_Logo.png + color: "#000000" + icon_background_color: "#000000" + ios: true + web: false + android: true + fullscreen: true + # Optionally keep branding image off for now + +# App icon generation config +flutter_icons: + android: true + ios: true + image_path: assets/icons/LL_Logo.png + adaptive_icon_background: "#000000" + adaptive_icon_foreground: assets/icons/LL_Logo.png + min_sdk_android: 21 \ No newline at end of file diff --git a/leaderboard_app/tool/pad_logo.dart b/leaderboard_app/tool/pad_logo.dart new file mode 100644 index 0000000..e69de29 From 729d5637aa3f5085194297cefcdf19e74fa3be9b Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 01:08:04 +0530 Subject: [PATCH 44/53] chore: onboarding color changes --- .../lib/pages/leetcode_verification_page.dart | 6 +++--- leaderboard_app/lib/pages/signin_page.dart | 17 +++-------------- leaderboard_app/lib/pages/signup_page.dart | 4 ++-- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/leaderboard_app/lib/pages/leetcode_verification_page.dart b/leaderboard_app/lib/pages/leetcode_verification_page.dart index 326d5f4..87b92eb 100644 --- a/leaderboard_app/lib/pages/leetcode_verification_page.dart +++ b/leaderboard_app/lib/pages/leetcode_verification_page.dart @@ -93,7 +93,7 @@ class _LeetCodeVerificationPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD7FE66), + backgroundColor: const Color(0xFFE3C17D), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -140,7 +140,7 @@ class _LeetCodeVerificationPageState extends State { if (_secondsLeft > 0) Text( 'Auto-checking... $_secondsLeft s left', - style: const TextStyle(color: Color(0xFFD7FE66)), + style: const TextStyle(color: Color(0xFFE3C17D)), ), ], ), @@ -149,7 +149,7 @@ class _LeetCodeVerificationPageState extends State { const Spacer(), TextButton( onPressed: () => context.go('/'), - child: const Text('Skip for now', style: TextStyle(color: Color(0xFFD7FE66))), + child: const Text('Skip for now', style: TextStyle(color: Color(0xFFE3C17D))), ), ], ), diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index b98fbfd..7dd3fba 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -99,18 +99,7 @@ class _SignInPageState extends State { ), ), ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () {}, - child: const Text( - 'Forgot Password?', - style: TextStyle(color: Color(0xFFD7FE66)), - ), - ), - ), - const SizedBox(height: 10), + const SizedBox(height: 20), if (_error != null) Padding( padding: const EdgeInsets.only(bottom: 8.0), @@ -121,7 +110,7 @@ class _SignInPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD7FE66), + backgroundColor: const Color(0xFFE3C17D), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -158,7 +147,7 @@ class _SignInPageState extends State { child: const Text( "Sign up", style: TextStyle( - color: Color(0xFFD7FE66), + color: Color(0xFFE3C17D), fontWeight: FontWeight.bold, decoration: TextDecoration.underline, ), diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index 95cb2e4..95fb269 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -135,7 +135,7 @@ class _SignUpPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD7FE66), + backgroundColor: const Color(0xFFE3C17D), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -172,7 +172,7 @@ class _SignUpPageState extends State { child: const Text( "Sign in", style: TextStyle( - color: Color(0xFFD7FE66), + color: Color(0xFFE3C17D), fontWeight: FontWeight.bold, decoration: TextDecoration.underline, ), From c31625b2555cbbc78a1c085fe48e82373d21a6ee Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 01:13:06 +0530 Subject: [PATCH 45/53] chore: ui cleanup --- leaderboard_app/lib/chatpage-components/message_list.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 5c821ea..4926d12 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -219,7 +219,8 @@ class _TextMessageState extends State<_TextMessage> { final isMe = widget.isMe; final bubbleColor = isMe ? const Color(0xFFE3C17D) : Colors.grey.shade900; final textColor = isMe ? Colors.black : Colors.white; - final nameColor = isMe ? Colors.black : (widget.msg["senderColor"] ?? Colors.white); + // Username color for other users set to grey500 as requested + final nameColor = isMe ? Colors.black : Colors.grey.shade500; // Base offset when timestamps visible (shift left a bit to emphasize reveal) final baseShift = widget.showTime ? -12.0 : 0.0; From d57467f6e578394bef308c83a478b31b22f0bfc4 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 01:31:32 +0530 Subject: [PATCH 46/53] chore: changed android application name --- leaderboard_app/android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leaderboard_app/android/app/src/main/AndroidManifest.xml b/leaderboard_app/android/app/src/main/AndroidManifest.xml index 512b0bf..2f6c491 100644 --- a/leaderboard_app/android/app/src/main/AndroidManifest.xml +++ b/leaderboard_app/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ Date: Fri, 3 Oct 2025 02:00:19 +0530 Subject: [PATCH 47/53] fix: replace fixed width with Spacer for better layout in message display --- leaderboard_app/lib/chatpage-components/message_list.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leaderboard_app/lib/chatpage-components/message_list.dart b/leaderboard_app/lib/chatpage-components/message_list.dart index 4926d12..55d0f2f 100644 --- a/leaderboard_app/lib/chatpage-components/message_list.dart +++ b/leaderboard_app/lib/chatpage-components/message_list.dart @@ -278,7 +278,7 @@ class _TextMessageState extends State<_TextMessage> { ), ), if (widget.showTime) ...[ - SizedBox(width: isMe ? 6 : 34), // 6 normal; 40 extra for others + Spacer(), Align( alignment: Alignment.center, child: Text( From 64d8037fd58fcd36c2112a72d8e4de8d0d820417 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 02:58:08 +0530 Subject: [PATCH 48/53] chore: semi-fix chat re-entering --- leaderboard_app/lib/pages/settings_page.dart | 8 +++ .../lib/provider/chat_provider.dart | 53 +++++++++++++++++-- .../lib/provider/chatlists_provider.dart | 10 ++++ .../lib/services/auth/auth_service.dart | 13 ++++- .../lib/services/chat/chat_service.dart | 39 ++++++++++++-- .../lib/services/core/dio_provider.dart | 6 +++ 6 files changed, 121 insertions(+), 8 deletions(-) diff --git a/leaderboard_app/lib/pages/settings_page.dart b/leaderboard_app/lib/pages/settings_page.dart index 6f26634..893c450 100644 --- a/leaderboard_app/lib/pages/settings_page.dart +++ b/leaderboard_app/lib/pages/settings_page.dart @@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:leaderboard_app/services/auth/auth_service.dart'; import 'package:leaderboard_app/provider/user_provider.dart'; import 'package:leaderboard_app/services/user/user_service.dart'; +import 'package:leaderboard_app/provider/chat_provider.dart'; +import 'package:leaderboard_app/provider/chatlists_provider.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { @@ -176,7 +178,13 @@ class SettingsPage extends StatelessWidget { style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), onPressed: () async { + // Clear auth token + any persisted data await context.read().logout(); + // Reset in-memory chat state so previous session messages/groups disappear + try { + context.read().reset(); + context.read().reset(); + } catch (_) {} if (!context.mounted) return; context.go('/signin'); }, diff --git a/leaderboard_app/lib/provider/chat_provider.dart b/leaderboard_app/lib/provider/chat_provider.dart index 97dc174..83e56ab 100644 --- a/leaderboard_app/lib/provider/chat_provider.dart +++ b/leaderboard_app/lib/provider/chat_provider.dart @@ -107,18 +107,45 @@ class ChatProvider extends ChangeNotifier { } Future sendMessage(String groupId, String text) async { - if (text.trim().isEmpty) return; - final trimmed = text.trim(); + final raw = text; + if (raw.trim().isEmpty) { + // ignore: avoid_print + print('[CHAT][SEND] Abort empty text'); + return; + } + // Ensure user + socket + await initIfNeeded(); + if (!ChatService.instance.isConnected) { + // ignore: avoid_print + print('[CHAT][SEND] Socket not connected – attempting ensureSocket'); + await _ensureSocket(); + } + if (!ChatService.instance.isConnected) { + // ignore: avoid_print + print('[CHAT][SEND] Still not connected after ensureSocket'); + } + // Ensure group joined (lightweight: if not joined, just emit join now) + if (!_joinedGroups.contains(groupId)) { + // ignore: avoid_print + print('[CHAT][SEND] Group $groupId not joined yet. Joining via socket (no history fetch).'); + _joinedGroups.add(groupId); + _groupMessages.putIfAbsent(groupId, () => []); + ChatService.instance.joinGroup(groupId); + } + final trimmed = raw.trim(); + // ignore: avoid_print + print('[CHAT][SEND] Attempt send group=$groupId len=${trimmed.length} user=$currentUserID'); final ok = await ChatService.instance.sendMessage( groupId, trimmed, sender: { 'id': currentUserID, - 'username': 'You', + 'username': currentUsername.isEmpty ? 'You' : currentUsername, }, ); if (!ok) { - // Append a system error message (optional) + // ignore: avoid_print + print('[CHAT][SEND] Failed path reached; appending system error message'); final list = (_groupMessages[groupId] ??= []); list.add({ 'id': 'err-${DateTime.now().microsecondsSinceEpoch}', @@ -204,6 +231,24 @@ class ChatProvider extends ChangeNotifier { return '$h:$m $ampm'; } + /// Reset all volatile chat-related state. Call this on user logout to ensure + /// no data from a previous session is visible after re-authentication. + void reset() { + _groupMessages.clear(); + _groupAttachmentVisibility.clear(); + _joinedGroups.clear(); + _groupCurrentPage.clear(); + _groupHasMore.clear(); + _loadingMoreGroups.clear(); + _currentUserId = null; + _currentUsername = null; + _connError = null; + // Disconnect socket so that next authenticated session re-establishes + // a new connection with the correct token / identity. + try { ChatService.instance.disconnect(); } catch (_) {} + notifyListeners(); + } + @override void dispose() { _sub?.cancel(); diff --git a/leaderboard_app/lib/provider/chatlists_provider.dart b/leaderboard_app/lib/provider/chatlists_provider.dart index e441eda..1da474c 100644 --- a/leaderboard_app/lib/provider/chatlists_provider.dart +++ b/leaderboard_app/lib/provider/chatlists_provider.dart @@ -159,4 +159,14 @@ class ChatListProvider extends ChangeNotifier { } if (changed) notifyListeners(); } + + /// Clear all loaded groups and transient loading / error flags. + void reset() { + _chatGroups = []; + _isLoading = false; + _error = null; + _isCreating = false; + _createError = null; + notifyListeners(); + } } \ No newline at end of file diff --git a/leaderboard_app/lib/services/auth/auth_service.dart b/leaderboard_app/lib/services/auth/auth_service.dart index f054d56..919eb27 100644 --- a/leaderboard_app/lib/services/auth/auth_service.dart +++ b/leaderboard_app/lib/services/auth/auth_service.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:leaderboard_app/models/auth_models.dart'; import 'package:leaderboard_app/services/core/api_client.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:leaderboard_app/services/core/dio_provider.dart'; class AuthService { final Dio _dio; @@ -44,7 +45,17 @@ class AuthService { Future logout() async { final prefs = await SharedPreferences.getInstance(); - await prefs.remove('authToken'); + // Clear all persisted user-specific data on logout to avoid leaking + // authentication state or cached profile details between accounts. + // If in the future some keys should persist across logins (e.g. theme), + // fetch their values first and re-set them after clear(). + await prefs.clear(); + DioProvider.reset(); + // Also reset any cached singletons that embed auth headers (e.g. Dio). + try { + // ignore: avoid_dynamic_calls + // Access the private singleton via reflection isn't possible; expose a static reset instead if needed. + } catch (_) {} } Future> getProfile() async { diff --git a/leaderboard_app/lib/services/chat/chat_service.dart b/leaderboard_app/lib/services/chat/chat_service.dart index 365ea3f..5bf71f9 100644 --- a/leaderboard_app/lib/services/chat/chat_service.dart +++ b/leaderboard_app/lib/services/chat/chat_service.dart @@ -101,8 +101,27 @@ class ChatService { /// Emit a message to server (server should broadcast back with `message:new`). Future sendMessage(String groupId, String text, {Map? sender}) async { - if (text.trim().isEmpty) return false; - if (!isConnected) return false; + if (text.trim().isEmpty) { + // ignore: avoid_print + print('[SOCKET][SEND] Abort: empty text'); + return false; + } + if (!isConnected) { + // ignore: avoid_print + print('[SOCKET][SEND] Not connected. Attempting lazy connect before send...'); + try { + // We cannot fetch token here directly; higher layer ensures ensureConnected. + // If still disconnected after this, fail. + } catch (e) { + // ignore: avoid_print + print('[SOCKET][SEND] Lazy connect exception: $e'); + } + if (!isConnected) { + // ignore: avoid_print + print('[SOCKET][SEND] Fail: socket still not connected'); + return false; + } + } final payload = { 'groupId': groupId, 'message': text.trim(), @@ -110,9 +129,13 @@ class ChatService { }; try { // Backend does not specify ack; emit fire-and-forget. + // ignore: avoid_print + print('[SOCKET][SEND] Emitting send_message payloadKeys=${payload.keys}'); _socket?.emit('send_message', payload); return true; - } catch (_) { + } catch (err) { + // ignore: avoid_print + print('[SOCKET][SEND] Exception while emitting: $err'); return false; } } @@ -137,4 +160,14 @@ class ChatService { _messageController.close(); _socket?.dispose(); } + + /// Explicitly disconnect socket (without closing stream) for logout so that + /// a subsequent login can establish a fresh authenticated connection. + void disconnect() { + try { + _socket?.disconnect(); + _socket?.destroy(); + } catch (_) {} + _socket = null; + } } \ No newline at end of file diff --git a/leaderboard_app/lib/services/core/dio_provider.dart b/leaderboard_app/lib/services/core/dio_provider.dart index 35c1b60..161d3a1 100644 --- a/leaderboard_app/lib/services/core/dio_provider.dart +++ b/leaderboard_app/lib/services/core/dio_provider.dart @@ -62,6 +62,12 @@ class DioProvider { options: opts, ); } + + /// Reset the cached Dio so a subsequent call to [getInstance] creates a + /// fresh client (used on logout to ensure new auth token picked up). + static void reset() { + _dio = null; + } } class _LogInterceptor extends Interceptor { From 873f2c8e90fa0373e9f0fb2389741bcef45ee3ad Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 03:22:48 +0530 Subject: [PATCH 49/53] fix: LOGOUT NOT WORKING PATCHED --- .../lib/services/auth/auth_service.dart | 7 ++ .../lib/services/chat/chat_service.dart | 97 +++++++++++++++---- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/leaderboard_app/lib/services/auth/auth_service.dart b/leaderboard_app/lib/services/auth/auth_service.dart index 919eb27..e0c5827 100644 --- a/leaderboard_app/lib/services/auth/auth_service.dart +++ b/leaderboard_app/lib/services/auth/auth_service.dart @@ -3,6 +3,7 @@ import 'package:leaderboard_app/models/auth_models.dart'; import 'package:leaderboard_app/services/core/api_client.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:leaderboard_app/services/core/dio_provider.dart'; +import 'package:leaderboard_app/services/chat/chat_service.dart'; class AuthService { final Dio _dio; @@ -26,6 +27,8 @@ class AuthService { return login; } else { await _saveAuth(response.token); + // Initialize a fresh socket connection with the new token. + try { await ChatService.instance.connectWithToken(response.token); } catch (_) {} return response; } } @@ -40,6 +43,8 @@ class AuthService { throw DioException(requestOptions: res.requestOptions, response: res, message: 'Token missing in response'); } await _saveAuth(response.token); + // After storing token, connect socket with new identity. + try { await ChatService.instance.connectWithToken(response.token); } catch (_) {} return response; } @@ -51,6 +56,8 @@ class AuthService { // fetch their values first and re-set them after clear(). await prefs.clear(); DioProvider.reset(); + // Proactively disconnect socket (in case ChatProvider not yet instantiated to reset it) + try { ChatService.instance.disconnect(); } catch (_) {} // Also reset any cached singletons that embed auth headers (e.g. Dio). try { // ignore: avoid_dynamic_calls diff --git a/leaderboard_app/lib/services/chat/chat_service.dart b/leaderboard_app/lib/services/chat/chat_service.dart index 5bf71f9..d509574 100644 --- a/leaderboard_app/lib/services/chat/chat_service.dart +++ b/leaderboard_app/lib/services/chat/chat_service.dart @@ -37,31 +37,13 @@ class ChatService { }); final completer = Completer(); socket.on('connect', (_) { - // Basic event logging hook - socket.onAny((event, data) { - // ignore: avoid_print - print('[SOCKET] event=$event data=${data is Map ? data.keys : data}'); - }); + _attachCommonListeners(socket); completer.complete(); }); socket.on('connect_error', (err) { lastError = err.toString(); if (!completer.isCompleted) completer.completeError(err); }); - // Server → Client: receive_message - socket.on('receive_message', (data) { - if (data is Map) { - try { - final msg = ChatMessage.fromSocket(_normalizeSocketPayload(Map.from(data))); - _messageController.add(msg); - } catch (e) { - // ignore - } - } - }); - // Joined group ack: add minimal log - socket.on('joined_group', (d) => print('[SOCKET] joined_group: $d')); - socket.on('error', (e) => print('[SOCKET] server_error: $e')); socket.connect(); _socket = socket; await completer.future.timeout(const Duration(seconds: 8)); @@ -170,4 +152,81 @@ class ChatService { } catch (_) {} _socket = null; } + + /// Force a brand-new socket connection using the supplied JWT. This should + /// be called immediately after a successful login to ensure the socket + /// authenticates as the new user and does not reuse prior connection state. + Future connectWithToken(String jwt, {List rejoinGroupIds = const []}) async { + // Tear down any existing connection fully. + disconnect(); + lastError = null; + _connecting = true; + final base = ApiConfig.baseUrl; + final wsBase = base.replaceFirst('/api', ''); + try { + final socket = io.io(wsBase, { + 'transports': ['websocket'], + 'autoConnect': false, + 'forceNew': true, // ensure a new engine.io session + 'auth': {'token': jwt}, + }); + final completer = Completer(); + socket.on('connect', (_) { + // ignore: avoid_print + print('[SOCKET] Connected (forceNew)'); + _attachCommonListeners(socket); + // Rejoin prior groups if provided + for (final gid in rejoinGroupIds) { + socket.emit('join_group', gid); + } + completer.complete(); + }); + socket.on('connect_error', (err) { + lastError = err.toString(); + if (!completer.isCompleted) completer.completeError(err); + }); + _socket = socket; + socket.connect(); + await completer.future.timeout(const Duration(seconds: 8)); + } catch (e) { + lastError = e.toString(); + rethrow; + } finally { + _connecting = false; + } + } + + /// Attach listeners common to both normal and forceNew connections. + void _attachCommonListeners(io.Socket socket) { + // Generic event tracer. + socket.onAny((event, data) { + // ignore: avoid_print + print('[SOCKET][EVENT] $event'); + }); + // Detailed receive logging. + socket.on('receive_message', (data) { + // ignore: avoid_print + print('[SOCKET][RECEIVE] raw=${data.runtimeType} -> $data'); + if (data is Map) { + try { + final msg = ChatMessage.fromSocket(_normalizeSocketPayload(Map.from(data))); + _messageController.add(msg); + } catch (e) { + // ignore: avoid_print + print('[SOCKET][RECEIVE] parse error: $e'); + } + } else { + // ignore: avoid_print + print('[SOCKET][RECEIVE] unexpected payload type; ignoring'); + } + }); + socket.on('joined_group', (d) { + // ignore: avoid_print + print('[SOCKET] joined_group: $d'); + }); + socket.on('error', (e) { + // ignore: avoid_print + print('[SOCKET] server_error: $e'); + }); + } } \ No newline at end of file From a35fe0cd3f049ab663de41871b3f3ed42ef4683f Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Fri, 3 Oct 2025 04:12:40 +0530 Subject: [PATCH 50/53] fix: update button colors for consistency across pages --- leaderboard_app/lib/pages/dashboard_page.dart | 2 +- .../lib/pages/leetcode_verification_page.dart | 6 +++--- leaderboard_app/lib/pages/no_internet_page.dart | 11 +++++++++-- leaderboard_app/lib/pages/signin_page.dart | 4 ++-- leaderboard_app/lib/pages/signup_page.dart | 4 ++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/leaderboard_app/lib/pages/dashboard_page.dart b/leaderboard_app/lib/pages/dashboard_page.dart index ebee5b2..bf43e4d 100644 --- a/leaderboard_app/lib/pages/dashboard_page.dart +++ b/leaderboard_app/lib/pages/dashboard_page.dart @@ -103,7 +103,7 @@ class _DashboardPageState extends State { _buildHeaderButton( Icons.local_fire_department, "${user.streak}", - colors.secondary, + Color(0xFFF6C156), ), ], ), diff --git a/leaderboard_app/lib/pages/leetcode_verification_page.dart b/leaderboard_app/lib/pages/leetcode_verification_page.dart index 87b92eb..b82f5d4 100644 --- a/leaderboard_app/lib/pages/leetcode_verification_page.dart +++ b/leaderboard_app/lib/pages/leetcode_verification_page.dart @@ -93,7 +93,7 @@ class _LeetCodeVerificationPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE3C17D), + backgroundColor: const Color(0xFFF6C156), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -140,7 +140,7 @@ class _LeetCodeVerificationPageState extends State { if (_secondsLeft > 0) Text( 'Auto-checking... $_secondsLeft s left', - style: const TextStyle(color: Color(0xFFE3C17D)), + style: const TextStyle(color: Color(0xFFF6C156)), ), ], ), @@ -149,7 +149,7 @@ class _LeetCodeVerificationPageState extends State { const Spacer(), TextButton( onPressed: () => context.go('/'), - child: const Text('Skip for now', style: TextStyle(color: Color(0xFFE3C17D))), + child: const Text('Skip for now', style: TextStyle(color: Color(0xFFF6C156))), ), ], ), diff --git a/leaderboard_app/lib/pages/no_internet_page.dart b/leaderboard_app/lib/pages/no_internet_page.dart index 07deee6..3f1dc14 100644 --- a/leaderboard_app/lib/pages/no_internet_page.dart +++ b/leaderboard_app/lib/pages/no_internet_page.dart @@ -6,6 +6,7 @@ class NoInternetPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.black, body: Center( child: Padding( padding: const EdgeInsets.all(24.0), @@ -14,11 +15,17 @@ class NoInternetPage extends StatelessWidget { children: [ const Icon(Icons.wifi_off, size: 96, color: Colors.grey), const SizedBox(height: 24), - const Text('No Internet Connection', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const Text('No Internet Connection', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white)), const SizedBox(height: 12), - const Text('Please check your connection. The app will continue once you are back online.', textAlign: TextAlign.center), + const Text('Please check your connection. The app will continue once you are back online.', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70)), const SizedBox(height: 32), FilledButton( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFFF6C156), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), + textStyle: const TextStyle(fontWeight: FontWeight.w600), + ), onPressed: () { // Simply trigger a rebuild; actual status is managed by provider listening. (context as Element).markNeedsBuild(); diff --git a/leaderboard_app/lib/pages/signin_page.dart b/leaderboard_app/lib/pages/signin_page.dart index 7dd3fba..19e52f6 100644 --- a/leaderboard_app/lib/pages/signin_page.dart +++ b/leaderboard_app/lib/pages/signin_page.dart @@ -110,7 +110,7 @@ class _SignInPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE3C17D), + backgroundColor: const Color(0xFFF6C156), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -147,7 +147,7 @@ class _SignInPageState extends State { child: const Text( "Sign up", style: TextStyle( - color: Color(0xFFE3C17D), + color: Color(0xFFF6C156), fontWeight: FontWeight.bold, decoration: TextDecoration.underline, ), diff --git a/leaderboard_app/lib/pages/signup_page.dart b/leaderboard_app/lib/pages/signup_page.dart index 95fb269..994ffd2 100644 --- a/leaderboard_app/lib/pages/signup_page.dart +++ b/leaderboard_app/lib/pages/signup_page.dart @@ -135,7 +135,7 @@ class _SignUpPageState extends State { height: 45, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE3C17D), + backgroundColor: const Color(0xFFF6C156), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -172,7 +172,7 @@ class _SignUpPageState extends State { child: const Text( "Sign in", style: TextStyle( - color: Color(0xFFE3C17D), + color: Color(0xFFF6C156), fontWeight: FontWeight.bold, decoration: TextDecoration.underline, ), From 44e3ce457bc31cd6d06463d45cd92a9f69418c71 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 21 Oct 2025 19:31:09 +0530 Subject: [PATCH 51/53] chore: changed app name --- leaderboard_app/android/app/build.gradle.kts | 4 ++-- .../main/kotlin/com/example/leaderboard_app/MainActivity.kt | 2 +- leaderboard_app/linux/CMakeLists.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/leaderboard_app/android/app/build.gradle.kts b/leaderboard_app/android/app/build.gradle.kts index 5434cd7..fa19038 100644 --- a/leaderboard_app/android/app/build.gradle.kts +++ b/leaderboard_app/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.example.leaderboard_app" + namespace = "com.dscvit.leeterboard" 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.leaderboard_app" + applicationId = "com.dscvit.leeterboard" // 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/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt b/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt index b1e3a68..b6659cc 100644 --- a/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt +++ b/leaderboard_app/android/app/src/main/kotlin/com/example/leaderboard_app/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.leaderboard_app +package com.dscvit.leeterboard import io.flutter.embedding.android.FlutterActivity diff --git a/leaderboard_app/linux/CMakeLists.txt b/leaderboard_app/linux/CMakeLists.txt index 1dc07dd..3e03366 100644 --- a/leaderboard_app/linux/CMakeLists.txt +++ b/leaderboard_app/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "leaderboard_app") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.leaderboard_app") +set(APPLICATION_ID "com.dscvit.leeterboard") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. From b929b6bd50b3fd3d899646727786876b896324c8 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 21 Oct 2025 19:58:35 +0530 Subject: [PATCH 52/53] fix: auth settings --- .../lib/chatpage-components/chat_view.dart | 1 - leaderboard_app/lib/main.dart | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index ea69d30..a06b54f 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -19,7 +19,6 @@ class _ChatViewState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final FocusNode myFocusNode = FocusNode(); - int _lastMessageCount = 0; // retained for possible future usage bool _didInitialAutoScroll = false; // guard to only auto-scroll once after history loads @override diff --git a/leaderboard_app/lib/main.dart b/leaderboard_app/lib/main.dart index dfcb97d..4c4cbf9 100644 --- a/leaderboard_app/lib/main.dart +++ b/leaderboard_app/lib/main.dart @@ -128,6 +128,18 @@ class _AppInitializerState extends State<_AppInitializer> { } Future _init() async { + // If not logged in, skip any data preload and go straight to router + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('authToken') ?? ''; + + if (token.isEmpty) { + if (mounted) { + setState(() => _ready = true); + FlutterNativeSplash.remove(); + } + return; + } + final connectivity = context.read(); // Wait a tick for connectivity to initialize await Future.delayed(const Duration(milliseconds: 50)); @@ -160,6 +172,11 @@ class _AppInitializerState extends State<_AppInitializer> { Future _preload() async { final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('authToken') ?? ''; + if (token.isEmpty) { + // Not logged in; nothing to preload. + return; + } final returning = prefs.getBool('returningUser') ?? false; final userProvider = context.read(); final dashboardProvider = context.read(); From 09e89ab0882c62aba6f9f4e93fc6bcd05a94b834 Mon Sep 17 00:00:00 2001 From: Ashvik Mishra Date: Tue, 21 Oct 2025 20:25:04 +0530 Subject: [PATCH 53/53] feat: auth refresh + group flow fix --- .../lib/chatpage-components/chat_view.dart | 4 +- leaderboard_app/lib/models/auth_models.dart | 4 + leaderboard_app/lib/pages/chatlists_page.dart | 16 +- leaderboard_app/lib/pages/groupinfo_page.dart | 5 +- .../lib/services/auth/auth_service.dart | 15 +- .../lib/services/core/api_client.dart | 31 +--- .../lib/services/core/dio_provider.dart | 142 ++++++++++++++++-- .../lib/services/core/token_manager.dart | 52 +++++++ .../services/dashboard/dashboard_service.dart | 20 ++- 9 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 leaderboard_app/lib/services/core/token_manager.dart diff --git a/leaderboard_app/lib/chatpage-components/chat_view.dart b/leaderboard_app/lib/chatpage-components/chat_view.dart index a06b54f..8854775 100644 --- a/leaderboard_app/lib/chatpage-components/chat_view.dart +++ b/leaderboard_app/lib/chatpage-components/chat_view.dart @@ -95,8 +95,8 @@ class _ChatViewState extends State { builder: (_) => GroupInfoPage(groupId: widget.groupId, initialName: widget.groupName), ), ).then((result) { - if (result is Map && result['leftGroup'] == true) { - if (mounted) Navigator.of(context).pop(); + if (result is Map && (result['leftGroup'] == true || result['deletedGroup'] == true)) { + if (mounted) Navigator.of(context).pop(result); } }); }, diff --git a/leaderboard_app/lib/models/auth_models.dart b/leaderboard_app/lib/models/auth_models.dart index 95474f9..bbc22a1 100644 --- a/leaderboard_app/lib/models/auth_models.dart +++ b/leaderboard_app/lib/models/auth_models.dart @@ -2,12 +2,14 @@ class AuthResponse { final bool success; final String message; final String token; + final String? refreshToken; final User user; AuthResponse({ required this.success, required this.message, required this.token, + this.refreshToken, required this.user, }); @@ -15,10 +17,12 @@ class AuthResponse { final data = (json['data'] ?? {}) as Map; final userJson = (data['user'] ?? json['user'] ?? {}) as Map; final token = (data['token'] ?? json['token'] ?? '') as String; + final refresh = (data['refreshToken'] ?? data['refresh_token'] ?? json['refreshToken'] ?? json['refresh_token'] ?? '') as String; return AuthResponse( success: json['success'] == true || json['ok'] == true, message: (json['message'] ?? json['msg'] ?? '') as String, token: token, + refreshToken: refresh.isEmpty ? null : refresh, user: User.fromJson(userJson), ); } diff --git a/leaderboard_app/lib/pages/chatlists_page.dart b/leaderboard_app/lib/pages/chatlists_page.dart index ff315e6..12949e6 100644 --- a/leaderboard_app/lib/pages/chatlists_page.dart +++ b/leaderboard_app/lib/pages/chatlists_page.dart @@ -203,6 +203,11 @@ class _ChatlistsPageState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Group created')), ); + // Refresh lists to reflect membership and server-derived fields + // (e.g., lastMessage, counts, privacy flags, etc.) + // Uses current search filter. + // Fire-and-forget; UI already shows snackbar feedback. + unawaited(_refresh()); } }, icon: provider.isCreating @@ -433,19 +438,26 @@ class _ChatlistsPageState extends State { } if (!mounted) return; if (isMember) { - Navigator.push( + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => ChatPage(groupId: groupId, groupName: groupName), ), ); + if (result is Map && (result['deletedGroup'] == true || result['leftGroup'] == true || result['updated'] == true)) { + await _refresh(); + } } else { - Navigator.push( + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => GroupInfoPage(groupId: groupId, initialName: groupName), ), ); + // If group was deleted or membership changed, refresh list on return + if (result is Map && (result['deletedGroup'] == true || result['leftGroup'] == true || result['updated'] == true)) { + await _refresh(); + } } }, child: Container( diff --git a/leaderboard_app/lib/pages/groupinfo_page.dart b/leaderboard_app/lib/pages/groupinfo_page.dart index 3d45d1e..8aba0b1 100644 --- a/leaderboard_app/lib/pages/groupinfo_page.dart +++ b/leaderboard_app/lib/pages/groupinfo_page.dart @@ -627,8 +627,9 @@ class _GroupInfoPageState extends State { final chatListProv = context.read(); chatListProv?.removeGroup(_group!.id); } - if (!mounted) return; - Navigator.of(context).pop(); + if (!mounted) return; + // Return a signal so the previous page can refresh its data. + Navigator.of(context).pop({'deletedGroup': true, 'groupId': _group!.id}); } catch (_) { setState(() => _error = 'Failed to delete group'); } finally { diff --git a/leaderboard_app/lib/services/auth/auth_service.dart b/leaderboard_app/lib/services/auth/auth_service.dart index e0c5827..21c93bb 100644 --- a/leaderboard_app/lib/services/auth/auth_service.dart +++ b/leaderboard_app/lib/services/auth/auth_service.dart @@ -4,6 +4,7 @@ import 'package:leaderboard_app/services/core/api_client.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:leaderboard_app/services/core/dio_provider.dart'; import 'package:leaderboard_app/services/chat/chat_service.dart'; +import 'package:leaderboard_app/services/core/token_manager.dart'; class AuthService { final Dio _dio; @@ -26,7 +27,7 @@ class AuthService { final login = await signIn(email: email, password: password); return login; } else { - await _saveAuth(response.token); + await _saveAuth(response.token, refreshToken: response.refreshToken); // Initialize a fresh socket connection with the new token. try { await ChatService.instance.connectWithToken(response.token); } catch (_) {} return response; @@ -42,20 +43,21 @@ class AuthService { if (response.token.isEmpty) { throw DioException(requestOptions: res.requestOptions, response: res, message: 'Token missing in response'); } - await _saveAuth(response.token); + await _saveAuth(response.token, refreshToken: response.refreshToken); // After storing token, connect socket with new identity. try { await ChatService.instance.connectWithToken(response.token); } catch (_) {} return response; } Future logout() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = await SharedPreferences.getInstance(); // Clear all persisted user-specific data on logout to avoid leaking // authentication state or cached profile details between accounts. // If in the future some keys should persist across logins (e.g. theme), // fetch their values first and re-set them after clear(). await prefs.clear(); - DioProvider.reset(); + await TokenManager.clearTokens(); + DioProvider.reset(); // Proactively disconnect socket (in case ChatProvider not yet instantiated to reset it) try { ChatService.instance.disconnect(); } catch (_) {} // Also reset any cached singletons that embed auth headers (e.g. Dio). @@ -79,8 +81,7 @@ class AuthService { return User.fromJson(userJson); } - Future _saveAuth(String token) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('authToken', token); + Future _saveAuth(String token, {String? refreshToken}) async { + await TokenManager.saveTokens(accessToken: token, refreshToken: refreshToken); } } diff --git a/leaderboard_app/lib/services/core/api_client.dart b/leaderboard_app/lib/services/core/api_client.dart index 472b26e..80e7f6a 100644 --- a/leaderboard_app/lib/services/core/api_client.dart +++ b/leaderboard_app/lib/services/core/api_client.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:leaderboard_app/config/api_config.dart'; +import 'package:leaderboard_app/services/core/dio_provider.dart'; class ApiClient { static final String kBaseUrl = ApiConfig.baseUrl; @@ -9,34 +9,7 @@ class ApiClient { ApiClient._internal(this.dio); static Future create({String? baseUrl}) async { - final prefs = await SharedPreferences.getInstance(); - final dio = Dio( - BaseOptions( - baseUrl: baseUrl ?? kBaseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 20), - headers: { - 'Content-Type': 'application/json', - }, - ), - ); - - dio.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) async { - final token = prefs.getString('authToken'); - if (token != null && token.isNotEmpty) { - options.headers['Authorization'] = 'Bearer $token'; - } - handler.next(options); - }, - onError: (e, handler) { - // You can add logging or global error handling here - handler.next(e); - }, - ), - ); - + final dio = await DioProvider.getInstance(baseUrl: baseUrl ?? kBaseUrl); return ApiClient._internal(dio); } } diff --git a/leaderboard_app/lib/services/core/dio_provider.dart b/leaderboard_app/lib/services/core/dio_provider.dart index 161d3a1..ffca666 100644 --- a/leaderboard_app/lib/services/core/dio_provider.dart +++ b/leaderboard_app/lib/services/core/dio_provider.dart @@ -1,38 +1,85 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart' show kIsWeb; // narrow imports -import 'package:shared_preferences/shared_preferences.dart'; import 'package:leaderboard_app/config/api_config.dart'; +import 'package:leaderboard_app/services/core/token_manager.dart'; /// Provides a configured singleton Dio instance with auth header + logging. class DioProvider { static Dio? _dio; + static Future? _refreshingFuture; static Future getInstance({String? baseUrl}) async { if (_dio != null) return _dio!; - final prefs = await SharedPreferences.getInstance(); + // Note: Token values are retrieved on-demand via TokenManager. final dio = Dio( BaseOptions( baseUrl: baseUrl ?? ApiConfig.baseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 20), + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 60), headers: {'Content-Type': 'application/json'}, ), ); dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) async { - final token = prefs.getString('authToken'); + // Skip auth header for explicit opt-out + if (options.extra['skipAuth'] == true) { + handler.next(options); + return; + } + final token = await TokenManager.getAccessToken(); if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); - }, onError: (e, handler) { + }, onError: (e, handler) async { // Simple retry for idempotent GETs on network issues if (_shouldRetry(e)) { _retry(dio, e.requestOptions).then(handler.resolve).catchError((_) => handler.next(e)); - } else { - handler.next(e); + return; } + + // Refresh on 401 Unauthorized, excluding auth endpoints to avoid loops + final status = e.response?.statusCode; + final path = e.requestOptions.path; + final isAuthPath = path.contains('/auth/login') || path.contains('/auth/signup') || path.contains('/auth/refresh'); + final alreadyRetried = e.requestOptions.extra['retried'] == true; + + if (status == 401 && !isAuthPath && !alreadyRetried) { + try { + final newToken = await _refreshTokenIfNeeded(dio); + if (newToken != null && newToken.isNotEmpty) { + // Clone and retry original request with new token + final opts = Options( + method: e.requestOptions.method, + headers: { + ...e.requestOptions.headers, + 'Authorization': 'Bearer $newToken', + }, + responseType: e.requestOptions.responseType, + contentType: e.requestOptions.contentType, + sendTimeout: e.requestOptions.sendTimeout, + receiveTimeout: e.requestOptions.receiveTimeout, + extra: { + ...e.requestOptions.extra, + 'retried': true, + }, + ); + final rerun = await dio.request( + e.requestOptions.path, + data: e.requestOptions.data, + queryParameters: e.requestOptions.queryParameters, + options: opts, + ); + handler.resolve(rerun); + return; + } + } catch (_) { + // fall through to next + } + } + + handler.next(e); })); // Basic log interceptor (custom to avoid extra dependency) @@ -43,10 +90,17 @@ class DioProvider { } static bool _shouldRetry(DioException e) { - return e.type == DioExceptionType.connectionError && e.requestOptions.method == 'GET'; + final isGet = e.requestOptions.method == 'GET'; + final isNet = e.type == DioExceptionType.connectionError || e.type == DioExceptionType.receiveTimeout; + if (!(isGet && isNet)) return false; + final attempts = (e.requestOptions.extra['retryCount'] as int?) ?? 0; + return attempts < 1; // retry at most once } static Future> _retry(Dio dio, RequestOptions requestOptions) async { + final attempts = (requestOptions.extra['retryCount'] as int?) ?? 0; + // brief backoff before retrying + await Future.delayed(Duration(milliseconds: 500 * (attempts + 1))); final opts = Options( method: requestOptions.method, headers: requestOptions.headers, @@ -54,6 +108,10 @@ class DioProvider { contentType: requestOptions.contentType, sendTimeout: requestOptions.sendTimeout, receiveTimeout: requestOptions.receiveTimeout, + extra: { + ...requestOptions.extra, + 'retryCount': attempts + 1, + }, ); return dio.request( requestOptions.path, @@ -68,6 +126,72 @@ class DioProvider { static void reset() { _dio = null; } + + /// Ensure only one refresh happens at a time. Returns new access token or null. + static Future _refreshTokenIfNeeded(Dio baseDio) async { + // If a refresh is already ongoing, await the same future + final ongoing = _refreshingFuture; + if (ongoing != null) { + return ongoing; + } + + final completer = Completer(); + _refreshingFuture = completer.future; + + try { + final newToken = await _callRefreshEndpoint(baseDio); + completer.complete(newToken); + return newToken; + } catch (err) { + completer.complete(null); + return null; + } finally { + _refreshingFuture = null; + } + } + + /// Call the refresh endpoint using a bare Dio to avoid interceptor loops. + static Future _callRefreshEndpoint(Dio baseDio) async { + final refreshToken = await TokenManager.getRefreshToken(); + final refreshDio = Dio( + BaseOptions( + baseUrl: baseDio.options.baseUrl, + connectTimeout: baseDio.options.connectTimeout, + receiveTimeout: baseDio.options.receiveTimeout, + headers: {'Content-Type': 'application/json'}, + ), + ); + + Response res; + try { + if (refreshToken != null && refreshToken.isNotEmpty) { + res = await refreshDio.post('/auth/refresh', data: {'refreshToken': refreshToken}); + } else { + // Some backends use HttpOnly cookie refresh; attempt without body + res = await refreshDio.post('/auth/refresh'); + } + } on DioException { + // Refresh failed + await TokenManager.clearTokens(); + return null; + } + + final data = res.data is Map + ? res.data as Map + : {}; + final payload = (data['data'] ?? data) as Map; + final newAccess = (payload['token'] ?? payload['accessToken'] ?? '') as String; + final newRefresh = (payload['refreshToken'] ?? payload['refresh_token'] ?? '') as String; + + if (newAccess.isEmpty) { + await TokenManager.clearTokens(); + return null; + } + + // Persist the new tokens + await TokenManager.saveTokens(accessToken: newAccess, refreshToken: newRefresh.isEmpty ? null : newRefresh); + return newAccess; + } } class _LogInterceptor extends Interceptor { diff --git a/leaderboard_app/lib/services/core/token_manager.dart b/leaderboard_app/lib/services/core/token_manager.dart new file mode 100644 index 0000000..c122f8a --- /dev/null +++ b/leaderboard_app/lib/services/core/token_manager.dart @@ -0,0 +1,52 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Centralized helpers for reading/writing auth tokens. +/// Keys are kept backward compatible with existing code. +class TokenManager { + static const String _kAccessTokenKey = 'authToken'; + static const String _kRefreshTokenKey = 'refreshToken'; + + /// Returns the stored access token (JWT) or null if not set. + static Future getAccessToken() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString(_kAccessTokenKey); + if (token == null || token.isEmpty) return null; + return token; + } + + /// Returns the stored refresh token or null if not present. + static Future getRefreshToken() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString(_kRefreshTokenKey); + if (token == null || token.isEmpty) return null; + return token; + } + + /// Persist both access and optional refresh tokens. + static Future saveTokens({required String accessToken, String? refreshToken}) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kAccessTokenKey, accessToken); + if (refreshToken != null && refreshToken.isNotEmpty) { + await prefs.setString(_kRefreshTokenKey, refreshToken); + } + } + + /// Update only the access token. + static Future saveAccessToken(String accessToken) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kAccessTokenKey, accessToken); + } + + /// Update only the refresh token. + static Future saveRefreshToken(String refreshToken) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kRefreshTokenKey, refreshToken); + } + + /// Remove tokens from storage. + static Future clearTokens() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_kAccessTokenKey); + await prefs.remove(_kRefreshTokenKey); + } +} diff --git a/leaderboard_app/lib/services/dashboard/dashboard_service.dart b/leaderboard_app/lib/services/dashboard/dashboard_service.dart index 4b628d0..e0a0edc 100644 --- a/leaderboard_app/lib/services/dashboard/dashboard_service.dart +++ b/leaderboard_app/lib/services/dashboard/dashboard_service.dart @@ -13,7 +13,10 @@ class DashboardService { } Future> getUserSubmissions() async { - final res = await _dio.get('/dashboard/submissions'); + final res = await _dio.get( + '/dashboard/submissions', + options: Options(receiveTimeout: const Duration(seconds: 60)), + ); final body = res.data as Map; // Attempt structured parsing try { @@ -33,7 +36,10 @@ class DashboardService { } Future getDailyQuestion() async { - final res = await _dio.get('/dashboard/daily'); + final res = await _dio.get( + '/dashboard/daily', + options: Options(receiveTimeout: const Duration(seconds: 60)), + ); final body = res.data as Map; final data = (body['data'] ?? body) as Map; final dq = (data['dailyQuestion'] ?? data['daily'] ?? data['question'] ?? data) as dynamic; @@ -42,7 +48,10 @@ class DashboardService { } Future> getTopUsers() async { - final res = await _dio.get('/dashboard/leaderboard'); + final res = await _dio.get( + '/dashboard/leaderboard', + options: Options(receiveTimeout: const Duration(seconds: 60)), + ); final body = res.data as Map; final data = (body['data'] ?? body) as Map; final raw = (data['leaderboard'] ?? data['users'] ?? data['results'] ?? data) as dynamic; @@ -57,7 +66,10 @@ class DashboardService { // New: explicit getLeaderboard support. Tries /leaderboard first, falls back to /dashboard/leaderboard. Future> getLeaderboard() async { try { - final res = await _dio.get('/leaderboard'); + final res = await _dio.get( + '/leaderboard', + options: Options(receiveTimeout: const Duration(seconds: 60)), + ); final body = res.data as Map; final data = (body['data'] ?? body) as Map; final raw = (data['leaderboard'] ?? data['users'] ?? []) as List;