Compare commits

..

17 commits

Author SHA1 Message Date
Mercurio d27ccfacd4 Minor version bump, add rank icons and leaderboard sorting 2025-01-30 21:25:42 +01:00
Mercurio 0041f5bc4c Resized 2v2 matchcontroller 2025-01-27 22:21:32 +01:00
Mercurio ce08cd7eb4 ok so we shouldn't leave the app set on the testing api. 2025-01-25 19:21:58 +01:00
Mercurio cabeaeff6c Version bump for pub 2025-01-25 18:20:03 +01:00
Mercurio 1481c5b292 Added 2 player mode
- Refactored join and create match UI
- Added OSK and UNC display in profile page
- Code cleanup and bug fixes. API version Bump
2025-01-25 18:17:04 +01:00
Mercurio ce1a54c58b Fuck flutter assets 2025-01-23 22:55:32 +01:00
Mercurio caac8bf978 Added rank view, minor match page refactor. bump version to 0.0.51 2025-01-23 22:35:26 +01:00
Mercurio 94088c0338 Update match creation to support 2v2 matches using TrueSkill as the ranking system 2025-01-21 22:39:47 +01:00
Mercurio b304ab3a7b Added API status page and git shortcuts on login screen, updated debug url to reflect new network infrastructure. started working on 2v2 match upgrading logic in api 2025-01-14 00:15:19 +01:00
Mercurio 4d27d3be11 Minor changes on match creation page, added version retrieval from api 2024-12-25 12:08:09 +01:00
Mercurio c872e40861 Added OSS license dialog 2024-12-23 22:10:06 +01:00
Mercurio b22af349a1 Merge branch 'main' of https://git.mercurio.moe/Mercury/dth-pingpong-mobileapp 2024-12-23 21:48:45 +01:00
Mercurio b6f8a4b320 Add global page for api url 2024-12-23 21:48:34 +01:00
Mercurio. 8b9b26ea4c add xcode definitions for ios build 2024-12-23 14:45:25 +01:00
Mercury. 27b41281c4 Update README.md 2024-12-22 21:24:16 +01:00
Mercury. b7d48043df Update README.md 2024-12-22 21:06:56 +01:00
Mercury. df55fcac32 Update README.md 2024-12-22 21:06:33 +01:00
39 changed files with 1175 additions and 303 deletions

View file

@ -1,3 +1,58 @@
# dth-pingpong-mobileapp
>
> Minimum supported Android API: 28 (Android 9).\
> Minimum required iOS version: 14
>
Flutter application for ping pong score tracking. supports friends, elo tracking, match creation and planning.
Mobile application for ping pong score tracking. supports friends ,elo tracking, match creation and planning.
![lang](https://img.shields.io/badge/Flutter-02569B?style=for-the-badge&logo=flutter&logoColor=white) ![os1](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)
---
# Installing
Installation options are available for major mobile platforms. Although we mainly support android, all the features are cross-compatible with iOS
### Stable release channel (Android only)
Grab one of the builds from the releases page or run:
```bash
flutter build apk
```
Production artifacts will be found in the `build\app\outputs\apk\` directory.
**RRC (Rolling Release Channel)**
There's also a Rolling release available as pre-release builds. They will be released in a much quicker update ring (every other commit i'd assume) and are mostly debug builds so they'll be very heavy and *likely* broken.
### Building instructions (Android)
Make sure you have [Flutter](https://flutter.dev/), [Android Studio](https://developer.android.com/studio), and [Visual Studio Code](https://code.visualstudio.com/) installed. Install the suggested Flutter, Dart, and linting extensions in Visual Studio Code.
Run the following commands to ensure all dependencies are met:
```bash
flutter doctor
```
You'll be guided through a first-time setup that checks for platform tools, dependencies, and missing components.
Once the setup is complete you can navigate to the repo folder and run
```bash
flutter run
```
If you want to build debug you can also run
```bash
flutter build apk --debug
```
Build artifacts for Android are saved under `build\app\outputs\apk`.
### Building instructions (iOS)
**MAJOR WORD OF WARNING: iOS builds are NOT officially supported, and we have NO clue as to why or how certain things just break on the xCode build (namely some icons and some api calls). if you encounter these issues you are free to patch them, and, if you seem fit, hit me up with a PR**
Make sure you have [Flutter](https://flutter.dev/), [xCode and Apple Developer Tools](https://apps.apple.com/en/app/xcode/id497799835?mt=12), [cocoapods](https://formulae.brew.sh/formula/cocoapods), and [Visual Studio Code](https://code.visualstudio.com/) installed.
Install the suggested Flutter, Dart, and linting extensions in Visual Studio Code and set up xcode for "iOS on iphone" developement. Once that is done, go in the project's folder and run the following command to ensure that your build environment is properly configured, any missing component will be highlighted
```bash
flutter doctor
```
Once you've done that, from the same terminal, run this command to generate a .IPA file - once you're done it's a matter of sideloading it to your device. since i've never owned an apple device i have no idea how to do that. iOS artifacts are saved in `build/ios/runner/`.
```bash
flutter build ios
```

View file

@ -3,6 +3,8 @@ analyzer:
errors:
library_private_types_in_public_api: ignore
prefer_const_constructors: ignore
prefer_const_literals_to_create_immutables: ignore
prefer_final_fields: ignore
use_build_context_synchronously: ignore
use_key_in_widget_constructors: ignore
use_super_parameters: ignore

BIN
assets/A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
assets/B.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/C.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
assets/D.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/E.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
assets/S.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
assets/SS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
assets/infdan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
assets/player_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/player_0_dp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/player_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/player_1_dp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/player_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/player_2_dp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
ios/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

44
ios/Podfile Normal file
View file

@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

23
ios/Podfile.lock Normal file
View file

@ -0,0 +1,23 @@
PODS:
- Flutter (1.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- Flutter (from `Flutter`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
COCOAPODS: 1.16.2

View file

@ -10,7 +10,9 @@
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 */; };
4ECA18A68D772963A75CDDEE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 664C6953382FB54D8ADFAA15 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
860A7C4827A4A1FA258EE3B0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A33A52767F410023C3017843 /* Pods_RunnerTests.framework */; };
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 */; };
@ -42,12 +44,19 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1D683FDEB78EA33536EDD9B0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
2310337C49F4840CC13DECEC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
2BD08DFC2DFC2958AE95FAD5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
664C6953382FB54D8ADFAA15 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7339FE983199E9675039DE4E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8D54E1F7566A817BB3B988AC /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
9702D4B740EAB4624B982FE0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -55,19 +64,38 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A33A52767F410023C3017843 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
035979494952341CA3553103 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
860A7C4827A4A1FA258EE3B0 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4ECA18A68D772963A75CDDEE /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2F1367A0823D5A1C2FD30B1E /* Frameworks */ = {
isa = PBXGroup;
children = (
664C6953382FB54D8ADFAA15 /* Pods_Runner.framework */,
A33A52767F410023C3017843 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@ -76,6 +104,20 @@
path = RunnerTests;
sourceTree = "<group>";
};
803619556C668AF928FE7E28 /* Pods */ = {
isa = PBXGroup;
children = (
2310337C49F4840CC13DECEC /* Pods-Runner.debug.xcconfig */,
7339FE983199E9675039DE4E /* Pods-Runner.release.xcconfig */,
2BD08DFC2DFC2958AE95FAD5 /* Pods-Runner.profile.xcconfig */,
1D683FDEB78EA33536EDD9B0 /* Pods-RunnerTests.debug.xcconfig */,
8D54E1F7566A817BB3B988AC /* Pods-RunnerTests.release.xcconfig */,
9702D4B740EAB4624B982FE0 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@ -94,6 +136,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
803619556C668AF928FE7E28 /* Pods */,
2F1367A0823D5A1C2FD30B1E /* Frameworks */,
);
sourceTree = "<group>";
};
@ -128,8 +172,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
B52AA023DC4D626077351632 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
035979494952341CA3553103 /* Frameworks */,
);
buildRules = (
);
@ -145,12 +191,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B9DF9E6804910D262BD7E7EA /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
64EEB9E634C86FA592F486D2 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -238,6 +286,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
64EEB9E634C86FA592F486D2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -253,6 +318,50 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B52AA023DC4D626077351632 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
B9DF9E6804910D262BD7E7EA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -327,6 +436,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -361,7 +471,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = R28MH57GSC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -370,6 +483,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dth.innodesi.pingpongapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@ -378,6 +492,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1D683FDEB78EA33536EDD9B0 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -395,6 +510,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8D54E1F7566A817BB3B988AC /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -410,6 +526,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9702D4B740EAB4624B982FE0 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -447,6 +564,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -504,6 +622,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -540,7 +659,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = R28MH57GSC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -549,6 +671,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dth.innodesi.pingpongapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -562,7 +685,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = R28MH57GSC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -571,6 +697,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dth.innodesi.pingpongapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View file

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -1,7 +1,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

3
lib/globals.dart Normal file
View file

@ -0,0 +1,3 @@
// lib/globals.dart
const String apiurl = "https://api.dthpp.mercurio.moe";
//const String apiurl = "http://192.168.1.120:9134";

View file

@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
title: 'Ping Pong Tracker',
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.dark(
primary: Colors.blue.shade900,
primary: Colors.blue.shade800,
),
),
home: EntryPoint(),

View file

@ -7,6 +7,9 @@ import 'views/joinmatch.dart';
import 'views/creatematch.dart';
import 'views/friendlist.dart';
import 'views/myprofile.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../globals.dart';
class HomePage extends StatefulWidget {
@override
@ -16,7 +19,6 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
// Define the pages for each section
final List<Widget> _pages = [
LeaderboardPage(),
JoinMatchPage(),
@ -40,12 +42,70 @@ class _HomePageState extends State<HomePage> {
);
}
Future<Map<String, String>> fetchCommitHashes() async {
const apiUrl = '$apiurl/version';
try {
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
String formatHash(String? hash) {
if (hash == null) return 'Unknown';
return '#${hash.substring(0, 8).toUpperCase()}';
}
return {
'backend': formatHash(data['backend']),
'frontend': formatHash(data['frontend']),
};
} else {
throw Exception('Failed to fetch commit hashes');
}
} catch (e) {
return {
'backend': 'Error fetching hash',
'frontend': 'Error fetching hash',
};
}
}
Future<void> _showOpenSourceLicenses() async {
final commitHashes = await fetchCommitHashes();
showDialog(
context: context,
builder: (BuildContext context) => AboutDialog(
applicationIcon: const Icon(Icons.code),
applicationLegalese: '© 2024 Thomas Bassi @ Defence Tech.',
applicationName: 'DTHPP',
applicationVersion:
'API: ${commitHashes['backend']} - UI: ${commitHashes['frontend']}',
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
"Do people even care about licenses? Is this ever going to be opened? Anywho, i hope you're enjoying the app and having fun on your break :D",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Ping Pong Tracker'),
actions: [
IconButton(
icon: Icon(Icons.info),
onPressed: _showOpenSourceLicenses,
tooltip: 'Open Source Licenses',
),
IconButton(
icon: Icon(Icons.logout),
onPressed: () => _logout(context),

View file

@ -3,7 +3,9 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'home.dart';
import '../globals.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
class LoginPage extends StatefulWidget {
@override
@ -18,11 +20,20 @@ class _LoginPageState extends State<LoginPage> {
bool _isLogin = true;
bool _isLoading = false;
final String baseUrl = 'http://api.dthpp.mercurio.moe';
Future<void> _handleAuth() async {
final email = _emailController.text.trim();
final password = _passwordController.text.trim();
final displayName = _displayNameController.text.trim();
// Input validation
if (email.isEmpty ||
password.isEmpty ||
(!_isLogin && displayName.isEmpty)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please fill in all required fields.')),
);
return;
}
setState(() {
_isLoading = true;
@ -38,13 +49,13 @@ class _LoginPageState extends State<LoginPage> {
context, MaterialPageRoute(builder: (context) => HomePage()));
}
} else {
final uid = await _register(email, password, _displayNameController.text.trim());
final uid = await _register(email, password, displayName);
if (uid != null) {
setState(() {
_isLogin = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registration successful! Please login.')));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Registration successful! Please login.')));
}
}
} catch (e) {
@ -58,7 +69,7 @@ class _LoginPageState extends State<LoginPage> {
}
Future<String?> _login(String email, String password) async {
final url = Uri.parse('$baseUrl/login');
final url = Uri.parse('$apiurl/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
@ -73,8 +84,9 @@ class _LoginPageState extends State<LoginPage> {
}
}
Future<String?> _register(String email, String password, String displayName) async {
final url = Uri.parse('$baseUrl/register');
Future<String?> _register(
String email, String password, String displayName) async {
final url = Uri.parse('$apiurl/register');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
@ -118,21 +130,61 @@ class _LoginPageState extends State<LoginPage> {
decoration: InputDecoration(labelText: 'Display Name'),
),
const SizedBox(height: 20),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
if (_isLoading)
CircularProgressIndicator()
else
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _handleAuth,
child: Text(_isLogin ? 'Login' : 'Register'),
),
TextButton(
ElevatedButton(
onPressed: () {
setState(() {
_isLogin = !_isLogin;
});
},
child: Text(_isLogin
? 'Don\'t have an account? Register'
: 'Already have an account? Login'),
child: Text(_isLogin ? 'Register' : 'Back to Login'),
),
],
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: Icon(FontAwesomeIcons.github),
onPressed: () async {
final url = Uri.parse(
'https://git.mercurio.moe/Mercury/dth-pingpong-mobileapp');
if (await canLaunchUrl(url)) {
await launchUrl(
url,
mode: LaunchMode
.externalApplication, // Ensures it opens in the browser
);
} else {
throw 'Could not launch $url';
}
}),
IconButton(
icon: Icon(FontAwesomeIcons.chartSimple),
onPressed: () async {
final url =
Uri.parse('https://kuma.mercurio.moe/status/dthpp');
if (await canLaunchUrl(url)) {
await launchUrl(
url,
mode: LaunchMode
.externalApplication, // Ensures it opens in the browser
);
} else {
throw 'Could not launch $url';
}
}),
],
),
],
),

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../globals.dart';
class CreateMatchPage extends StatefulWidget {
@override
@ -11,10 +12,11 @@ class CreateMatchPage extends StatefulWidget {
class _CreateMatchPageState extends State<CreateMatchPage> {
String? _matchId;
bool _isLoading = false;
bool _isTwoPlayerModeEnabled = false;
final String _createMatchApiUrl = 'http://api.dthpp.mercurio.moe/creatematch'; // Replace with your API endpoint
final String _createMatchApiUrl = '$apiurl/creatematch';
final String _createDoubleMatchUrl = '$apiurl/creatematch_2v2';
// Method to create a match
Future<void> _createMatch() async {
setState(() {
_isLoading = true;
@ -32,8 +34,10 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
}
try {
final String apiUrl =
_isTwoPlayerModeEnabled ? _createDoubleMatchUrl : _createMatchApiUrl;
final response = await http.post(
Uri.parse(_createMatchApiUrl),
Uri.parse(apiUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({'token': token}),
);
@ -41,7 +45,10 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_matchId = data['match_id'].toString();
_matchId = _isTwoPlayerModeEnabled
? data['match_id'].toString() +
'D' // Append "D" for two-player mode
: data['match_id'].toString();
});
_showToast('Match created successfully!');
} else {
@ -56,7 +63,6 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
}
}
// Show a Toast message (SnackBar in Flutter)
void _showToast(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
@ -66,9 +72,6 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Create Match'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
@ -88,6 +91,12 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'Due to current limitations in how we handle matchmaking, only the joining player can control the match. This is only a temporary solution to a problem we are actively fixing.',
style: TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator() // Show loading spinner
: ElevatedButton(
@ -95,6 +104,16 @@ class _CreateMatchPageState extends State<CreateMatchPage> {
child: Text('Create Match'),
),
SizedBox(height: 16),
SwitchListTile(
title: Text('Enable 2 player mode'),
value: _isTwoPlayerModeEnabled,
onChanged: (bool value) {
setState(() {
_isTwoPlayerModeEnabled = value;
});
},
),
SizedBox(height: 16),
if (_matchId != null)
Text(
'Your Match ID: $_matchId',

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../globals.dart';
class AddFriendPage extends StatefulWidget {
@override
@ -13,8 +14,8 @@ class _AddFriendPageState extends State<AddFriendPage> {
List<dynamic> _friends = [];
bool _isLoading = false;
final String _addFriendApiUrl = 'http://api.dthpp.mercurio.moe/add_friend';
final String _getFriendsApiUrl = 'http://api.dthpp.mercurio.moe/get_friends';
final String _addFriendApiUrl = '$apiurl/add_friend';
final String _getFriendsApiUrl = '$apiurl/get_friends';
// Method to add a friend
Future<void> _addFriend(String friendUid) async {

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../globals.dart';
class JoinMatchPage extends StatefulWidget {
@override
@ -12,14 +13,23 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
TextEditingController _matchIdController = TextEditingController();
bool _isJoined = false;
bool _isLoading = false;
bool _is2v2Mode = false;
int _selectedSlot = 2;
int _player1Score = 0;
int _player2Score = 0;
int _player3Score = 0;
int _player4Score = 0;
String? _matchId;
String? _player1name;
String? _player2name;
List<String> _players = [];
bool _canEdit = false;
final String _joinMatchApiUrl = 'http://api.dthpp.mercurio.moe/joinmatch'; // Replace with your API endpoint
final String _endMatchApiUrl = 'http://api.dthpp.mercurio.moe/endmatch'; // Replace with your API endpoint
final String _joinMatchApiUrl = '$apiurl/joinmatch';
final String _joinMatch2v2ApiUrl = '$apiurl/joinmatch_2v2';
final String _endMatchApiUrl = '$apiurl/endmatch';
final String _endFourApiUrl = '$apiurl/endfour';
// Join Match Function
Future<void> _joinMatch() async {
setState(() {
_isLoading = true;
@ -38,15 +48,52 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
}
try {
if (_is2v2Mode) {
// Joining a 2v2 match
final response = await http.post(
Uri.parse(_joinMatchApiUrl),
Uri.parse(_joinMatch2v2ApiUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({'token': token, 'match_id': int.parse(matchId)}),
body: json.encode({
'token': token,
'match_id': int.parse(matchId),
'slot': _selectedSlot,
}),
);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
setState(() {
_isJoined = true;
_matchId = matchId;
_players = List<String>.from(
responseData['players'].map((p) => p['name']));
_canEdit = responseData['canEdit'];
_player1Score = 0;
_player2Score = 0;
_player3Score = 0;
_player4Score = 0;
});
_showToast('Joined match successfully!');
} else {
_showToast('Failed to join 2v2 match.');
}
} else {
// Joining a 1v1 match
final response = await http.post(
Uri.parse(_joinMatchApiUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'token': token,
'match_id': int.parse(matchId),
}),
);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
setState(() {
_isJoined = true;
_player1name = responseData['player1_name'];
_player2name = responseData['player2_name'];
_player1Score = 0;
_player2Score = 0;
_matchId = matchId;
@ -55,6 +102,7 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
} else {
_showToast('Failed to join match.');
}
}
} catch (e) {
_showToast('Error: $e');
} finally {
@ -64,18 +112,6 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
}
}
// Increment/Decrement Player Scores
void _updateScore(int player, int delta) {
setState(() {
if (player == 1) {
_player1Score += delta;
} else if (player == 2) {
_player2Score += delta;
}
});
}
// End Match Function
Future<void> _endMatch() async {
setState(() {
_isLoading = true;
@ -93,6 +129,27 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
}
try {
if (_is2v2Mode) {
// End the 2v2 match using the /endfour API
final response = await http.post(
Uri.parse(_endFourApiUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'match_id': int.parse(_matchId!),
'player1_team1_score': _player1Score,
'player2_team1_score': _player2Score,
'player1_team2_score': _player3Score,
'player2_team2_score': _player4Score,
}),
);
if (response.statusCode == 200) {
_showToast('2v2 match ended successfully!');
} else {
_showToast('Failed to end 2v2 match.');
}
} else {
// End the 1v1 match
final response = await http.post(
Uri.parse(_endMatchApiUrl),
headers: {'Content-Type': 'application/json'},
@ -105,10 +162,10 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
if (response.statusCode == 200) {
_showToast('Match ended successfully!');
Navigator.pop(context);
} else {
_showToast('Failed to end match.');
}
}
} catch (e) {
_showToast('Error: $e');
} finally {
@ -127,61 +184,244 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Join Match'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? Center(child: CircularProgressIndicator())
: _isJoined
? Column(
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Display for 2v2 match
if (_is2v2Mode && _canEdit) ...[
Text('2v2 Match',
style:
TextStyle(color: Colors.white, fontSize: 24)),
SizedBox(height: 16),
// First Team
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Text(_players[0],
style: TextStyle(color: Colors.white)),
Row(
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () => _updateScore(1, -1),
),
Text(
'Player 1 Score: $_player1Score',
style: TextStyle(fontSize: 20),
),
icon: Icon(Icons.remove,
color: Colors.white),
onPressed: () => _updateScore(1, -1)),
Container(
width: 70,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2),
borderRadius:
BorderRadius.circular(8)),
child: Center(
child: Text('$_player1Score',
style: TextStyle(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add),
onPressed: () => _updateScore(1, 1),
icon: Icon(Icons.add,
color: Colors.white),
onPressed: () => _updateScore(1, 1)),
],
),
],
),
SizedBox(height: 16),
// Player 2 Score Controls
SizedBox(width: 20),
Column(
children: [
Text(_players[1],
style: TextStyle(color: Colors.white)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () => _updateScore(2, -1),
),
Text(
'Player 2 Score: $_player2Score',
style: TextStyle(fontSize: 20),
),
icon: Icon(Icons.remove,
color: Colors.white),
onPressed: () => _updateScore(2, -1)),
Container(
width: 70,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2),
borderRadius:
BorderRadius.circular(8)),
child: Center(
child: Text('$_player2Score',
style: TextStyle(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add),
onPressed: () => _updateScore(2, 1),
icon: Icon(Icons.add,
color: Colors.white),
onPressed: () => _updateScore(2, 1)),
],
),
],
),
],
),
SizedBox(height: 32),
// End Match Button
// Second Team
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Text(_players[2],
style: TextStyle(color: Colors.white)),
Row(
children: [
IconButton(
icon: Icon(Icons.remove,
color: Colors.white),
onPressed: () => _updateScore(3, -1)),
Container(
width: 70,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2),
borderRadius:
BorderRadius.circular(8)),
child: Center(
child: Text('$_player3Score',
style: TextStyle(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add,
color: Colors.white),
onPressed: () => _updateScore(3, 1)),
],
),
],
),
SizedBox(width: 20),
Column(
children: [
Text(_players[3],
style: TextStyle(color: Colors.white)),
Row(
children: [
IconButton(
icon: Icon(Icons.remove,
color: Colors.white),
onPressed: () => _updateScore(4, -1)),
Container(
width: 75,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2),
borderRadius:
BorderRadius.circular(8)),
child: Center(
child: Text('$_player4Score',
style: TextStyle(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add,
color: Colors.white),
onPressed: () => _updateScore(4, 1)),
],
),
],
),
],
),
],
// 1v1 Match UI
if (!_is2v2Mode) ...[
Text(_player1name ?? 'Player 1',
style: TextStyle(color: Colors.white)),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () => _updateScore(1, -1)),
Container(
width: 100,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(8)),
child: Center(
child: Text('$_player1Score',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add, color: Colors.white),
onPressed: () => _updateScore(1, 1)),
],
),
SizedBox(height: 8),
Divider(
color: Colors.white,
thickness: 2,
indent: 80,
endIndent: 80),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () => _updateScore(2, -1)),
Container(
width: 100,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(8)),
child: Center(
child: Text('$_player2Score',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24)))),
IconButton(
icon: Icon(Icons.add, color: Colors.white),
onPressed: () => _updateScore(2, 1)),
],
),
SizedBox(height: 8),
Text(_player2name ?? 'Player 2',
style: TextStyle(color: Colors.white)),
],
SizedBox(height: 32),
ElevatedButton(
onPressed: _endMatch,
child: Text('End Match'),
),
],
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -196,7 +436,34 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
),
),
SizedBox(height: 16),
// Join Match Button
// Toggle for 2v2 Mode
SwitchListTile(
title: Text('Enable 2v2 Mode'),
value: _is2v2Mode,
onChanged: (value) {
setState(() {
_is2v2Mode = value;
});
},
),
// If 2v2 is selected, display slot selection
if (_is2v2Mode) ...[
DropdownButton<int>(
value: _selectedSlot,
items: [2, 3, 4].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text('Slot $value'),
);
}).toList(),
onChanged: (int? newValue) {
setState(() {
_selectedSlot = newValue!;
});
},
),
],
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_matchIdController.text.isNotEmpty) {
@ -212,4 +479,18 @@ class _JoinMatchPageState extends State<JoinMatchPage> {
),
);
}
void _updateScore(int playerIndex, int increment) {
setState(() {
if (playerIndex == 1) {
_player1Score += increment;
} else if (playerIndex == 2) {
_player2Score += increment;
} else if (playerIndex == 3) {
_player3Score += increment;
} else if (playerIndex == 4) {
_player4Score += increment;
}
});
}
}

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../../globals.dart';
class LeaderboardPage extends StatefulWidget {
@override
@ -10,8 +11,9 @@ class LeaderboardPage extends StatefulWidget {
class _LeaderboardPageState extends State<LeaderboardPage> {
List<dynamic> _leaderboard = [];
bool _isLoading = true;
bool _sortByOskMu = false;
final String _apiUrl = 'http://api.dthpp.mercurio.moe/leaderboards';
final String _leaderboardApi = '$apiurl/leaderboards';
Future<void> _fetchLeaderboard() async {
setState(() {
@ -19,12 +21,13 @@ class _LeaderboardPageState extends State<LeaderboardPage> {
});
try {
final response = await http.get(Uri.parse(_apiUrl));
final response = await http.get(Uri.parse(_leaderboardApi));
if (response.statusCode == 200) {
List<dynamic> data = json.decode(response.body);
setState(() {
_leaderboard = data;
_sortLeaderboard();
_isLoading = false;
});
} else {
@ -41,6 +44,23 @@ class _LeaderboardPageState extends State<LeaderboardPage> {
}
}
void _sortLeaderboard() {
setState(() {
if (_sortByOskMu) {
_leaderboard.sort((a, b) => b['osk_mu'].compareTo(a['osk_mu']));
} else {
_leaderboard.sort((a, b) => b['elo_rating'].compareTo(a['elo_rating']));
}
});
}
void _toggleSort() {
setState(() {
_sortByOskMu = !_sortByOskMu;
_sortLeaderboard();
});
}
void _showError(String message) {
showDialog(
context: context,
@ -68,6 +88,16 @@ class _LeaderboardPageState extends State<LeaderboardPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Leaderboard'),
actions: [
IconButton(
icon: Icon(Icons.sort),
onPressed: _toggleSort,
tooltip: _sortByOskMu ? 'Sort by Elo' : 'Sort by Osk_Mu',
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
@ -76,16 +106,29 @@ class _LeaderboardPageState extends State<LeaderboardPage> {
itemCount: _leaderboard.length,
itemBuilder: (context, index) {
var player = _leaderboard[index];
String truncatedOskMu = player['osk_mu'].toStringAsFixed(3);
String assetName = _sortByOskMu
? 'assets/player_${index}_dp.png'
: 'assets/player_${index}.png';
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
contentPadding: EdgeInsets.all(10),
leading: CircleAvatar(
child: Text(player['player_name'][0].toUpperCase()),
child: Text(player['player_name'][0].toUpperCase())),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(player['player_name']),
if (index < 3)
Image.asset(assetName, width: 45, height: 45),
],
),
title: Text(player['player_name']),
subtitle: Text('Elo Rating: ${player['elo_rating']}'),
trailing: Text('Friend Code: ${player['friend_code']}'),
subtitle: Text(
'Elo: ${player['elo_rating']} | TSC: $truncatedOskMu'),
trailing: Text('UID: ${player['friend_code']}'),
),
);
},

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../globals.dart';
class ProfilePage extends StatefulWidget {
@override
@ -14,9 +15,11 @@ class _ProfilePageState extends State<ProfilePage> {
String? _name;
int? _uid;
int? _elo;
double? _mu;
double? _unc;
List<dynamic> _matches = [];
final String _getProfileApiUrl = 'http://api.dthpp.mercurio.moe/getprofile';
final String _getProfileApiUrl = '$apiurl/getprofile';
@override
void initState() {
@ -46,6 +49,8 @@ class _ProfilePageState extends State<ProfilePage> {
_name = data['name'];
_uid = data['uid'];
_elo = data['elo'];
_mu = data['osk_mu'];
_unc = data['osk_sig'];
_matches = data['matches'];
_isLoading = false;
});
@ -73,6 +78,87 @@ class _ProfilePageState extends State<ProfilePage> {
);
}
String getRankImage(int? elo) {
if (elo == null || elo < -100) return 'assets/none.png';
if (elo >= 120) return 'assets/infdan.png';
if (elo >= 90) return 'assets/SS.png';
if (elo >= 60) return 'assets/S.png';
if (elo >= 30) return 'assets/A.png';
if (elo >= 0) return 'assets/B.png';
if (elo >= -30) return 'assets/C.png';
if (elo >= -60) return 'assets/D.png';
return 'assets/E.png';
}
void _showExplanationDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('ELO, OSK, and UNC Explanation'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
// ELO Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"ELO (Elo Rating):",
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 4.0),
Text(
"ELO is a widely-used rating system designed to measure the relative skill levels of players in two-player games. It was my initial pick for a testing environment since I only really thought about 1v1 matches, and because it had a readily available Python implementation. I'm lazy.",
),
SizedBox(height: 8.0),
],
),
// OSK Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"OSK (OpenSkill Mu):",
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 4.0),
Text(
"OSKmu is a skill rating system based on the OpenSkill model, which is a probabilistic framework for estimating a player's skill. Unlike ELO, which is purely a point-based system, OpenSkill Mu takes into account not just the outcome of matches but also the degree of uncertainty in a player's skill estimation. Since I set up the system to have a 0-base-elo, I had to adapt the OpenSkill implementation to a standard 25 OSK and 8.33 uncertainty.",
),
SizedBox(height: 8.0),
],
),
// UNC Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Uncertainty (UNC):",
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 4.0),
Text(
"This is a measure of how confident the system is about a player's skill rating. A higher uncertainty value means the system is less confident about the accuracy of the player's skill estimation, while a lower uncertainty indicates more confidence in the player's rating.",
),
],
),
],
),
),
actions: <Widget>[
TextButton(
child: Text('Close'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -87,7 +173,6 @@ class _ProfilePageState extends State<ProfilePage> {
padding: EdgeInsets.all(16.0),
child: Row(
children: [
// Profile Icon
CircleAvatar(
backgroundColor: _generateRandomColor(),
child: Text(
@ -99,7 +184,6 @@ class _ProfilePageState extends State<ProfilePage> {
radius: 40,
),
SizedBox(width: 16),
// Profile Info
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -110,15 +194,36 @@ class _ProfilePageState extends State<ProfilePage> {
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
SizedBox(height: 6),
Row(
children: [
Text('UID: ${_uid ?? 'N/A'}'),
Text('ELO: ${_elo ?? 'N/A'}'),
SizedBox(
width: 30,
),
Image.asset(
getRankImage(_elo),
width: 100,
height: 30,
),
],
),
Row(
children: [
Text(
'ELO: ${_elo ?? 'N/A'} | OSK: ${_mu != null ? _mu?.toStringAsFixed(3) : 'N/A'} | UNC: ${_unc != null ? _unc?.toStringAsFixed(3) : 'N/A'}'),
IconButton(
icon: Icon(Icons.help_outline),
onPressed: _showExplanationDialog,
tooltip: 'What are ELO, OSK, and UNC?',
),
],
),
],
),
],
),
),
SizedBox(height: 16),
// Recent Matches
Expanded(
child: _matches.isEmpty
@ -144,7 +249,8 @@ class _ProfilePageState extends State<ProfilePage> {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Match ID: ${match['match_id']}',
@ -153,7 +259,8 @@ class _ProfilePageState extends State<ProfilePage> {
fontWeight: FontWeight.bold,
),
),
Text('Opponent: ${match['opponent_name']}'),
Text(
'Opponent: ${match['opponent_name']}'),
Row(
children: [
Text(

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -5,8 +5,10 @@
import FlutterMacOS
import Foundation
import package_info
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

43
macos/Podfile Normal file
View file

@ -0,0 +1,43 @@
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View file

@ -1,8 +1,8 @@
name: pingpongapp
description: "A new Flutter project."
description: "DTH Ping Pong Score tracking app"
publish_to: 'none'
version: 0.0.32+1
version: 0.0.57+1
environment:
sdk: '>=3.4.3 <4.0.0'
@ -14,6 +14,10 @@ dependencies:
shared_preferences: ^2.3.3
http: ^1.2.2
logger: ^2.5.0
package_info: ^2.0.2
font_awesome_flutter: ^10.8.0
url_launcher: ^6.3.1
fl_chart: ^0.70.2
dev_dependencies:
flutter_test:
@ -23,9 +27,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware