Menambahkan kode aplikasi Android dan firmware ESP32

This commit is contained in:
Thomas Santoso 2026-08-06 19:54:56 +07:00
parent aad83206b9
commit d47097528c
191 changed files with 12815 additions and 0 deletions

45
Aplikasi Android/.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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@ -0,0 +1,30 @@
# 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: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# 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'

View File

@ -0,0 +1,4 @@
{
"idf.currentSetup": "C:\\esp\\v6.0\\esp-idf",
"idf.portWin": "COM3"
}

View File

@ -0,0 +1,17 @@
# flutter_application_1
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:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
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.

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
Aplikasi Android/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,47 @@
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.flutter_application_1"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_application_1"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "4585400589",
"project_id": "esp32-app-b9329",
"storage_bucket": "esp32-app-b9329.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:4585400589:android:cf008c2634f5f47afda844",
"android_client_info": {
"package_name": "com.example.flutter_application_1"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDOI5vGA7A1tguXTbxxizFo5y2mIFfqb3g"
}
],
"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,44 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<application
android:label="ESP32 APP"
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">
<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>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.bluetooth.device.action.FOUND"/>
</intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,340 @@
package com.example.flutter_application_1
import android.annotation.SuppressLint
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.KeyEvent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val channelName = "app.bluetooth.platform"
private val requestEnableBluetoothCode = 9001
private val discoveryTimeoutMs = 12000L
private var platformChannel: MethodChannel? = null
private var pendingEnableBluetoothResult: MethodChannel.Result? = null
private var pendingScanResult: MethodChannel.Result? = null
private val discoveredDevices = linkedMapOf<String, MutableMap<String, Any?>>()
private val mainHandler = Handler(Looper.getMainLooper())
private var discoveryReceiverRegistered = false
private var controlCaptureEnabled = false
private var volumeUpPressed = false
private var volumeDownPressed = false
private val discoveryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothDevice.ACTION_FOUND -> {
val device: BluetoothDevice? =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
appendDiscoveredDevice(device)
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> finishDiscoveryScan()
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
platformChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelName,
)
platformChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"getSdkInt" -> result.success(Build.VERSION.SDK_INT)
"isBluetoothEnabled" -> {
val adapter = BluetoothAdapter.getDefaultAdapter()
result.success(adapter?.isEnabled == true)
}
"requestEnableBluetooth" -> requestEnableBluetooth(result)
"isLocationServiceEnabled" -> result.success(isLocationServiceEnabled())
"openLocationSettings" -> {
openLocationSettings()
result.success(true)
}
"scanBluetoothDevices" -> scanBluetoothDevices(result)
"cancelBluetoothDiscovery" -> {
result.success(cancelBluetoothDiscovery())
}
"setControlCaptureEnabled" -> {
controlCaptureEnabled = call.arguments as? Boolean ?: false
if (!controlCaptureEnabled) {
volumeUpPressed = false
volumeDownPressed = false
}
result.success(true)
}
else -> result.notImplemented()
}
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val keyCode = event.keyCode
if (controlCaptureEnabled &&
(keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
) {
handleControlKeyEvent(event)
return true
}
return super.dispatchKeyEvent(event)
}
private fun handleControlKeyEvent(event: KeyEvent) {
val key = when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> "volume_up"
KeyEvent.KEYCODE_VOLUME_DOWN -> "volume_down"
else -> return
}
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (event.repeatCount > 0) {
return
}
if (key == "volume_up") {
if (volumeUpPressed) return
volumeUpPressed = true
} else {
if (volumeDownPressed) return
volumeDownPressed = true
}
emitHardwareButtonEvent(key = key, action = "down")
}
KeyEvent.ACTION_UP -> {
if (key == "volume_up") {
if (!volumeUpPressed) return
volumeUpPressed = false
} else {
if (!volumeDownPressed) return
volumeDownPressed = false
}
emitHardwareButtonEvent(key = key, action = "up")
}
}
}
private fun emitHardwareButtonEvent(key: String, action: String) {
platformChannel?.invokeMethod(
"hardwareButtonEvent",
mapOf(
"key" to key,
"action" to action,
),
)
}
@SuppressLint("MissingPermission")
private fun cancelBluetoothDiscovery(): Boolean {
val adapter = BluetoothAdapter.getDefaultAdapter() ?: return false
return try {
if (adapter.isDiscovering) {
adapter.cancelDiscovery()
}
true
} catch (_: SecurityException) {
false
}
}
private fun requestEnableBluetooth(result: MethodChannel.Result) {
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter == null) {
result.success(false)
return
}
if (adapter.isEnabled) {
result.success(true)
return
}
if (pendingEnableBluetoothResult != null) {
result.success(false)
return
}
pendingEnableBluetoothResult = result
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(intent, requestEnableBluetoothCode)
}
@SuppressLint("MissingPermission")
private fun scanBluetoothDevices(result: MethodChannel.Result) {
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter == null) {
result.error("UNAVAILABLE", "Bluetooth adapter tidak tersedia.", null)
return
}
if (!adapter.isEnabled) {
result.error("BLUETOOTH_OFF", "Bluetooth belum aktif.", null)
return
}
if (pendingScanResult != null) {
result.error("SCAN_IN_PROGRESS", "Pemindaian sedang berlangsung.", null)
return
}
pendingScanResult = result
discoveredDevices.clear()
registerDiscoveryReceiverIfNeeded()
try {
if (adapter.isDiscovering) {
adapter.cancelDiscovery()
}
val started = adapter.startDiscovery()
if (!started) {
finishDiscoveryScan()
return
}
mainHandler.postDelayed({
if (pendingScanResult != null) {
finishDiscoveryScan()
}
}, discoveryTimeoutMs)
} catch (security: SecurityException) {
cleanupDiscoveryReceiver()
pendingScanResult?.error(
"PERMISSION_DENIED",
"Izin Bluetooth atau lokasi belum tersedia.",
security.message,
)
pendingScanResult = null
} catch (exception: Exception) {
cleanupDiscoveryReceiver()
pendingScanResult?.error(
"SCAN_FAILED",
"Gagal memulai pemindaian Bluetooth.",
exception.message,
)
pendingScanResult = null
}
}
@SuppressLint("MissingPermission")
private fun finishDiscoveryScan() {
val adapter = BluetoothAdapter.getDefaultAdapter()
try {
if (adapter?.isDiscovering == true) {
adapter.cancelDiscovery()
}
} catch (_: SecurityException) {
// Ignore best-effort cancel.
}
mainHandler.removeCallbacksAndMessages(null)
cleanupDiscoveryReceiver()
val payload = discoveredDevices.values.map { HashMap(it) }
pendingScanResult?.success(payload)
pendingScanResult = null
}
@SuppressLint("MissingPermission")
private fun appendDiscoveredDevice(device: BluetoothDevice?) {
if (device == null) return
val address = device.address ?: return
if (address.isBlank()) return
val existing = discoveredDevices[address]
val name = try {
device.name
} catch (_: SecurityException) {
null
}
if (existing == null) {
discoveredDevices[address] = mutableMapOf(
"name" to (name ?: ""),
"address" to address,
)
return
}
val existingName = (existing["name"] as? String).orEmpty()
if (existingName.isBlank() && !name.isNullOrBlank()) {
existing["name"] = name
}
}
private fun registerDiscoveryReceiverIfNeeded() {
if (discoveryReceiverRegistered) return
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
}
registerReceiver(discoveryReceiver, filter)
discoveryReceiverRegistered = true
}
private fun cleanupDiscoveryReceiver() {
if (!discoveryReceiverRegistered) return
try {
unregisterReceiver(discoveryReceiver)
} catch (_: Exception) {
// Receiver may already be unregistered.
}
discoveryReceiverRegistered = false
}
private fun isLocationServiceEnabled(): Boolean {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as? LocationManager
?: return false
return try {
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (_: Exception) {
false
}
}
private fun openLocationSettings() {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == requestEnableBluetoothCode) {
pendingEnableBluetoothResult?.success(resultCode == Activity.RESULT_OK)
pendingEnableBluetoothResult = null
}
}
override fun onDestroy() {
cleanupDiscoveryReceiver()
mainHandler.removeCallbacksAndMessages(null)
platformChannel = null
super.onDestroy()
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1,18 @@
<?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>
</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,18 @@
<?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>
</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>

View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=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.14-all.zip

View File

@ -0,0 +1,29 @@
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.11.1" 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 "2.2.20" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"esp32-app-b9329","appId":"1:4585400589:android:cf008c2634f5f47afda844","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"esp32-app-b9329","configurations":{"android":"1:4585400589:android:cf008c2634f5f47afda844"}}}}}}

34
Aplikasi Android/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,24 @@
<?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>
</dict>
</plist>

View File

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

View File

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

View File

@ -0,0 +1,620 @@
// !$*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 */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.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>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.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 */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.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 */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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 = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1;
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.flutterApplication1.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.flutterApplication1.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.flutterApplication1.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterApplication1;
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.flutterApplication1;
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,101 @@
<?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"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
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"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
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,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

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,37 @@
<?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 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="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</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="168" height="185"/>
</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>

View File

@ -0,0 +1,70 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Flutter Application 1</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>flutter_application_1</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>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</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>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

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

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_theme.dart';
import 'package:flutter_application_1/app/app_lifecycle_handler.dart';
import 'package:flutter_application_1/features/splash/splash_page.dart';
class SkateboardApp extends StatelessWidget {
const SkateboardApp({super.key});
@override
Widget build(BuildContext context) {
return AppLifecycleHandler(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ESP32 APP',
theme: AppTheme.light,
home: const SplashPage(),
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
class AppLifecycleHandler extends StatefulWidget {
final Widget child;
const AppLifecycleHandler({
super.key,
required this.child,
});
@override
State<AppLifecycleHandler> createState() => _AppLifecycleHandlerState();
}
class _AppLifecycleHandlerState extends State<AppLifecycleHandler>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!mounted) {
return;
}
context.read<AppSessionViewModel>().handleAppLifecycleStateChanged(state);
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/core/enums/activity_access_state_enum.dart';
import 'package:flutter_application_1/features/activity/activity_login_gate_page.dart';
import 'package:flutter_application_1/features/activity/activity_offline_blocked_page.dart';
import 'package:flutter_application_1/features/activity/activity_page.dart';
import 'package:flutter_application_1/features/auth/login_page.dart';
import 'package:flutter_application_1/features/device_connection/pages/connect_device_page.dart';
import 'package:flutter_application_1/features/home/home_page.dart';
import 'package:flutter_application_1/shared/dialogs/confirmation_dialog.dart';
import 'package:flutter_application_1/shared/dialogs/warning_dialog.dart';
class AppShell extends StatefulWidget {
const AppShell({super.key});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late final PageController _pageController;
AppSessionViewModel? _session;
bool _isShowingConnectionLostDialog = false;
Future<bool> _confirmExitToLogin() async {
final session = context.read<AppSessionViewModel>();
await session.setBlockingDialogVisible(true);
if (!mounted) {
return false;
}
final shouldExit = await ConfirmationDialog.show(
context,
title: 'Keluar aplikasi?',
message:
'Jika Anda keluar, koneksi ke skateboard akan diputus dan sesi saat ini akan berakhir.',
confirmText: 'Keluar',
cancelText: 'Batal',
isDestructive: true,
);
if (!mounted) {
return false;
}
await session.setBlockingDialogVisible(false);
if (!shouldExit || !mounted) {
return false;
}
await session.disconnectAndEndSession();
if (!mounted) {
return false;
}
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
return true;
}
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: 0);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
context.read<AppSessionViewModel>().setControlSurfaceActive(true);
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final newSession = context.read<AppSessionViewModel>();
if (_session == newSession) {
return;
}
_session?.removeListener(_handleSessionStateChanged);
_session = newSession;
_session?.addListener(_handleSessionStateChanged);
}
Future<void> _handleSessionStateChanged() async {
final session = _session;
if (!mounted || session == null || _isShowingConnectionLostDialog) {
return;
}
if (!session.shouldShowConnectionLostDialog) {
return;
}
_isShowingConnectionLostDialog = true;
final message = session.connectionLostDialogMessage;
session.consumeConnectionLostDialog();
await session.setBlockingDialogVisible(true);
if (!mounted) {
_isShowingConnectionLostDialog = false;
return;
}
await WarningDialog.show(
context,
title: 'Koneksi terputus',
message: message,
buttonText: 'Ke Connect Device',
);
if (!mounted) {
_isShowingConnectionLostDialog = false;
return;
}
await session.setBlockingDialogVisible(false);
if (!mounted) {
_isShowingConnectionLostDialog = false;
return;
}
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const ConnectDevicePage()),
(route) => false,
);
_isShowingConnectionLostDialog = false;
}
@override
void dispose() {
_session?.removeListener(_handleSessionStateChanged);
_session?.setControlSurfaceActive(false);
_pageController.dispose();
super.dispose();
}
Widget _buildActivityArea(ActivityAccessState state) {
switch (state) {
case ActivityAccessState.unauthenticated:
return const ActivityLoginGatePage();
case ActivityAccessState.offlineBlocked:
return const ActivityOfflineBlockedPage();
case ActivityAccessState.ready:
return const ActivityPage();
}
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
return PopScope<Object?>(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final currentPage = _pageController.hasClients
? (_pageController.page ?? 0).round()
: 0;
if (currentPage != 0) {
await _pageController.animateToPage(
0,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
);
return;
}
await _confirmExitToLogin();
},
child: Scaffold(
body: PageView(
controller: _pageController,
children: [
HomePage(onExitRequested: _confirmExitToLogin),
_buildActivityArea(session.activityAccessState),
],
),
),
);
}
}

View File

@ -0,0 +1,5 @@
enum ActivityAccessState {
unauthenticated,
offlineBlocked,
ready,
}

View File

@ -0,0 +1,5 @@
enum AppMode {
guestOffline,
signedInOnline,
signedInOffline,
}

View File

@ -0,0 +1,10 @@
enum DeviceConnectionState {
initial,
autoReconnecting,
scanning,
connecting,
handshaking,
connected,
disconnected,
reconnectRequired,
}

View File

@ -0,0 +1,5 @@
enum RecordingState {
idle,
recording,
stopping,
}

View File

@ -0,0 +1,5 @@
enum RideMode {
eco,
normal,
boost,
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class AppColors {
static const Color white = Color(0xFFFFFFFF);
static const Color primaryBlue = Color(0xFF2563FF);
static const Color primaryBlueDark = Color(0xFF1F56F0);
static const Color textDark = Color(0xFF111827);
static const Color textMuted = Color(0xFF9CA3AF);
static const Color inputBorder = Color(0xFFE5E7EB);
static const Color shadow = Color(0x14000000);
static const Color green = Color(0xFF16A34A);
static const Color red = Color(0xFFDC2626);
static const Color orange = Color(0xFFF59E0B);
static const Color lightBlue = Color(0xFFF3F7FF);
static const Color divider = Color(0xFFF1F5F9);
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static ThemeData get light {
final base = ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: AppColors.white,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primaryBlue,
surface: AppColors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.white,
elevation: 0,
surfaceTintColor: Colors.transparent,
),
);
final textTheme = GoogleFonts.montserratTextTheme(base.textTheme).apply(
bodyColor: AppColors.textDark,
displayColor: AppColors.textDark,
);
return base.copyWith(
textTheme: textTheme,
primaryTextTheme: GoogleFonts.montserratTextTheme(base.primaryTextTheme).apply(
bodyColor: AppColors.textDark,
displayColor: AppColors.textDark,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
textStyle: GoogleFonts.montserrat(fontWeight: FontWeight.w700),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
textStyle: GoogleFonts.montserrat(fontWeight: FontWeight.w700),
),
),
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/auth/login_page.dart';
import 'package:flutter_application_1/shared/widgets/primary_button.dart';
class ActivityLoginGatePage extends StatelessWidget {
const ActivityLoginGatePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.lock_outline,
size: 68,
color: AppColors.primaryBlue,
),
const SizedBox(height: 18),
const Text(
'Login to access Activity',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 10),
const Text(
'Activity history and ride recording are only available for signed-in users.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppColors.textMuted,
),
),
const SizedBox(height: 24),
PrimaryButton(
text: 'GO TO LOGIN',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LoginPage()),
);
},
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class ActivityOfflineBlockedPage extends StatelessWidget {
const ActivityOfflineBlockedPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.cloud_off_outlined,
size: 68,
color: AppColors.orange,
),
SizedBox(height: 18),
Text(
'Activity unavailable',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
SizedBox(height: 10),
Text(
'Reconnect internet and device connection to access activity and ride history.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppColors.textMuted,
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,653 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/activity/ride_session_model.dart';
import 'package:flutter_application_1/shared/dialogs/confirmation_dialog.dart';
import 'package:flutter_application_1/shared/dialogs/session_name_dialog.dart';
import 'package:provider/provider.dart';
String _formatRideDistance(double distanceKm) {
final meters = distanceKm * 1000.0;
if (meters < 1000.0) {
if (meters < 10.0) {
return '${meters.toStringAsFixed(2)} m';
}
if (meters < 100.0) {
return '${meters.toStringAsFixed(1)} m';
}
return '${meters.toStringAsFixed(0)} m';
}
return '${distanceKm.toStringAsFixed(2)} km';
}
class ActivityPage extends StatefulWidget {
const ActivityPage({super.key});
@override
State<ActivityPage> createState() => _ActivityPageState();
}
class _ActivityPageState extends State<ActivityPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
context.read<AppSessionViewModel>().loadActivityData();
});
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Activity',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
color: AppColors.textDark,
),
),
const SizedBox(height: 20),
if (session.activityErrorMessage != null) ...[
_ActivityErrorBanner(
message: session.activityErrorMessage!,
onDismiss: session.clearActivityError,
),
const SizedBox(height: 16),
],
session.isRecordingActive
? _ActiveRecordingCard(
durationText: _formatDuration(session.activeRecordingDuration),
distanceText: _formatRideDistance(session.recordingDistanceKm),
onStop: session.isActivityBusy
? null
: () => _showSessionNameDialog(context, session),
)
: _StartRideCard(
onStart: session.isActivityBusy
? null
: () => session.startRideRecording(),
),
if (!session.isRecordingActive) ...[
const SizedBox(height: 16),
_SummaryCards(
totalDistanceKm: session.totalDistanceKmRaw,
totalRides: session.totalRecordings,
),
],
const SizedBox(height: 22),
Row(
children: [
const Expanded(
child: Text(
'Recent Sessions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
),
if (!session.isRecordingActive)
TextButton(
onPressed: session.isActivityBusy
? null
: () => session.loadActivityData(force: true),
child: const Text('Refresh'),
),
],
),
const SizedBox(height: 12),
if (session.isActivityBusy && session.recentRideSessions.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: CircularProgressIndicator(),
),
)
else if (session.recentRideSessions.isEmpty)
const _EmptySessionsState()
else
...session.recentRideSessions.map(
(item) => _SessionHistoryItem(
item: item,
onDelete: session.isActivityBusy
? null
: () => _confirmDeleteSession(context, session, item),
),
),
],
),
),
),
);
}
Future<void> _showSessionNameDialog(
BuildContext context,
AppSessionViewModel session,
) async {
await session.setBlockingDialogVisible(true);
if (!context.mounted) {
return;
}
final sessionName = await SessionNameDialog.show(context);
if (!context.mounted) {
return;
}
await session.setBlockingDialogVisible(false);
if (!context.mounted) {
return;
}
if (sessionName == null) {
return;
}
final normalizedName = sessionName.trim().isEmpty
? 'Recording Session'
: sessionName.trim();
await session.stopRideRecordingAndSave(sessionName: normalizedName);
}
Future<void> _confirmDeleteSession(
BuildContext context,
AppSessionViewModel session,
RideSessionModel item,
) async {
await session.setBlockingDialogVisible(true);
if (!context.mounted) {
return;
}
final shouldDelete = await ConfirmationDialog.show(
context,
title: 'Hapus session?',
message:
'Session "${item.title}" akan dihapus dari riwayat perjalanan Anda.',
confirmText: 'Hapus',
cancelText: 'Batal',
isDestructive: true,
);
if (!context.mounted) {
return;
}
await session.setBlockingDialogVisible(false);
if (!context.mounted) {
return;
}
if (!shouldDelete) {
return;
}
await session.deleteRideSession(item);
}
static String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}
class _ActivityErrorBanner extends StatelessWidget {
final String message;
final VoidCallback onDismiss;
const _ActivityErrorBanner({
required this.message,
required this.onDismiss,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFFF1F2),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFECDD3)),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: AppColors.red),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
color: AppColors.textDark,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: onDismiss,
icon: const Icon(Icons.close, color: AppColors.textMuted),
),
],
),
);
}
}
class _StartRideCard extends StatelessWidget {
final VoidCallback? onStart;
const _StartRideCard({required this.onStart});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.primaryBlue, AppColors.primaryBlueDark],
),
borderRadius: BorderRadius.circular(24),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Start Recording',
style: TextStyle(
color: AppColors.white,
fontSize: 22,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
const Text(
'Mulai recording kapan saja untuk menyimpan jarak dan durasi session yang sedang berjalan.',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: onStart,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.white,
foregroundColor: AppColors.primaryBlue,
elevation: 0,
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: const Text(
'START RECORDING',
style: TextStyle(fontWeight: FontWeight.w800),
),
),
],
),
);
}
}
class _ActiveRecordingCard extends StatelessWidget {
final String durationText;
final String distanceText;
final VoidCallback? onStop;
const _ActiveRecordingCard({
required this.durationText,
required this.distanceText,
required this.onStop,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFDDE7FF)),
boxShadow: const [
BoxShadow(
color: AppColors.shadow,
blurRadius: 16,
offset: Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: AppColors.primaryBlue,
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
const Text(
'Recording Active',
style: TextStyle(
color: AppColors.textDark,
fontSize: 22,
fontWeight: FontWeight.w800,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _RecordingMetric(
label: 'Active Time',
value: durationText,
),
),
const SizedBox(width: 12),
Expanded(
child: _RecordingMetric(
label: 'Active Distance',
value: distanceText,
),
),
],
),
const SizedBox(height: 18),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onStop,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.white,
foregroundColor: AppColors.primaryBlue,
elevation: 0,
side: const BorderSide(color: AppColors.inputBorder),
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: const Text(
'STOP RECORDING',
style: TextStyle(fontWeight: FontWeight.w800),
),
),
),
],
),
);
}
}
class _RecordingMetric extends StatelessWidget {
final String label;
final String value;
const _RecordingMetric({
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textMuted,
),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: AppColors.textDark,
),
),
],
),
);
}
}
class _SummaryCards extends StatelessWidget {
final double totalDistanceKm;
final int totalRides;
const _SummaryCards({
required this.totalDistanceKm,
required this.totalRides,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: _SummaryCard(
title: 'Total Distance',
value: _formatRideDistance(totalDistanceKm),
icon: Icons.route,
),
),
const SizedBox(width: 12),
Expanded(
child: _SummaryCard(
title: 'Total Sessions',
value: '$totalRides',
icon: Icons.history,
),
),
],
);
}
}
class _SummaryCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
const _SummaryCard({
required this.title,
required this.value,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: AppColors.primaryBlue),
const SizedBox(height: 10),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w700,
color: AppColors.textMuted,
),
),
const SizedBox(height: 6),
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: AppColors.textDark,
),
),
],
),
);
}
}
class _SessionHistoryItem extends StatelessWidget {
final RideSessionModel item;
final VoidCallback? onDelete;
const _SessionHistoryItem({
required this.item,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.inputBorder),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 6),
Text(
_formatDate(item.startedAt),
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 12,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatRideDistance(item.distanceKmRaw),
style: const TextStyle(
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 6),
Text(
_formatHistoryDuration(item.durationSec),
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 12,
),
),
],
),
const SizedBox(width: 10),
IconButton(
onPressed: onDelete,
icon: const Icon(Icons.delete_outline),
),
],
),
);
}
static String _formatDate(DateTime date) {
final year = date.year.toString().padLeft(4, '0');
final month = date.month.toString().padLeft(2, '0');
final day = date.day.toString().padLeft(2, '0');
return '$year-$month-$day';
}
static String _formatHistoryDuration(int durationSec) {
final duration = Duration(seconds: durationSec);
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours}h ${minutes.toString().padLeft(2, '0')}m';
}
return '${minutes.toString().padLeft(2, '0')}m ${seconds.toString().padLeft(2, '0')}s';
}
}
class _EmptySessionsState extends StatelessWidget {
const _EmptySessionsState();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.inputBorder),
),
child: const Text(
'Belum ada recording session yang tersimpan.',
style: TextStyle(
color: AppColors.textMuted,
fontWeight: FontWeight.w600,
),
),
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class RideSessionModel {
final String id;
final String title;
final DateTime startedAt;
final DateTime endedAt;
final int durationSec;
final double distanceKmRaw;
const RideSessionModel({
required this.id,
required this.title,
required this.startedAt,
required this.endedAt,
required this.durationSec,
required this.distanceKmRaw,
});
factory RideSessionModel.fromFirestore(
DocumentSnapshot<Map<String, dynamic>> doc,
) {
final data = doc.data() ?? <String, dynamic>{};
return RideSessionModel(
id: doc.id,
title: (data['title'] as String?)?.trim().isNotEmpty == true
? (data['title'] as String).trim()
: 'Ride Session',
startedAt: _readDateTime(data['startedAt']) ?? DateTime.now(),
endedAt: _readDateTime(data['endedAt']) ?? DateTime.now(),
durationSec: (data['durationSec'] as num?)?.toInt() ?? 0,
distanceKmRaw: (data['distanceKmRaw'] as num?)?.toDouble() ?? 0.0,
);
}
Map<String, dynamic> toFirestoreMap() {
return <String, dynamic>{
'title': title,
'startedAt': startedAt,
'endedAt': endedAt,
'durationSec': durationSec,
'distanceKmRaw': distanceKmRaw,
};
}
static DateTime? _readDateTime(Object? raw) {
if (raw is Timestamp) {
return raw.toDate();
}
if (raw is DateTime) {
return raw;
}
return null;
}
}

View File

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/auth/signup_page.dart';
import 'package:flutter_application_1/features/device_connection/pages/connect_device_page.dart';
import 'package:flutter_application_1/shared/widgets/brand_logo.dart';
import 'package:flutter_application_1/shared/widgets/custom_text_field.dart';
import 'package:flutter_application_1/shared/widgets/primary_button.dart';
import 'package:flutter_application_1/shared/widgets/secondary_button.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key, this.successMessage});
final String? successMessage;
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void initState() {
super.initState();
if (widget.successMessage != null && widget.successMessage!.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_showMessageWithMessenger(
ScaffoldMessenger.of(context),
widget.successMessage!,
);
});
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _goToConnectWithNavigator(NavigatorState navigator) {
navigator.pushReplacement(
MaterialPageRoute(builder: (_) => const ConnectDevicePage()),
);
}
Future<void> _handleLogin(BuildContext context) async {
FocusScope.of(context).unfocus();
final session = context.read<AppSessionViewModel>();
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) {
_showMessageWithMessenger(messenger, 'Email dan password wajib diisi.');
return;
}
final success = await session.loginWithEmailPassword(
email: email,
password: password,
);
if (!mounted) {
return;
}
if (success) {
_goToConnectWithNavigator(navigator);
return;
}
_showMessageWithMessenger(
messenger,
session.authErrorMessage ?? 'Login gagal. Coba lagi.',
);
}
void _handleOfflineMode(BuildContext context) {
FocusScope.of(context).unfocus();
final session = context.read<AppSessionViewModel>();
final navigator = Navigator.of(context);
session.continueOfflineMode();
_goToConnectWithNavigator(navigator);
}
void _showMessageWithMessenger(
ScaffoldMessengerState messenger,
String message,
) {
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
return Scaffold(
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth =
constraints.maxWidth > 430 ? 430 : constraints.maxWidth;
final double horizontalPadding = maxWidth < 360 ? 16 : 24;
final double logoWidth = maxWidth < 360 ? 220 : 250;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 430),
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: 16,
),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 32,
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(child: BrandLogo(width: logoWidth)),
const SizedBox(height: 46),
CustomTextField(
hintText: 'Email Address',
prefixIcon: Icons.mail_outline,
keyboardType: TextInputType.emailAddress,
controller: _emailController,
),
const SizedBox(height: 14),
CustomTextField(
hintText: 'Password',
prefixIcon: Icons.lock_outline,
obscureText: true,
controller: _passwordController,
),
const SizedBox(height: 24),
PrimaryButton(
text: 'LOGIN',
isLoading: session.isAuthBusy,
onPressed: () => _handleLogin(context),
),
const SizedBox(height: 28),
SecondaryButton(
text: 'Continue in Offline Mode',
enabled: !session.isAuthBusy,
onPressed: () => _handleOfflineMode(context),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Don't have an account?",
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 6),
GestureDetector(
onTap: session.isAuthBusy
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const SignUpPage(),
),
);
},
child: Text(
'Sign Up',
style: TextStyle(
color: session.isAuthBusy
? AppColors.textMuted
: AppColors.primaryBlue,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
],
),
],
),
),
),
),
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/auth/login_page.dart';
import 'package:flutter_application_1/shared/widgets/brand_logo.dart';
import 'package:flutter_application_1/shared/widgets/custom_text_field.dart';
import 'package:flutter_application_1/shared/widgets/primary_button.dart';
class SignUpPage extends StatefulWidget {
const SignUpPage({super.key});
@override
State<SignUpPage> createState() => _SignUpPageState();
}
class _SignUpPageState extends State<SignUpPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _handleCreateAccount(BuildContext context) async {
FocusScope.of(context).unfocus();
final session = context.read<AppSessionViewModel>();
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
final email = _emailController.text.trim();
final password = _passwordController.text;
final confirmPassword = _confirmPasswordController.text;
if (email.isEmpty || password.isEmpty || confirmPassword.isEmpty) {
_showMessageWithMessenger(messenger, 'Semua field wajib diisi.');
return;
}
if (password.length < 6) {
_showMessageWithMessenger(messenger, 'Password minimal 6 karakter.');
return;
}
if (password != confirmPassword) {
_showMessageWithMessenger(messenger, 'Confirm password tidak sama.');
return;
}
final success = await session.createAccountWithEmailPassword(
email: email,
password: password,
);
if (!mounted) {
return;
}
if (success) {
await session.logoutToLogin();
if (!mounted) {
return;
}
navigator.pushAndRemoveUntil(
MaterialPageRoute(
builder: (_) => const LoginPage(
successMessage: 'Akun berhasil dibuat. Silakan login.',
),
),
(route) => false,
);
return;
}
_showMessageWithMessenger(
messenger,
session.authErrorMessage ?? 'Gagal membuat akun. Coba lagi.',
);
}
void _showMessageWithMessenger(
ScaffoldMessengerState messenger,
String message,
) {
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
return Scaffold(
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth =
constraints.maxWidth > 430 ? 430 : constraints.maxWidth;
final double horizontalPadding = maxWidth < 360 ? 16 : 24;
final double logoWidth = maxWidth < 360 ? 220 : 250;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 430),
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: 16,
),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 32,
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(child: BrandLogo(width: logoWidth)),
const SizedBox(height: 46),
CustomTextField(
hintText: 'Email Address',
prefixIcon: Icons.mail_outline,
keyboardType: TextInputType.emailAddress,
controller: _emailController,
),
const SizedBox(height: 14),
CustomTextField(
hintText: 'Password',
prefixIcon: Icons.lock_outline,
obscureText: true,
controller: _passwordController,
),
const SizedBox(height: 14),
CustomTextField(
hintText: 'Confirm Password',
prefixIcon: Icons.check_circle_outline,
obscureText: true,
controller: _confirmPasswordController,
),
const SizedBox(height: 24),
PrimaryButton(
text: 'CREATE ACCOUNT',
isLoading: session.isAuthBusy,
onPressed: () => _handleCreateAccount(context),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Already have an account?',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 6),
GestureDetector(
onTap: session.isAuthBusy
? null
: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const LoginPage(),
),
);
},
child: Text(
'Login',
style: TextStyle(
color: session.isAuthBusy
? AppColors.textMuted
: AppColors.primaryBlue,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
],
),
],
),
),
),
),
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class PlaceholderPage extends StatelessWidget {
final String title;
const PlaceholderPage({
super.key,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.white,
elevation: 0,
surfaceTintColor: Colors.transparent,
title: Text(
title,
style: const TextStyle(
color: AppColors.textDark,
fontWeight: FontWeight.w700,
),
),
iconTheme: const IconThemeData(color: AppColors.textDark),
),
body: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
),
);
}
}

View File

@ -0,0 +1,18 @@
class BluetoothDeviceModel {
const BluetoothDeviceModel({
required this.name,
required this.address,
});
final String name;
final String address;
String get displayName => name.trim().isEmpty ? address : name;
factory BluetoothDeviceModel.fromMap(Map<String, dynamic> map) {
return BluetoothDeviceModel(
name: (map['name'] ?? '').toString(),
address: (map['address'] ?? '').toString(),
);
}
}

View File

@ -0,0 +1,343 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/app/app_shell.dart';
import 'package:flutter_application_1/core/enums/device_connection_state_enum.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/auth/login_page.dart';
import 'package:flutter_application_1/features/device_connection/data/bluetooth_device_model.dart';
import 'package:flutter_application_1/features/device_connection/widgets/connecting_status_view.dart';
import 'package:flutter_application_1/features/device_connection/widgets/reconnect_status_card.dart';
import 'package:flutter_application_1/features/device_connection/widgets/scan_devices_list.dart';
import 'package:flutter_application_1/shared/dialogs/warning_dialog.dart';
import 'package:provider/provider.dart';
class ConnectDevicePage extends StatefulWidget {
const ConnectDevicePage({super.key});
@override
State<ConnectDevicePage> createState() => _ConnectDevicePageState();
}
class _ConnectDevicePageState extends State<ConnectDevicePage> {
AppSessionViewModel? _session;
bool _navigatingToAppShell = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) {
return;
}
final session = context.read<AppSessionViewModel>();
if (session.deviceConnectionState == DeviceConnectionState.reconnectRequired ||
session.deviceConnectionState == DeviceConnectionState.autoReconnecting) {
await session.reconnectLastDevice();
} else {
session.setConnectFlowInitial();
await _startScan();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final newSession = context.read<AppSessionViewModel>();
if (_session == newSession) {
return;
}
_session?.removeListener(_handleSessionStateChanged);
_session = newSession;
_session?.addListener(_handleSessionStateChanged);
}
@override
void dispose() {
_session?.removeListener(_handleSessionStateChanged);
super.dispose();
}
Future<void> _handleSessionStateChanged() async {
final session = _session;
if (!mounted || session == null || _navigatingToAppShell) {
return;
}
if (session.deviceConnectionState != DeviceConnectionState.connected ||
!session.isDeviceConnected) {
return;
}
_navigatingToAppShell = true;
await Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const AppShell()),
);
_navigatingToAppShell = false;
}
Future<void> _startScan() async {
final session = context.read<AppSessionViewModel>();
final success = await session.scanBluetoothDevices();
if (!mounted || success || session.connectionErrorMessage == null) {
return;
}
await WarningDialog.show(
context,
title: 'Bluetooth bermasalah',
message: session.connectionErrorMessage!,
buttonText: 'Mengerti',
);
}
Future<void> _connectToDevice(BluetoothDeviceModel device) async {
final session = context.read<AppSessionViewModel>();
final result = await session.connectToBluetoothDevice(device);
if (!mounted) {
return;
}
switch (result) {
case DeviceHandshakeResult.success:
return;
case DeviceHandshakeResult.invalidDevice:
await WarningDialog.show(
context,
title: 'Bukan skateboard',
message:
'Perangkat Bluetooth yang dipilih bukan skateboard. Silakan pilih perangkat skateboard yang benar.',
buttonText: 'Mengerti',
);
return;
case DeviceHandshakeResult.failed:
await WarningDialog.show(
context,
title: 'Koneksi gagal',
message: session.connectionErrorMessage ??
'Gagal terhubung ke perangkat Bluetooth. Coba lagi.',
buttonText: 'Mengerti',
);
return;
}
}
Future<void> _goBackToLogin() async {
final session = context.read<AppSessionViewModel>();
await session.logoutToLogin();
if (!mounted) {
return;
}
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginPage()),
);
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
final state = session.deviceConnectionState;
final isScanning = state == DeviceConnectionState.scanning;
final isConnecting = state == DeviceConnectionState.connecting;
final isHandshaking = state == DeviceConnectionState.handshaking;
final isReconnecting = state == DeviceConnectionState.autoReconnecting ||
state == DeviceConnectionState.reconnectRequired;
final selectedDevice = session.selectedBluetoothDevice;
final devices = session.availableBluetoothDevices;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
await _goBackToLogin();
},
child: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(18, 20, 18, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFC9DAFF)),
),
child: const Icon(
Icons.bluetooth,
color: AppColors.primaryBlue,
),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bluetooth',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
SizedBox(height: 4),
Text(
'Turn on Bluetooth and power on your skateboard',
style: TextStyle(
fontSize: 14,
height: 1.45,
color: AppColors.textMuted,
),
),
],
),
),
],
),
const SizedBox(height: 26),
_ScanButton(
isScanning: isScanning,
isBusy: isConnecting || isHandshaking || isReconnecting,
onPressed: isScanning || isConnecting || isHandshaking || isReconnecting
? null
: _startScan,
),
if ((isConnecting || isHandshaking) && selectedDevice != null) ...[
const SizedBox(height: 16),
ConnectingStatusView(
deviceName: selectedDevice.displayName,
isHandshaking: isHandshaking,
),
],
if (isReconnecting) ...[
const SizedBox(height: 16),
const ReconnectStatusCard(
title: 'Trying to reconnect to your skateboard',
message:
'We are retrying the last known skateboard connection. Control remains disabled until the connection becomes valid again.',
),
],
const SizedBox(height: 20),
const Text(
'AVAILABLE DEVICES',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
letterSpacing: 0.5,
color: AppColors.textMuted,
),
),
const SizedBox(height: 14),
if (devices.isEmpty && !isScanning)
Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.inputBorder),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Belum ada perangkat ditemukan.',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
SizedBox(height: 8),
Text(
'Pastikan skateboard atau perangkat Bluetooth di sekitar sudah aktif, lalu tekan tombol utama di atas untuk mencoba lagi.',
style: TextStyle(
fontSize: 14,
height: 1.45,
color: AppColors.textMuted,
),
),
],
),
)
else if (devices.isNotEmpty)
ScanDevicesList(
devices: devices,
selectedDeviceAddress: selectedDevice?.address,
isBusy: isConnecting || isHandshaking,
onDeviceTap: _connectToDevice,
),
if (isScanning) ...[
const SizedBox(height: 6),
const Text(
'Scanning nearby Bluetooth devices...',
style: TextStyle(
fontSize: 13,
color: AppColors.textMuted,
),
),
],
],
),
),
),
),
);
}
}
class _ScanButton extends StatelessWidget {
final bool isScanning;
final bool isBusy;
final VoidCallback? onPressed;
const _ScanButton({
required this.isScanning,
required this.isBusy,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final disabled = isScanning || isBusy;
return SizedBox(
height: 56,
width: double.infinity,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor:
disabled ? const Color(0xFF8EAEFB) : AppColors.primaryBlue,
foregroundColor: AppColors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
isScanning ? 'Scanning...' : 'Scan for Devices',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/features/device_connection/pages/connect_device_page.dart';
import 'package:provider/provider.dart';
class ReconnectDevicePage extends StatefulWidget {
const ReconnectDevicePage({super.key});
@override
State<ReconnectDevicePage> createState() => _ReconnectDevicePageState();
}
class _ReconnectDevicePageState extends State<ReconnectDevicePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
context.read<AppSessionViewModel>().reconnectLastDevice();
});
}
@override
Widget build(BuildContext context) {
return const ConnectDevicePage();
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/features/device_connection/data/bluetooth_device_model.dart';
class BluetoothDeviceTile extends StatelessWidget {
final BluetoothDeviceModel device;
final bool isSelected;
final bool isBusy;
final VoidCallback? onTap;
const BluetoothDeviceTile({
super.key,
required this.device,
this.isSelected = false,
this.isBusy = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final borderColor =
isSelected ? AppColors.primaryBlue : AppColors.inputBorder;
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: isBusy ? null : onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: borderColor,
width: isSelected ? 1.4 : 1.1,
),
boxShadow: const [
BoxShadow(
color: AppColors.shadow,
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
height: 34,
width: 34,
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.bluetooth,
size: 18,
color: AppColors.textMuted,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.displayName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
const SizedBox(height: 2),
Text(
device.address,
style: const TextStyle(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
),
),
if (isBusy)
const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class ConnectingStatusView extends StatelessWidget {
final String deviceName;
final bool isHandshaking;
const ConnectingStatusView({
super.key,
required this.deviceName,
this.isHandshaking = false,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2.4),
),
const SizedBox(width: 14),
Expanded(
child: Text(
isHandshaking
? 'Validating $deviceName...'
: 'Connecting to $deviceName...',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class ReconnectStatusCard extends StatelessWidget {
final String title;
final String message;
const ReconnectStatusCard({
super.key,
required this.title,
required this.message,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2.2),
),
SizedBox(width: 12),
Expanded(
child: Text(
'Reconnecting...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
const SizedBox(height: 6),
Text(
message,
style: const TextStyle(
fontSize: 13,
height: 1.45,
color: AppColors.textMuted,
),
),
],
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/features/device_connection/data/bluetooth_device_model.dart';
import 'package:flutter_application_1/features/device_connection/widgets/bluetooth_device_tile.dart';
class ScanDevicesList extends StatelessWidget {
final List<BluetoothDeviceModel> devices;
final String? selectedDeviceAddress;
final bool isBusy;
final ValueChanged<BluetoothDeviceModel> onDeviceTap;
const ScanDevicesList({
super.key,
required this.devices,
required this.onDeviceTap,
this.selectedDeviceAddress,
this.isBusy = false,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
for (int i = 0; i < devices.length; i++) ...[
BluetoothDeviceTile(
device: devices[i],
isSelected: selectedDeviceAddress == devices[i].address,
isBusy: isBusy && selectedDeviceAddress == devices[i].address,
onTap: () => onDeviceTap(devices[i]),
),
if (i != devices.length - 1) const SizedBox(height: 14),
],
],
);
}
}

View File

@ -0,0 +1,762 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/core/enums/ride_mode_enum.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
import 'package:flutter_application_1/shared/widgets/brand_logo.dart';
import 'package:provider/provider.dart';
class HomePage extends StatelessWidget {
final Future<bool> Function()? onExitRequested;
const HomePage({super.key, this.onExitRequested});
static const Color _pageBackground = Color(0xFFF7F7FA);
static const Color _trackColor = Color(0xFFE3E8F0);
static const Color _cardShadow = Color(0x140F172A);
double _modeMaxSpeed(RideMode mode) {
switch (mode) {
case RideMode.eco:
return 10;
case RideMode.normal:
return 15;
case RideMode.boost:
return 20;
}
}
Color _modeTint(RideMode mode) {
switch (mode) {
case RideMode.eco:
return const Color(0xFFEAF8EF);
case RideMode.normal:
return const Color(0xFFEEF4FF);
case RideMode.boost:
return const Color(0xFFFDEEEE);
}
}
Future<void> _handleExit(BuildContext context) async {
await onExitRequested?.call();
}
@override
Widget build(BuildContext context) {
final session = context.watch<AppSessionViewModel>();
final connected = session.isDeviceConnected;
final speed = session.speedKmh;
final battery = session.batteryPercent;
final batteryVoltage = session.batteryVoltage;
final ping = session.pingMs;
final mode = session.rideMode;
final maxSpeed = _modeMaxSpeed(mode);
final progress = connected && speed != null
? (speed / maxSpeed).clamp(0.0, 1.0)
: 0.0;
final displaySpeed = connected && speed != null ? speed.round().toString() : '--';
final displayBattery = connected && battery != null ? '$battery%' : '--';
final displayBatteryVoltage = connected && batteryVoltage != null
? '${batteryVoltage.toStringAsFixed(1)} V'
: 'Voltage belum tersedia';
final displayPing = connected && ping != null ? '${ping}ms' : '--';
return Scaffold(
backgroundColor: _pageBackground,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BrandLogo(
width: 170,
showSubtitle: false,
centered: false,
),
const SizedBox(height: 6),
Text(
connected ? 'Connected' : 'Disconnected',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: connected ? AppColors.green : AppColors.red,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(999),
boxShadow: const [
BoxShadow(
color: _cardShadow,
blurRadius: 18,
offset: Offset(0, 4),
),
],
),
child: Text(
displayPing,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.textDark,
),
),
),
const SizedBox(width: 8),
InkWell(
borderRadius: BorderRadius.circular(999),
onTap: () => _handleExit(context),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: AppColors.white,
shape: BoxShape.circle,
border: Border.all(
color: connected ? const Color(0xFFF4C7C7) : AppColors.inputBorder,
width: 1.2,
),
boxShadow: const [
BoxShadow(
color: _cardShadow,
blurRadius: 18,
offset: Offset(0, 4),
),
],
),
child: Icon(
Icons.logout_rounded,
color: connected ? AppColors.red : AppColors.textMuted,
size: 20,
),
),
),
],
),
const SizedBox(height: 18),
_InfoCard(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 16),
child: LayoutBuilder(
builder: (context, constraints) {
final dialSize = math.min(constraints.maxWidth - 24, 242.0);
return Center(
child: SizedBox(
width: dialSize,
height: dialSize,
child: Stack(
alignment: Alignment.center,
children: [
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: progress),
duration: const Duration(milliseconds: 100),
curve: Curves.linear,
builder: (context, animatedProgress, _) {
return CustomPaint(
size: Size.square(dialSize),
painter: _SpeedometerPainter(
progress: animatedProgress,
trackColor: _trackColor,
progressColor: AppColors.primaryBlue,
),
);
},
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
displaySpeed,
style: const TextStyle(
fontSize: 56,
height: 1,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 8),
const Text(
'KM/H',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textMuted,
letterSpacing: 0.6,
),
),
],
),
],
),
),
);
},
),
),
const SizedBox(height: 12),
_InfoCard(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'BATTERY',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
letterSpacing: 1.1,
color: AppColors.textMuted,
),
),
const SizedBox(height: 8),
Text(
displayBattery,
style: const TextStyle(
fontSize: 28,
height: 1,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 6),
Text(
displayBatteryVoltage,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
),
),
],
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _ModeButton(
label: 'ECO',
subLabel: '10 KM/H',
selected: mode == RideMode.eco,
enabled: connected,
activeColor: AppColors.green,
activeTint: _modeTint(RideMode.eco),
onTap: () => session.setRideMode(RideMode.eco),
),
),
const SizedBox(width: 8),
Expanded(
child: _ModeButton(
label: 'NORMAL',
subLabel: '15 KM/H',
selected: mode == RideMode.normal,
enabled: connected,
activeColor: AppColors.primaryBlue,
activeTint: _modeTint(RideMode.normal),
onTap: () => session.setRideMode(RideMode.normal),
),
),
const SizedBox(width: 8),
Expanded(
child: _ModeButton(
label: 'BOOST',
subLabel: '20 KM/H',
selected: mode == RideMode.boost,
enabled: connected,
activeColor: AppColors.red,
activeTint: _modeTint(RideMode.boost),
onTap: () => session.setRideMode(RideMode.boost),
),
),
],
),
const SizedBox(height: 12),
_PwmCalibrationCard(
session: session,
enabled: connected,
),
],
),
),
),
);
}
}
class _InfoCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
const _InfoCard({required this.child, required this.padding});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: padding,
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: const [
BoxShadow(
color: HomePage._cardShadow,
blurRadius: 22,
offset: Offset(0, 12),
),
],
),
child: child,
);
}
}
class _ModeButton extends StatelessWidget {
final String label;
final String subLabel;
final bool selected;
final bool enabled;
final Color activeColor;
final Color activeTint;
final VoidCallback onTap;
const _ModeButton({
required this.label,
required this.subLabel,
required this.selected,
required this.enabled,
required this.activeColor,
required this.activeTint,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final background = !enabled
? const Color(0xFFF8FAFC)
: selected
? activeTint
: AppColors.white;
final borderColor = !enabled
? AppColors.inputBorder
: selected
? activeColor
: const Color(0xFFE6EAF0);
final labelColor = !enabled
? AppColors.textMuted
: selected
? activeColor
: AppColors.textDark;
final subLabelColor = !enabled
? AppColors.textMuted
: selected
? activeColor
: const Color(0xFF6B7280);
return InkWell(
borderRadius: BorderRadius.circular(24),
onTap: enabled ? onTap : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 100),
curve: Curves.linear,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: borderColor, width: selected ? 1.6 : 1.0),
boxShadow: const [
BoxShadow(
color: Color(0x0D0F172A),
blurRadius: 12,
offset: Offset(0, 6),
),
],
),
child: Column(
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: labelColor,
),
),
const SizedBox(height: 6),
Text(
subLabel,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: subLabelColor,
),
),
],
),
),
);
}
}
class _PwmCalibrationCard extends StatelessWidget {
final AppSessionViewModel session;
final bool enabled;
const _PwmCalibrationCard({
required this.session,
required this.enabled,
});
String _modeLabel(RideMode mode) {
switch (mode) {
case RideMode.eco:
return 'ECO';
case RideMode.normal:
return 'NORMAL';
case RideMode.boost:
return 'BOOST';
}
}
@override
Widget build(BuildContext context) {
return _InfoCard(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Expanded(
child: Text(
'PWM CALIBRATION',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
color: AppColors.textDark,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFFFF7ED),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFFED7AA)),
),
child: const Text(
'TEMP',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w900,
color: Color(0xFFC2410C),
),
),
),
],
),
const SizedBox(height: 6),
Text(
'Mode aktif: ${_modeLabel(session.rideMode)}. Kalibrasi duty 01023. Auto-rem dimatikan; rem manual dan ESTOP tetap aktif.',
style: const TextStyle(
fontSize: 12,
height: 1.35,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
),
),
const SizedBox(height: 14),
_PwmAdjustRow(
label: 'MIN DUTY',
valueLabel: session.selectedCalibrationMinDutyLabel,
helperText: 'tenaga awal motor',
enabled: enabled,
onMinusLarge: () => session.adjustSelectedMinDuty(-50),
onMinusSmall: () => session.adjustSelectedMinDuty(-10),
onPlusSmall: () => session.adjustSelectedMinDuty(10),
onPlusLarge: () => session.adjustSelectedMinDuty(50),
),
const SizedBox(height: 10),
_PwmAdjustRow(
label: 'MAX DUTY',
valueLabel: session.selectedCalibrationMaxDutyLabel,
helperText: 'batas tenaga maksimal',
enabled: enabled,
onMinusLarge: () => session.adjustSelectedMaxDuty(-50),
onMinusSmall: () => session.adjustSelectedMaxDuty(-10),
onPlusSmall: () => session.adjustSelectedMaxDuty(10),
onPlusLarge: () => session.adjustSelectedMaxDuty(50),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: enabled
? () => session.applySelectedPwmCalibration()
: null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: const Text(
'APPLY PWM',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: enabled ? () => session.triggerEmergencyStop() : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.red,
foregroundColor: AppColors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: const Text(
'ESTOP',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
),
),
),
),
],
),
],
),
);
}
}
class _PwmAdjustRow extends StatelessWidget {
final String label;
final String valueLabel;
final String helperText;
final bool enabled;
final VoidCallback onMinusLarge;
final VoidCallback onMinusSmall;
final VoidCallback onPlusSmall;
final VoidCallback onPlusLarge;
const _PwmAdjustRow({
required this.label,
required this.valueLabel,
required this.helperText,
required this.enabled,
required this.onMinusLarge,
required this.onMinusSmall,
required this.onPlusSmall,
required this.onPlusLarge,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE6EAF0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
color: AppColors.textDark,
),
),
const SizedBox(height: 4),
Text(
helperText,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
),
),
],
),
),
Text(
valueLabel,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
color: AppColors.textDark,
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _PwmStepButton(
label: '-50',
enabled: enabled,
onTap: onMinusLarge,
),
),
const SizedBox(width: 8),
Expanded(
child: _PwmStepButton(
label: '-10',
enabled: enabled,
onTap: onMinusSmall,
),
),
const SizedBox(width: 8),
Expanded(
child: _PwmStepButton(
label: '+10',
enabled: enabled,
onTap: onPlusSmall,
),
),
const SizedBox(width: 8),
Expanded(
child: _PwmStepButton(
label: '+50',
enabled: enabled,
onTap: onPlusLarge,
),
),
],
),
],
),
);
}
}
class _PwmStepButton extends StatelessWidget {
final String label;
final bool enabled;
final VoidCallback onTap;
const _PwmStepButton({
required this.label,
required this.enabled,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: enabled ? onTap : null,
child: Container(
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: enabled ? AppColors.white : const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: enabled ? AppColors.inputBorder : const Color(0xFFE5E7EB),
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
height: 1,
fontWeight: FontWeight.w900,
color: enabled ? AppColors.textDark : AppColors.textMuted,
),
),
),
);
}
}
class _SpeedometerPainter extends CustomPainter {
final double progress;
final Color trackColor;
final Color progressColor;
const _SpeedometerPainter({
required this.progress,
required this.trackColor,
required this.progressColor,
});
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = (size.width / 2) - 18;
const trackWidth = 13.0;
const progressWidth = 15.5;
final rect = Rect.fromCircle(center: center, radius: radius);
final trackPaint = Paint()
..color = trackColor
..style = PaintingStyle.stroke
..strokeWidth = trackWidth
..strokeCap = StrokeCap.round;
final progressPaint = Paint()
..color = progressColor
..style = PaintingStyle.stroke
..strokeWidth = progressWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, -math.pi / 2, math.pi * 2, false, trackPaint);
if (progress <= 0) {
return;
}
final clamped = progress.clamp(0.0, 1.0);
final sweepAngle = math.pi * 2 * clamped;
canvas.drawArc(rect, -math.pi / 2, sweepAngle, false, progressPaint);
}
@override
bool shouldRepaint(covariant _SpeedometerPainter oldDelegate) {
return oldDelegate.progress != progress ||
oldDelegate.trackColor != trackColor ||
oldDelegate.progressColor != progressColor;
}
}

View File

@ -0,0 +1,41 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/features/auth/login_page.dart';
import 'package:flutter_application_1/shared/widgets/brand_logo.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
@override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginPage()),
);
});
}
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
final double logoWidth = screenWidth * 0.48;
return Scaffold(
body: SafeArea(
child: Center(
child: BrandLogo(
width: logoWidth.clamp(180.0, 240.0),
),
),
),
);
}
}

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: 'AIzaSyDOI5vGA7A1tguXTbxxizFo5y2mIFfqb3g',
appId: '1:4585400589:android:cf008c2634f5f47afda844',
messagingSenderId: '4585400589',
projectId: 'esp32-app-b9329',
storageBucket: 'esp32-app-b9329.firebasestorage.app',
);
}

View File

@ -0,0 +1,20 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_application_1/app/app.dart';
import 'package:flutter_application_1/app/app_session_view_model.dart';
import 'package:flutter_application_1/firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(
ChangeNotifierProvider(
create: (_) => AppSessionViewModel(),
child: const SkateboardApp(),
),
);
}

View File

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class ConfirmationDialog extends StatelessWidget {
final String title;
final String message;
final String confirmText;
final String cancelText;
final bool isDestructive;
const ConfirmationDialog({
super.key,
required this.title,
required this.message,
required this.confirmText,
this.cancelText = 'Cancel',
this.isDestructive = false,
});
static Future<bool> show(
BuildContext context, {
required String title,
required String message,
required String confirmText,
String cancelText = 'Cancel',
bool isDestructive = false,
}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (_) => ConfirmationDialog(
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
isDestructive: isDestructive,
),
);
return result ?? false;
}
@override
Widget build(BuildContext context) {
final confirmBackground = isDestructive ? AppColors.red : AppColors.primaryBlue;
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 10),
Text(
message,
style: const TextStyle(
fontSize: 14,
height: 1.5,
color: AppColors.textMuted,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(false),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
side: const BorderSide(color: AppColors.inputBorder),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
cancelText,
style: const TextStyle(
color: AppColors.textDark,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
backgroundColor: confirmBackground,
foregroundColor: AppColors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
confirmText,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class SessionNameDialog extends StatefulWidget {
final String title;
final String hintText;
final String confirmText;
final String cancelText;
const SessionNameDialog({
super.key,
this.title = 'Save Ride Session',
this.hintText = 'Enter session name',
this.confirmText = 'Save',
this.cancelText = 'Cancel',
});
static Future<String?> show(BuildContext context) {
return showDialog<String>(
context: context,
barrierDismissible: true,
builder: (_) => const SessionNameDialog(),
);
}
@override
State<SessionNameDialog> createState() => _SessionNameDialogState();
}
class _SessionNameDialogState extends State<SessionNameDialog> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 10),
const Text(
'Give this ride a name before saving it to your session history.',
style: TextStyle(
fontSize: 14,
height: 1.5,
color: AppColors.textMuted,
),
),
const SizedBox(height: 20),
TextField(
controller: _controller,
autofocus: true,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
hintText: widget.hintText,
filled: true,
fillColor: AppColors.lightBlue,
contentPadding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 18,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: AppColors.inputBorder),
borderRadius: BorderRadius.circular(18),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 1.4,
),
borderRadius: BorderRadius.circular(18),
),
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
side: const BorderSide(color: AppColors.inputBorder),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
widget.cancelText,
style: const TextStyle(
color: AppColors.textDark,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(_controller.text.trim()),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
widget.confirmText,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class WarningDialog extends StatelessWidget {
final String title;
final String message;
final String buttonText;
const WarningDialog({
super.key,
required this.title,
required this.message,
this.buttonText = 'OK',
});
static Future<void> show(
BuildContext context, {
required String title,
required String message,
String buttonText = 'OK',
}) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) => WarningDialog(
title: title,
message: message,
buttonText: buttonText,
),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 52,
width: 52,
decoration: BoxDecoration(
color: const Color(0xFFFEECEC),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.warning_amber_rounded,
color: AppColors.red,
size: 28,
),
),
const SizedBox(height: 18),
Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppColors.textDark,
),
),
const SizedBox(height: 10),
Text(
message,
style: const TextStyle(
fontSize: 14,
height: 1.5,
color: AppColors.textMuted,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
child: Text(
buttonText,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_1/core/theme/app_colors.dart';
class BrandLogo extends StatelessWidget {
final double width;
final bool showSubtitle;
final bool centered;
const BrandLogo({
super.key,
this.width = 230,
this.showSubtitle = true,
this.centered = true,
});
@override
Widget build(BuildContext context) {
final double titleSize = width * 0.19;
final double subtitleSize = width * 0.04;
final textAlign = centered ? TextAlign.center : TextAlign.left;
final crossAxisAlignment =
centered ? CrossAxisAlignment.center : CrossAxisAlignment.start;
return SizedBox(
width: width,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: crossAxisAlignment,
children: [
RichText(
textAlign: textAlign,
text: TextSpan(
children: [
TextSpan(
text: 'ESP32 ',
style: TextStyle(
fontSize: titleSize,
fontWeight: FontWeight.w900,
fontStyle: FontStyle.italic,
color: AppColors.textDark,
letterSpacing: -1,
),
),
TextSpan(
text: 'APP',
style: TextStyle(
fontSize: titleSize,
fontWeight: FontWeight.w900,
fontStyle: FontStyle.italic,
color: AppColors.primaryBlue,
letterSpacing: -1,
),
),
],
),
),
if (showSubtitle) ...[
const SizedBox(height: 6),
Text(
'ADVANCED SKATEBOARD CONTROLLER',
textAlign: textAlign,
style: TextStyle(
fontSize: subtitleSize,
fontWeight: FontWeight.w500,
color: AppColors.textMuted,
letterSpacing: 0.6,
),
),
],
],
),
);
}
}

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