first commit

This commit is contained in:
abilhabibillah 2025-07-18 14:12:39 +07:00
commit bff1688ae2
181 changed files with 10182 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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

45
.metadata Normal file
View File

@ -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: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: linux
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: macos
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: windows
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# 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'

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# ta_running
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,46 @@
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")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.ta_running"
compileSdk = 34
ndkVersion = "27.2.12479018"
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.ta_running"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
targetSdk = 34
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 = "../.."
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "888886225940",
"project_id": "ta-running",
"storage_bucket": "ta-running.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:888886225940:android:c49c02701a3ce79804cb01",
"android_client_info": {
"package_name": "com.example.ta_running"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDcIQatv2KWTh96025wlFNEbH3xH2MK88k"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Running"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.ta_running
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#FFFFFF">
</shape>
</item>
<item>
<bitmap android:gravity="fill" android:src="@drawable/run"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

29
android/build.gradle.kts Normal file
View File

@ -0,0 +1,29 @@
plugins {
// ...
// Add the dependency for the Google services Gradle plugin
id("com.google.gms.google-services") version "4.3.15" apply false
}
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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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.10.2-all.zip

View File

@ -0,0 +1,28 @@
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.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

1
firebase.json Normal file
View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"ta-running","appId":"1:888886225940:android:c49c02701a3ce79804cb01","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"ta-running","configurations":{"android":"1:888886225940:android:c49c02701a3ce79804cb01"}}}}}}

BIN
images/run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; 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>"; };
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>"; };
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; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
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>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 = 12.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.taRunning;
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.taRunning.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.taRunning.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.taRunning.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 = AppIcon;
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 = 12.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 = AppIcon;
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 = 12.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.taRunning;
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.taRunning;
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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
{"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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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.

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="512" height="512"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

51
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Ta Running</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ta_running</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIStatusBarHidden</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -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.
}
}

1
lib/app/routes.dart Normal file
View File

@ -0,0 +1 @@
// TODO Implement this library.

62
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,62 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for ios - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDcIQatv2KWTh96025wlFNEbH3xH2MK88k',
appId: '1:888886225940:android:c49c02701a3ce79804cb01',
messagingSenderId: '888886225940',
projectId: 'ta-running',
storageBucket: 'ta-running.firebasestorage.app',
);
}

33
lib/main.dart Normal file
View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:ta_running/screens/splash_screen.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await initializeDateFormatting('id_ID', null); // locale Indonesia
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('id', ''), // Bahasa Indonesia
Locale('en', ''), // Bahasa Inggris (default backup)
],
home: const SplashScreen(),
);
}
}

10
lib/routes.dart Normal file
View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'screens/splash_screen.dart';
import 'screens/auth/login_screen.dart';
import 'screens/auth/register_screen.dart';
final Map<String, WidgetBuilder> appRoutes = {
'/': (context) => SplashScreen(),
'/login': (context) => LoginScreen(),
'/register': (context) => RegisterScreen(),
};

View File

@ -0,0 +1,135 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class AllActivityScreen extends StatelessWidget {
const AllActivityScreen({super.key});
Future<Map<String, Map<String, double>>> fetchWeeklySummary() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return {};
final snapshot = await FirebaseFirestore.instance.collection('activities').get();
final Map<String, Map<String, double>> weeklyData = {};
final allDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
for (var doc in snapshot.docs) {
final data = doc.data()['data'];
if (data == null || data['userId'] != user.uid) continue;
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
if (timestamp == null) continue;
final weekStart = DateTime(timestamp.year, timestamp.month, timestamp.day)
.subtract(Duration(days: timestamp.weekday - 1));
final weekKey = DateFormat('dd MMM yyyy').format(weekStart);
final hari = DateFormat('EEEE', 'id_ID').format(timestamp);
final jarak = (data['jarak'] ?? 0).toDouble();
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'M' ? jarak / 1000.0 : jarak;
weeklyData.putIfAbsent(weekKey, () => {
for (var day in allDays) day: 0.0
});
weeklyData[weekKey]![hari] = (weeklyData[weekKey]![hari] ?? 0) + jarakKm;
}
return weeklyData;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("View All Weeks", style: TextStyle(color: Colors.white),),
automaticallyImplyLeading: true,
backgroundColor: Colors.blueAccent,
iconTheme: const IconThemeData(color: Colors.white),
),
body: FutureBuilder<Map<String, Map<String, double>>>(
future: fetchWeeklySummary(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.data ?? {};
if (data.isEmpty) {
return const Center(child: Text("Belum ada data aktivitas lari."));
}
final sortedWeeks = data.keys.toList()
..sort((a, b) => DateFormat('dd MMM yyyy').parse(b).compareTo(DateFormat('dd MMM yyyy').parse(a)));
return ListView.builder(
itemCount: sortedWeeks.length,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
itemBuilder: (context, index) {
final week = sortedWeeks[index];
final daysData = data[week]!;
final totalKm = daysData.values.fold(0.0, (a, b) => a + b);
return Card(
color: Colors.white,
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.calendar_today, size: 20, color: Colors.blue),
const SizedBox(width: 8),
Text(
'Minggu dimulai: $week',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 12),
Column(
children: daysData.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key, style: const TextStyle(fontSize: 14)),
Text('${entry.value.toStringAsFixed(2)} KM',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
],
),
);
}).toList(),
),
const Divider(height: 24, thickness: 1),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Icon(Icons.directions_run, size: 18, color: Colors.green),
const SizedBox(width: 6),
Text(
'Total: ${totalKm.toStringAsFixed(2)} KM',
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
],
),
),
);
},
);
},
),
);
}
}

View File

@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:ta_running/screens/main_screen.dart'; // tetap sama
import 'register_screen.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:ta_running/screens/super_user_dashboard.dart'; // pastikan path sesuai
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
Future<void> _login() async {
if (_formKey.currentState!.validate()) {
try {
final userCredential = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
final user = userCredential.user;
if (user != null) {
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
final role = doc.data()?['role'] ?? 'runner';
if (role == 'superuser') {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => SuperUserDashboard()),
(route) => false,
);
} else {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainScreen()),
(route) => false,
);
}
}
} on FirebaseAuthException catch (e) {
String message = '';
switch (e.code) {
case 'user-not-found':
message = 'Email tidak ditemukan. Silakan daftar terlebih dahulu.';
break;
case 'wrong-password':
message = 'Password salah. Silakan coba lagi.';
break;
case 'invalid-email':
message = 'Format email tidak valid.';
break;
case 'user-disabled':
message = 'Akun ini telah dinonaktifkan.';
break;
default:
message = 'Terjadi kesalahan saat login.';
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Login gagal: ${e.toString()}")));
}
}
}
@override
Widget build(BuildContext context) {
final inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blueAccent),
);
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: Container(
decoration: const BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 40), // untuk push tulisan agak ke bawah
child: const Center(
child: Text(
"Login",
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Form(
key: _formKey,
child: Column(
children: [
// Logo di atas
Image.asset(
'images/run.png',
width: 95,
height: 95,
fit: BoxFit.contain,
),
const SizedBox(height: 19),
// Judul
Text(
'Running',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 32),
// Email input
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: "Email",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.email, color: Colors.blueAccent),
),
keyboardType: TextInputType.emailAddress,
validator: (value) => value == null || value.isEmpty ? "Masukkan email" : null,
),
const SizedBox(height: 20),
// Password input
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Password",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.lock, color: Colors.blueAccent),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off, color: Colors.blueAccent),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: (value) => value == null || value.isEmpty ? "Masukkan password" : null,
),
const SizedBox(height: 36),
// Login Button full width
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 5,
),
child: const Text(
"Login",
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 12),
// Link ke Register
TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RegisterScreen()),
),
child: RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'Belum punya akun? ',
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: 'Register',
style: const TextStyle(
fontSize: 16,
color: Colors.blueAccent,
fontWeight: FontWeight.w600,
),
),
],
),
),
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:ta_running/screens/auth/login_screen.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
String? _selectedGender;
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
Future<void> _register() async {
if (_formKey.currentState!.validate()) {
try {
final userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
// Simpan data tambahan ke Firestore
await FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid).set({
'username': _usernameController.text.trim(),
'gender': _selectedGender,
'email': _emailController.text.trim(),
'role': 'runner',
});
// Arahkan ke login setelah berhasil
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false,
);
} on FirebaseAuthException catch (e) {
String message = '';
if (e.code == 'email-already-in-use') {
message = 'Email sudah digunakan.';
} else {
message = 'Registrasi gagal: ${e.message}';
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Registrasi gagal: ${e.toString()}")));
}
}
}
@override
Widget build(BuildContext context) {
final inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blueAccent),
);
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: Container(
decoration: const BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 40), // untuk push tulisan agak ke bawah
child: const Center(
child: Text(
"Register",
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Form(
key: _formKey,
child: Column(
children: [
// Logo di atas
Image.asset(
'images/run.png',
width: 95,
height: 95,
fit: BoxFit.contain,
),
const SizedBox(height: 19),
// Judul
Text(
'Daftar Akun',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 32),
// Username
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: "Username",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
),
validator: (value) => value == null || value.isEmpty ? "Masukkan username" : null,
),
const SizedBox(height: 20),
// Gender Dropdown
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: "Gender",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.wc, color: Colors.blueAccent),
),
value: _selectedGender,
items: const [
DropdownMenuItem(value: 'Laki-laki', child: Text('Laki-laki')),
DropdownMenuItem(value: 'Perempuan', child: Text('Perempuan')),
],
onChanged: (value) => setState(() => _selectedGender = value),
validator: (value) => value == null ? "Pilih gender" : null,
),
const SizedBox(height: 20),
// Email
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: "Email",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.email, color: Colors.blueAccent),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return "Masukkan email";
final emailRegex = RegExp(r"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$");
if (!emailRegex.hasMatch(value)) return "Format email tidak valid";
return null;
},
),
const SizedBox(height: 20),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Password",
border: inputBorder,
focusedBorder: inputBorder.copyWith(borderSide: const BorderSide(color: Colors.blueAccent, width: 2)),
prefixIcon: const Icon(Icons.lock, color: Colors.blueAccent),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off, color: Colors.blueAccent),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: (value) {
if (value == null || value.isEmpty) return "Masukkan password";
if (!RegExp(r'^(?=.*[a-zA-Z])(?=.*\d).{6,}$').hasMatch(value)) {
return "Password harus kombinasi huruf & angka";
}
return null;
},
),
const SizedBox(height: 36),
// Register Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _register,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 5,
),
child: const Text(
"Daftar",
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 12),
// Link Login
TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
),
child: RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'Sudah punya akun? ',
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: 'Login',
style: const TextStyle(
fontSize: 16,
color: Colors.blueAccent,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,204 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; // Untuk mendapatkan email user yang login
class CreateEventPage extends StatefulWidget {
@override
_CreateEventPageState createState() => _CreateEventPageState();
}
class _CreateEventPageState extends State<CreateEventPage> {
final _nameController = TextEditingController();
final _lapController = TextEditingController();
final _formKey = GlobalKey<FormState>(); // Tambahkan FormKey
static const int MIN_LAPS = 3; // Konstanta untuk minimal putaran
// Warna utama yang terinspirasi dari gambar Anda
final Color primaryBlue = Color(0xFF42A5F5); // Biru muda yang cerah
final Color lightBlue = Color(0xFFBBDEFB); // Biru yang lebih terang untuk background
final Color darkBlue = Color(0xFF1976D2); // Biru yang lebih gelap untuk teks/ikon
void _createEvent() async {
// Validasi form secara keseluruhan
if (!_formKey.currentState!.validate()) {
return; // Hentikan jika ada error validasi
}
final eventName = _nameController.text.trim();
final totalLaps = int.parse(_lapController.text.trim()); // Sudah dipastikan valid oleh validator
// Validasi tambahan untuk minimal putaran
if (totalLaps < MIN_LAPS) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Jumlah Putaran Tidak Valid', style: TextStyle(color: darkBlue)),
content: Text('Minimal jumlah putaran adalah $MIN_LAPS.', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK', style: TextStyle(color: primaryBlue)),
),
],
),
);
return; // Hentikan proses jika putaran kurang dari minimal
}
final totalDistance = totalLaps * 400; // 1 putaran = 400 meter
String createdBy = 'unknown_user@example.com'; // Default value
User? currentUser = FirebaseAuth.instance.currentUser;
if (currentUser != null && currentUser.email != null) {
createdBy = currentUser.email!;
} else {
print("Peringatan: Email user tidak ditemukan. Menggunakan email default.");
}
try {
await FirebaseFirestore.instance.collection('events').add({
'name': eventName,
'created_at': Timestamp.now(),
'created_by': createdBy,
'start_status': false,
'participants': {},
'total_laps': totalLaps,
'total_distance': totalDistance,
'type': 'track',
'is_started_permanently': false, // Tambahkan field ini
'is_finished_permanently': false, // Tambahkan field ini
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Event "$eventName" berhasil dibuat ($totalLaps putaran = ${totalDistance / 1000} KM).'),
backgroundColor: Colors.green, // Warna hijau untuk sukses
));
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Gagal membuat event: $e'),
backgroundColor: Colors.red, // Warna merah untuk error
));
print('Error creating event: $e');
}
}
@override
void dispose() {
_nameController.dispose();
_lapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50], // Background lembut
appBar: AppBar(
title: Text(
'Buat Event Baru',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: primaryBlue, // Warna AppBar
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20), // Sudut melengkung di kiri bawah
bottomRight: Radius.circular(20), // Sudut melengkung di kanan bawah
),
),
iconTheme: IconThemeData(color: Colors.white), // Warna ikon back
elevation: 5, // Tambahkan sedikit bayangan
),
body: SingleChildScrollView( // Agar bisa di-scroll jika keyboard muncul
child: Padding(
padding: EdgeInsets.all(24), // Padding lebih besar
child: Form( // Wrap dengan Form widget
key: _formKey, // Pasang GlobalKey
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // Agar elemen mengisi lebar
children: [
Center( // Wrap dengan Center agar gambar di tengah
child: Image.asset(
'images/run.png', // Path ke gambar logo Anda
height: 80, // Sesuaikan tinggi gambar
width: 80, // Sesuaikan lebar gambar
// Anda bisa menambahkan fit: BoxFit.contain, jika perlu
),
),
SizedBox(height: 24),
TextFormField( // Ganti TextField dengan TextFormField
controller: _nameController,
decoration: InputDecoration(
labelText: 'Nama Event',
hintText: 'Misal: Jember Marathon',
border: OutlineInputBorder( // Border melengkung
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder( // Warna border saat fokus
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryBlue, width: 2),
),
// prefixIcon: Icon(Icons.event, color: primaryBlue), // Ikon
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama event tidak boleh kosong';
}
return null;
},
),
SizedBox(height: 20), // Spasi antar input
TextFormField( // Ganti TextField dengan TextFormField
controller: _lapController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Jumlah Putaran (lap)',
hintText: 'Minimal 3 putaran',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryBlue, width: 2),
),
// prefixIcon: Icon(Icons.sports_score, color: primaryBlue), // Ikon
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah putaran tidak boleh kosong';
}
final n = int.tryParse(value);
if (n == null || n <= 0) {
return 'Masukkan angka lebih dari 0';
}
return null;
},
),
SizedBox(height: 32), // Spasi sebelum tombol
ElevatedButton.icon(
onPressed: _createEvent,
// icon: Icon(Icons.add_circle, color: Colors.white),
label: Text(
'Buat Event',
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: primaryBlue, // Warna tombol biru
padding: EdgeInsets.symmetric(vertical: 16), // Padding lebih besar
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // Sudut melengkung
),
elevation: 5, // Bayangan tombol
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,429 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class DetailHasilLariScreen extends StatefulWidget {
final Map<String, dynamic> data;
const DetailHasilLariScreen({super.key, required this.data});
@override
State<DetailHasilLariScreen> createState() => _DetailHasilLariScreenState();
}
class _DetailHasilLariScreenState extends State<DetailHasilLariScreen> {
List<double> kecepatanList = [];
bool loading = true;
@override
void initState() {
super.initState();
fetchLapData();
}
Future<void> fetchLapData() async {
final jenis = widget.data['type'];
// =====================
// NON-LINTASAN
// =====================
if (jenis == 'non-lintasan') {
final duration = widget.data['duration'];
final rawJarak = widget.data['jarak'] ?? widget.data['distance'] ?? 0.0;
final satuan = widget.data['satuan'] ?? 'M';
final jarakKm = satuan == 'KM' ? rawJarak.toDouble() : rawJarak.toDouble() / 1000.0;
if (duration is int && jarakKm > 0) {
final durSec = duration / 1000;
final pacePerKmSec = durSec / jarakKm;
// Gunakan floor agar hanya simulasikan per-km (jangan 1.5 jadi 2)
List<double> tempPaces = List.generate(jarakKm.floor(), (_) => pacePerKmSec);
widget.data['pace_sec_per_km'] = pacePerKmSec;
setState(() {
kecepatanList = tempPaces;
loading = false;
});
return;
}
}
// =====================
// LINTASAN
// =====================
final docId = widget.data['docId'];
final totalLaps = widget.data['putaran'];
if (docId == null || totalLaps == null) {
setState(() => loading = false);
return;
}
final doc = await FirebaseFirestore.instance.collection('activities').doc(docId).get();
final laps = doc['data']['laps'];
if (laps == null || laps is! Map) {
setState(() => loading = false);
return;
}
final rawStart = widget.data['startTime'];
DateTime? startTime;
if (rawStart is Timestamp) {
startTime = rawStart.toDate();
} else if (rawStart is int) {
startTime = DateTime.fromMillisecondsSinceEpoch(rawStart);
}
final lapZero = laps['lap_0'];
DateTime? prevTime;
if (lapZero != null && lapZero['timestamp'] != null) {
prevTime = DateTime.parse(lapZero['timestamp']);
} else if (startTime != null) {
prevTime = startTime;
}
List<double> tempSpeeds = [];
final jarak = widget.data['jarak'] ?? 0;
final jarakPerLap = jarak / totalLaps;
for (int i = 1; i <= totalLaps; i++) {
final lapData = laps['lap_$i'];
if (lapData == null || lapData['timestamp'] == null) continue;
final timestamp = DateTime.parse(lapData['timestamp']);
if (prevTime != null) {
final durationMs = timestamp.difference(prevTime).inMilliseconds;
if (durationMs <= 0) {
tempSpeeds.add(0.0);
} else {
final speed = jarakPerLap / (durationMs / 1000);
tempSpeeds.add(speed);
}
}
prevTime = timestamp;
}
setState(() {
kecepatanList = tempSpeeds;
loading = false;
});
}
void _confirmDelete(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Hapus Aktivitas?'),
content: const Text('Apakah kamu yakin ingin menghapus aktivitas ini?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Batal'),
),
TextButton(
onPressed: () async {
Navigator.pop(ctx);
await _deleteActivity(context);
},
child: const Text('Hapus', style: TextStyle(color: Colors.red)),
),
],
),
);
}
Future<void> _deleteActivity(BuildContext context) async {
try {
final docId = widget.data['docId'];
if (docId != null) {
await FirebaseFirestore.instance.collection('activities').doc(docId).delete();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aktivitas berhasil dihapus')),
);
Navigator.pop(context);
} else {
throw Exception('ID dokumen tidak ditemukan');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menghapus: $e')),
);
}
}
String getRunPeriodLabel(DateTime time) {
final hour = time.hour;
if (hour >= 6 && hour <= 10) {
return 'Berlari Pagi';
} else if (hour >= 11 && hour <= 14) {
return 'Berlari Siang';
} else if (hour >= 15 && hour <= 17) {
return 'Berlari Sore';
} else {
return 'Berlari Malam';
}
}
@override
Widget build(BuildContext context) {
final data = widget.data;
final timestamp = (data['timestamp'] as Timestamp).toDate();
final jenis = data['type'] == 'lintasan' ? 'Lintasan' : 'Non-Lintasan';
final duration = data['duration'];
DateTime? startTime;
final rawStart = data['startTime'];
if (rawStart is Timestamp) {
startTime = rawStart.toDate();
} else if (rawStart is int) {
startTime = DateTime.fromMillisecondsSinceEpoch(rawStart);
}
final formattedStart = startTime != null ? DateFormat('HH:mm:ss').format(startTime) : '-';
String durationFormatted = '-';
if (duration is int) {
final d = Duration(milliseconds: duration);
durationFormatted = d.toString().split('.').first.padLeft(8, "0");
}
String? paceFormatted;
if (data['type'] == 'non-lintasan' && data['pace_sec_per_km'] != null) {
final paceSec = (data['pace_sec_per_km'] as num).toDouble();
final paceMin = paceSec ~/ 60;
final paceSecRem = (paceSec % 60).round();
paceFormatted = '$paceMin:${paceSecRem.toString().padLeft(2, '0')} /km';
}
final rawJarak = data['jarak'] ?? data['distance'] ?? 0.0;
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'KM' ? rawJarak.toDouble() : rawJarak.toDouble() / 1000.0;
final putaran = data['putaran'];
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: Container(
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
const Text(
"Detail Hasil Lari",
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
tooltip: 'Hapus Aktivitas',
onPressed: () => _confirmDelete(context),
),
],
),
),
),
backgroundColor: Colors.white,
body: Padding(
padding: const EdgeInsets.only(top: 30, left: 20, right: 20, bottom: 16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade300, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 4),
blurRadius: 8,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
getRunPeriodLabel(timestamp),
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
Text(
DateFormat('dd MMMM yyyy, HH:mm').format(timestamp),
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// KIRI
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
durationFormatted,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Durasi', style: TextStyle(color: Colors.white70)),
if (data['type'] == 'non-lintasan' && paceFormatted != null) ...[
const SizedBox(height: 12),
Text(
paceFormatted!,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Pace', style: TextStyle(color: Colors.white70)),
],
if (data['type'] == 'lintasan') ...[
const SizedBox(height: 12),
Text(
'$putaran',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Putaran', style: TextStyle(color: Colors.white70)),
],
],
),
// KANAN
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${jarakKm.toStringAsFixed(2)} KM',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Jarak', style: TextStyle(color: Colors.white70)),
const SizedBox(height: 12),
Text(
jenis,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Jenis Aktivitas', style: TextStyle(color: Colors.white70)),
],
),
],
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 32),
if (data['type'] == 'lintasan') ...[
const SizedBox(height: 32),
const Text("Splits Kecepatan", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
loading
? const Center(child: CircularProgressIndicator())
: kecepatanList.isEmpty
? const Text("Data kecepatan tidak tersedia.")
: Column(
children: List.generate(kecepatanList.length, (index) {
final speed = kecepatanList[index];
final maxSpeed = kecepatanList.reduce((a, b) => a > b ? a : b);
final barFraction = (speed / maxSpeed).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
children: [
SizedBox(width: 40, child: Text("Lap ${index + 1}", style: const TextStyle(fontSize: 14))),
const SizedBox(width: 8),
Expanded(
child: Stack(
children: [
Container(
height: 20,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: barFraction,
child: Container(
height: 20,
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
),
const SizedBox(width: 8),
Text("${speed.toStringAsFixed(2)} m/s", style: const TextStyle(fontSize: 14)),
],
),
);
}),
),
],
],
),
),
),
);
}
}

View File

@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({super.key});
@override
State<EditProfileScreen> createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final TextEditingController usernameController = TextEditingController();
String? selectedGender;
bool isLoading = true;
@override
void initState() {
super.initState();
fetchUserData();
}
Future<void> fetchUserData() async {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
final data = doc.data();
if (data != null) {
usernameController.text = data['username'] ?? '';
selectedGender = data['gender'] ?? '';
}
}
setState(() {
isLoading = false;
});
}
Future<void> saveProfile() async {
final username = usernameController.text.trim();
if (username.isEmpty || username.length < 3) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Peringatan"),
content: const Text("Username minimal 3 karakter"),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.pop(context),
),
],
),
);
return;
}
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
await FirebaseFirestore.instance.collection('users').doc(user.uid).update({
'username': username,
'gender': selectedGender,
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil berhasil diperbarui!'),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(50),
duration: Duration(seconds: 2),
),
);
Navigator.pop(context, true);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Edit profile', style: TextStyle(color: Colors.white),),
automaticallyImplyLeading: true,
backgroundColor: Colors.blueAccent,
iconTheme: const IconThemeData(color: Colors.white),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const SizedBox(height: 20),
Card(
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: usernameController,
decoration: const InputDecoration(labelText: "Username"),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedGender,
items: const [
DropdownMenuItem(value: 'Laki-laki', child: Text('Laki-laki')),
DropdownMenuItem(value: 'Perempuan', child: Text('Perempuan')),
],
onChanged: (value) {
setState(() {
selectedGender = value;
});
},
decoration: const InputDecoration(labelText: "Jenis Kelamin"),
),
],
),
),
),
const Spacer(),
ElevatedButton.icon(
onPressed: saveProfile,
icon: const Icon(Icons.save),
label: const Text("Simpan Perubahan"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,816 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class EventDetailPage extends StatefulWidget {
final String eventId;
final String eventName;
EventDetailPage({required this.eventId, required this.eventName});
@override
_EventDetailPageState createState() => _EventDetailPageState();
}
class _EventDetailPageState extends State<EventDetailPage> {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
late FirebaseDatabase _rtdb;
late DatabaseReference _eventStatusRef;
late DatabaseReference _scanResultRef;
late DatabaseReference _eventActivityDataRef;
// Map untuk menyimpan status finished dan current laps DARI RTDB (Hanya relevan saat event aktif)
Map<String, bool> _liveParticipantFinishedStatus = {};
Map<String, int> _liveParticipantCurrentLaps = {};
// Map untuk mapping UID RFID ke Firestore userId
Map<String, String> _rfidUidToUserIdMap = {};
// List untuk menyimpan subscriptions agar bisa di-cancel saat dispose
final List<StreamSubscription> _rtdbSubscriptions = [];
// Variabel untuk menyimpan status event global dari RTDB (hanya untuk kontrol internal sementara)
bool _rtdbEventStarted = false;
bool _rtdbEventFinished = false;
// StreamSubscription untuk Realtime Database listeners
StreamSubscription? _activityDataSubscription;
StreamSubscription? _finishedSubscription;
StreamSubscription? _lapsSubscription;
StreamSubscription? _scanResultSubscription;
// Warna utama yang terinspirasi dari gambar Anda
final Color primaryBlue = Color(0xFF42A5F5); // Biru muda yang cerah
final Color lightBlue = Color(0xFFBBDEFB); // Biru yang lebih terang untuk background
final Color darkBlue = Color(0xFF1976D2); // Biru yang lebih gelap untuk teks/ikon
@override
void initState() {
super.initState();
_rtdb = FirebaseDatabase.instanceFor(
app: Firebase.app(),
databaseURL: "https://ta-running-default-rtdb.asia-southeast1.firebasedatabase.app",
);
_eventStatusRef = _rtdb.ref('event_status/${widget.eventId}');
_scanResultRef = _rtdb.ref('scan_result/${widget.eventId}');
_eventActivityDataRef = _rtdb.ref('event_activity_data/${widget.eventId}');
_preloadRfidToUserIdsMap().then((_) {
_listenToRealtimeEventStatus(); // Mendengarkan status global dari RTDB (untuk internal saja)
});
}
@override
void dispose() {
for (var subscription in _rtdbSubscriptions) {
subscription.cancel();
}
super.dispose();
}
Future<void> _preloadRfidToUserIdsMap() async {
try {
final eventDoc = await _firestore.collection('events').doc(widget.eventId).get();
if (eventDoc.exists) {
final participantsData = eventDoc.data()?['participants'] as Map<String, dynamic>?;
if (participantsData != null) {
participantsData.forEach((userId, data) {
final rfidUid = data['rfid_uid'] as String?;
if (rfidUid != null) {
_rfidUidToUserIdMap[rfidUid] = userId;
}
});
print("Preloaded RFID UID to User ID Map: $_rfidUidToUserIdMap");
}
}
} catch (e) {
print("Error preloading RFID UID to User ID map: $e");
}
}
// Listener Realtime Database untuk status event global (hanya untuk tujuan internal)
void _listenToRealtimeEventStatus() {
_rtdbSubscriptions.add(_eventStatusRef.onValue.listen((event) async {
if (event.snapshot.exists && event.snapshot.value != null) {
final eventStatusData = event.snapshot.value as Map<dynamic, dynamic>;
setState(() {
_rtdbEventStarted = eventStatusData['started'] ?? false;
_rtdbEventFinished = eventStatusData['finished'] ?? false;
});
// Ambil status permanen dari Firestore untuk perbandingan
final firestoreDoc = await _firestore.collection('events').doc(widget.eventId).get();
final firestoreData = firestoreDoc.data();
bool isEventFinishedPermanently = firestoreData?['is_finished_permanently'] ?? false;
// Jika event baru saja selesai di RTDB oleh ESP32, dan belum di Firestore
if (_rtdbEventFinished && !isEventFinishedPermanently) {
_endEventPermanentlyFromRTDB();
}
} else {
setState(() {
_rtdbEventStarted = false;
_rtdbEventFinished = false;
});
}
print("Internal RTDB Event Status: Started=$_rtdbEventStarted, Finished=$_rtdbEventFinished");
}));
}
// Fungsi baru: jika RTDB menandakan selesai, dan Firestore belum, akhiri permanen
Future<void> _endEventPermanentlyFromRTDB() async {
print("RTDB menandakan event selesai, mengakhiri secara permanen di Firestore.");
try {
await _firestore.collection('events').doc(widget.eventId).update({
'is_started_permanently': false,
'is_finished_permanently': true,
});
// Bersihkan RTDB setelah status permanen di Firestore diupdate
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
print("✅ Event diakhiri secara permanen di Firestore dan RTDB dibersihkan.");
} catch (e) {
print("❌ Gagal mengakhiri event secara permanen dari RTDB: $e");
}
}
// Fungsi untuk mengelola listener Realtime Database
void _manageRealtimeListeners(bool activate) {
if (activate) {
// Aktifkan listener aktivitas peserta jika belum ada
if (_activityDataSubscription == null) {
print("Mengaktifkan Realtime Database activity listeners...");
// Gunakan onChildAdded untuk inisialisasi awal dan onChildChanged untuk update
_activityDataSubscription = _eventActivityDataRef.onChildAdded.listen((event) {
final uidTag = event.snapshot.key;
if (uidTag != null) {
// Subscribe ke 'finished'
_finishedSubscription = _eventActivityDataRef.child(uidTag).child('finished').onValue.listen((finishedEvent) async {
if (finishedEvent.snapshot.exists && finishedEvent.snapshot.value != null) {
final finished = finishedEvent.snapshot.value as bool;
setState(() {
_liveParticipantFinishedStatus[uidTag] = finished;
});
final userId = _rfidUidToUserIdMap[uidTag];
if (userId != null) {
try {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.finished': finished,
});
if (finished) {
final totalTimeSnapshot = await _eventActivityDataRef.child(uidTag).child('duration').get();
if (totalTimeSnapshot.exists && totalTimeSnapshot.value != null) {
final totalTimeMs = totalTimeSnapshot.value as int;
String formattedTime = _formatDuration(Duration(milliseconds: totalTimeMs));
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.total_time': formattedTime,
});
}
}
} catch (e) {
print("❌ Gagal update Firestore participants.$userId.finished/total_time: $e");
}
}
}
});
_rtdbSubscriptions.add(_finishedSubscription!);
// Subscribe ke 'laps'
_lapsSubscription = _eventActivityDataRef.child(uidTag).child('laps').onValue.listen((lapsEvent) async {
if (lapsEvent.snapshot.exists && lapsEvent.snapshot.value != null) {
final lapsData = lapsEvent.snapshot.value as Map<dynamic, dynamic>;
final lapsCount = lapsData.length;
setState(() {
_liveParticipantCurrentLaps[uidTag] = lapsCount;
});
final userId = _rfidUidToUserIdMap[uidTag];
if (userId != null) {
try {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.laps': lapsCount > 0 ? lapsCount - 1 : 0,
});
Map<String, DateTime> currentTimestamps = {};
lapsData.forEach((key, value) {
if (value is Map && value.containsKey('timestamp')) {
currentTimestamps[key.toString()] = DateTime.parse(value['timestamp']);
}
});
Map<String, double> lapDurations = {};
final sortedKeys = currentTimestamps.keys.toList()..sort((a, b) => a.compareTo(b));
for (int i = 1; i < sortedKeys.length; i++) {
final prevTimestamp = currentTimestamps[sortedKeys[i - 1]];
final currentTimestamp = currentTimestamps[sortedKeys[i]];
if (prevTimestamp != null && currentTimestamp != null) {
final durationSeconds = currentTimestamp.difference(prevTimestamp).inMilliseconds / 1000.0;
lapDurations['lap_${i}'] = durationSeconds;
}
}
if (lapDurations.isNotEmpty) {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.lap_durations': lapDurations,
});
}
} catch (e) {
print("❌ Gagal update Firestore participants.$userId.laps atau lap_durations: $e");
}
}
}
});
_rtdbSubscriptions.add(_lapsSubscription!);
}
});
_rtdbSubscriptions.add(_activityDataSubscription!);
}
// Aktifkan listener scan RFID jika belum aktif
if (_scanResultSubscription == null) {
print("Mengaktifkan scan_result listener...");
final scanRef = _rtdb.ref('scan_result/${widget.eventId}');
_scanResultSubscription = scanRef.onChildAdded.listen((event) {
final scannedUID = event.snapshot.key;
final value = event.snapshot.value;
if (scannedUID != null && value == true) {
processUID(scannedUID, widget.eventId);
}
});
_rtdbSubscriptions.add(_scanResultSubscription!);
// Panggil processUID untuk data yang sudah ada saat ini juga (saat listener diaktifkan)
scanRef.get().then((snapshot) {
if (snapshot.exists) {
final data = snapshot.value as Map<dynamic, dynamic>;
data.forEach((key, value) {
if (value == true) {
processUID(key, widget.eventId);
}
});
}
});
}
} else {
// Nonaktifkan semua listener jika event tidak dimulai secara permanen atau sudah selesai
print("Menonaktifkan Realtime Database listeners...");
_activityDataSubscription?.cancel();
_finishedSubscription?.cancel();
_lapsSubscription?.cancel();
_scanResultSubscription?.cancel();
// Set semua subscription ke null agar bisa diinisialisasi ulang
_activityDataSubscription = null;
_finishedSubscription = null;
_lapsSubscription = null;
_scanResultSubscription = null;
// Juga kosongkan data live RTDB jika listener dimatikan
_liveParticipantFinishedStatus.clear();
_liveParticipantCurrentLaps.clear();
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
String threeDigitMilliseconds = (duration.inMilliseconds.remainder(1000)).toString().padLeft(3, "0");
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds.$threeDigitMilliseconds";
}
void processUID(String scannedUID, String eventId) async {
print("🔍 Mencari user dengan idGelang = '$scannedUID'");
final querySnapshot = await _firestore
.collection('users')
.where('idGelang', isEqualTo: scannedUID.trim())
.limit(1)
.get();
if (querySnapshot.docs.isEmpty) {
print('❌ UID $scannedUID tidak ditemukan di Firestore.');
return;
}
final userDoc = querySnapshot.docs.first;
final userId = userDoc.id;
final userData = userDoc.data();
_rfidUidToUserIdMap[scannedUID] = userId;
print("Mapped RFID UID $scannedUID to User ID $userId");
final eventRef = _firestore.collection('events').doc(eventId);
final eventSnapshot = await eventRef.get();
Map<String, dynamic> participants = eventSnapshot.data()?['participants'] ?? {};
Map<String, dynamic> existing = participants[userId] ?? {};
final participantData = {
'name': userData['username'] ?? existing['name'] ?? '-',
'user_id': userId,
'rfid_uid': scannedUID,
'laps': existing['laps'] ?? 0,
'finished': existing['finished'] ?? false,
'total_time': existing['total_time'] ?? '-',
'lap_durations': existing['lap_durations'] ?? {},
};
await eventRef.update({
'participants.$userId': participantData,
});
print("🎉 Peserta ${participantData['name']} berhasil dimasukkan/diupdate di event $eventId di Firestore.");
}
@override
Widget build(BuildContext context) {
final docRef = _firestore.collection('events').doc(widget.eventId);
return Scaffold(
backgroundColor: Colors.grey[50], // Background lembut
body: StreamBuilder<DocumentSnapshot>(
stream: docRef.snapshots(),
builder: (context, firestoreSnapshot) {
if (!firestoreSnapshot.hasData) {
return Center(child: CircularProgressIndicator(color: primaryBlue));
}
if (firestoreSnapshot.hasError) {
return Center(child: Text('Error: ${firestoreSnapshot.error}', style: TextStyle(color: Colors.red)));
}
final firestoreData = firestoreSnapshot.data!.data() as Map<String, dynamic>?;
if (firestoreData == null) {
return Center(child: Text('Data event tidak ditemukan di Firestore.'));
}
// Ambil status permanen dari Firestore
bool isEventStartedPermanently = firestoreData['is_started_permanently'] ?? false;
bool isEventFinishedPermanently = firestoreData['is_finished_permanently'] ?? false;
// Kelola listener RTDB berdasarkan status permanen event
WidgetsBinding.instance.addPostFrameCallback((_) {
_manageRealtimeListeners(isEventStartedPermanently && !isEventFinishedPermanently);
});
// Logic untuk menentukan status yang ditampilkan di UI
String displayEventStatusText;
Color displayEventStatusColor;
if (isEventFinishedPermanently) {
displayEventStatusText = "Sudah Selesai Permanen";
displayEventStatusColor = Colors.green.shade700;
} else if (isEventStartedPermanently) {
displayEventStatusText = "Sedang Berlangsung";
displayEventStatusColor = Colors.orange.shade700;
} else {
displayEventStatusText = "Belum Dimulai";
displayEventStatusColor = Colors.red.shade700;
}
final totalLaps = firestoreData['total_laps'] ?? 0;
final totalDistance = firestoreData['total_distance'] ?? 0;
final createdBy = firestoreData['created_by'] ?? '-';
final createdAt = firestoreData['created_at'] != null
? (firestoreData['created_at'] as Timestamp).toDate()
: null;
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180.0, // Tinggi AppBar saat expanded
floating: false,
pinned: true, // AppBar tetap terlihat saat scroll
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20), // Sudut melengkung di kiri bawah
bottomRight: Radius.circular(20), // Sudut melengkung di kanan bawah
),
),
backgroundColor: primaryBlue, // Warna dasar AppBar
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
titlePadding: EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
// Pastikan background juga melengkung
background: ClipRRect( // Tambahkan ClipRRect di sini
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryBlue, darkBlue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 24.0), // Beri sedikit padding atas agar tidak terlalu mepet
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event,
color: Colors.white,
size: 48,
),
SizedBox(height: 8),
Text(
widget.eventName,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
Shadow(
blurRadius: 4.0,
color: Colors.black.withOpacity(0.3),
offset: Offset(1.0, 1.0),
),
],
),
textAlign: TextAlign.center,
),
SizedBox(height: 4),
Text(
"${totalLaps} Putaran | ${(totalDistance / 1000).toStringAsFixed(2)} KM",
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
),
actions: [
IconButton(
icon: Icon(Icons.delete_forever, color: Colors.white),
tooltip: 'Hapus Event',
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Konfirmasi Hapus', style: TextStyle(color: darkBlue)),
content: Text('Apakah Anda yakin ingin menghapus event "${widget.eventName}" dan semua datanya?', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Batal', style: TextStyle(color: primaryBlue)),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Hapus', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true) {
await docRef.delete();
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event "${widget.eventName}" telah dihapus.')),
);
}
},
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi Detail Event
Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
color: Colors.white, // CARD PUTIH
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.person, "Dibuat oleh", createdBy),
SizedBox(height: 8),
if (createdAt != null)
_buildInfoRow(
Icons.calendar_today,
"Tanggal dibuat",
"${createdAt.day}/${createdAt.month}/${createdAt.year} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}",
),
SizedBox(height: 16),
Divider(color: Colors.grey[300]),
SizedBox(height: 16),
Text("Status Event", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: darkBlue)),
SizedBox(height: 8),
Row(
children: [
Icon(Icons.play_circle_outline, size: 20, color: Colors.grey[700]),
SizedBox(width: 8),
Text(
displayEventStatusText, // Teks status baru
style: TextStyle(
fontSize: 15,
color: displayEventStatusColor, // Warna status baru
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 4),
Row(
children: [
Icon(Icons.check_circle_outline, size: 20, color: Colors.grey[700]),
SizedBox(width: 8),
Text(
"Selesai Permanen: ${isEventFinishedPermanently ? 'Ya ✅' : 'Tidak ❌'}",
style: TextStyle(
fontSize: 15,
color: isEventFinishedPermanently ? Colors.green.shade700 : Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 24),
// Tombol Mulai Event
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: (isEventStartedPermanently || isEventFinishedPermanently)
? null
: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Mulai Event', style: TextStyle(color: darkBlue)),
content: Text('Peserta akan mulai scan RFID untuk event ini.', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('Batal', style: TextStyle(color: primaryBlue))),
TextButton(onPressed: () => Navigator.pop(context, true), child: Text('Mulai', style: TextStyle(color: Colors.green))),
],
),
);
if (confirm == true) {
final currentEventData = await docRef.get();
final currentTotalLaps = (currentEventData.data() as Map<String, dynamic>?)?['total_laps'] ?? 0;
await docRef.update({
'is_started_permanently': true,
'is_finished_permanently': false,
'start_time': Timestamp.now(),
}).catchError((e) {
print("❌ Gagal update Firestore: $e");
});
await _eventStatusRef.set({
'started': true,
'scan_mode': true,
'timestamp': ServerValue.timestamp,
'total_laps_target': currentTotalLaps,
'finished': false,
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event dimulai. Peserta dapat scan RFID sekarang.')),
);
}
},
icon: Icon(Icons.play_arrow, color: Colors.white),
label: Text(
'Mulai Event (Scan RFID)',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: (isEventStartedPermanently || isEventFinishedPermanently) ? Colors.grey : Colors.green.shade600,
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
),
),
),
// Tombol Akhiri Event Manual
if (isEventStartedPermanently && !isEventFinishedPermanently)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Akhiri Event', style: TextStyle(color: darkBlue)),
content: Text('Apakah Anda yakin ingin mengakhiri event ini secara manual? Status event akan permanen selesai dan data Realtime akan dihapus.', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('Batal', style: TextStyle(color: primaryBlue))),
TextButton(onPressed: () => Navigator.pop(context, true), child: Text('Akhiri', style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
await docRef.update({
'is_started_permanently': false,
'is_finished_permanently': true,
});
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event telah diakhiri secara manual dan disimpan secara permanen.')),
);
}
},
icon: Icon(Icons.stop, color: Colors.white),
label: Text(
'Akhiri Event Manual',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
),
),
),
),
],
),
),
),
SizedBox(height: 24),
Text("Peserta Event:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: darkBlue)),
SizedBox(height: 1),
],
),
),
),
// Daftar Peserta
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
sliver: ((firestoreData['participants'] as Map<String, dynamic>?)?.entries.isNotEmpty ?? false)
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final participantEntry = (firestoreData['participants'] as Map<String, dynamic>).entries.toList()[index];
final participant = participantEntry.value;
final name = participant['name'] ?? 'Nama tidak ditemukan';
final rfidUid = participant['rfid_uid'] as String?;
bool participantFinishedDisplay;
int participantLapsDisplay;
String participantTotalTimeDisplay = participant['total_time'] ?? '-';
// Prioritaskan data live (RTDB) jika event sedang berjalan (permanen)
if (isEventStartedPermanently && !isEventFinishedPermanently && _liveParticipantFinishedStatus.containsKey(rfidUid)) {
participantFinishedDisplay = _liveParticipantFinishedStatus[rfidUid]!;
} else {
participantFinishedDisplay = participant['finished'] ?? false; // Ambil dari Firestore
}
if (isEventStartedPermanently && !isEventFinishedPermanently && _liveParticipantCurrentLaps.containsKey(rfidUid)) {
participantLapsDisplay = _liveParticipantCurrentLaps[rfidUid]! > 0 ? _liveParticipantCurrentLaps[rfidUid]! - 1 : 0;
} else {
participantLapsDisplay = participant['laps'] ?? 0; // Ambil dari Firestore
}
return Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white, // CARD PUTIH
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: darkBlue,
),
),
SizedBox(height: 8),
_buildParticipantInfoRow(Icons.credit_card, "UID RFID", rfidUid ?? 'N/A'),
_buildParticipantInfoRow(Icons.sports_score, "Putaran", "$participantLapsDisplay"),
_buildParticipantInfoRow(
Icons.flag,
"Selesai",
participantFinishedDisplay ? 'Ya ✅' : 'Tidak ❌',
valueColor: participantFinishedDisplay ? Colors.green.shade700 : Colors.red.shade700,
),
if (participantFinishedDisplay && participantTotalTimeDisplay != '-')
_buildParticipantInfoRow(
Icons.timer,
"Waktu Total",
participantTotalTimeDisplay,
valueColor: darkBlue,
),
],
),
),
);
},
childCount: (firestoreData['participants'] as Map<String, dynamic>).length,
),
)
: SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Belum ada peserta yang terdaftar untuk event ini.",
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
),
),
),
),
SliverToBoxAdapter(
child: SizedBox(height: 24), // Tambahan spasi di bagian bawah
),
],
);
},
),
);
}
// Helper widget untuk baris informasi event
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: primaryBlue),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
),
Text(
value,
style: TextStyle(fontSize: 15, color: Colors.grey[900]),
),
],
),
),
],
);
}
// Helper widget untuk baris informasi peserta
Widget _buildParticipantInfoRow(IconData icon, String label, String value, {Color? valueColor}) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey[600]),
SizedBox(width: 8),
Text(
"$label: ",
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
Expanded(
child: Text(
value,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? darkBlue),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}

View File

@ -0,0 +1,379 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
class EventResultPage extends StatelessWidget {
final String eventId;
final String eventName;
EventResultPage({required this.eventId, required this.eventName});
final Color primaryBlue = Color(0xFF42A5F5);
final Color lightBlue = Color(0xFFBBDEFB);
final Color darkBlue = Color(0xFF1976D2);
final Color successGreen = Colors.green.shade700;
final Color warningOrange = Colors.orange.shade700;
final Color dangerRed = Colors.red.shade700;
@override
Widget build(BuildContext context) {
final docRef = FirebaseFirestore.instance.collection('events').doc(eventId);
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: Text(
'Hasil Event: $eventName',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: primaryBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
iconTheme: IconThemeData(color: Colors.white),
elevation: 5,
),
body: StreamBuilder<DocumentSnapshot>(
stream: docRef.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: primaryBlue));
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}', style: TextStyle(color: dangerRed)));
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return Center(child: Text('Event tidak ditemukan.', style: TextStyle(color: Colors.grey[600])));
}
final data = snapshot.data!.data() as Map<String, dynamic>?;
if (data == null || data['participants'] == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_alt_outlined, size: 60, color: Colors.grey[400]),
SizedBox(height: 16),
Text('Belum ada peserta yang terdaftar untuk event ini.', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
],
),
);
}
final participants = data['participants'] as Map<String, dynamic>;
if (participants.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_alt_outlined, size: 60, color: Colors.grey[400]),
SizedBox(height: 16),
Text('Belum ada peserta yang terdaftar untuk event ini.', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
],
),
);
}
List<MapEntry<String, dynamic>> sortedParticipants = participants.entries.toList();
sortedParticipants.sort((a, b) {
final pA = a.value as Map<String, dynamic>;
final pB = b.value as Map<String, dynamic>;
final finishedA = pA['finished'] == true;
final finishedB = pB['finished'] == true;
if (finishedA && !finishedB) return -1;
if (!finishedA && finishedB) return 1;
if (finishedA && finishedB) {
final timeA = _parseTimeToSeconds(pA['total_time']);
final timeB = _parseTimeToSeconds(pB['total_time']);
return timeA.compareTo(timeB);
}
final lapsA = pA['laps'] ?? 0;
final lapsB = pB['laps'] ?? 0;
return lapsB.compareTo(lapsA);
});
return ListView.builder(
padding: EdgeInsets.all(16),
itemCount: sortedParticipants.length,
itemBuilder: (context, index) {
final entry = sortedParticipants[index];
final p = entry.value as Map<String, dynamic>;
final name = p['name'] ?? 'Peserta Tidak Dikenal';
final laps = p['laps'] ?? 0;
final finished = p['finished'] == true;
final totalTime = p['total_time'] ?? '-';
final rfidUid = p['rfid_uid'] ?? '-';
final lapDurations = p['lap_durations'] as Map<String, dynamic>? ?? {};
String participantStatus = finished ? 'Selesai' : 'Berlangsung';
Color participantStatusColor = finished ? successGreen : warningOrange;
String formattedTotalTime = '-';
if (finished && totalTime != '-') {
try {
final duration = Duration(milliseconds: _parseMilliseconds(totalTime));
formattedTotalTime = _formatDuration(duration);
} catch (e) {
formattedTotalTime = totalTime;
}
}
List<Widget> kecepatanWidgets = [];
final sortedLapKeys = lapDurations.keys.toList()..sort((a, b) {
final intA = int.parse(a.split('_')[1]);
final intB = int.parse(b.split('_')[1]);
return intA.compareTo(intB);
});
for (String lapKey in sortedLapKeys) {
final dynamic durasiDetikRaw = lapDurations[lapKey];
double durasiDetik = 0.0;
if (durasiDetikRaw is int) {
durasiDetik = durasiDetikRaw.toDouble();
} else if (durasiDetikRaw is double) {
durasiDetik = durasiDetikRaw;
}
String formattedDuration = '${durasiDetik.toStringAsFixed(2)} s';
if (durasiDetik > 0) {
final kecepatan = 400 / durasiDetik;
kecepatanWidgets.add(
Row(
children: [
SizedBox(width: 24),
Text(
"Lap ${lapKey.split('_')[1]}: ${kecepatan.toStringAsFixed(2)} m/s ",
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
),
Text(
"($formattedDuration)",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
);
} else {
kecepatanWidgets.add(
Padding(
padding: const EdgeInsets.only(left: 24.0),
child: Text(
"Lap ${lapKey.split('_')[1]}: Durasi tidak valid",
style: TextStyle(fontSize: 13, color: Colors.grey[500], fontStyle: FontStyle.italic),
),
),
);
}
}
return Card(
margin: EdgeInsets.only(bottom: 16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: darkBlue,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: participantStatusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
participantStatus,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: participantStatusColor,
),
),
),
],
),
SizedBox(height: 8),
Divider(color: Colors.grey[200]),
SizedBox(height: 8),
_buildInfoRow("RFID Tag", rfidUid, darkBlue),
_buildInfoRow("Putaran Selesai", "$laps", darkBlue),
if (finished) _buildInfoRow("Waktu Total", formattedTotalTime, darkBlue),
if (kecepatanWidgets.isNotEmpty) ...[
SizedBox(height: 12),
Text(
"Detail Kecepatan per Lap:",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: darkBlue,
),
),
SizedBox(height: 8),
...kecepatanWidgets,
],
// Tombol hapus peserta
SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: IconButton( // <-- DIUBAH DARI TextButton.icon MENJADI IconButton
icon: Icon(Icons.delete_outline, color: dangerRed, size: 24), // Ukuran ikon diperbesar sedikit
tooltip: 'Hapus Peserta', // Tooltip tetap ada saat ditekan lama
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Konfirmasi Hapus Peserta', style: TextStyle(color: dangerRed)),
content: Text('Apakah Anda yakin ingin menghapus peserta "$name" dari event ini?', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Batal', style: TextStyle(color: primaryBlue)),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Hapus', style: TextStyle(color: dangerRed)),
),
],
),
);
if (confirm == true) {
try {
await docRef.update({
'participants.${entry.key}': FieldValue.delete(),
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Peserta "$name" berhasil dihapus.'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menghapus peserta: $e'), backgroundColor: Colors.red),
);
print('Error deleting participant: $e');
}
}
},
),
),
],
),
),
);
},
);
},
),
);
}
// Helper widget untuk baris informasi yang clean tanpa ikon
Widget _buildInfoRow(String label, String value, Color valueColor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
"$label:",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: Colors.grey[700]),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(fontSize: 15, color: valueColor),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
// Helper untuk parsing waktu (misal "00:01:23.456" menjadi milidetik)
int _parseMilliseconds(String timeString) {
if (timeString == '-') return 0;
final parts = timeString.split(':');
if (parts.length < 3) return 0; // Invalid format
int hours = int.parse(parts[0]);
int minutes = int.parse(parts[1]);
double secondsWithMs = double.parse(parts[2]);
int totalMilliseconds = ((hours * 3600 + minutes * 60 + secondsWithMs) * 1000).toInt();
return totalMilliseconds;
}
// Helper untuk format total_time dari milidetik
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String threeDigits(int n) => n.toString().padLeft(3, "0");
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
String milliseconds = threeDigits(duration.inMilliseconds.remainder(1000));
String hours = twoDigits(duration.inHours);
if (duration.inHours > 0) {
return "$hours:$minutes:$seconds.$milliseconds";
} else {
return "$minutes:$seconds.$milliseconds";
}
}
// Helper untuk mengonversi string waktu (HH:MM:SS.ms) ke total detik (untuk sorting)
double _parseTimeToSeconds(String timeString) {
if (timeString == '-') return double.infinity;
try {
final parts = timeString.split(':');
if (parts.length != 3) return double.infinity;
final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
final secondsParts = parts[2].split('.');
final seconds = int.parse(secondsParts[0]);
final milliseconds = secondsParts.length > 1 ? int.parse(secondsParts[1]) : 0;
return (hours * 3600 + minutes * 60 + seconds + milliseconds / 1000).toDouble();
} catch (e) {
return double.infinity;
}
}
}

View File

@ -0,0 +1,214 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'event_result_page.dart'; // Import halaman hasil peserta
class HasilEventPage extends StatelessWidget {
// Warna utama yang terinspirasi dari gambar Anda
final Color primaryBlue = Color(0xFF42A5F5); // Biru muda yang cerah
final Color lightBlue = Color(0xFFBBDEFB); // Biru yang lebih terang untuk background
final Color darkBlue = Color(0xFF1976D2); // Biru yang lebih gelap untuk teks/ikon
@override
Widget build(BuildContext context) {
final eventCollection = FirebaseFirestore.instance.collection('events');
return Scaffold(
backgroundColor: Colors.grey[50], // Background lembut
appBar: AppBar(
title: Text(
'Daftar Hasil Event',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: primaryBlue, // Warna AppBar
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20), // Sudut melengkung di kiri bawah
bottomRight: Radius.circular(20), // Sudut melengkung di kanan bawah
),
),
iconTheme: IconThemeData(color: Colors.white), // Warna ikon back
elevation: 5, // Tambahkan sedikit bayangan
),
body: StreamBuilder<QuerySnapshot>(
stream: eventCollection.orderBy('created_at', descending: true).snapshots(), // Urutkan terbaru di atas
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator(color: primaryBlue));
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}', style: TextStyle(color: Colors.red)));
}
final docs = snapshot.data!.docs;
if (docs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.event_note, size: 60, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
"Belum ada event yang tersedia.",
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
],
),
);
}
return ListView.builder(
padding: EdgeInsets.all(16.0), // Padding menyeluruh untuk ListView
itemCount: docs.length,
itemBuilder: (context, index) {
final doc = docs[index];
final data = doc.data() as Map<String, dynamic>;
final eventName = data['name'] ?? 'Tanpa Nama';
final totalLaps = data['total_laps'] ?? 0;
final totalDistance = data['total_distance'] ?? 0;
final participantsMap = data['participants'] as Map<String, dynamic>? ?? {}; // Ambil map peserta
// Ambil status permanen dari Firestore
bool isEventFinishedPermanently = data['is_finished_permanently'] ?? false;
bool isEventStartedPermanently = data['is_started_permanently'] ?? false;
String displayEventStatusText;
Color displayEventStatusColor;
if (isEventFinishedPermanently) {
displayEventStatusText = "Selesai"; // Lebih ringkas
displayEventStatusColor = Colors.green.shade700;
} else if (isEventStartedPermanently) {
displayEventStatusText = "Berlangsung"; // Lebih ringkas
displayEventStatusColor = Colors.orange.shade700;
} else {
displayEventStatusText = "Belum Mulai"; // Lebih ringkas
displayEventStatusColor = Colors.red.shade700;
}
return Card(
margin: EdgeInsets.only(bottom: 16), // Spasi antar card
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // Radius pada Card
),
color: Colors.white, // Warna Card putih
child: InkWell( // Gunakan InkWell untuk efek visual saat ditekan
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EventResultPage(
eventId: doc.id,
eventName: eventName,
),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column( // Menggunakan Column untuk menata konten
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row( // Baris untuk Nama Event dan Status
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
eventName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: darkBlue,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: displayEventStatusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
displayEventStatusText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: displayEventStatusColor,
),
),
),
],
),
SizedBox(height: 4),
Text(
"${totalLaps} Putaran | ${(totalDistance / 1000).toStringAsFixed(2)} KM",
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
SizedBox(height: 12), // Spasi sebelum daftar peserta
Text(
"Peserta (${participantsMap.length}):",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: darkBlue,
),
),
SizedBox(height: 8),
if (participantsMap.isEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Belum ada peserta terdaftar.",
style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic, color: Colors.grey[600]),
),
)
else
// List nama peserta
...participantsMap.entries.map((entry) {
final p = entry.value as Map<String, dynamic>;
final name = p['name'] ?? 'Nama tidak ditemukan';
final finished = p['finished'] == true;
final currentLaps = p['laps'] ?? 0;
String statusText = finished ? 'Selesai' : 'Berlangsung ($currentLaps putaran)';
Color statusColor = finished ? Colors.green.shade700 : Colors.orange.shade700;
return Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 4.0),
child: Row(
children: [
Icon(Icons.person, size: 16, color: Colors.grey[700]), // Ikon orang
SizedBox(width: 8),
Expanded(
child: Text(
name,
style: TextStyle(fontSize: 14, color: Colors.grey[900]),
overflow: TextOverflow.ellipsis,
),
),
Text(
statusText,
style: TextStyle(fontSize: 12, color: statusColor),
),
],
),
);
}).toList(),
],
),
),
),
);
},
);
},
),
);
}
}

View File

@ -0,0 +1,425 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'detail_hasil_screen.dart';
import 'all_activity_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Future<String> getUsername() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return '';
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
return doc.data()?['username'] ?? '';
}
Future<Map<String, double>> fetchRunningData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return {};
final now = DateTime.now();
final startOfWeek = DateTime(now.year, now.month, now.day - (now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 7)).subtract(const Duration(seconds: 1));
final snapshot = await FirebaseFirestore.instance.collection('activities').get();
final Map<String, double> distancePerDay = {};
for (var doc in snapshot.docs) {
final data = doc.data()['data'];
if (data == null || data['userId'] != user.uid) continue;
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
if (timestamp == null || timestamp.isBefore(startOfWeek) || timestamp.isAfter(endOfWeek)) continue;
final day = DateFormat('EEEE', 'id_ID').format(timestamp);
final jarak = (data['jarak'] ?? 0).toDouble();
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'M' ? jarak / 1000.0 : jarak;
distancePerDay[day] = (distancePerDay[day] ?? 0) + jarakKm;
}
const sortedDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
for (var dayName in sortedDays) {
distancePerDay.putIfAbsent(dayName, () => 0.0);
}
return distancePerDay;
}
Future<List<Map<String, dynamic>>> fetchRingkasanData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return [];
final snapshot = await FirebaseFirestore.instance.collection('activities').get();
final List<Map<String, dynamic>> result = [];
for (var doc in snapshot.docs) {
final data = doc.data()['data']; // penting!
if (data == null || data['userId'] != user.uid) continue;
result.add({
...data,
'docId': doc.id, // tambahkan docId untuk referensi penghapusan nanti
});
}
result.sort((a, b) {
final aTime = (a['timestamp'] as Timestamp?)?.toDate() ?? DateTime(2000);
final bTime = (b['timestamp'] as Timestamp?)?.toDate() ?? DateTime(2000);
return bTime.compareTo(aTime);
});
return result;
}
String getGreeting() {
final hour = DateTime.now().hour;
if (hour >= 6 && hour <= 10) {
return 'Selamat Pagi';
} else if (hour >= 11 && hour <= 14) {
return 'Selamat Siang';
} else if (hour >= 15 && hour <= 17) {
return 'Selamat Sore';
} else {
return 'Selamat Malam';
}
}
DateTime? parseTimestamp(dynamic raw) {
if (raw is Timestamp) {
return raw.toDate();
} else if (raw is DateTime) {
return raw;
} else if (raw is String) {
return DateTime.tryParse(raw);
} else if (raw is int) {
return DateTime.fromMillisecondsSinceEpoch(raw);
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: FutureBuilder(
future: getUsername(),
builder: (context, snapshot) {
final greeting = getGreeting();
final username = snapshot.data ?? '...';
return Container(
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$greeting, $username',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Image.asset(
'images/run.png',
height: 55,
width: 55,
),
],
),
);
},
),
),
body: RefreshIndicator(
onRefresh: () async {
// Memanggil kembali data setelah pull-to-refresh
fetchRunningData();
fetchRingkasanData();
},
child: Padding(
padding: const EdgeInsets.only(top: 0.0, bottom: 0.0, right: 14.0, left: 14.0),
child: FutureBuilder(
future: Future.wait([fetchRunningData(), fetchRingkasanData(), getUsername()]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final runningData = snapshot.data?[0] as Map<String, double>? ?? {};
final ringkasanData = snapshot.data?[1] as List<Map<String, dynamic>>? ?? [];
final sortedDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
final values = sortedDays.map((day) => runningData[day] ?? 0.0).toList();
final totalMinggu = values.fold(0.0, (a, b) => a + b);
int totalDurasiDetik = 0;
final now = DateTime.now();
final startOfWeek = DateTime(now.year, now.month, now.day - (now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 7)).subtract(const Duration(seconds: 1));
for (var data in ringkasanData) {
final timestamp = parseTimestamp(data['timestamp']);
if (timestamp == null) continue;
if (timestamp.isBefore(startOfWeek) || timestamp.isAfter(endOfWeek)) continue;
totalDurasiDetik += (((data['duration'] ?? data['durasi'] ?? 0) as num).toInt() ~/ 1000);
}
String formatDurasi(int totalSeconds) {
final jam = totalSeconds ~/ 3600;
final menit = (totalSeconds % 3600) ~/ 60;
final detik = totalSeconds % 60;
return '${jam}j ${menit}m ${detik}d';
}
final totalDurasiFormatted = formatDurasi(totalDurasiDetik);
final maxValue = values.reduce((a, b) => a > b ? a : b);
final highestDay = sortedDays[values.indexOf(maxValue)];
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Minggu ini',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AllActivityScreen()),
);
},
child: const Text(
'View all weeks',
style: TextStyle(color: Colors.blue),
),
),
],
),
const SizedBox(height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Jarak',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
Text(
'${totalMinggu.toStringAsFixed(2)} km',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Waktu',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
Text(
totalDurasiFormatted,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 170,
child: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 6.0),
child: LineChart(
LineChartData(
minY: 0.0,
maxY: (values.isEmpty ? 5.0 : values.reduce((a, b) => a > b ? a : b)),
gridData: FlGridData(
show: true,
drawVerticalLine: true,
drawHorizontalLine: false,
verticalInterval: 1,
getDrawingVerticalLine: (value) {
return FlLine(
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
);
},
),
extraLinesData: ExtraLinesData(
verticalLines: [
VerticalLine(
x: 0,
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
),
VerticalLine(
x: (sortedDays.length - 1).toDouble(),
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
),
],
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
int index = value.toInt();
if (index < 0 || index >= sortedDays.length) {
return const SizedBox.shrink();
}
EdgeInsets padding = EdgeInsets.zero;
if (index == 0) {
padding = const EdgeInsets.only(left: 12);
} else if (index == sortedDays.length - 1) {
padding = const EdgeInsets.only(right: 12);
}
return Padding(
padding: padding.add(const EdgeInsets.only(top: 4)), // tambah jarak dari grafik ke teks
child: Text(
sortedDays[index].substring(0, 3),
style: const TextStyle(fontSize: 10),
),
);
},
),
),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: (value, _) {
if (value == 0.0 || value == maxValue || value == (maxValue + 1)) {
return Padding(
padding: const EdgeInsets.only(left: 8),
child: Text('${value.toStringAsFixed(1)} KM', style: const TextStyle(fontSize: 10)),
);
}
return const SizedBox.shrink();
},
),
),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])),
isCurved: false,
barWidth: 3,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: Colors.lightBlueAccent.withOpacity(0.3),
),
color: Colors.blue,
),
],
),
),
),
),
const SizedBox(height: 24),
Text('Hari Teraktif: $highestDay (${maxValue.toStringAsFixed(2)} KM)',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 16),
const Text('Ringkasan Hasil Lari',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
ringkasanData.isEmpty
? const Text('Belum ada data untuk hari teraktif.')
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: ringkasanData.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final data = ringkasanData[index];
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
if (timestamp == null) return const SizedBox();
final jarak = (data['jarak'] ?? 0).toDouble();
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'M' ? jarak / 1000.0 : jarak;
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailHasilLariScreen(
data: {
...data,
'docId': ringkasanData[index]['docId'] ?? '', // tambahkan docId kalau tersedia
},
),
),
);
},
child: Card(
color: const Color.fromARGB(255, 60, 193, 255),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Padding(
padding: const EdgeInsets.only(top: 12.0), // geser ke bawah
child: const Icon(Icons.directions_run),
),
title: Text('${jarak.toStringAsFixed(0)} $satuan'),
subtitle: Text(
'${DateFormat('dd MMM yyyy, HH:mm').format(timestamp)}\n(${jarakKm.toStringAsFixed(2)} KM)',
),
isThreeLine: true,
trailing: Padding(
padding: const EdgeInsets.only(top: 12.0), // geser ke bawah
child: const Icon(Icons.arrow_forward_ios, size: 16),
),
),
),
);
},
),
],
),
);
},
),
),
)
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:ta_running/screens/start_activity_screen.dart';
import 'home_screen.dart';
import 'profile_screen.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const StartActivityScreen(),
const ProfileScreen(),
];
void _onTabTapped(int index) {
setState(() => _selectedIndex = index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_selectedIndex],
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2), // Warna bayangan hitam dengan transparansi
blurRadius: 10, // Ukuran blur bayangan
offset: Offset(0, -4), // Posisi bayangan
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onTabTapped,
showSelectedLabels: true, // label tampil saat aktif
showUnselectedLabels: false, // label disembunyikan saat tidak aktif
selectedItemColor: Colors.blueAccent,
unselectedItemColor: Colors.grey,
backgroundColor: Colors.white,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.play_arrow),
label: 'Start',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
),
),
);
}
}

View File

@ -0,0 +1,118 @@
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'result_screen.dart';
class MonitoringScreen extends StatefulWidget {
final String userId;
const MonitoringScreen({Key? key, required this.userId}) : super(key: key);
@override
State<MonitoringScreen> createState() => _MonitoringScreenState();
}
class _MonitoringScreenState extends State<MonitoringScreen> {
late DatabaseReference _activityRef;
StreamSubscription<DatabaseEvent>? _subscription;
bool _navigated = false;
@override
void initState() {
super.initState();
final db = FirebaseDatabase.instanceFor(
app: Firebase.app(),
databaseURL: "https://ta-running-default-rtdb.asia-southeast1.firebasedatabase.app",
);
_activityRef = db.ref("activities/${widget.userId}");
_subscription = _activityRef.onValue.listen((event) {
final raw = event.snapshot.value;
if (raw is Map) {
final data = Map<String, dynamic>.from(raw);
// Cek dan simpan startTime jika status == 'running' dan startTime belum ada
if (data['status'] == 'running' && data['startTime'] == null) {
final now = DateTime.now().toUtc(); // ambil SEKALI dalam UTC
final nowMillis = now.millisecondsSinceEpoch;
final nowIso = now.toIso8601String();
_activityRef.update({
'startTime': nowMillis,
'laps/lap_0/timestamp': nowIso,
});
print("⏱️ startTime dan lap_0 disimpan ke RTDB: $nowIso");
}
// Saat status 'done', proses navigasi dan simpan durasi
if (!_navigated && data['status'] == 'done') {
_navigated = true;
_subscription?.cancel();
int? dur;
if (data['startTime'] != null) {
final now = DateTime.now().millisecondsSinceEpoch;
dur = now - (data['startTime'] as int);
_activityRef.update({'duration': dur});
data['duration'] = dur;
}
final type = data['type'];
final rawJarak = data['jarak'];
final satuan = data['satuan'] ?? 'M';
double jarakKm = 0;
if (rawJarak != null) {
jarakKm = satuan == 'KM'
? (rawJarak as num).toDouble()
: (rawJarak as num).toDouble() / 1000.0;
}
if (type == 'non-lintasan' && jarakKm > 0 && dur != null) {
final durSec = dur / 1000.0;
final pacePerKmSec = durSec / jarakKm;
_activityRef.update({'pace_sec_per_km': pacePerKmSec});
data['pace_sec_per_km'] = pacePerKmSec;
}
data['uidTag'] = widget.userId;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ResultScreen(activityData: data),
),
);
}
}
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.blueAccent),
SizedBox(height: 20),
Text(
'Aktivitas sedang berlangsung...',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More