first commit

This commit is contained in:
dyahnax 2026-06-29 13:06:07 +07:00
commit aad6ee8df8
245 changed files with 10891 additions and 0 deletions

2
smart_cycling/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

119
smart_cycling/.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.buildlog/
.history
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock

View File

@ -0,0 +1,19 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/async" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/core" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/html" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/io" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/math" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file://C:\flutter/bin/cache/dart-sdk/lib/typed_data" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,15 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime">
<CLASSES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/flutter_application_1.iml" filepath="$PROJECT_DIR$/flutter_application_1.iml" />
<module fileurl="file://$PROJECT_DIR$/android/flutter_application_1_android.iml" filepath="$PROJECT_DIR$/android/flutter_application_1_android.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method />
</configuration>
</component>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="FileEditorManager">
<leaf>
<file leaf-file-name="main.dart" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/lib/main.dart">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="ToolWindowManager">
<editor active="true" />
<layout>
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
</layout>
</component>
<component name="ProjectView">
<navigator currentView="ProjectPane" proportions="" version="1">
</navigator>
<panes>
<pane id="ProjectPane">
<option name="show-excluded-files" value="false" />
</pane>
</panes>
</component>
<component name="PropertiesComponent">
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="dart.analysis.tool.window.force.activate" value="true" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
</project>

45
smart_cycling/.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "c519ee916eaeb88923e67befb89c0f1dabfa83e6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: android
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: ios
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: linux
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: macos
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: web
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: windows
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
# 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'

13
smart_cycling/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome with API",
"request": "launch",
"type": "chrome",
"url": "http://localhost/smart_cycling_api",
"webRoot": "${workspaceFolder}"
}
]
}

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

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

2
smart_cycling/README.md Normal file
View File

@ -0,0 +1,2 @@
# smart_cycling
Aplikasi Rekomendasi Rute Bersepeda Menggunakan Metode SAW

24
smart_cycling/TODO.md Normal file
View File

@ -0,0 +1,24 @@
# TODO: Fix Koneksi API Laragon di Android Flutter
## Status: Testing Phase
### Step 1: [DONE] Analyze & Plan (api_config.dart IP=192.168.1.7)
### Step 2: [DONE] Update api_config.dart (IP updated + debug print URL)
### Step 3: [DONE] Flutter clean & pub get
### Step 4: [CURRENT] Test & Verify:
- Install/run Android emulator via Android Studio/AVD Manager
- Start Laragon (Apache + MySQL running?)
- Run `flutter run -d android`
- **Cek console Flutter**: Cari "API Base URL: android-emulator -> http://10.0.2.2/..."
- Test login screen - lihat error/snackbar + network log
- Jika masih fail: Coba IP alternatif 192.168.1.7 di config untuk device fisik
### Step 5: [PENDING] Confirm success & cleanup debug code
**Instructions:**
1. Jalankan `flutter devices` untuk lihat available devices/emulator
2. Start emulator jika belum
4. Share console output/error dari login attempt!
Updated files ready. Test sekarang & beri hasil untuk langkah berikutnya.

View File

@ -0,0 +1,31 @@
# 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.
analyzer:
errors:
depend_on_referenced_packages: ignore
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

13
smart_cycling/android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.example.flutter_application_1"
compileSdk = 35
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
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 = 24
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.debug
}
}
}
flutter {
source = "../.."
}

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

View File

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

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: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 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,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return 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.5.1" apply false
id "org.jetbrains.kotlin.android" version "1.9.24" apply false
}
include ":app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

34
smart_cycling/ios/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.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 = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
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,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,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,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>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>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

109
smart_cycling/lib/main.dart Normal file
View File

@ -0,0 +1,109 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:device_preview/device_preview.dart';
import 'dart:convert'; // Untuk mengubah Map ke String
import 'package:shared_preferences/shared_preferences.dart';
import 'package:smart_cycling/screens/home_screen.dart';
import 'package:smart_cycling/screens/login_screen.dart';
import 'package:smart_cycling/screens/register_screen.dart';
import 'package:smart_cycling/screens/splash_screen.dart';
import 'package:smart_cycling/screens/success_screen.dart';
import 'package:smart_cycling/screens/welcome_screen.dart';
import 'package:smart_cycling/screens/edit_profile_screen.dart';
// Variabel database tetap global
Map<String, String> userDatabase = {'admin': '12345'};
// Fungsi untuk menyimpan database ke penyimpanan lokal
Future<void> saveUserDatabase() async {
final prefs = await SharedPreferences.getInstance();
String encodedData = jsonEncode(userDatabase); // Ubah Map ke teks JSON
await prefs.setString('local_users', encodedData);
}
// Fungsi untuk mengambil database dari penyimpanan lokal saat aplikasi dibuka
Future<void> loadUserDatabase() async {
final prefs = await SharedPreferences.getInstance();
String? encodedData = prefs.getString('local_users');
if (encodedData != null) {
userDatabase = Map<String, String>.from(jsonDecode(encodedData));
}
}
void main() async {
// Pastikan sistem Flutter siap sebelum memuat data
WidgetsFlutterBinding.ensureInitialized();
await loadUserDatabase(); // Ambil data lama sebelum aplikasi jalan
runApp(
DevicePreview(
enabled: !kReleaseMode,
builder: (context) => const MyApp(),
),
);
}
// State Global untuk Dark Mode
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.light);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ThemeMode>(
valueListenable: themeNotifier,
builder: (_, ThemeMode currentMode, __) {
return MaterialApp(
locale: DevicePreview.locale(context),
builder: DevicePreview.appBuilder,
title: 'Smart Cycling',
themeMode: currentMode,
// Tema Terang
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
fontFamily: 'Poppins',
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: Colors.black),
titleTextStyle: TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, fontFamily: 'Poppins'),
),
),
// Tema Gelap
darkTheme: ThemeData(
scaffoldBackgroundColor: const Color(0xFF121212),
fontFamily: 'Poppins',
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
surface: const Color(0xFF1E1E1E),
),
useMaterial3: true,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
titleTextStyle: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, fontFamily: 'Poppins'),
),
),
debugShowCheckedModeBanner: false,
initialRoute: '/splash',
routes: {
'/splash': (context) => const SplashScreen(),
'/welcome': (context) => const WelcomeScreen(),
'/login': (context) => const LoginScreen(),
'/register': (context) => const RegisterScreen(),
'/success': (context) => const SuccessScreen(),
'/home': (context) => const HomeScreen(),
'/edit_profile': (context) => const EditProfileScreen(),
},
);
},
);
}
}

View File

@ -0,0 +1,271 @@
import 'dart:ui';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../utils/api_config.dart';
import '../utils/session_manager.dart';
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({super.key});
@override
State<EditProfileScreen> createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _usernameController;
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: SessionManager.userName);
_usernameController = TextEditingController(text: SessionManager.userEmail);
}
Future<void> _updateProfile() async {
// 1. Validasi Dasar
if (!_formKey.currentState!.validate()) return;
// 2. Cek apakah ada perubahan
bool isNameChanged = _nameController.text != SessionManager.userName;
bool isUsernameChanged = _usernameController.text != SessionManager.userEmail;
bool isPasswordChanged = _passwordController.text.isNotEmpty;
if (!isNameChanged && !isUsernameChanged && !isPasswordChanged) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Tidak ada perubahan yang dilakukan.")),
);
return;
}
// 3. Validasi Password
if (isPasswordChanged) {
if (_passwordController.text != _confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Konfirmasi password tidak cocok!")),
);
return;
}
if (_passwordController.text.length < 5) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Password minimal 5 karakter!")),
);
return;
}
}
setState(() => _isLoading = true);
try {
final response = await http.post(
Uri.parse("${ApiConfig.baseUrl}/edit_profile.php"),
body: {
'id_users': SessionManager.userId.toString(),
'nama': _nameController.text,
'username': _usernameController.text,
'password': _passwordController.text,
},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
SessionManager.saveSession(
SessionManager.userId!,
_nameController.text,
_usernameController.text,
SessionManager.userPhoto,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Profil berhasil diperbarui!")),
);
Navigator.pop(context, true);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(data['message'] ?? "Terjadi kesalahan")),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Tidak terhubung ke server: $e")),
);
}
}
if (mounted) setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
// Latar belakang gradien agar efek kaca terlihat nyata
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 20.0),
child: Column(
children: [
const SizedBox(height: 20),
// Header Icon
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.manage_accounts, size: 60, color: Colors.white),
),
const SizedBox(height: 16),
const Text(
"Perbarui Profil",
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: Colors.white),
),
const Text(
"Kelola informasi akun Anda",
style: TextStyle(fontSize: 14, color: Colors.white70),
),
const SizedBox(height: 40),
// FORM DENGAN EFEK GLASSMORPHISM
ClipRRect(
borderRadius: BorderRadius.circular(30),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
padding: const EdgeInsets.all(28.0),
decoration: BoxDecoration(
color: isDark
? Colors.black.withOpacity(0.3)
: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5),
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("NAMA", isDark),
_buildTextField(_nameController, Icons.person_outline, "Nama Anda", isDark),
const SizedBox(height: 20),
_buildLabel("USERNAME", isDark),
_buildTextField(_usernameController, Icons.alternate_email, "Username Anda", isDark),
const SizedBox(height: 20),
_buildLabel("PASSWORD BARU", isDark),
_buildTextField(_passwordController, Icons.lock_outline, "Minimal 5 karakter", isDark, isPassword: true),
const SizedBox(height: 20),
// KONFIRMASI PASSWORD (Tampil hanya jika password diisi)
_buildLabel("KONFIRMASI PASSWORD", isDark),
_buildTextField(_confirmPasswordController, Icons.lock_reset, "Ulangi password", isDark, isPassword: true),
const SizedBox(height: 40),
// TOMBOL SIMPAN (IKUT GLASS STYLE)
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading ? null : _updateProfile,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF3949AB),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 0,
),
child: _isLoading
? const CircularProgressIndicator(color: Color(0xFF3949AB))
: const Text(
"SIMPAN PERUBAHAN",
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1),
),
),
),
],
),
),
),
),
),
const SizedBox(height: 40),
],
),
),
),
// Tombol Back Custom (Dipindah ke paling bawah agar di atas lapisan lain)
Positioned(
top: 50,
left: 20,
child: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
],
),
),
);
}
Widget _buildLabel(String text, bool isDark) {
return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
text,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white70, letterSpacing: 1.2),
),
);
}
Widget _buildTextField(TextEditingController controller, IconData icon, String hint, bool isDark, {bool isPassword = false}) {
return TextFormField(
controller: controller,
obscureText: isPassword,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 14),
prefixIcon: Icon(icon, color: Colors.white70, size: 22),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: Colors.white.withOpacity(0.3)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Colors.white, width: 2),
),
),
);
}
}

View File

@ -0,0 +1,269 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../widgets/responsive_layout.dart';
class HistoryDetailScreen extends StatelessWidget {
final Map<String, dynamic> historyItem;
const HistoryDetailScreen({super.key, required this.historyItem});
String _formatTime(int seconds) {
bool isNegative = seconds < 0;
int absSecs = seconds.abs();
int m = absSecs ~/ 60;
int s = absSecs % 60;
String result = "${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
return isNegative ? "+$result" : result;
}
String _formatDate(String dateStr) {
try {
DateTime dt = DateTime.parse(dateStr);
return DateFormat('dd MMMM yyyy, HH:mm', 'id_ID').format(dt);
} catch (e) {
return dateStr;
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
// Parsing Data
double totalDistance = double.tryParse(historyItem['jarak'].toString().replaceAll(RegExp(r'[^0-9.]'), '')) ?? 0;
double traveledDistance = double.tryParse(historyItem['jarak_tempuh'].toString()) ?? 0;
double remainingDistance = totalDistance - traveledDistance;
if (remainingDistance < 0) remainingDistance = 0;
int timeSpentSeconds = int.tryParse(historyItem['waktu_tempuh'].toString()) ?? 0;
int targetTimeSeconds = int.tryParse(historyItem['target_waktu'].toString()) ?? 0;
// Sisa waktu = Target - Terpakai.
// Jika positif (misal 10 - 8 = 2) -> Bagus.
// Jika negatif (misal 10 - 13 = -3) -> Telat, diformat jadi +03:00 di _formatTime.
int remainingTimeSeconds = targetTimeSeconds - timeSpentSeconds;
String speed = historyItem['kecepatan_rata_rata']?.toString() ?? "0";
String status = historyItem['status']?.toString().toUpperCase() ?? "GAGAL";
bool isSuccess = status == "BERHASIL";
return ResponsiveLayout(
child: Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
appBar: AppBar(
title: const Text(
"Detail Perjalanan",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A237E), Color(0xFF121212)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// CARD STATUS
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isSuccess
? [const Color(0xFF11998e), const Color(0xFF38ef7d)]
: [const Color(0xFFcb2d3e), const Color(0xFFef473a)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: (isSuccess ? Colors.green : Colors.red).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
)
]
),
child: Column(
children: [
Icon(
isSuccess ? Icons.check_circle_outline : Icons.error_outline,
size: 64,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
isSuccess ? "MISI BERHASIL" : "MISI GAGAL",
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: 8),
Text(
_formatDate(historyItem['tanggal'] ?? ''),
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
const SizedBox(height: 24),
// INFORMASI RUTE
_buildSectionHeader("Informasi Rute", isDark),
_buildDetailTile(
icon: Icons.directions_bike,
title: "Nama Rute",
value: historyItem['nama_rute'] ?? '-',
isDark: isDark,
),
_buildDetailTile(
icon: Icons.straighten,
title: "Jarak Rute",
value: "${historyItem['jarak'] ?? '-'} Km",
isDark: isDark,
),
_buildDetailTile(
icon: Icons.terrain,
title: "Kondisi Medan",
value: historyItem['kondisi_medan'] ?? '-',
isDark: isDark,
),
_buildDetailTile(
icon: Icons.hourglass_top,
title: "Target Waktu Perjalanan",
value: targetTimeSeconds > 0 ? _formatTime(targetTimeSeconds) : "-",
isDark: isDark,
),
const SizedBox(height: 24),
// DETAIL METRIK
_buildSectionHeader("Metrik Perjalanan", isDark),
_buildDetailTile(
icon: Icons.straighten,
title: "Jarak yang ditempuh",
value: "${traveledDistance.toStringAsFixed(2)} km",
subtitle: "Sisa Jarak : ${remainingDistance.toStringAsFixed(2)} km",
isDark: isDark,
),
_buildDetailTile(
icon: Icons.timer,
title: "Waktu yang ditempuh",
value: _formatTime(timeSpentSeconds),
subtitle: "Sisa Waktu : ${_formatTime(remainingTimeSeconds)}",
subtitleColor: remainingTimeSeconds < 0 ? Colors.redAccent : Colors.blueAccent,
isDark: isDark,
),
_buildDetailTile(
icon: Icons.speed,
title: "Kecepatan Rata-rata",
value: "$speed km/jam",
isDark: isDark,
),
],
),
),
),
),
),
);
}
Widget _buildSectionHeader(String title, bool isDark) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0, left: 4.0),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white70,
),
),
);
}
Widget _buildDetailTile({
required IconData icon,
required String title,
required String value,
String? subtitle,
Color? subtitleColor,
required bool isDark,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blueAccent.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.blueAccent, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Colors.white54,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: subtitleColor ?? Colors.blueAccent,
fontWeight: FontWeight.w500,
),
),
]
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,351 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../utils/session_manager.dart';
import 'package:http/http.dart' as http;
import '../utils/api_config.dart';
import 'history_detail_screen.dart';
class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
List<dynamic> _history = [];
bool _isLoading = true;
bool _isSelectionMode = false;
final Set<dynamic> _selectedIds = {};
@override
void initState() {
super.initState();
_loadHistory();
}
Future<void> _loadHistory() async {
final url = Uri.parse("${ApiConfig.baseUrl}/get_history.php?id_users=${SessionManager.userId}");
try {
final response = await http.get(url);
if (response.statusCode == 200) {
List<dynamic> decodedHistory = jsonDecode(response.body);
setState(() {
_history = decodedHistory;
_isLoading = false;
});
}
} catch (e) {
debugPrint("Gagal memuat data riwayat dari server: $e");
setState(() {
_isLoading = false;
});
}
}
Future<void> _clearHistory() async {
final url = Uri.parse("${ApiConfig.baseUrl}/clear_history.php");
try {
final response = await http.post(url, body: {
'id_users': SessionManager.userId.toString(),
});
if (response.statusCode == 200) {
_loadHistory();
}
} catch (e) {
debugPrint("Gagal menghapus data riwayat di server: $e");
}
}
Future<void> _deleteHistoryItem(dynamic id) async {
final url = Uri.parse("${ApiConfig.baseUrl}/delete_history_item.php");
try {
final response = await http.post(url, body: {
'id': id.toString(),
});
if (response.statusCode == 200) {
_loadHistory();
}
} catch (e) {
debugPrint("Gagal menghapus item riwayat: $e");
}
}
Future<void> _deleteSelectedItems() async {
if (_selectedIds.isEmpty) return;
final url = Uri.parse("${ApiConfig.baseUrl}/delete_history_item.php");
try {
// Loop delete sequentially for now as the API expects single ID
// Alternatively, update API to accept multiple IDs
for (var id in _selectedIds) {
await http.post(url, body: {
'id': id.toString(),
});
}
setState(() {
_selectedIds.clear();
_isSelectionMode = false;
});
_loadHistory();
} catch (e) {
debugPrint("Gagal menghapus beberapa item riwayat: $e");
}
}
String _formatDuration(dynamic seconds) {
if (seconds == null) return "-";
int secs = int.tryParse(seconds.toString()) ?? 0;
if (secs <= 0) return "0 mnt";
int m = secs ~/ 60;
if (m >= 60) {
int h = m ~/ 60;
int remainingM = m % 60;
return "$h jam $remainingM mnt";
}
return "$m mnt";
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
automaticallyImplyLeading: !_isSelectionMode,
title: Text(_isSelectionMode ? "${_selectedIds.length} Terpilih" : "Riwayat Gowes",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
centerTitle: true,
backgroundColor: _isSelectionMode ? Colors.redAccent.withOpacity(0.8) : Colors.transparent,
elevation: 0,
leading: _isSelectionMode ? IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () {
setState(() {
_isSelectionMode = false;
_selectedIds.clear();
});
},
) : null,
actions: [
if (_history.isNotEmpty)
_isSelectionMode
? IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
onPressed: _selectedIds.isEmpty ? null : () => _confirmDeleteSelected(),
)
: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.white70),
onPressed: () {
setState(() {
_isSelectionMode = true;
});
},
)
],
),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A237E), Color(0xFF121212)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.blueAccent))
: _history.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemCount: _history.length,
itemBuilder: (context, index) {
final item = _history[index];
return _buildHistoryCard(item);
},
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history_toggle_off, size: 80, color: Colors.white.withOpacity(0.2)),
const SizedBox(height: 20),
Text(
"Belum ada riwayat rute",
style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 18),
),
],
),
);
}
Widget _buildHistoryCard(dynamic item) {
DateTime date = DateTime.parse(item['tanggal']);
String formattedDate = DateFormat('dd MMM yyyy, HH:mm').format(date);
String statusText = item['status'].toString().toUpperCase();
if (statusText == "0") statusText = "GAGAL";
bool isSuccess = statusText == "BERHASIL";
bool isSelected = _selectedIds.contains(item['id']);
return GestureDetector(
onTap: () {
if (_isSelectionMode) {
setState(() {
if (isSelected) {
_selectedIds.remove(item['id']);
} else {
_selectedIds.add(item['id']);
}
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HistoryDetailScreen(historyItem: item),
),
);
}
},
onLongPress: () {
if (!_isSelectionMode) {
setState(() {
_isSelectionMode = true;
_selectedIds.add(item['id']);
});
}
},
child: Container(
margin: const EdgeInsets.only(bottom: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? Colors.blueAccent.withOpacity(0.15) : Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: isSelected ? Colors.blueAccent : Colors.white.withOpacity(0.1), width: isSelected ? 2 : 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item['nama_rute'],
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSuccess ? Colors.green.withOpacity(0.2) : Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isSuccess ? Colors.green : Colors.red, width: 0.5),
),
child: Text(
statusText,
style: TextStyle(
color: isSuccess ? Colors.greenAccent : Colors.redAccent,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
if (_isSelectionMode)
Icon(
isSelected ? Icons.check_circle : Icons.radio_button_unchecked,
color: isSelected ? Colors.blueAccent : Colors.white38,
size: 24,
),
],
),
const SizedBox(height: 12),
Row(
children: [
_buildInfoMini(Icons.calendar_today, formattedDate),
],
),
const SizedBox(height: 16),
Divider(color: Colors.white.withOpacity(0.05)),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildStatDetail(Icons.straighten, "${item['jarak_tempuh'] ?? item['jarak']} KM", "JARAK")),
Expanded(child: _buildStatDetail(Icons.timer, _formatDuration(item['waktu_tempuh']), "WAKTU")),
Expanded(child: _buildStatDetail(Icons.speed, "${item['kecepatan_rata_rata'] ?? '-'} km/j", "SPEED")),
Expanded(child: _buildStatDetail(
isSuccess ? Icons.check_circle : Icons.cancel,
isSuccess ? "Berhasil" : "Gagal",
"MISI"
)),
],
),
],
),
),
),
),
),
);
}
Widget _buildInfoMini(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.white54, size: 14),
const SizedBox(width: 8),
Text(text, style: const TextStyle(color: Colors.white54, fontSize: 12)),
],
);
}
Widget _buildStatDetail(IconData icon, String value, String label) {
return Column(
children: [
Icon(icon, color: Colors.blueAccent, size: 20),
const SizedBox(height: 6),
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14)),
Text(label, style: const TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold)),
],
);
}
void _confirmDeleteSelected() {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: const Color(0xFF1E2140),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text("Hapus Riwayat", style: TextStyle(color: Colors.white)),
content: Text("Anda yakin ingin menghapus ${_selectedIds.length} riwayat rute yang dipilih?",
style: const TextStyle(color: Colors.white70)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("BATAL", style: TextStyle(color: Colors.white54))),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteSelectedItems();
},
child: const Text("HAPUS", style: TextStyle(color: Colors.redAccent))
),
],
),
);
}
}

View File

@ -0,0 +1,879 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../utils/api_config.dart';
import 'package:fl_chart/fl_chart.dart';
import 'search_route_screen.dart';
import 'history_screen.dart';
import 'history_detail_screen.dart';
import 'profile_screen.dart'; // Tambahkan import Profil
// Import session manager untuk nama dinamis
import '../utils/session_manager.dart';
import '../widgets/responsive_layout.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final List<Widget> _pages;
_HomeScreenState() : _pages = [] {
_pages.addAll([
HomeDashboardContent(onPlanRoute: () => _onItemTapped(1)),
const SearchRouteScreen(),
const HistoryScreen(),
const ProfileScreen(),
]);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
child: PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
if (_selectedIndex != 0) {
setState(() {
_selectedIndex = 0;
});
}
},
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
unselectedLabelStyle: const TextStyle(fontSize: 12),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_filled), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.location_searching), label: 'Cari Rute'),
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Riwayat'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
],
),
),
),
);
}
}
class HomeDashboardContent extends StatefulWidget {
final VoidCallback onPlanRoute;
const HomeDashboardContent({super.key, required this.onPlanRoute});
@override
State<HomeDashboardContent> createState() => _HomeDashboardContentState();
}
class _HomeDashboardContentState extends State<HomeDashboardContent> {
bool _isWeekly = true; // Toggle Mingguan/Bulanan
List<dynamic> _history = [];
List<dynamic> _recentRoutes = [];
double _totalDistance = 0;
int _totalSeconds = 0;
bool _isLoading = true;
int _selectedMonth = DateTime.now().month;
int _selectedYear = DateTime.now().year;
int _selectedWeekIndex = 0; // 0: Semua Minggu (Bulanan), 1-4: Minggu spesifik
int? _selectedIndex; // Index titik yang dipilih di grafik
List<double> _chartDataPoints = [];
List<String> _chartLabels = [];
double _maxDistance = 10.0;
@override
void initState() {
super.initState();
_loadDashboardData();
}
String _getMonthName(int month) {
const names = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
return names[month - 1];
}
String _getShortMonthName(int month) {
const names = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
return names[month - 1];
}
List<Map<String, DateTime>> _getWeekRanges(int month, int year) {
List<Map<String, DateTime>> ranges = [];
DateTime date = DateTime(year, month, 1);
int daysToSunday = (7 - date.weekday) % 7;
DateTime currentSunday = date.add(Duration(days: daysToSunday));
while (currentSunday.month == month) {
DateTime saturday = currentSunday.add(const Duration(days: 6));
ranges.add({
'start': currentSunday,
'end': saturday,
});
currentSunday = currentSunday.add(const Duration(days: 7));
}
return ranges;
}
List<String> _generateWeeks(int month, int year) {
List<String> weeks = ["Semua Minggu"];
List<Map<String, DateTime>> ranges = _getWeekRanges(month, year);
for (var range in ranges) {
DateTime start = range['start']!;
DateTime end = range['end']!;
if (start.month == end.month) {
weeks.add("${start.day} - ${end.day} ${_getMonthName(start.month)}");
} else {
weeks.add("${start.day} ${_getMonthName(start.month)} - ${end.day} ${_getMonthName(end.month)}");
}
}
return weeks;
}
Future<void> _loadDashboardData() async {
final url = Uri.parse("${ApiConfig.baseUrl}/get_history.php?id_users=${SessionManager.userId}");
try {
final response = await http.get(url);
if (response.statusCode == 200) {
List<dynamic> decodedHistory = jsonDecode(response.body);
setState(() {
_history = decodedHistory;
_recentRoutes = decodedHistory.take(3).toList();
_calculateStats();
_isLoading = false;
});
}
} catch (e) {
debugPrint("Gagal memuat data riwayat dari server: $e");
setState(() {
_isLoading = false;
});
}
}
void _calculateStats() {
double dist = 0;
int secs = 0;
DateTime startDate;
DateTime endDate;
List<Map<String, DateTime>> ranges = _getWeekRanges(_selectedMonth, _selectedYear);
if (_selectedWeekIndex == 0) {
_isWeekly = false;
if (ranges.isNotEmpty) {
startDate = ranges.first['start']!;
endDate = ranges.last['end']!;
_chartDataPoints = List.filled(ranges.length, 0.0);
_chartLabels = [];
for (var range in ranges) {
DateTime start = range['start']!;
DateTime end = range['end']!;
if (start.month == end.month) {
_chartLabels.add("${start.day}-${end.day} ${_getShortMonthName(start.month)}");
} else {
_chartLabels.add("${start.day} ${_getShortMonthName(start.month)}-${end.day} ${_getShortMonthName(end.month)}");
}
}
} else {
startDate = DateTime(_selectedYear, _selectedMonth, 1);
endDate = DateTime(_selectedYear, _selectedMonth + 1, 0);
_chartDataPoints = [];
_chartLabels = [];
}
} else {
_isWeekly = true;
int index = _selectedWeekIndex - 1;
if (index >= 0 && index < ranges.length) {
startDate = ranges[index]['start']!;
endDate = ranges[index]['end']!;
int daysCount = endDate.difference(startDate).inDays + 1;
_chartDataPoints = List.filled(daysCount, 0.0);
const dayNames = ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"];
_chartLabels = List.generate(daysCount, (i) {
DateTime d = startDate.add(Duration(days: i));
return "${dayNames[d.weekday % 7]}, ${d.day}";
});
} else {
startDate = DateTime(_selectedYear, _selectedMonth, 1);
endDate = DateTime(_selectedYear, _selectedMonth + 1, 0);
_chartDataPoints = [];
_chartLabels = [];
}
}
for (var trip in _history) {
DateTime tripDate = DateTime.parse(trip['tanggal']);
if (tripDate.isAfter(startDate.subtract(const Duration(seconds: 1))) &&
tripDate.isBefore(endDate.add(const Duration(days: 1)))) {
double tripDist = double.tryParse(trip['jarak_tempuh']?.toString() ?? "0") ?? 0;
int tripSecs = int.tryParse(trip['waktu_tempuh']?.toString() ?? "0") ?? 0;
int dataIndex = -1;
if (_isWeekly) {
dataIndex = tripDate.difference(startDate).inDays;
} else {
for (int i = 0; i < ranges.length; i++) {
if (tripDate.isAfter(ranges[i]['start']!.subtract(const Duration(seconds: 1))) &&
tripDate.isBefore(ranges[i]['end']!.add(const Duration(days: 1)))) {
dataIndex = i;
break;
}
}
}
if (dataIndex >= 0 && dataIndex < _chartDataPoints.length) {
_chartDataPoints[dataIndex] += tripDist;
if (_selectedIndex == null || _selectedIndex == dataIndex) {
dist += tripDist;
secs += tripSecs;
}
}
}
}
double maxVal = _chartDataPoints.isNotEmpty ? _chartDataPoints.reduce((a, b) => a > b ? a : b) : 0;
_maxDistance = maxVal > 0 ? maxVal : 10.0;
setState(() {
_totalDistance = dist;
_totalSeconds = secs;
});
}
String _formatDuration(int seconds) {
if (seconds <= 0) return "0 menit";
int h = seconds ~/ 3600;
int m = (seconds % 3600) ~/ 60;
if (h > 0) return "$h jam $m menit";
return "$m menit";
}
String getStatSubtitle(bool isDistance) {
if (_selectedIndex != null) {
if (_isWeekly) {
return isDistance
? "Total jarak yang Anda tempuh pada hari ini"
: "Total durasi yang Anda tempuh pada hari ini";
} else {
return isDistance
? "Total jarak yang Anda tempuh pada minggu ini"
: "Total durasi yang Anda tempuh pada minggu ini";
}
} else {
if (_isWeekly) {
return isDistance
? "Total jarak yang Anda tempuh pada minggu ini"
: "Total durasi yang Anda tempuh pada minggu ini";
} else {
return isDistance
? "Total jarak yang Anda tempuh pada bulan ini"
: "Total durasi yang Anda tempuh pada bulan ini";
}
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF121212), const Color(0xFF1E1E1E), const Color(0xFF121212)]
: [const Color(0xFFF8FAFC), const Color(0xFFF1F5F9), const Color(0xFFE2E8F0)],
),
),
child: SafeArea(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Selamat ${DateTime.now().hour < 12 ? "Pagi" : DateTime.now().hour < 15 ? "Siang" : DateTime.now().hour < 18 ? "Sore" : "Malam"},',
style: TextStyle(fontSize: 14, color: isDark ? Colors.blueGrey.shade200 : Colors.blueGrey, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Text(
SessionManager.userName ?? "User",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
color: isDark ? Colors.white : const Color(0xFF1E293B),
letterSpacing: -0.5
),
),
],
),
],
),
const SizedBox(height: 24),
Text('Tren Aktivitas Anda', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF1E293B), letterSpacing: -0.5)),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF252836) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedMonth,
isExpanded: true,
items: List.generate(12, (i) => DropdownMenuItem(
value: i + 1,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(_getMonthName(i + 1)),
),
)),
onChanged: (val) {
setState(() {
_selectedMonth = val!;
_selectedWeekIndex = 0;
_selectedIndex = null;
_calculateStats();
});
},
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF252836) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedWeekIndex,
isExpanded: true,
items: List.generate(_generateWeeks(_selectedMonth, _selectedYear).length, (i) => DropdownMenuItem(
value: i,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(_generateWeeks(_selectedMonth, _selectedYear)[i]),
),
)),
onChanged: (val) {
setState(() {
_selectedWeekIndex = val!;
_selectedIndex = null;
_calculateStats();
});
},
),
),
),
),
],
),
const SizedBox(height: 20),
_buildActivityChart(),
const SizedBox(height: 24),
// --- KARTU STATISTIK ---
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatCard(
'Jarak',
'${_totalDistance.toStringAsFixed(1)} km',
Icons.directions_bike,
Colors.blue,
getStatSubtitle(true)
),
const SizedBox(width: 16),
_buildStatCard(
'Waktu',
_formatDuration(_totalSeconds),
Icons.timer,
Colors.orange,
getStatSubtitle(false)
),
],
),
const SizedBox(height: 32),
// --- KARTU VISUAL GRADIENT ---
_buildPlanRouteCard(context),
const SizedBox(height: 32),
Text('Rute Terakhir', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF1E293B), letterSpacing: -0.5)),
const SizedBox(height: 16),
_recentRoutes.isEmpty
? const Text("Belum ada rute yang diselesaikan", style: TextStyle(color: Colors.grey))
: Column(
children: _recentRoutes.map((route) => _buildRecentRouteItem(route)).toList(),
),
],
),
),
),
),
);
}
Widget _buildFilterChip(String label, bool isActive, VoidCallback onTap) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isActive ? Colors.blueAccent : (isDark ? const Color(0xFF1E1E1E) : Colors.white),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isActive ? Colors.blueAccent : (isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200)),
boxShadow: isActive ? [BoxShadow(color: Colors.blue.withOpacity(isDark ? 0.1 : 0.2), blurRadius: 8)] : null,
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isActive ? Colors.white : Colors.grey.shade600,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
);
}
Widget _buildActivityChart() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
height: 240,
width: double.infinity,
padding: const EdgeInsets.fromLTRB(15, 25, 20, 10),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.3 : 0.04),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05), width: 1) : null,
),
child: _chartDataPoints.isEmpty
? const Center(child: Text("Tidak ada data", style: TextStyle(color: Colors.grey)))
: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
horizontalInterval: 0.2,
verticalInterval: 1,
getDrawingHorizontalLine: (value) => FlLine(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade100, strokeWidth: 1),
getDrawingVerticalLine: (value) => FlLine(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade100, strokeWidth: 1),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
int index = value.toInt();
if (index >= 0 && index < _chartLabels.length) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
_chartLabels[index],
style: TextStyle(
color: _selectedIndex == index ? Colors.orangeAccent : (isDark ? Colors.white54 : Colors.grey),
fontSize: 10,
fontWeight: _selectedIndex == index ? FontWeight.bold : FontWeight.normal,
),
),
);
}
return const SizedBox();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 45,
interval: _maxDistance > 0 ? (_maxDistance / 5).ceilToDouble().clamp(1.0, 100.0) : 2.0,
getTitlesWidget: (value, meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
"${value.toStringAsFixed(0)} km",
style: TextStyle(color: isDark ? Colors.white54 : Colors.grey, fontSize: 10),
),
);
},
),
),
),
borderData: FlBorderData(
show: false,
),
minX: 0,
maxX: (_chartDataPoints.length - 1).toDouble(),
minY: 0,
maxY: (_maxDistance * 1.25).ceilToDouble(),
lineBarsData: [
LineChartBarData(
spots: List.generate(_chartDataPoints.length, (i) => FlSpot(i.toDouble(), _chartDataPoints[i])),
isCurved: true,
color: Colors.blueAccent,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
bool isSelected = _selectedIndex == index;
return FlDotCirclePainter(
radius: isSelected ? 6 : 4,
color: isSelected ? Colors.orangeAccent : Colors.blue,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
belowBarData: BarAreaData(
show: true,
color: Colors.blueAccent.withOpacity(0.05),
),
),
],
lineTouchData: LineTouchData(
touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) {
if (!event.isInterestedForInteractions || touchResponse == null || touchResponse.lineBarSpots == null) {
return;
}
if (event is FlTapDownEvent || event is FlTapUpEvent) {
final spotIndex = touchResponse.lineBarSpots!.first.spotIndex;
setState(() {
if (_selectedIndex == spotIndex) {
_selectedIndex = null;
} else {
_selectedIndex = spotIndex;
}
_calculateStats();
});
}
},
handleBuiltInTouches: true,
),
),
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color, String subtitle) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Expanded(
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark ? const Color(0xFF252836) : Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: isDark ? Colors.black.withOpacity(0.4) : color.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
border: Border.all(
color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200,
width: 1,
),
),
child: Stack(
children: [
// Thick left accent border
Positioned(
left: 0,
top: 0,
bottom: 0,
child: Container(width: 6, color: color),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(height: 16),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: isDark ? Colors.white : const Color(0xFF1E293B),
letterSpacing: -0.5
)
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: isDark ? Colors.white.withOpacity(0.9) : Colors.blueGrey.shade700,
fontSize: 14,
fontWeight: FontWeight.w700
)
),
const SizedBox(height: 6),
Text(
subtitle,
style: TextStyle(
color: isDark ? Colors.white54 : Colors.grey.shade500,
fontSize: 11,
fontStyle: FontStyle.italic,
height: 1.2
)
),
],
),
),
],
),
),
);
}
Widget _buildPlanRouteCard(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF3B82F6), Color(0xFF2563EB)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 25,
offset: const Offset(0, 12),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Siap untuk gowes hari ini?',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: -0.5)),
const SizedBox(height: 8),
const Text('Temukan rute terbaik yang aman dan nyaman untukmu.',
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: widget.onPlanRoute,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue.shade700,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Mulai Jelajah', style: TextStyle(fontWeight: FontWeight.w800)),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded, size: 16),
],
),
),
],
),
);
}
Widget _buildNotificationBadge(BuildContext context) {
return Stack(
children: [
IconButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Belum ada notifikasi baru"))),
icon: const Icon(Icons.notifications_active, color: Colors.blueAccent, size: 30),
),
Positioned(
right: 8, top: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
constraints: const BoxConstraints(minWidth: 8, minHeight: 8),
),
)
],
);
}
Widget _buildRecentRouteItem(dynamic route) {
final isDark = Theme.of(context).brightness == Brightness.dark;
String name = route['nama_rute'] ?? '-';
String distance = '${route['jarak_tempuh'] ?? route['jarak'] ?? 0} km';
String statusText = route['status']?.toString().toUpperCase() ?? 'GAGAL';
if (statusText == "0") statusText = "GAGAL";
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HistoryDetailScreen(historyItem: route),
),
);
},
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.2 : 0.03),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05), width: 1) : null,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade700],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.blue.withOpacity(isDark ? 0.1 : 0.3), blurRadius: 8, offset: const Offset(0, 4))
],
),
child: const Icon(Icons.directions_bike_rounded, color: Colors.white, size: 24),
),
const SizedBox(width: 18),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
color: isDark ? Colors.white : const Color(0xFF1E293B)
),
overflow: TextOverflow.ellipsis
),
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.straighten, size: 14, color: isDark ? Colors.blueGrey.shade400 : Colors.blueGrey.shade300),
const SizedBox(width: 4),
Text(
distance,
style: TextStyle(
color: isDark ? Colors.blueGrey.shade300 : Colors.blueGrey.shade400,
fontSize: 13,
fontWeight: FontWeight.w500
)
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusText == "BERHASIL" ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
statusText,
style: TextStyle(
color: statusText == "BERHASIL" ? Colors.greenAccent : Colors.redAccent,
fontSize: 10,
fontWeight: FontWeight.w900
)
),
),
],
),
],
),
),
Icon(Icons.arrow_forward_ios_rounded, size: 16, color: isDark ? Colors.white24 : Colors.blueGrey.shade200),
],
),
),
);
}
}

View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../widgets/custom_button.dart';
import '../widgets/custom_text_field.dart';
import '../utils/session_manager.dart';
import '../utils/api_config.dart';
import '../widgets/responsive_layout.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
bool isLoading = false;
Future<void> loginUser() async {
setState(() => isLoading = true);
// Mengambil baseUrl dari pengaturan sentral di utils/api_config.dart
final url = Uri.parse("${ApiConfig.baseUrl}/login.php");
try {
final response = await http.post(url, body: {
'username': usernameController.text,
'password': passwordController.text,
});
final data = jsonDecode(response.body);
if (!mounted) return;
if (data['status'] == 'success') {
// Simpan Nama, Username, dan Photo ke SessionManager
SessionManager.saveSession(
int.parse(data['user']['id_users'].toString()),
data['user']['nama'],
data['user']['username'],
data['user']['photo_url']
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login Berhasil!')),
);
Navigator.pushReplacementNamed(context, '/home');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(data['message'])),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Gagal terhubung ke server!')),
);
} finally {
if (mounted) setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ResponsiveLayout(
child: Scaffold(
backgroundColor: const Color(0xFF3949AB),
body: SingleChildScrollView(
child: Column(
children: [
Container(
height: 260,
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
bottom: false,
child: Center(
child: Image.asset(
'assets/images/cyclist.png',
height: 140,
fit: BoxFit.contain,
),
),
),
),
Transform.translate(
offset: const Offset(0, -40),
child: Container(
constraints: BoxConstraints(
minHeight: screenHeight - 220,
),
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40),
topRight: Radius.circular(40),
),
),
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 40.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Hello!',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 26,
fontWeight: FontWeight.w800,
color: Color(0xFF3949AB),
),
),
const SizedBox(height: 32),
CustomTextField(
hintText: 'Username',
controller: usernameController,
prefixIcon: Icons.person_outline,
),
const SizedBox(height: 16),
CustomTextField(
hintText: 'Password',
obscureText: true,
controller: passwordController,
prefixIcon: Icons.lock_outline,
),
const SizedBox(height: 16),
const SizedBox(height: 24),
isLoading
? const Center(child: CircularProgressIndicator())
: CustomButton(
text: 'Log In',
onPressed: () {
if (usernameController.text.isNotEmpty && passwordController.text.isNotEmpty) {
loginUser();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Isi semua kolom!')),
);
}
},
),
const SizedBox(height: 24),
],
),
),
),
],
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,415 @@
import 'dart:ui';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart'; // import kIsWeb
import 'package:http/http.dart' as http;
import '../utils/session_manager.dart';
import '../utils/api_config.dart';
import '../main.dart'; // import themeNotifier
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
bool _isUploading = false;
// Fungsi untuk mengunggah foto profil
Future<void> _pickAndUploadPhoto() async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 70);
if (pickedFile != null && mounted) {
setState(() => _isUploading = true);
try {
var request = http.MultipartRequest('POST', Uri.parse('${ApiConfig.baseUrl}/upload_photo.php'));
request.fields['id_users'] = SessionManager.userId.toString();
if (kIsWeb) {
// Web needs readAsBytes since path is just a blob URL
final bytes = await pickedFile.readAsBytes();
request.files.add(http.MultipartFile.fromBytes(
'photo',
bytes,
filename: pickedFile.name,
));
} else {
// Android/Mobile can use path
request.files.add(await http.MultipartFile.fromPath('photo', pickedFile.path));
}
var response = await request.send();
if (response.statusCode == 200) {
var responseData = await response.stream.bytesToString();
var data = jsonDecode(responseData);
// Check for 'status' instead of 'success' to match backend
if (data['status'] == 'success') {
setState(() {
SessionManager.userPhoto = data['photo_url']; // Save url photo
});
if(mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Foto profil diunggah!')));
} else {
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal: ${data['message']}')));
}
}
} catch (e) {
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal unggah foto: $e')));
}
setState(() => _isUploading = false);
}
}
// Fungsi Hapus Akun
void _deleteAccount() {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text("Hapus Akun Permanen", style: TextStyle(color: Colors.red)),
content: const Text("Apakah Anda yakin ingin menghapus akun ini secara permanen? Data yang dihapus tidak dapat dipulihkan."),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Batal", style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(ctx); // Tutup dialog konfirmasi
try {
final response = await http.post(
Uri.parse('${ApiConfig.baseUrl}/delete_account.php'),
body: {'id_users': SessionManager.userId.toString()},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success' && mounted) {
// Kosongkan sesi lokal
SessionManager.userId = null;
SessionManager.userName = null;
SessionManager.userEmail = null;
SessionManager.userPhoto = null;
// Tampilkan pop-up keberhasilan
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Column(
children: [
Icon(Icons.check_circle_outline, color: Colors.greenAccent[400], size: 70),
const SizedBox(height: 16),
const Text("Akun Berhasil Dihapus", style: TextStyle(fontWeight: FontWeight.bold)),
],
),
content: const Text(
"Akun Anda telah dihapus secara permanen.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15),
),
);
},
);
// Berikan jeda 2.5 detik agar user sempat membaca popup
await Future.delayed(const Duration(milliseconds: 2500));
if (mounted) {
Navigator.pushNamedAndRemoveUntil(context, '/welcome', (route) => false);
}
} else {
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal menghapus: ${data['message']}')));
}
} catch(e) {
if(mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Terjadi kesalahan koneksi saat menghapus akun')));
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
child: const Text("Hapus", style: TextStyle(color: Colors.white)),
),
],
);
}
);
}
// Fungsi untuk dummy dialog pusat bantuan
void _showInfoDialog(String title, String content) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
content: Text(content),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Tutup")),
],
)
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SingleChildScrollView(
child: Column(
children: [
// --- KARTU HEADER PROFIL DENGAN GRADIENT ---
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 70, left: 24, right: 24, bottom: 40),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF2196F3), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Row(
children: [
// Avatar dengan GestureDetector untuk Edit Photo
GestureDetector(
onTap: _isUploading ? null : _pickAndUploadPhoto,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 38,
backgroundColor: Colors.blueAccent,
backgroundImage: (SessionManager.userPhoto != null && SessionManager.userPhoto!.isNotEmpty)
? NetworkImage("${ApiConfig.baseUrl}/${SessionManager.userPhoto}?t=${DateTime.now().millisecondsSinceEpoch}")
: null,
child: (SessionManager.userPhoto == null || SessionManager.userPhoto!.isEmpty)
? const Icon(Icons.person, size: 45, color: Colors.white)
: null,
),
if (_isUploading)
const CircularProgressIndicator(color: Colors.white)
else
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle),
child: const Icon(Icons.camera_alt, color: Colors.white, size: 14),
),
)
],
),
),
),
const SizedBox(width: 20),
// Info nama dan email
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SessionManager.userName ?? "Pengguna",
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
SessionManager.userEmail ?? "Tidak ada data",
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// --- KONTEN MENU PROFIL ---
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// --- BAGIAN PENGATURAN AKUN ---
_buildSectionHeader("Akun", isDark),
_buildGlassMenuTile(Icons.person_outline, "Edit Profil", () async {
final updated = await Navigator.pushNamed(context, '/edit_profile');
// Jika data kembali bernilai true, refresh halaman profil untuk menampilkan nama baru
if (updated == true) {
setState(() {});
}
}, isDark),
// Dark Mode Switch ditaruh di Akun
_buildGlassMenuTile(
themeNotifier.value == ThemeMode.dark ? Icons.dark_mode : Icons.light_mode,
"Mode Gelap",
() {
setState(() {
if(themeNotifier.value == ThemeMode.dark) {
themeNotifier.value = ThemeMode.light;
} else {
themeNotifier.value = ThemeMode.dark;
}
});
},
isDark,
trailingWidget: Switch(
value: themeNotifier.value == ThemeMode.dark,
onChanged: (val) {
setState(() {
themeNotifier.value = val ? ThemeMode.dark : ThemeMode.light;
});
},
activeColor: Colors.blueAccent,
),
),
const SizedBox(height: 24),
// --- BAGIAN DUKUNGAN & LAINNYA ---
_buildSectionHeader("Dukungan & Lainnya", isDark),
_buildGlassMenuTile(Icons.help_outline, "Pusat Bantuan", () => _showInfoDialog("Pusat Bantuan", "Jika Anda memiliki kendala teknis atau saran perbaikan, hubungi tim kami di cs@smartcycling.com atau hubungi layanan Hotline bebas pulsa kami 1500-099."), isDark),
_buildGlassMenuTile(Icons.info_outline, "Tentang Aplikasi", () => _showInfoDialog("Tentang Aplikasi", "Smart Cycling App Versi 1.0.\nDikembangkan secara khusus untuk memberikan rekomendasi rute bagi pesepeda secara efisien."), isDark),
_buildGlassMenuTile(Icons.privacy_tip_outlined, "Kebijakan Privasi", () => _showInfoDialog("Kebijakan Privasi", "Informasi lokasi dan preferensi rute Anda tersimpan dengan aman hanya pada sesi aktif. Kami tidak memperjualbelikan rekam jejak pengguna."), isDark),
const SizedBox(height: 32),
// --- FITUR HAPUS AKUN & KELUAR ---
_buildGlassMenuTile(
Icons.delete_forever,
"Hapus Akun",
_deleteAccount,
isDark,
iconColor: Colors.orangeAccent,
textColor: Colors.orangeAccent,
showChevron: false,
),
_buildGlassMenuTile(
Icons.logout,
"Keluar",
() {
// Tampilkan pop-up konfirmasi
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Konfirmasi Keluar", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red)),
content: const Text("Apakah anda yakin ingin keluar?"),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(), // Tutup dialog
child: const Text("Batal", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); // Tutup dialog
// Kosongkan sesi lokal
SessionManager.userId = null;
SessionManager.userName = null;
SessionManager.userEmail = null;
SessionManager.userPhoto = null;
Navigator.pushNamedAndRemoveUntil(context, '/welcome', (route) => false); // Arahkan ke welcome & bersihkan stack
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text("Keluar", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
);
},
);
},
isDark,
iconColor: Colors.redAccent,
textColor: Colors.redAccent,
showChevron: false, // Hilangkan panah khusus untuk tombol keluar
),
const SizedBox(height: 24),
],
),
),
],
),
),
);
}
// --- HELPER WIDGETS ---
Widget _buildSectionHeader(String title, bool isDark) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, bottom: 8.0, top: 4.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white70 : Colors.black87,
),
),
),
);
}
Widget _buildGlassMenuTile(IconData icon, String title, VoidCallback onTap, bool isDark, {Color? iconColor, Color? textColor, bool showChevron = true, Widget? trailingWidget}) {
Color defaultTxColor = isDark ? Colors.white : Colors.black87;
Color defaultIcColor = isDark ? Colors.white70 : Colors.black54;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: isDark ? Colors.white.withOpacity(0.1) : Colors.white.withOpacity(0.55), // Transparansi efek kaca yang disesuaikan tema
borderRadius: BorderRadius.circular(15),
border: Border.all(color: isDark ? Colors.white.withOpacity(0.2) : Colors.white.withOpacity(0.9), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), // Fitur blur khas glassmorphism
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 4.0),
leading: Icon(icon, color: iconColor ?? defaultIcColor, size: 26),
title: Text(
title,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: textColor ?? defaultTxColor),
),
trailing: trailingWidget ?? (showChevron ? Icon(Icons.chevron_right, color: isDark ? Colors.white38 : Colors.black38, size: 20) : null),
onTap: onTap,
),
),
),
);
}
}

View File

@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../widgets/custom_button.dart';
import '../widgets/custom_text_field.dart';
import '../utils/api_config.dart';
import '../widgets/responsive_layout.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final nameController = TextEditingController();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();
bool isLoading = false;
Future<void> registerUser() async {
// 1. Validasi Konfirmasi Password secara lokal di Flutter
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Konfirmasi password tidak cocok!')),
);
return;
}
setState(() => isLoading = true);
// Mengambil baseUrl dari pengaturan sentral di utils/api_config.dart
final url = Uri.parse("${ApiConfig.baseUrl}/register.php");
try {
final response = await http.post(url, body: {
'nama': nameController.text,
'username': usernameController.text,
'password': passwordController.text,
});
final data = jsonDecode(response.body);
if (!mounted) return;
if (data['status'] == 'success') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registrasi Berhasil! Silakan Login.')),
);
Navigator.pop(context); // Kembali ke halaman Login
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(data['message'])),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Gagal terhubung ke server!')),
);
} finally {
if (mounted) setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ResponsiveLayout(
child: Scaffold(
backgroundColor: const Color(0xFF3949AB),
body: SingleChildScrollView(
child: Column(
children: [
Container(
height: 260,
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
bottom: false,
child: Center(
child: Image.asset(
'assets/images/cyclist.png',
height: 140,
fit: BoxFit.contain,
),
),
),
),
Transform.translate(
offset: const Offset(0, -40),
child: Container(
constraints: BoxConstraints(
minHeight: screenHeight - 220,
),
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40),
topRight: Radius.circular(40),
),
),
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 40.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Welcome!',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 26,
fontWeight: FontWeight.w800,
color: Color(0xFF3949AB),
),
),
const SizedBox(height: 32),
CustomTextField(
hintText: 'Nama',
controller: nameController,
prefixIcon: Icons.badge_outlined,
),
const SizedBox(height: 16),
CustomTextField(
hintText: 'Username',
controller: usernameController,
prefixIcon: Icons.person_outline,
),
const SizedBox(height: 16),
CustomTextField(
hintText: 'Password',
obscureText: true,
controller: passwordController,
prefixIcon: Icons.lock_outline,
),
const SizedBox(height: 16),
CustomTextField(
hintText: 'Konfirmasi Password',
obscureText: true,
controller: confirmPasswordController,
prefixIcon: Icons.lock_reset_outlined,
),
const SizedBox(height: 24),
isLoading
? const Center(child: CircularProgressIndicator())
: CustomButton(
text: 'Sign Up',
onPressed: () {
if (nameController.text.isNotEmpty && usernameController.text.isNotEmpty && passwordController.text.isNotEmpty) {
registerUser();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Data tidak boleh kosong!')),
);
}
},
),
const SizedBox(height: 24),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,156 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'navigation_screen.dart';
class RouteRecommendationScreen extends StatelessWidget {
final List<dynamic> routes;
final String selectedSpeed;
const RouteRecommendationScreen({
super.key,
required this.routes,
required this.selectedSpeed,
});
// Logika penghitungan waktu berdasarkan kecepatan
int calculateAdjustedTime(int baseTime) {
if (selectedSpeed == "Lambat") {
return (baseTime * 1.3).round(); // +30% waktu
} else if (selectedSpeed == "Cepat") {
return (baseTime * 0.8).round(); // -20% waktu
}
return baseTime; // Sedang (Base)
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text("Rekomendasi Rute",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: routes.isEmpty
? const Center(child: Text("Tidak ada rute yang ditemukan.", style: TextStyle(color: Colors.white)))
: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: routes.length,
itemBuilder: (context, index) {
final route = routes[index];
final adjustedTime = calculateAdjustedTime((route['waktu_dasar'] as num).toInt());
final double accuracy = ((route['skor'] as num).toDouble() * 100);
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: ListTile(
contentPadding: const EdgeInsets.all(20),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
route['nama_rute'],
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.greenAccent.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"${accuracy.toStringAsFixed(1)}% Akurasi",
style: const TextStyle(color: Colors.greenAccent, fontSize: 12, fontWeight: FontWeight.bold),
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.straighten, color: Colors.white70, size: 16),
const SizedBox(width: 8),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text("${route['jarak']} KM", style: const TextStyle(color: Colors.white70)),
),
),
const SizedBox(width: 20),
const Icon(Icons.terrain, color: Colors.white70, size: 16),
const SizedBox(width: 8),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text("Medan ${route['kondisi_medan']}", style: const TextStyle(color: Colors.white70)),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.timer, color: Colors.white, size: 18),
const SizedBox(width: 8),
Text(
"Target Waktu : $adjustedTime menit",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16),
),
],
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NavigationScreen(
routeData: route,
targetMinutes: adjustedTime,
),
),
);
},
trailing: const Icon(Icons.chevron_right, color: Colors.white),
),
),
),
),
);
},
),
),
),
);
}
}

View File

@ -0,0 +1,299 @@
import 'dart:ui';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../widgets/custom_text_field.dart';
import '../utils/api_config.dart';
import 'route_recommendation_screen.dart';
class SearchRouteScreen extends StatefulWidget {
const SearchRouteScreen({super.key});
@override
State<SearchRouteScreen> createState() => _SearchRouteScreenState();
}
class _SearchRouteScreenState extends State<SearchRouteScreen> {
String? selectedBikeType;
String? selectedUserType;
String selectedSpeed = "Sedang"; // Default: Sedang
bool _isLoading = false;
// Fungsi untuk memanggil API Rekomendasi SAW
Future<void> _findRoutes() async {
if (selectedUserType == null || selectedBikeType == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tolong pilih tipe pengguna dan jenis sepeda!')),
);
return;
}
setState(() => _isLoading = true);
try {
final response = await http.post(
Uri.parse('${ApiConfig.baseUrl}/recommend_routes.php'),
body: {
'jenis_sepeda': selectedBikeType,
'tipe_pengguna': selectedUserType,
},
);
final data = jsonDecode(response.body);
if (!mounted) return;
if (data['status'] == 'success') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RouteRecommendationScreen(
routes: data['data'],
selectedSpeed: selectedSpeed,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal: ${data['message']}')),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 20.0),
child: Column(
children: [
const SizedBox(height: 40),
// Header Section
const Icon(Icons.explore_outlined,
size: 60, color: Colors.white),
const SizedBox(height: 16),
const Text(
"Cari Rute Terbaik",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: 'Montserrat',
),
),
const Text(
"Tentukan kriteria perjalanan Anda",
style: TextStyle(fontSize: 15, color: Colors.white70),
),
const SizedBox(height: 40),
// GLASSMORPHISM FORM CARD
ClipRRect(
borderRadius: BorderRadius.circular(30),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. TIPE PENGGUNA
_buildLabel("TIPE PENGGUNA"),
_buildGlassDropdown(
['Profesional', 'Amatir'],
Icons.person_outline,
"Pilih Tipe Pengguna",
(val) => setState(() => selectedUserType = val),
selectedUserType,
),
const SizedBox(height: 24),
// 2. JENIS SEPEDA
_buildLabel("JENIS SEPEDA"),
_buildGlassDropdown(
['Road Bike', 'MTB', 'Hybrid'],
Icons.pedal_bike,
"Pilih Jenis Sepeda",
(val) => setState(() => selectedBikeType = val),
selectedBikeType,
),
const SizedBox(height: 24),
// 3. KECEPATAN (3 Pilihan: Lambat, Sedang, Cepat)
_buildLabel("MODE BERSEPEDA"),
const SizedBox(height: 8),
Row(
children: [
_buildSpeedCard(
"Lambat", Icons.directions_walk),
const SizedBox(width: 10),
_buildSpeedCard(
"Sedang", Icons.directions_bike),
const SizedBox(width: 10),
_buildSpeedCard("Cepat", Icons.flash_on),
],
),
const SizedBox(height: 32),
// SUBMIT BUTTON
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading ? null : _findRoutes,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF3949AB),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 0,
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF3949AB),
),
)
: const Text(
"TEMUKAN RUTE",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 1.2,
),
),
),
),
],
),
),
),
),
const SizedBox(height: 40),
],
),
),
),
],
),
),
);
}
Widget _buildLabel(String text) {
return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
text,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white.withOpacity(0.8),
letterSpacing: 1.5,
),
),
);
}
Widget _buildGlassDropdown(List<String> items, IconData icon,
String hint, void Function(String?) onChanged, String? value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: DropdownButtonHideUnderline(
child: DropdownButtonFormField<String>(
value: value,
dropdownColor: const Color(0xFF1E2140).withOpacity(0.9),
style:
const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
decoration: InputDecoration(
border: InputBorder.none,
icon: Icon(icon, color: Colors.white70, size: 22),
),
items: items.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: const TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: onChanged,
hint: Text(hint, style: const TextStyle(color: Colors.white54)),
),
),
);
}
Widget _buildSpeedCard(String label, IconData icon) {
bool isSelected = selectedSpeed == label;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => selectedSpeed = label),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: isSelected ? Colors.white : Colors.white.withOpacity(0.3),
width: 1.5,
),
),
child: Column(
children: [
Icon(
icon,
color: isSelected ? const Color(0xFF3949AB) : Colors.white70,
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFF3949AB) : Colors.white70,
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,73 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../widgets/responsive_layout.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
// Durasi splash screen diperpanjang (awalnya 3 detik, ditambah 3 detik menjadi 6 detik)
Timer(const Duration(seconds: 6), () {
// Pindah ke halaman welcome dan hapus splash screen dari tumpukan navigasi
Navigator.pushReplacementNamed(context, '/welcome');
});
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
child: Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)], // Mix deep indigo and cyan
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/cyclist.png', height: 180),
const SizedBox(height: 32),
const Text(
'Smart Cycling',
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 34,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 1.0,
),
),
const SizedBox(height: 12),
Text(
'Aplikasi Rekomendasi Rute Bersepeda\n yang Aman, Nyaman, dan Efisien',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.8),
letterSpacing: 0.5,
height: 1.5,
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import '../widgets/custom_button.dart';
import '../widgets/responsive_layout.dart';
class SuccessScreen extends StatelessWidget {
const SuccessScreen({super.key});
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
child: Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset('assets/images/success_check.png', height: 150),
const SizedBox(height: 32),
const Text(
'Akun Berhasil Dibuat',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 48),
CustomButton(
text: 'Mulai',
onPressed: () {
// Arahkan ke halaman utama aplikasi setelah registrasi sukses
Navigator.pushReplacementNamed(context, '/home');
},
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import '../widgets/responsive_layout.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
child: Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF00BCD4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Column(
children: [
const Spacer(flex: 3),
Image.asset(
'assets/images/cyclist.png',
height: 180,
fit: BoxFit.contain,
),
const SizedBox(height: 24),
const Text(
"Smart Cycling",
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
const Spacer(flex: 3),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Container(
width: double.infinity,
height: 55,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/register');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: const Color(0xFF3949AB),
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: const Text(
'Get Started',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, fontFamily: 'Montserrat'),
),
),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/login');
},
child: Text(
'I already have an account',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontFamily: 'Montserrat',
),
),
),
const Spacer(flex: 1),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,40 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../utils/api_keys.dart';
class WeatherService {
static const String _baseUrl = "https://api.openweathermap.org/data/2.5/weather";
Future<Map<String, dynamic>?> fetchWeather(double lat, double lon) async {
try {
final response = await http.get(
Uri.parse("$_baseUrl?lat=$lat&lon=$lon&appid=${ApiKeys.openWeatherApiKey}&units=metric&lang=id"),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return null;
}
} catch (e) {
return null;
}
}
Future<Map<String, dynamic>?> fetchForecast(double lat, double lon) async {
try {
final String forecastUrl = "https://api.openweathermap.org/data/2.5/forecast";
final response = await http.get(
Uri.parse("$forecastUrl?lat=$lat&lon=$lon&appid=${ApiKeys.openWeatherApiKey}&units=metric&lang=id"),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return null;
}
} catch (e) {
return null;
}
}
}

View File

@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart';
class ApiConfig {
// Gunakan URL Ngrok Statis (Permanen & Bisa di semua jaringan)
static const String ngrokUrl = "https://domelike-reliable-imagines.ngrok-free.dev";
static String get baseUrl {
if (kIsWeb) {
return "http://localhost/smart_cycling_api";
}
// Semua HP Android sekarang akan menggunakan jalur Ngrok
return "$ngrokUrl/smart_cycling_api";
}
}

View File

@ -0,0 +1,3 @@
class ApiKeys {
static const String openWeatherApiKey = "0da5730273364f17eb13f09491dfc8ba";
}

View File

@ -0,0 +1,67 @@
import 'dart:math' as math;
import 'package:latlong2/latlong.dart';
class NavigationHelper {
static const Distance _distanceCalculator = Distance();
/// Menghitung jarak antara dua titik dalam meter
static double calculateDistance(LatLng p1, LatLng p2) {
return _distanceCalculator.as(LengthUnit.Meter, p1, p2);
}
/// Menghitung bearing (sudut) antara dua titik (0-360 derajat)
static double calculateBearing(LatLng p1, LatLng p2) {
double lat1 = p1.latitude * math.pi / 180;
double lon1 = p1.longitude * math.pi / 180;
double lat2 = p2.latitude * math.pi / 180;
double lon2 = p2.longitude * math.pi / 180;
double dLon = lon2 - lon1;
double y = math.sin(dLon) * math.cos(lat2);
double x = math.cos(lat1) * math.sin(lat2) -
math.sin(lat1) * math.cos(lat2) * math.cos(dLon);
double radians = math.atan2(y, x);
return (radians * 180 / math.pi + 360) % 360;
}
/// Menghitung jarak terdekat dari titik ke sebuah segmen garis (Cross Track Distance)
static double getDistanceToSegment(LatLng p, LatLng s1, LatLng s2) {
final double l2 = math.pow(calculateDistance(s1, s2), 2).toDouble();
if (l2 == 0) return calculateDistance(p, s1);
// Proyeksi titik ke garis
double t = ((p.latitude - s1.latitude) * (s2.latitude - s1.latitude) +
(p.longitude - s1.longitude) * (s2.longitude - s1.longitude)) /
(math.pow(s2.latitude - s1.latitude, 2) +
math.pow(s2.longitude - s1.longitude, 2));
t = math.max(0, math.min(1, t));
LatLng projection = LatLng(
s1.latitude + t * (s2.latitude - s1.latitude),
s1.longitude + t * (s2.longitude - s1.longitude),
);
return calculateDistance(p, projection);
}
/// Menentukan apakah ada belokan tajam di titik tertentu pada rute
static String? detectManeuver(List<LatLng> points, int index) {
if (index <= 0 || index >= points.length - 1) return null;
double bearing1 = calculateBearing(points[index - 1], points[index]);
double bearing2 = calculateBearing(points[index], points[index + 1]);
double diff = (bearing2 - bearing1 + 360) % 360;
if (diff > 180) diff -= 360;
if (diff > 25 && diff < 160) {
return "Belok Kanan";
} else if (diff < -25 && diff > -160) {
return "Belok Kiri";
}
return null;
}
}

View File

@ -0,0 +1,16 @@
class SessionManager {
// Variabel statis untuk menyimpan data pengguna yang login
static int? userId;
static String? userName;
static String? userEmail;
static String? userPhoto; // Path lokal atau URL gambar
// Fungsi untuk mengupdate info saat login berhasil
static void saveSession(int id, String name, String email, [String? photo]) {
userId = id;
userName = name;
userEmail = email;
userPhoto = photo;
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const CustomButton({
super.key,
required this.text,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3949AB), // Dark Blue to match theme
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 0,
),
onPressed: onPressed,
child: Text(
text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
),
),
),
);
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget {
final String hintText;
final bool obscureText;
final TextEditingController? controller;
final IconData? prefixIcon;
const CustomTextField({
super.key,
required this.hintText,
this.obscureText = false,
this.controller,
this.prefixIcon,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
obscureText: obscureText,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, fontFamily: 'Montserrat'),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey.shade500, fontWeight: FontWeight.normal, fontSize: 14),
prefixIcon: prefixIcon != null
? Icon(prefixIcon, color: Colors.grey.shade600, size: 20)
: null,
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 24),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: Colors.grey.shade300, width: 1.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: const BorderSide(color: Color(0xFF3949AB), width: 2.0),
),
),
);
}
}

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget {
final Widget child;
const ResponsiveLayout({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
// Kini aplikasi akan menempati layar secara penuh di seluruh tipe perangkat,
// sehingga terlihat natural di HP maupun Tablet tanpa ada border sisa di kiri-kanan.
return child;
}
}

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