diff --git a/.gitignore b/.gitignore index f913413..834ecd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ *.iml .gradle /local.properties -/.idea/workspace.xml +/.idea/caches /.idea/libraries -/.idea/sonarlint +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild +.cxx +local.properties +.kotlin/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index b625119..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -ParkingLotDemo \ No newline at end of file diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 9021732..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index 303262c..0000000 Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 663459a..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
-
-
\ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 099a47f..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..4bccd57 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 8e2e3ee..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..6d0ee1c 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 158d51d..0ad17cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,20 +1,6 @@ + - - - + diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 65e9598..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..94c96f6 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,318 @@ + + + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/0/6/063032c471ffd7d160be37cce333732f26f8d931 b/.idea/sonarlint/issuestore/0/6/063032c471ffd7d160be37cce333732f26f8d931 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb deleted file mode 100644 index 41aabc7..0000000 --- a/.idea/sonarlint/issuestore/index.pb +++ /dev/null @@ -1,3 +0,0 @@ - -t -Dapp/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt,0/6/063032c471ffd7d160be37cce333732f26f8d931 \ No newline at end of file diff --git a/.idea/sonarlint/securityhotspotstore/0/6/063032c471ffd7d160be37cce333732f26f8d931 b/.idea/sonarlint/securityhotspotstore/0/6/063032c471ffd7d160be37cce333732f26f8d931 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/index.pb b/.idea/sonarlint/securityhotspotstore/index.pb deleted file mode 100644 index 41aabc7..0000000 --- a/.idea/sonarlint/securityhotspotstore/index.pb +++ /dev/null @@ -1,3 +0,0 @@ - -t -Dapp/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt,0/6/063032c471ffd7d160be37cce333732f26f8d931 \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index d0e577e..347326c 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,44 @@ -*Read this in other languages: [English](README.md), [中文](README.zh-tw.md).* - -# ParkingDemo -Taipei City Parking Lot Information Query System Demo. +# ParkingLot +Example of parking lot query APP developed using Android. Get it on Google Play ### Screenshots
- - - + + +
- - - + + +
### Supported Android Versions -- Android 5.1 Lollipop(API level 21) or higher. +- Android Nougat 7.0(API level 24) or higher. ### Prepare -Add your [GoogleMaps](https://developers.google.com/maps/documentation/android-api/) key to strings.xml and turn on API from [Google Cloud Platform](https://console.cloud.google.com/). - -### Data resource -臺北市資料大平臺 - [臺北市停車場資訊](https://data.taipei/#/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c). - -### Used libraries -1. [Gson](https://github.com/google/gson) -2. [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) -3. [Maps](https://developers.google.com/maps/documentation/android-sdk/map?hl=zh-tw) -4. [Maps-utils](https://github.com/googlemaps/android-maps-utils) -5. [Material](https://material.io/) -6. [OkHttp3](https://github.com/square/okhttp) -7. [Retrofit2](https://github.com/square/retrofit) -8. [Room](https://developer.android.com/topic/libraries/architecture/room) -9. [RxJava2](https://github.com/ReactiveX/RxJava) -10. [Sqlcipher](https://github.com/sqlcipher/android-database-sqlcipher) -11. [ViewBinding](https://developer.android.com/topic/libraries/view-binding) \ No newline at end of file +Add your [GoogleMaps](https://developers.google.com/maps/documentation/android-api/) key to local.defaults.properties and turn on API from [Google Cloud Platform](https://console.cloud.google.com/). + +### Open Data resource +Data Taipei - [臺北市停車場資訊](https://data.taipei/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c). + +### Libraries +* [Android Jetpack](https://developer.android.com/jetpack) + * [Compose](https://developer.android.com/jetpack/androidx/releases/compose) + * [DataStore](https://developer.android.com/jetpack/androidx/releases/datastore) + * [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) + * [Navigation](https://developer.android.com/jetpack/androidx/releases/navigation) + * [Paging3](https://developer.android.com/jetpack/androidx/releases/paging) + * [Room](https://developer.android.com/jetpack/androidx/releases/room) + * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) +* [Android Maps Compose](https://github.com/googlemaps/android-maps-compose) +* [Coroutines](https://developer.android.com/kotlin/coroutines) +* [Dagger](https://github.com/google/dagger) +* [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization) +* [LeakCanary](https://square.github.io/leakcanary/getting_started/) +* [Material](https://m2.material.io/develop/android) +* [Retrofit2](https://square.github.io/retrofit/) +* [Secrets Gradle](https://developers.google.com/maps/documentation/places/android-sdk/secrets-gradle-plugin?hl=zh-tw) \ No newline at end of file diff --git a/README.zh-tw.md b/README.zh-tw.md deleted file mode 100644 index 2050b8e..0000000 --- a/README.zh-tw.md +++ /dev/null @@ -1,41 +0,0 @@ -*其他語言版本: [English](README.md), [中文](README.zh-tw.md).* - -# ParkingDemo -台北市停車場查詢系統。 - -Get it on Google Play - -### 畫面截圖 -
- - - -
- -
- - - -
- -### 支援Android版本 -- Android 5.1 Lollipop(API level 21)或更高。 - -### 前置準備 -添加[GoogleMaps](https://developers.google.com/maps/documentation/android-api/) 金鑰至strings.xml並在[Google Cloud Platform](https://console.cloud.google.com/)啟用API。 - -### 資料來源 -臺北市資料大平臺 - [臺北市停車場資訊](https://data.taipei/#/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c)。 - -### 使用函示庫 -1. [Gson](https://github.com/google/gson) -2. [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) -3. [Maps](https://developers.google.com/maps/documentation/android-sdk/map?hl=zh-tw) -4. [Maps-utils](https://github.com/googlemaps/android-maps-utils) -5. [Material](https://material.io/) -6. [OkHttp3](https://github.com/square/okhttp) -7. [Retrofit2](https://github.com/square/retrofit) -8. [Room](https://developer.android.com/topic/libraries/architecture/room) -9. [RxJava2](https://github.com/ReactiveX/RxJava) -10. [Sqlcipher](https://github.com/sqlcipher/android-database-sqlcipher) -11. [ViewBinding](https://developer.android.com/topic/libraries/view-binding) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 1d737ce..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,104 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'com.google.devtools.ksp' -} - -android { - compileSdk 34 - - defaultConfig { - applicationId "com.a1573595.parkingdemo" - minSdk 21 - targetSdk 34 - versionCode 5 - versionName "1.2.0" - ndkVersion "23.1.7779620" - - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), - 'proguard-gson.pro', 'proguard-rules.pro', 'proguard-sqlite.pro', - 'proguard-okhttp.pro', 'proguard-retrofit2.pro' - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - ndk { - debugSymbolLevel 'FULL' - } - } - - buildTypes { - debug { - debuggable true - } - release { - debuggable false - minifyEnabled true - shrinkResources true - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } - buildFeatures { - viewBinding true - } - testOptions.unitTests { - includeAndroidResources = true - } - namespace 'com.a1573595.parkinglotdemo' -} - -dependencies { - def retrofit_version = "2.9.0" - def okHttp_version = '4.12.0' - def room_version = "2.6.1" - def paging_version = "3.2.1" - - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.activity:activity-ktx:1.8.2' - - implementation "androidx.paging:paging-runtime-ktx:$paging_version" - implementation "androidx.paging:paging-rxjava3:$paging_version" - - implementation 'com.google.android.gms:play-services-maps:18.2.0' - implementation 'com.google.android.gms:play-services-location:21.2.0' - implementation 'com.google.maps.android:android-maps-utils:3.8.2' - - implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' - - implementation "com.squareup.retrofit2:retrofit:$retrofit_version" - implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version" - implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_version" - - implementation 'com.google.code.gson:gson:2.10.1' - - implementation 'androidx.datastore:datastore-preferences-rxjava3:1.0.0' - - implementation "androidx.room:room-runtime:$room_version" - ksp "androidx.room:room-compiler:$room_version" - implementation "androidx.room:room-rxjava3:$room_version" - implementation 'net.zetetic:android-database-sqlcipher:4.5.4' - implementation "androidx.sqlite:sqlite-ktx:2.4.0" - - implementation 'com.jakewharton.timber:timber:5.0.1' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13' - - testImplementation 'junit:junit:4.13.2' - testImplementation 'androidx.arch.core:core-testing:2.2.0' - testImplementation 'androidx.test.ext:junit-ktx:1.1.5' - testImplementation 'androidx.test:core-ktx:1.5.0' - testImplementation 'org.robolectric:robolectric:4.11.1' - testImplementation 'io.mockk:mockk:1.13.10' - - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..0293444 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,111 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.devtools.ksp) + alias(libs.plugins.google.dagger.hilt.android) + alias(libs.plugins.jetbrains.android) + alias(libs.plugins.jetbrains.compose.compiler) + alias(libs.plugins.jetbrains.kotlin.serialization) + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("dagger.hilt.android.plugin") + id("kotlin-parcelize") +} + +android { + namespace = "com.a1573595.parkingdemo" + compileSdk = 34 + + defaultConfig { + applicationId = "com.a1573595.parkingdemo" + minSdk = 24 + targetSdk = 34 + versionCode = 5 + versionName = "1.2.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", "proguard-retrofit2.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + implementation(libs.maps.compose) + implementation(libs.maps.compose.utils) + + // Dagger Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) + + // DataStore + implementation(libs.androidx.datastore.preferences) + + // Paging3 + implementation(libs.androidx.paging.runtime.ktx) + implementation(libs.androidx.paging.compose) + + // Retrofit + implementation(libs.retrofit) + implementation(libs.logging.interceptor) + implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit2.kotlinx.serialization.converter) + + // Room + implementation(libs.androidx.room.runtime) + ksp(libs.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.paging) + + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + debugImplementation(libs.leakcanary.android) + + testImplementation(libs.junit) + + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) +} + +secrets { + defaultPropertiesFileName = "local.defaults.properties" +} \ No newline at end of file diff --git a/app/proguard-gson.pro b/app/proguard-gson.pro deleted file mode 100644 index 2ce8b74..0000000 --- a/app/proguard-gson.pro +++ /dev/null @@ -1,25 +0,0 @@ -# Gson uses generic type information stored in a class file when working with fields. Proguard -# removes such information by default, so configure it to keep all of it. --keepattributes Signature - -# For using GSON @Expose annotation --keepattributes *Annotation* - -# Gson specific classes --dontwarn sun.misc.** -#-keep class com.google.gson.stream.** { *; } - -# Application classes that will be serialized/deserialized over Gson --keepclassmembers class com.alfred.model.** { *; } - -# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, -# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) --keep class * extends com.google.gson.TypeAdapter --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer - -# Prevent R8 from leaving Data object members always null --keepclassmembers,allowobfuscation class * { - @com.google.gson.annotations.SerializedName ; -} \ No newline at end of file diff --git a/app/proguard-sqlite.pro b/app/proguard-sqlite.pro deleted file mode 100644 index 12b772f..0000000 --- a/app/proguard-sqlite.pro +++ /dev/null @@ -1,2 +0,0 @@ --keep,includedescriptorclasses class net.sqlcipher.** { *; } --keep,includedescriptorclasses interface net.sqlcipher.** { *; } \ No newline at end of file diff --git a/app/proguard-square-okhttp.pro b/app/proguard-square-okhttp.pro deleted file mode 100644 index 70825b7..0000000 --- a/app/proguard-square-okhttp.pro +++ /dev/null @@ -1,11 +0,0 @@ -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* - -# OkHttp platform used only on JVM and when Conscrypt dependency is available. --dontwarn okhttp3.internal.platform.ConscryptPlatform \ No newline at end of file diff --git a/app/proguard-square-retrofit2.pro b/app/proguard-square-retrofit2.pro index 0d2b0fc..6c1daaf 100644 --- a/app/proguard-square-retrofit2.pro +++ b/app/proguard-square-retrofit2.pro @@ -5,6 +5,9 @@ # Retrofit does reflection on method and parameter annotations. -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + # Retain service method parameters when optimizing. -keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; @@ -26,4 +29,20 @@ # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. -if interface * { @retrofit2.http.* ; } --keep,allowobfuscation interface <1> \ No newline at end of file +-keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# R8 full mode strips generic signatures from return types if not kept. +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> + +# With R8 full mode generic signatures are stripped for classes that are not kept. +-keep,allowobfuscation,allowshrinking class retrofit2.Response \ No newline at end of file diff --git a/app/release/app-release.aab b/app/release/app-release.aab deleted file mode 100644 index 60b53fc..0000000 Binary files a/app/release/app-release.aab and /dev/null differ diff --git a/app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt similarity index 83% rename from app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt index 679447a..0d26a65 100644 --- a/app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.a1573595.parkinglotdemo +package com.a1573595.parkingdemo import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.a1573595.parkinglotdemo", appContext.packageName) + assertEquals("com.a1573595.parkinglot", appContext.packageName) } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d685349..053008f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,49 +1,36 @@ - + + android:theme="@style/Theme.ParkingLot" + tools:targetApi="31"> + android:value="${MAPS_API_KEY}" /> + android:theme="@style/Theme.ParkingLot"> - - - - + \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt b/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt new file mode 100644 index 0000000..597f015 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt @@ -0,0 +1,7 @@ +package com.a1573595.parkingdemo + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class ParkingLotApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt b/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt new file mode 100644 index 0000000..a184b6c --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt @@ -0,0 +1,30 @@ +package com.a1573595.parkingdemo.common + +sealed class AsyncValue { + data object Loading : AsyncValue() + + data class Error(val throwable: Throwable) : AsyncValue() + + data class Data(val data: T) : AsyncValue() + + val isLoading: Boolean + get() = this is Loading + + val isError: Boolean + get() = this is Error + + val isSuccess: Boolean + get() = this is Data + + val error: Throwable? + get() = (this as? Error)?.throwable + + val requireError: Throwable + get() = (this as Error).throwable + + val value: T? + get() = (this as? Data)?.data + + val requireValue: T + get() = (this as Data).data +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt new file mode 100644 index 0000000..4996e1c --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt @@ -0,0 +1,29 @@ +package com.a1573595.parkingdemo.common + +import android.os.Build +import android.util.Base64 as AndroidBase64 +import java.util.Base64 as JavaBase64 + +object Base64EncodeDecode { + fun String.encodeToBase64(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + JavaBase64.getUrlEncoder().encodeToString(this.toByteArray()) + } else { + AndroidBase64.encodeToString( + this.toByteArray(), + AndroidBase64.URL_SAFE or AndroidBase64.NO_PADDING + ) + } + + fun String.decodeFromBase64(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String(JavaBase64.getUrlDecoder().decode(this)) + } else { + String( + AndroidBase64.decode( + this, + AndroidBase64.URL_SAFE or AndroidBase64.NO_PADDING + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt new file mode 100644 index 0000000..c7f192f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt @@ -0,0 +1,5 @@ +package com.a1573595.parkingdemo.common + +object Constants { + const val BASE_URL = "https://tcgbusfs.blob.core.windows.net/blobtcmsv/" +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt new file mode 100644 index 0000000..9ce682c --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt @@ -0,0 +1,15 @@ +package com.a1573595.parkingdemo.common + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.transform + +fun Double?.isNullOrEmpty(): Boolean = this == null || this == 0.0 + +fun Flow.throttleLatest(delayMillis: Long): Flow = this + .conflate() + .transform { + emit(it) + delay(delayMillis) + } \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt new file mode 100644 index 0000000..ceb1733 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt @@ -0,0 +1,7 @@ +package com.a1573595.parkingdemo.common + +import android.annotation.SuppressLint +import java.text.SimpleDateFormat + +@SuppressLint("SimpleDateFormat") +val dateFormatIso8601 = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt b/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt new file mode 100644 index 0000000..7c2c543 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt @@ -0,0 +1,63 @@ +package com.a1573595.parkingdemo.common + +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.tan + +class LatLngConverter { + companion object { + private const val a = 6378137.0 + private const val b = 6356752.314245 + private const val lon0 = 121 * Math.PI / 180 + private const val k0 = 0.9999 + private const val dx = 250000 + + fun twd97ToLonLat(x: Double, y: Double): Pair { + var x = x + var y = y + val dy = 0.0 + val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5) + x -= dx.toDouble() + y -= dy + + // Calculate the Meridional Arc + val M = y / k0 + + // Calculate Footprint Latitude + val mu = M / (a * (1.0 - e.pow(2.0) / 4.0 - 3 * e.pow(4.0) / 64.0 - 5 * e.pow(6.0) / 256.0)) + val e1 = (1.0 - (1.0 - e.pow(2.0)).pow(0.5)) / (1.0 + (1.0 - e.pow(2.0)).pow(0.5)) + val j1 = 3 * e1 / 2 - 27 * e1.pow(3.0) / 32.0 + val j2 = 21 * e1.pow(2.0) / 16 - 55 * e1.pow(4.0) / 32.0 + val j3 = 151 * e1.pow(3.0) / 96.0 + val j4 = 1097 * e1.pow(4.0) / 512.0 + val fp = mu + j1 * sin(2 * mu) + j2 * sin(4 * mu) + j3 * sin(6 * mu) + j4 * sin(8 * mu) + + // Calculate Latitude and Longitude + val e2 = (e * a / b).pow(2.0) + val c1 = (e2 * cos(fp)).pow(2.0) + val t1 = tan(fp).pow(2.0) + val r1 = a * (1 - e.pow(2.0)) / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(3.0 / 2.0) + val n1 = a / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(0.5) + val D = x / (n1 * k0) + + // Calculate latitude + val q1 = n1 * tan(fp) / r1 + val q2 = D.pow(2.0) / 2.0 + val q3 = (5 + 3 * t1 + 10 * c1 - 4 * c1.pow(2.0) - 9 * e2) * D.pow(4.0) / 24.0 + val q4 = (61 + 90 * t1 + 298 * c1 + 45 * t1.pow(2.0) - 3 * c1.pow(2.0) - 252 * e2) * D.pow(6.0) / 720.0 + var lat = fp - q1 * (q2 - q3 + q4) + + // Calculate longitude + val q6 = (1 + 2 * t1 + c1) * D.pow(3.0) / 6 + val q7 = (5 - 2 * c1 + 28 * t1 - 3 * c1.pow(2.0) + 8 * e2 + 24 * t1.pow(2.0)) * D.pow(5.0) / 120.0 + var lon = lon0 + (D - q6 + q7) / cos(fp) + + // Convert to degrees + lat = lat * 180 / Math.PI + lon = lon * 180 / Math.PI + + return Pair(lat, lon) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt new file mode 100644 index 0000000..fab032f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt @@ -0,0 +1,9 @@ +package com.a1573595.parkingdemo.common + +import kotlinx.serialization.json.Json + +val jsonConvert = Json { + allowStructuredMapKeys = true + isLenient = true + ignoreUnknownKeys = true +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt new file mode 100644 index 0000000..f42110d --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt @@ -0,0 +1,21 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.a1573595.parkingdemo.domain.model.ParkingLot + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ParkingLot::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE, + ), + ], +) +data class Favorite( + @PrimaryKey + val id: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt new file mode 100644 index 0000000..512aa10 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt @@ -0,0 +1,23 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert +import com.a1573595.parkingdemo.domain.model.ParkingLot +import kotlinx.coroutines.flow.Flow + +@Dao +interface FavoriteDao { + @Query("SELECT * FROM Favorite INNER JOIN ParkingLot ON Favorite.id = ParkingLot.id Order By Favorite.rowid DESC") + fun getParkingLotListFlow(): Flow> + + @Query("SELECT * FROM Favorite WHERE id LIKE :id") + fun getFavoriteByIdFlow(id: String): Flow + + @Upsert + suspend fun upsert(favorite: Favorite) + + @Delete + suspend fun delete(favorite: Favorite) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt new file mode 100644 index 0000000..cc11eaa --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt @@ -0,0 +1,21 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.a1573595.parkingdemo.domain.model.ParkingLot + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ParkingLot::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE, + ), + ], +) +data class History( + @PrimaryKey + val id: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt new file mode 100644 index 0000000..adfd6c3 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt @@ -0,0 +1,27 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import com.a1573595.parkingdemo.domain.model.ParkingLot +import kotlinx.coroutines.flow.Flow + +@Dao +interface HistoryDao { + @Query("SELECT * FROM History INNER JOIN ParkingLot ON History.id = ParkingLot.id Order By History.rowid DESC") + fun getParkingLotListFlow(): Flow> + + @Upsert + suspend fun upsert(history: History) + + @Delete + suspend fun delete(history: History) + + @Transaction + suspend fun deleteAndUpsert(history: History) { + delete(history) + upsert(history) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt new file mode 100644 index 0000000..968eeb9 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt @@ -0,0 +1,30 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.room.Upsert +import androidx.sqlite.db.SupportSQLiteQuery +import com.a1573595.parkingdemo.domain.model.ParkingLot +import kotlinx.coroutines.flow.Flow + +@Dao +interface ParkingLotDao { + @Query("SELECT * FROM ParkingLot Order By id") + fun getParkingLotListFlow(): Flow> + + @Query("SELECT * FROM ParkingLot WHERE id=:id") + suspend fun getParkingLotById(id: String): ParkingLot? + + @Query("SELECT * FROM ParkingLot WHERE name LIKE '%' || :keyword || '%' OR address LIKE '%' || :keyword || '%' Order By id") + fun pagingSource(keyword: String): PagingSource + + @RawQuery(observedEntities = [ParkingLot::class]) + fun pagingSource(query: SupportSQLiteQuery): PagingSource + + @Transaction + @Upsert + suspend fun upsertAll(parkingLotList: List): List +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt new file mode 100644 index 0000000..d5fd929 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt @@ -0,0 +1,22 @@ +package com.a1573595.parkingdemo.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.a1573595.parkingdemo.domain.model.ParkingLot + +@Database( + version = 1, + entities = [ + ParkingLot::class, + Favorite::class, + History::class, + ], +// exportSchema = false, +) +abstract class ParkingLotDatabase : RoomDatabase() { + abstract val parkingLotDao: ParkingLotDao + + abstract val favoriteDao: FavoriteDao + + abstract val historyDao: HistoryDao +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt b/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt new file mode 100644 index 0000000..ae6bbf4 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt @@ -0,0 +1,36 @@ +package com.a1573595.parkingdemo.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ParkingLotDataSet( + val data: Data, +) + +@Serializable +data class Data( + val park: List, +) + +@Serializable +data class Park( + val id: String?, + val area: String?, + val name: String?, + val summary: String?, + val address: String?, + val tel: String?, + @SerialName("payex") + val payEx: String?, + val tw97x: Double?, + val tw97y: Double?, + @SerialName("totalcar") + val totalCar: Int?, + @SerialName("totalmotor") + val totalMotor: Int?, + @SerialName("totalbike") + val totalBike: Int?, + @SerialName("totalbus") + val totalBus: Int?, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt b/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt new file mode 100644 index 0000000..6c18006 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt @@ -0,0 +1,10 @@ +package com.a1573595.parkingdemo.data.network + +import okhttp3.ResponseBody +import retrofit2.http.GET + +fun interface ParkingLotApi { + @GET("TCMSV_alldesc.gz") +// suspend fun getParkingLotDataSet(): Response + suspend fun getParkingLotDataSet(): ResponseBody +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt b/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt new file mode 100644 index 0000000..17a249e --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt @@ -0,0 +1,119 @@ +package com.a1573595.parkingdemo.data.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.sqlite.db.SimpleSQLiteQuery +import com.a1573595.parkingdemo.common.isNullOrEmpty +import com.a1573595.parkingdemo.common.jsonConvert +import com.a1573595.parkingdemo.data.local.Favorite +import com.a1573595.parkingdemo.data.local.FavoriteDao +import com.a1573595.parkingdemo.data.local.History +import com.a1573595.parkingdemo.data.local.HistoryDao +import com.a1573595.parkingdemo.data.local.ParkingLotDao +import com.a1573595.parkingdemo.data.model.ParkingLotDataSet +import com.a1573595.parkingdemo.data.network.ParkingLotApi +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.io.ByteArrayOutputStream +import java.util.zip.GZIPInputStream +import javax.inject.Inject + +class ParkingLotRepositoryImpl @Inject constructor( + private val dataStore: DataStore, + private val parkingLotDao: ParkingLotDao, + private val favoriteDao: FavoriteDao, + private val historyDao: HistoryDao, + private val parkingLotApi: ParkingLotApi, +) : ParkingLotRepository { + private companion object { + val KEY_LAST_UPDATE_TIME = longPreferencesKey(name = "lastUpdateTime") + } + + override val lastUpdateTimeFlow: Flow + get() = dataStore.data.map { + it[KEY_LAST_UPDATE_TIME] + } + + override val parkingLotListFlow: Flow> + get() = parkingLotDao.getParkingLotListFlow() + + override val favoriteParkingLotListFlow: Flow> + get() = favoriteDao.getParkingLotListFlow() + + override val historyParkingLotListFlow: Flow> + get() = historyDao.getParkingLotListFlow() + + override suspend fun fetchParkingLotDataSet(): Unit { + val respond = parkingLotApi.getParkingLotDataSet() + val inputStream = GZIPInputStream(respond.byteStream()) + + val buffer = ByteArray(256) + val outputStream = ByteArrayOutputStream() + + var length: Int + while (inputStream.read(buffer).also { length = it } >= 0) { + outputStream.write(buffer, 0, length) + } + + val dataSet = jsonConvert + .decodeFromString(outputStream.toString("UTF-8")) + + val parkingLotList = + dataSet.data.park.filterNot { it.id.isNullOrEmpty() && it.tw97x.isNullOrEmpty() && it.tw97y.isNullOrEmpty() } + .map { ParkingLot.fromPark(it) } + + parkingLotDao.upsertAll(parkingLotList) + dataStore.edit { + it[KEY_LAST_UPDATE_TIME] = System.currentTimeMillis() + } + } + + override fun searchParkingLotPagingDataFlow( + keyword: String, + hasBus: Boolean, + hasCar: Boolean, + hasMotor: Boolean, + hasBike: Boolean, + ): Flow> { + val builder = StringBuilder() + builder.append("SELECT * FROM ParkingLot") + builder.append(" WHERE (name LIKE '%%$keyword%%' OR address LIKE '%%$keyword%%')") + + if (hasBus) { + builder.append(" AND totalBus > 0") + } + if (hasCar) { + builder.append(" AND totalCar > 0") + } + if (hasMotor) { + builder.append(" AND totalMotor > 0") + } + if (hasBike) { + builder.append(" AND totalBike > 0") + } + + return Pager( + config = PagingConfig(pageSize = 30, prefetchDistance = 2), + pagingSourceFactory = { parkingLotDao.pagingSource(SimpleSQLiteQuery(builder.toString())) }, + ).flow + } + + override suspend fun getParkingLotById(id: String): ParkingLot? = parkingLotDao.getParkingLotById(id)?.apply { + historyDao.deleteAndUpsert(History(id)) + } + + override fun getFavoriteByIdFlow(id: String): Flow = favoriteDao.getFavoriteByIdFlow(id) + + override suspend fun upsertFavoriteById(id: String): Unit = favoriteDao.upsert(Favorite(id)) + + override suspend fun deleteFavoriteById(id: String): Unit = favoriteDao.delete(Favorite(id)) + + override suspend fun deleteHistoryById(id: String): Unit = historyDao.delete(History(id)) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt new file mode 100644 index 0000000..10005c6 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt @@ -0,0 +1,64 @@ +package com.a1573595.parkingdemo.di + +import android.app.Application +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.room.Room +import com.a1573595.parkingdemo.data.local.FavoriteDao +import com.a1573595.parkingdemo.data.local.HistoryDao +import com.a1573595.parkingdemo.data.local.ParkingLotDao +import com.a1573595.parkingdemo.data.local.ParkingLotDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocalModule { + @Provides + @Singleton + fun provideParkingLotDataStore(@ApplicationContext appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + produceFile = { + appContext.preferencesDataStoreFile("parkingLot_preferences") + } + ) + + @Provides + @Singleton + fun provideParkingLotDatabase( + application: Application + ): ParkingLotDatabase { + return Room.databaseBuilder( + context = application, + klass = ParkingLotDatabase::class.java, + name = "news_db" + ) + .fallbackToDestructiveMigration() + .build() + } + + @Provides + @Singleton + fun provideParkingLotDao( + database: ParkingLotDatabase + ): ParkingLotDao = database.parkingLotDao + + @Provides + @Singleton + fun provideFavoriteDao( + database: ParkingLotDatabase + ): FavoriteDao = database.favoriteDao + + @Provides + @Singleton + fun provideHistoryDao( + database: ParkingLotDatabase + ): HistoryDao = database.historyDao +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt new file mode 100644 index 0000000..4698fd8 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt @@ -0,0 +1,50 @@ +package com.a1573595.parkingdemo.di + +import android.content.Context +import com.a1573595.parkingdemo.BuildConfig +import com.a1573595.parkingdemo.common.Constants +import com.a1573595.parkingdemo.data.network.ParkingLotApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun provideOkHttpClient( + interceptor: HttpLoggingInterceptor, + @ApplicationContext context: Context + ): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + } + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + } + + @Provides + @Singleton + fun providesParkingLotApi(client: OkHttpClient): ParkingLotApi = Retrofit.Builder() + .client(client) + .baseUrl(Constants.BASE_URL) + .build() + .create(ParkingLotApi::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt new file mode 100644 index 0000000..2e3492a --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt @@ -0,0 +1,15 @@ +package com.a1573595.parkingdemo.di + +import com.a1573595.parkingdemo.data.repository.ParkingLotRepositoryImpl +import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + @Provides + fun provideParkingLotRepository(newsRepositoryImpl: ParkingLotRepositoryImpl): ParkingLotRepository = newsRepositoryImpl +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt new file mode 100644 index 0000000..dbc97ee --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt @@ -0,0 +1,44 @@ +package com.a1573595.parkingdemo.domain.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.a1573595.parkingdemo.common.LatLngConverter +import com.a1573595.parkingdemo.data.model.Park + +@Entity +data class ParkingLot( + @PrimaryKey val id: String, + val area: String, + val name: String, + val summary: String, + val address: String, + val tel: String, + val payEx: String, + val lat: Double, + val lon: Double, + val totalCar: Int, + val totalMotor: Int, + val totalBike: Int, + val totalBus: Int, +) { + companion object { + fun fromPark(park: Park): ParkingLot { + val latlonPair = LatLngConverter.twd97ToLonLat(park.tw97x!!, park.tw97y!!) + return ParkingLot( + id = park.id!!, + area = park.area ?: "Unknown Area", + name = park.name ?: "Unknown Name", + summary = park.summary ?: "No Summary Available", + address = park.address ?: "No Address Available", + tel = park.tel ?: "No Tel Available", + payEx = park.payEx ?: "No PayEx Available", + lat = latlonPair.first, + lon = latlonPair.second, + totalCar = park.totalCar ?: 0, + totalMotor = park.totalMotor ?: 0, + totalBike = park.totalBike ?: 0, + totalBus = park.totalBus ?: 0, + ) + } + } +} diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt new file mode 100644 index 0000000..2daef54 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt @@ -0,0 +1,36 @@ +package com.a1573595.parkingdemo.domain.repository + +import androidx.paging.PagingData +import com.a1573595.parkingdemo.data.local.Favorite +import com.a1573595.parkingdemo.domain.model.ParkingLot +import kotlinx.coroutines.flow.Flow + +interface ParkingLotRepository { + val lastUpdateTimeFlow: Flow + + val parkingLotListFlow: Flow> + + val favoriteParkingLotListFlow: Flow> + + val historyParkingLotListFlow: Flow> + + suspend fun fetchParkingLotDataSet(): Unit + + fun searchParkingLotPagingDataFlow( + keyword: String, + hasBus: Boolean, + hasCar: Boolean, + hasMotor: Boolean, + hasBike: Boolean, + ): Flow> + + suspend fun getParkingLotById(id: String): ParkingLot? + + fun getFavoriteByIdFlow(id: String): Flow + + suspend fun upsertFavoriteById(id: String): Unit + + suspend fun deleteFavoriteById(id: String): Unit + + suspend fun deleteHistoryById(id: String): Unit +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt new file mode 100644 index 0000000..8a9f1c3 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt @@ -0,0 +1,22 @@ +package com.a1573595.parkingdemo.domain.usecase + +import com.a1573595.parkingdemo.data.local.Favorite +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject + +class FavoriteUseCase @Inject constructor( + private val parkingLotRepository: ParkingLotRepository, +) { + operator fun invoke(): Flow> = + parkingLotRepository.favoriteParkingLotListFlow.flowOn(Dispatchers.IO) + + fun getById(id: String): Flow = parkingLotRepository.getFavoriteByIdFlow(id).flowOn(Dispatchers.IO) + + suspend fun upsertById(id: String): Unit = parkingLotRepository.upsertFavoriteById(id) + + suspend fun deleteById(id: String): Unit = parkingLotRepository.deleteFavoriteById(id) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt new file mode 100644 index 0000000..945668a --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt @@ -0,0 +1,17 @@ +package com.a1573595.parkingdemo.domain.usecase + +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject + +class HistoryUseCase @Inject constructor( + private val parkingLotRepository: ParkingLotRepository, +) { + operator fun invoke(): Flow> = + parkingLotRepository.historyParkingLotListFlow.flowOn(Dispatchers.IO) + + suspend fun deleteById(id: String): Unit = parkingLotRepository.deleteHistoryById(id) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt new file mode 100644 index 0000000..80efbc3 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt @@ -0,0 +1,39 @@ +package com.a1573595.parkingdemo.domain.usecase + +import androidx.paging.PagingData +import com.a1573595.parkingdemo.common.dateFormatIso8601 +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import java.util.Date +import javax.inject.Inject + +class ParkingLotUseCase @Inject constructor( + private val parkingLotRepository: ParkingLotRepository, +) { + val lastUpdateTimeFlow: Flow + get() = parkingLotRepository.lastUpdateTimeFlow.map { + it?.let { value -> + dateFormatIso8601.format(Date(value)) + } + }.flowOn(Dispatchers.IO) + + operator fun invoke(): Flow> = parkingLotRepository.parkingLotListFlow.flowOn(Dispatchers.IO) + + suspend fun fetchDataSet(): Unit = parkingLotRepository.fetchParkingLotDataSet() + + fun searchPagingDataFlow( + keyword: String = "", + hasBus: Boolean = false, + hasCar: Boolean = false, + hasMotor: Boolean = false, + hasBike: Boolean = false, + ): Flow> = + parkingLotRepository.searchParkingLotPagingDataFlow(keyword, hasBus, hasCar, hasMotor, hasBike) + .flowOn(Dispatchers.IO) + + suspend fun getParkingLotById(id: String): ParkingLot? = parkingLotRepository.getParkingLotById(id) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt new file mode 100644 index 0000000..7a34bda --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt @@ -0,0 +1,24 @@ +package com.a1573595.parkingdemo.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.a1573595.parkingdemo.ui.component.DoubleBackPress +import com.a1573595.parkingdemo.ui.navigation.NavGraph +import com.a1573595.parkingdemo.ui.theme.ParkingLotTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ParkingLotTheme { + DoubleBackPress() + NavGraph() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt new file mode 100644 index 0000000..37d052d --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt @@ -0,0 +1,24 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun BorderCard(content: @Composable ColumnScope.() -> Unit) { + Card( + border = BorderStroke( + Dimens.dp1, + Color.Gray, + ), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + content = content, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt new file mode 100644 index 0000000..7d8ea03 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt @@ -0,0 +1,45 @@ +package com.a1573595.parkingdemo.ui.component + +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.a1573595.parkingdemo.R + +import kotlinx.coroutines.delay + +sealed class BackPress { + data object Idle : BackPress() + data object InitialTouch : BackPress() +} + +@Composable +fun DoubleBackPress() { + var showToast by remember { mutableStateOf(false) } + var backPressState by remember { mutableStateOf(BackPress.Idle) } + + val context = LocalContext.current + + if(showToast){ + Toast.makeText(context, stringResource(R.string.press_again_to_exit), Toast.LENGTH_SHORT).show() + showToast= false + } + + LaunchedEffect(key1 = backPressState) { + if (backPressState == BackPress.InitialTouch) { + delay(2000) + backPressState = BackPress.Idle + } + } + + BackHandler(backPressState == BackPress.Idle) { + backPressState = BackPress.InitialTouch + showToast = true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt new file mode 100644 index 0000000..79db8ed --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt @@ -0,0 +1,43 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun ErrorBody(throwable: Throwable) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + Icons.Filled.Error, + contentDescription = null, + modifier = Modifier.fillMaxSize(.2f), + ) + Text( + text = throwable.message ?: "Unknown Error", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + } +} + +@Preview(showSystemUi = true) +@Composable +fun ErrorBodyPreview() { + ErrorBody(Exception("Preview")) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt new file mode 100644 index 0000000..560ba3a --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt @@ -0,0 +1,22 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun LoadMoreFooter() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(Dimens.dp16), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt new file mode 100644 index 0000000..92a3651 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt @@ -0,0 +1,26 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun LoadingBody() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(modifier = Modifier.fillMaxWidth(.2f)) + } +} + +@Preview(showSystemUi = true) +@Composable +fun LoadingBodyPreview() { + LoadingBody() +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt new file mode 100644 index 0000000..240649b --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt @@ -0,0 +1,38 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NavigationAppBar( + onBackClick: () -> Unit, + title: String, +) { + TopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + navigationIcon = { + IconButton( + onClick = onBackClick + ) { + Icon( + imageVector = Icons.Filled.ArrowBackIosNew, + contentDescription = "back", + ) + } + }, + title = { + Text(text = title) + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt new file mode 100644 index 0000000..7036ecb --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt @@ -0,0 +1,27 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun NoMoreFooter() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(Dimens.dp16), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.no_more), + modifier = Modifier.align(Alignment.Center), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt new file mode 100644 index 0000000..cf2a219 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt @@ -0,0 +1,71 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.domain.model.ParkingLot + +@Composable +fun ParkingLotItem( + parkingLot: ParkingLot, + onClick: (String) -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + elevation = CardDefaults.cardElevation(4.dp), + onClick = { onClick(parkingLot.id) } + ) { + Text( + text = parkingLot.name, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + Text( + text = "${parkingLot.area}${parkingLot.address}", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.bus_number, parkingLot.totalBus), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = stringResource(R.string.car_number, parkingLot.totalCar), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = stringResource(R.string.motor_number, parkingLot.totalMotor), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = stringResource(R.string.bike_number, parkingLot.totalBike), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt new file mode 100644 index 0000000..09b9c6e --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt @@ -0,0 +1,75 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemKey +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun ParkingLotLazyColumn( + lazyListState: LazyListState, + parkingLotList: LazyPagingItems, + onParkingLotItemClick: (String) -> Unit, +) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .padding(top = Dimens.dp16) + .padding(horizontal = Dimens.dp16), + ) { + items( + count = parkingLotList.itemCount, + key = parkingLotList.itemKey { it.id } + ) { index -> + parkingLotList[index]?.let { + ParkingLotItem( + it, + onClick = onParkingLotItemClick, + ) + } + } + parkingLotList.loadState.apply { + when { + append is LoadState.Loading -> item { LoadMoreFooter() } + refresh is LoadState.NotLoading && append is LoadState.NotLoading -> item { NoMoreFooter() } + } + } + } +} + +@Composable +fun ParkingLotLazyColumn( + lazyListState: LazyListState, + parkingLotList: List, + onDelete: (String) -> Unit, + onClick: (String) -> Unit, +) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = Dimens.dp16), + ) { + items( + count = parkingLotList.size, + key = { index -> parkingLotList[index].id } + ) { index -> + SwipeToDismissContainer( + onDelete = { onDelete(parkingLotList[index].id) }, + ) { + ParkingLotItem( + parkingLotList[index], + onClick = onClick, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt new file mode 100644 index 0000000..5a01cc4 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt @@ -0,0 +1,103 @@ +package com.a1573595.parkingdemo.ui.component + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import com.a1573595.parkingdemo.ui.theme.Dimens + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwipeToDismissContainer( + onDelete: () -> Unit, + content: @Composable RowScope.() -> Unit, +) { + val swipeState = rememberSwipeToDismissBoxState( + confirmValueChange = { newState -> + newState == SwipeToDismissBoxValue.EndToStart + }, + ) + + val icon: ImageVector + val alignment: Alignment + val color: Color + + when (swipeState.dismissDirection) { + SwipeToDismissBoxValue.EndToStart -> { + icon = Icons.Outlined.Delete + alignment = Alignment.CenterEnd + color = Color.Red + } + + SwipeToDismissBoxValue.StartToEnd -> { + icon = Icons.Outlined.Edit + alignment = Alignment.CenterStart + color = Color.Green.copy(alpha = 0.3f) + } + + SwipeToDismissBoxValue.Settled -> { + icon = Icons.Outlined.Delete + alignment = Alignment.CenterEnd +// color = MaterialTheme.colorScheme.errorContainer + color = Color.Transparent + } + } + + when (swipeState.currentValue) { + SwipeToDismissBoxValue.EndToStart -> { + onDelete() + } + + SwipeToDismissBoxValue.StartToEnd -> { +// LaunchedEffect(swipeState) { +// onEdit() +// swipeState.snapTo(SwipeToDismissBoxValue.Settled) +// } + } + + SwipeToDismissBoxValue.Settled -> {} + } + + SwipeToDismissBox( + state = swipeState, + enableDismissFromStartToEnd = false, + enableDismissFromEndToStart = true, + modifier = Modifier.animateContentSize(), + backgroundContent = { + Box( + modifier = Modifier + .padding(Dimens.dp8) + .fillMaxSize() + .background(color), + contentAlignment = alignment, + ) { + Icon( + modifier = Modifier + .minimumInteractiveComponentSize() + .size(Dimens.dp32), + imageVector = icon, + contentDescription = null, + tint = Color.White, + ) + } + }, + content = content, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt new file mode 100644 index 0000000..d99dd9f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt @@ -0,0 +1,73 @@ +package com.a1573595.parkingdemo.ui.navigation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.a1573595.parkingdemo.ui.screen.HomeScreen +import com.a1573595.parkingdemo.ui.screen.detail.DetailScreen +import com.a1573595.parkingdemo.ui.screen.favorite.FavoriteScreen +import com.a1573595.parkingdemo.ui.screen.history.HistoryScreen +import com.a1573595.parkingdemo.ui.screen.map.MapScreen +import com.a1573595.parkingdemo.ui.screen.search.SearchScreen + +@Composable +fun NavGraph() { + val navController = rememberNavController() + Column( + modifier = Modifier.fillMaxSize() + ) { + NavHost( + modifier = Modifier + .fillMaxSize() + .weight(1f), + navController = navController, + startDestination = NavRoute.Home.route, + ) { + val onParkingLotItemClick: (String) -> Unit = { + navController.navigate(NavRoute.Detail.passParkingLotId(it)) + } + + composable(NavRoute.Home.route) { + HomeScreen( + onMapClick = { navController.navigate(NavRoute.Map.route) }, + onSearchClick = { navController.navigate(NavRoute.Search.route) }, + onFavoriteClick = { navController.navigate(NavRoute.Favorite.route) }, + onHistoryClick = { navController.navigate(NavRoute.History.route) }, + ) + } + composable(NavRoute.Map.route) { + MapScreen( + onBackClick = { navController.popBackStack() }, + onParkingLotItemClick = onParkingLotItemClick, + ) + } + composable(NavRoute.Search.route) { + SearchScreen( + onBackClick = { navController.popBackStack() }, + onParkingLotItemClick = onParkingLotItemClick, + ) + } + composable(NavRoute.Favorite.route) { + FavoriteScreen( + onBackClick = { navController.popBackStack() }, + onParkingLotItemClick = onParkingLotItemClick, + ) + } + composable(NavRoute.History.route) { + HistoryScreen( + onBackClick = { navController.popBackStack() }, + onParkingLotItemClick = onParkingLotItemClick, + ) + } + composable(NavRoute.Detail.route) { + DetailScreen( + onBackClick = { navController.popBackStack() }, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt new file mode 100644 index 0000000..68e9f7b --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt @@ -0,0 +1,20 @@ +package com.a1573595.parkingdemo.ui.navigation + +sealed class NavRoute(val route: String) { + companion object { + const val KEY_ID = "id" + } + + data object Home : NavRoute("home") + data object Map : NavRoute("map") + data object Search : NavRoute("search") + data object Favorite : NavRoute("favorite") + data object History : NavRoute("history") + data object Detail : NavRoute("detail/{$KEY_ID}") { + fun passParkingLotId(id: String): String { + return this.route.replace( + oldValue = "{$KEY_ID}", newValue = id + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt new file mode 100644 index 0000000..05abf47 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt @@ -0,0 +1,228 @@ +package com.a1573595.parkingdemo.ui.screen + +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.component.ErrorBody +import com.a1573595.parkingdemo.ui.component.LoadingBody +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun HomeScreen( + onMapClick: () -> Unit, + onSearchClick: () -> Unit, + onFavoriteClick: () -> Unit, + onHistoryClick: () -> Unit, + viewModel: HomeViewModel = hiltViewModel(), +) { + val uiState = viewModel.uiState.value + + Scaffold( + topBar = { + HomeAppBar( + isRefreshAble = uiState.isSuccess, + onRefreshClick = { + viewModel.refreshData() + }, + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(horizontal = Dimens.dp16) + ) { + when { + uiState.isLoading -> LoadingBody() + uiState.isError -> ErrorBody(throwable = uiState.requireError) + else -> HomeBody( + uiState = uiState.requireValue, + onMapClick = onMapClick, + onSearchClick = onSearchClick, + onFavoriteClick = onFavoriteClick, + onHistoryClick = onHistoryClick, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeAppBar( + isRefreshAble: Boolean, + onRefreshClick: () -> Unit, +) { + TopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + title = { + Text( + stringResource(R.string.parking_lot), + style = MaterialTheme.typography.headlineMedium, + ) + }, + actions = { + if (isRefreshAble) { + IconButton(onClick = { onRefreshClick() }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + ) + } + } + }, + ) +} + +@Composable +fun HomeBody( + uiState: HomeUiState, + onMapClick: () -> Unit, + onSearchClick: () -> Unit, + onFavoriteClick: () -> Unit, + onHistoryClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Dimens.dp16) + ) { + Text( + text = stringResource( + R.string.total_of_parking_lots_download_at, + uiState.numberOfParkingLots, + uiState.lastUpTime + ), + style = MaterialTheme.typography.titleLarge, + ) + Image( + painterResource(R.mipmap.ic_launcher_foreground), + contentDescription = "", + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentScale = ContentScale.Crop, + ) + ImageElevatedButton( + onClick = onMapClick, + backgroundColor = android.R.color.holo_green_light, + drawableId = R.drawable.ic_map, + title = stringResource(R.string.map) + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(Dimens.dp16), + ) + ImageElevatedButton( + onClick = onSearchClick, + backgroundColor = android.R.color.holo_blue_light, + drawableId = R.drawable.ic_list, + title = stringResource(R.string.search) + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(Dimens.dp16), + ) + ImageElevatedButton( + onClick = onFavoriteClick, + backgroundColor = android.R.color.holo_red_light, + drawableId = R.drawable.ic_favorite, + title = stringResource(R.string.favorite) + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(Dimens.dp16), + ) + ImageElevatedButton( + onClick = onHistoryClick, + backgroundColor = android.R.color.holo_orange_light, + drawableId = R.drawable.ic_history, + title = stringResource(R.string.history) + ) + } +} + +@Composable +fun ImageElevatedButton( + onClick: () -> Unit, + @ColorRes backgroundColor: Int, + @DrawableRes drawableId: Int, + title: String, +) { + ElevatedButton( + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = Dimens.dp4, + ), + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(id = backgroundColor), + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Image( + modifier = Modifier.height(32.dp), + painter = painterResource(id = drawableId), + contentDescription = title, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun ImageElevatedButtonPreview() { + ImageElevatedButton( + onClick = { }, + backgroundColor = android.R.color.holo_green_light, + drawableId = R.drawable.ic_map, + title = "Map Mode", + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt new file mode 100644 index 0000000..9cd731f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt @@ -0,0 +1,6 @@ +package com.a1573595.parkingdemo.ui.screen + +data class HomeUiState( + val lastUpTime: String, + val numberOfParkingLots: Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt new file mode 100644 index 0000000..840cd5e --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt @@ -0,0 +1,42 @@ +package com.a1573595.parkingdemo.ui.screen + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.a1573595.parkingdemo.common.AsyncValue +import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.zip +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val parkingLotUseCase: ParkingLotUseCase, +) : ViewModel() { + private val _uiState = mutableStateOf>(AsyncValue.Loading) + + val uiState: State> = _uiState + + init { + viewModelScope.launch { + parkingLotUseCase.lastUpdateTimeFlow.zip(parkingLotUseCase()) { lastUpdateTime, parkingLotList -> + if (lastUpdateTime.isNullOrEmpty()) { + refreshData() + } else { + _uiState.value = AsyncValue.Data(HomeUiState(lastUpdateTime, parkingLotList.size)) + } + }.catch { + _uiState.value = AsyncValue.Error(it) + }.collect() + } + } + + fun refreshData() = viewModelScope.launch { + _uiState.value = AsyncValue.Loading + parkingLotUseCase.fetchDataSet() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt new file mode 100644 index 0000000..1a2a822 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt @@ -0,0 +1,229 @@ +package com.a1573595.parkingdemo.ui.screen.detail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsBike +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.DirectionsBus +import androidx.compose.material.icons.filled.DirectionsCar +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Money +import androidx.compose.material.icons.filled.Motorcycle +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.hilt.navigation.compose.hiltViewModel +import com.a1573595.parkingdemo.common.AsyncValue +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.ui.component.BorderCard +import com.a1573595.parkingdemo.ui.component.ErrorBody +import com.a1573595.parkingdemo.ui.component.LoadingBody +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun DetailScreen( + onBackClick: () -> Unit, + viewModel: DetailViewModel = hiltViewModel(), +) { + val uiState = viewModel.uiState.value + + Scaffold( + topBar = { + DetailAppBar( + uiState = uiState, + onBackClick = onBackClick, + onFavoriteClick = { viewModel.updateFavorite() }, + ) + } + ) { innerPadding -> + Box( + modifier = Modifier.padding(innerPadding) + ) { + when { + uiState.isLoading -> LoadingBody() + uiState.isError -> ErrorBody(throwable = uiState.requireError) + else -> DetailBody(uiState.requireValue.parkingLot) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailAppBar( + uiState: AsyncValue, + onBackClick: () -> Unit, + onFavoriteClick: () -> Unit, +) { + TopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + navigationIcon = { + IconButton( + onClick = onBackClick + ) { + Icon( + imageVector = Icons.Filled.ArrowBackIosNew, + contentDescription = "back", + ) + } + }, + title = { + if (uiState.isSuccess) + Text( + text = uiState.requireValue.parkingLot.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + actions = { + if (uiState.isSuccess) + IconButton(onClick = { onFavoriteClick() }) { + Icon( + imageVector = if (uiState.requireValue.isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, + contentDescription = "favorite", + tint = Color.Red, + ) + } + }, + ) +} + +@Composable +fun DetailBody(parkingLot: ParkingLot) { + Column( + modifier = Modifier.padding(Dimens.dp16), + ) { + Text( + text = parkingLot.name, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = "${parkingLot.area} ${parkingLot.address}", + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(Dimens.dp16)) + BorderCard { + Column( + modifier = Modifier.padding( + horizontal = Dimens.dp8, + vertical = Dimens.dp16, + ), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + ) { + Icon( + imageVector = Icons.Filled.DirectionsBus, + contentDescription = null, + modifier = Modifier.size(Dimens.dp48), + ) + Icon( + imageVector = Icons.Filled.DirectionsCar, + contentDescription = null, + modifier = Modifier.size(Dimens.dp48), + ) + Icon( + imageVector = Icons.Filled.Motorcycle, + contentDescription = null, + modifier = Modifier.size(Dimens.dp48), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.DirectionsBike, + contentDescription = null, + modifier = Modifier.size(Dimens.dp48), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "${parkingLot.totalBus}", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = "${parkingLot.totalCar}", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = "${parkingLot.totalMotor}", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + Text( + text = "${parkingLot.totalBike}", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + } + } + } + Spacer(modifier = Modifier.height(Dimens.dp32)) + Row { + Icon( + imageVector = Icons.Filled.Phone, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + Text( + text = parkingLot.tel, + style = MaterialTheme.typography.bodyMedium, + ) + } + Spacer(modifier = Modifier.height(Dimens.dp32)) + Row { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + Text( + text = parkingLot.summary, + style = MaterialTheme.typography.titleSmall, + ) + } + Spacer(modifier = Modifier.height(Dimens.dp32)) + Row { + Icon( + imageVector = Icons.Filled.Money, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + Text( + text = parkingLot.payEx, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt new file mode 100644 index 0000000..b10481b --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt @@ -0,0 +1,8 @@ +package com.a1573595.parkingdemo.ui.screen.detail + +import com.a1573595.parkingdemo.domain.model.ParkingLot + +data class DetailUiState( + val isFavorite: Boolean, + val parkingLot: ParkingLot, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt new file mode 100644 index 0000000..1640cd3 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt @@ -0,0 +1,52 @@ +package com.a1573595.parkingdemo.ui.screen.detail + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.a1573595.parkingdemo.common.AsyncValue +import com.a1573595.parkingdemo.domain.usecase.FavoriteUseCase +import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase +import com.a1573595.parkingdemo.ui.navigation.NavRoute +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DetailViewModel @Inject constructor( + private val handle: SavedStateHandle, + private val parkingLotUseCase: ParkingLotUseCase, + private val favoriteUseCase: FavoriteUseCase, +) : ViewModel() { + private val id = handle.get(NavRoute.KEY_ID)!! + + private val _uiState: MutableState> = mutableStateOf(AsyncValue.Loading) + + val uiState: State> = _uiState + + init { + viewModelScope.launch { + parkingLotUseCase.getParkingLotById(id)!!.let { + _uiState.value = AsyncValue.Data(DetailUiState(false, it)) + } + + favoriteUseCase.getById(id).collect { + _uiState.value = AsyncValue.Data(_uiState.value.requireValue.copy(isFavorite = it != null)) + } + } + } + + fun updateFavorite() { + val isFavorite = _uiState.value.requireValue.isFavorite + + viewModelScope.launch { + if (isFavorite) { + favoriteUseCase.deleteById(id) + } else { + favoriteUseCase.upsertById(id) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt new file mode 100644 index 0000000..799123a --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt @@ -0,0 +1,84 @@ +package com.a1573595.parkingdemo.ui.screen.favorite + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VerticalAlignTop +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.component.NavigationAppBar +import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun FavoriteScreen( + onBackClick: () -> Unit, + onParkingLotItemClick: (String) -> Unit, + viewModel: FavoriteViewModel = hiltViewModel(), +) { + Scaffold( + topBar = { + NavigationAppBar( + onBackClick = onBackClick, + title = stringResource(id = R.string.favorite), + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(vertical = Dimens.dp16) + ) { + val scrollToTop = remember { mutableStateOf(false) } + val lazyListState: LazyListState = rememberLazyListState() + + LaunchedEffect(key1 = scrollToTop.value) { + if (scrollToTop.value) { + scrollToTop.value = false + lazyListState.scrollToItem(0) + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + val parkingLotList = viewModel.parkingLotListFlow.collectAsState(initial = emptyList()) + ParkingLotLazyColumn( + lazyListState, parkingLotList.value, + onDelete = { viewModel.deleteById(it) }, + onClick = onParkingLotItemClick, + ) + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(Dimens.dp32), + onClick = { + scrollToTop.value = true + }, + ) { + Icon( + Icons.Filled.VerticalAlignTop, + modifier = Modifier.size(Dimens.dp32), + contentDescription = null, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt new file mode 100644 index 0000000..fdced4d --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt @@ -0,0 +1,21 @@ +package com.a1573595.parkingdemo.ui.screen.favorite + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.usecase.FavoriteUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FavoriteViewModel @Inject constructor( + private val favoriteUseCase: FavoriteUseCase, +) : ViewModel() { + val parkingLotListFlow: Flow> = favoriteUseCase() + + fun deleteById(id: String) = viewModelScope.launch { + favoriteUseCase.deleteById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt new file mode 100644 index 0000000..288144a --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt @@ -0,0 +1,84 @@ +package com.a1573595.parkingdemo.ui.screen.history + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VerticalAlignTop +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.component.NavigationAppBar +import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun HistoryScreen( + onBackClick: () -> Unit, + onParkingLotItemClick: (String) -> Unit, + viewModel: HistoryViewModel = hiltViewModel(), +) { + Scaffold( + topBar = { + NavigationAppBar( + onBackClick = onBackClick, + title = stringResource(id = R.string.history), + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(vertical = Dimens.dp16) + ) { + val scrollToTop = remember { mutableStateOf(false) } + val lazyListState: LazyListState = rememberLazyListState() + + LaunchedEffect(key1 = scrollToTop.value) { + if (scrollToTop.value) { + scrollToTop.value = false + lazyListState.scrollToItem(0) + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + val parkingLotList = viewModel.parkingLotListFlow.collectAsState(initial = emptyList()) + ParkingLotLazyColumn( + lazyListState, parkingLotList.value, + onDelete = { viewModel.deleteById(it) }, + onClick = onParkingLotItemClick, + ) + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(Dimens.dp32), + onClick = { + scrollToTop.value = true + }, + ) { + Icon( + Icons.Filled.VerticalAlignTop, + modifier = Modifier.size(Dimens.dp32), + contentDescription = null, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt new file mode 100644 index 0000000..2c1bd5f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt @@ -0,0 +1,21 @@ +package com.a1573595.parkingdemo.ui.screen.history + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.a1573595.parkingdemo.domain.usecase.HistoryUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HistoryViewModel @Inject constructor( + private val historyUseCase: HistoryUseCase, +) : ViewModel() { + val parkingLotListFlow: Flow> = historyUseCase() + + fun deleteById(id: String) = viewModelScope.launch { + historyUseCase.deleteById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt new file mode 100644 index 0000000..39ff06c --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt @@ -0,0 +1,109 @@ +package com.a1573595.parkingdemo.ui.screen.map + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.component.NavigationAppBar +import com.a1573595.parkingdemo.ui.screen.map.bean.ParkingLotCluster +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapUiSettings +import com.google.maps.android.compose.MapsComposeExperimentalApi +import com.google.maps.android.compose.clustering.Clustering +import com.google.maps.android.compose.clustering.rememberClusterManager +import com.google.maps.android.compose.rememberCameraPositionState + +@OptIn(MapsComposeExperimentalApi::class) +@Composable +fun MapScreen( + onBackClick: () -> Unit, + onParkingLotItemClick: (String) -> Unit, + viewModel: MapViewModel = hiltViewModel(), +) { + val uiSettings by remember { mutableStateOf(MapUiSettings()) } + val cameraPositionState = rememberCameraPositionState { + val taipeiLatLng = LatLng( + 25.0329694, + 121.56541770000001 + ) + position = CameraPosition.fromLatLngZoom(taipeiLatLng, 15f) + } + + Scaffold( + topBar = { + NavigationAppBar( + onBackClick = onBackClick, + title = stringResource(id = R.string.map), + ) + }, + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + GoogleMap( + modifier = Modifier + .fillMaxSize(), + uiSettings = uiSettings, + cameraPositionState = cameraPositionState, + ) { + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + val screenWidth = configuration.screenWidthDp.dp + val clusterManager = rememberClusterManager() + + val parkingLotList = viewModel.parkingLotClusterListFlow.collectAsState(initial = emptyList()) + + SideEffect { + clusterManager?.setAlgorithm( + NonHierarchicalViewBasedAlgorithm( + screenWidth.value.toInt(), + screenHeight.value.toInt(), + ) + ) + + clusterManager?.setOnClusterClickListener { + val zoom = cameraPositionState.position.zoom + cameraPositionState.position = CameraPosition.fromLatLngZoom(it.position, zoom + 1) + false + } + + clusterManager?.setOnClusterItemInfoWindowClickListener { + onParkingLotItemClick(it.id) + } + } + + val items = remember { mutableStateListOf() } + LaunchedEffect(parkingLotList.value) { + items.addAll(parkingLotList.value) + } + + if (clusterManager != null) { + Clustering( + items = items, + clusterManager = clusterManager, + ) + } + } + } + } +} + diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt new file mode 100644 index 0000000..ea04a16 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt @@ -0,0 +1,18 @@ +package com.a1573595.parkingdemo.ui.screen.map + +import androidx.lifecycle.ViewModel +import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase +import com.a1573595.parkingdemo.ui.screen.map.bean.ParkingLotCluster +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class MapViewModel @Inject constructor( + private val parkingLotUseCase: ParkingLotUseCase, +) : ViewModel() { + val parkingLotClusterListFlow: Flow> = parkingLotUseCase().map { + it.map { ParkingLotCluster.fromParkingLot(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt new file mode 100644 index 0000000..e139121 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt @@ -0,0 +1,29 @@ +package com.a1573595.parkingdemo.ui.screen.map.bean + +import com.a1573595.parkingdemo.domain.model.ParkingLot +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.clustering.ClusterItem + +data class ParkingLotCluster( + private val _position: LatLng, + val id: String, + val name: String, + val description: String, +) : ClusterItem { + companion object { + fun fromParkingLot(park: ParkingLot): ParkingLotCluster = ParkingLotCluster( + _position = LatLng(park.lat, park.lon), + id = park.id, + name = park.name, + description = "${park.area}${park.address}", + ) + } + + override fun getPosition(): LatLng = _position + + override fun getTitle(): String = name + + override fun getSnippet(): String = description + + override fun getZIndex(): Float? = null +} diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt new file mode 100644 index 0000000..9a8a3eb --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt @@ -0,0 +1,8 @@ +package com.a1573595.parkingdemo.ui.screen.search + +enum class FilterType { + BUS, + CAR, + MOTOR, + BIKE, +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt new file mode 100644 index 0000000..f9effe6 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt @@ -0,0 +1,233 @@ +package com.a1573595.parkingdemo.ui.screen.search + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.VerticalAlignTop +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import com.a1573595.parkingdemo.R +import com.a1573595.parkingdemo.ui.component.ErrorBody +import com.a1573595.parkingdemo.ui.component.LoadingBody +import com.a1573595.parkingdemo.ui.component.NavigationAppBar +import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn +import com.a1573595.parkingdemo.ui.theme.Dimens + +@Composable +fun SearchScreen( + onBackClick: () -> Unit, + onParkingLotItemClick: (String) -> Unit, + viewModel: SearchViewModel = hiltViewModel(), +) { + Scaffold( + topBar = { + NavigationAppBar( + onBackClick = onBackClick, + title = stringResource(id = R.string.search), + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(vertical = Dimens.dp16) + ) { + val scrollToTop = remember { mutableStateOf(false) } + val lazyListState: LazyListState = rememberLazyListState() + + val uiState = viewModel.uiState.value + + LaunchedEffect(key1 = scrollToTop.value) { + if (scrollToTop.value) { + scrollToTop.value = false + lazyListState.scrollToItem(0) + } + } + + TextFieldSearchBar( + backgroundColor = Color.Transparent, + value = uiState.keyword, + onClear = { + viewModel.updateKeyword("") + }, + onValueChange = { + viewModel.updateKeyword(it) + }, + ) + Row(modifier = Modifier.padding(horizontal = Dimens.dp16)) { + FilterChip( + selected = uiState.hasBus, + modifier = Modifier.weight(1f), + onClick = { viewModel.updateFilter(FilterType.BUS) }, + label = { + Text( + text = stringResource(id = R.string.bus), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + FilterChip( + selected = uiState.hasCar, + modifier = Modifier.weight(1f), + onClick = { viewModel.updateFilter(FilterType.CAR) }, + label = { + Text( + text = stringResource(id = R.string.car), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + FilterChip( + selected = uiState.hasMotor, + modifier = Modifier.weight(1f), + onClick = { viewModel.updateFilter(FilterType.MOTOR) }, + label = { + Text( + text = stringResource(id = R.string.motor), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + Spacer(modifier = Modifier.width(Dimens.dp16)) + FilterChip( + selected = uiState.hasBike, + modifier = Modifier.weight(1f), + onClick = { viewModel.updateFilter(FilterType.BIKE) }, + label = { + Text( + text = stringResource(id = R.string.bike), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + } + Box( + modifier = Modifier.fillMaxSize() + ) { + uiState.parkingLotPagingDataFlow.collectAsLazyPagingItems().let { + with(it.loadState.refresh) { + if (this is LoadState.Loading) { + LoadingBody() + } else if (this is LoadState.Error) { + ErrorBody(this.error) + } + } + ParkingLotLazyColumn(lazyListState, it, onParkingLotItemClick) + } + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(Dimens.dp32), + onClick = { + scrollToTop.value = true + }, + ) { + Icon( + Icons.Filled.VerticalAlignTop, + modifier = Modifier.size(Dimens.dp32), + contentDescription = null, + ) + } + } + } + } +} + +@Composable +fun TextFieldSearchBar( + modifier: Modifier = Modifier, + backgroundColor: Color, + value: String, + onClear: () -> Unit, + onValueChange: (String) -> Unit, +) { + val interactionSource = remember { + MutableInteractionSource() + } + + TextField( + interactionSource = interactionSource, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = Dimens.dp16) + .border( + width = Dimens.dp1, + color = Color.Black, + shape = MaterialTheme.shapes.medium, + ), + shape = MaterialTheme.shapes.medium, + colors = TextFieldDefaults.colors( + focusedContainerColor = backgroundColor, + unfocusedContainerColor = backgroundColor, + disabledIndicatorColor = backgroundColor, + errorIndicatorColor = backgroundColor, + focusedIndicatorColor = backgroundColor, + unfocusedIndicatorColor = backgroundColor, + ), + singleLine = true, + leadingIcon = { + Icon( + Icons.Filled.Search, + modifier = Modifier.size(Dimens.dp32), + contentDescription = null, + ) + }, + trailingIcon = { + if (value.isNotEmpty()) { + Icon( + Icons.Filled.Clear, + modifier = Modifier + .size(Dimens.dp32) + .clickable { onClear() }, + contentDescription = null, + ) + } + }, + placeholder = { + Text( + text = stringResource(R.string.keyword), + style = MaterialTheme.typography.bodyLarge, + ) + }, + value = value, + onValueChange = onValueChange, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt new file mode 100644 index 0000000..488d5e3 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt @@ -0,0 +1,14 @@ +package com.a1573595.parkingdemo.ui.screen.search + +import androidx.paging.PagingData +import com.a1573595.parkingdemo.domain.model.ParkingLot +import kotlinx.coroutines.flow.Flow + +data class SearchUiState( + val keyword: String, + val hasBus: Boolean = false, + val hasCar: Boolean = false, + val hasMotor: Boolean = false, + val hasBike: Boolean = false, + val parkingLotPagingDataFlow: Flow>, +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt new file mode 100644 index 0000000..b6131a8 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt @@ -0,0 +1,58 @@ +package com.a1573595.parkingdemo.ui.screen.search + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val parkingLotUseCase: ParkingLotUseCase, +) : ViewModel() { + private val _uiState = mutableStateOf( + SearchUiState( + keyword = "", + parkingLotPagingDataFlow = parkingLotUseCase.searchPagingDataFlow().cachedIn(viewModelScope) + ) + ) + + val uiState: State = _uiState + + fun updateKeyword(value: String) { + _uiState.value = uiState.value.copy(keyword = value) + + updateFlow() + } + + fun updateFilter(filterType: FilterType) { + when (filterType) { + FilterType.BUS -> _uiState.value = uiState.value.copy(hasBus = !uiState.value.hasBus) + FilterType.CAR -> _uiState.value = uiState.value.copy(hasCar = !uiState.value.hasCar) + FilterType.MOTOR -> _uiState.value = uiState.value.copy(hasMotor = !uiState.value.hasMotor) + FilterType.BIKE -> _uiState.value = uiState.value.copy(hasBike = !uiState.value.hasBike) + } + + updateFlow() + } + + @OptIn(FlowPreview::class) + private fun updateFlow() { + val state = uiState.value + + parkingLotUseCase.searchPagingDataFlow( + state.keyword, + state.hasBus, + state.hasCar, + state.hasMotor, + state.hasBike, + ).debounce(1000).cachedIn(viewModelScope).let { + _uiState.value = uiState.value.copy(parkingLotPagingDataFlow = it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt new file mode 100644 index 0000000..c94419f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.a1573595.parkingdemo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt new file mode 100644 index 0000000..3d2a691 --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt @@ -0,0 +1,17 @@ +package com.a1573595.parkingdemo.ui.theme + +import androidx.compose.ui.unit.dp + +object Dimens { + val dp1 = 1.dp + val dp2 = 2.dp + val dp4 = 4.dp + val dp8 = 8.dp + val dp12 = 12.dp + val dp16 = 16.dp + val dp20 = 20.dp + val dp24 = 24.dp + val dp32 = 32.dp + val dp48 = 48.dp + val dp64 = 64.dp +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt new file mode 100644 index 0000000..ad4106c --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt @@ -0,0 +1,57 @@ +package com.a1573595.parkingdemo.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ParkingLotTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt new file mode 100644 index 0000000..5863c1f --- /dev/null +++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.a1573595.parkingdemo.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt deleted file mode 100644 index 2b373c7..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.a1573595.parkinglotdemo - -import android.content.Context -import android.content.res.Resources -import androidx.appcompat.app.AppCompatActivity - -abstract class BaseActivity : AppCompatActivity() { - override fun attachBaseContext(base: Context) { - val configuration = base.resources.configuration - - if (configuration.fontScale != 1.0f) { - configuration.fontScale = 1.0f - super.attachBaseContext(base.createConfigurationContext(configuration)) - return - } - - super.attachBaseContext(base) - } - - override fun getResources(): Resources { - val resources = super.getResources() - - if (resources?.configuration?.fontScale != 1.0f) { - val configuration = resources.configuration - configuration.fontScale = 1.0f - - val context = createConfigurationContext(configuration) - return context.resources - } - - return resources - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt deleted file mode 100644 index 2711a1f..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.a1573595.parkinglotdemo - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable - -abstract class BaseViewModel(application: Application) : AndroidViewModel(application) { - private val disposable: CompositeDisposable = CompositeDisposable() - - internal fun addDisposable(d: Disposable) { - disposable.add(d) - } - - override fun onCleared() { - disposable.clear() - super.onCleared() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt b/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt deleted file mode 100644 index 7f6a617..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.a1573595.parkinglotdemo - -import android.app.Application -import android.content.Context -import android.content.res.Resources -import androidx.appcompat.app.AppCompatDelegate -import androidx.datastore.preferences.core.stringPreferencesKey -import com.a1573595.parkinglotdemo.database.ParkingLotDataStore -import com.a1573595.parkinglotdemo.database.ParkingLotDatabase -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.plugins.RxJavaPlugins -import timber.log.Timber -import java.util.* - -class MainApplication : Application() { - override fun onCreate() { - super.onCreate() - - ParkingLotDataStore.build(this) - - ParkingLotDataStore.ds.updateDataAsync { - val mutablePreferences = it.toMutablePreferences() - val password = it[stringPreferencesKey("DatabaseKey")] ?: UUID.randomUUID().toString() - mutablePreferences[stringPreferencesKey("DatabaseKey")] = password - - ParkingLotDatabase.build(this, password) - - Single.just(mutablePreferences) - } - - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - } - - RxJavaPlugins.setErrorHandler { - Timber.e(it) - } - - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - - override fun attachBaseContext(base: Context) { - val configuration = base.resources.configuration - - if (configuration.fontScale != 1.0f) { - configuration.fontScale = 1.0f - super.attachBaseContext(base.createConfigurationContext(configuration)) - return - } - - super.attachBaseContext(base) - } - - override fun getResources(): Resources { - val resources = super.getResources() - - if (resources?.configuration?.fontScale != 1.0f) { - val configuration = resources.configuration - configuration.fontScale = 1.0f - - val context = createConfigurationContext(configuration) - return context.resources - } - - return resources - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt b/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt deleted file mode 100644 index ba1eb67..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.a1573595.parkinglotdemo.api - -import io.reactivex.rxjava3.core.Single -import okhttp3.ResponseBody -import retrofit2.http.GET -import retrofit2.http.Url - -fun interface ApiInterface { - @GET - fun downloadFileWithDynamicUrlSync(@Url fileUrl: String): Single -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt b/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt deleted file mode 100644 index 6bb9f44..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.a1573595.parkinglotdemo.api - -import com.a1573595.parkinglotdemo.BuildConfig -import okhttp3.* -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory -import java.util.concurrent.TimeUnit - -object NetWorkService { - val apiInterface: ApiInterface - - init { - val logger = HttpLoggingInterceptor() - logger.level = - if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE - - val client = OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .addInterceptor(logger) - .build() - - val retrofit = Retrofit.Builder() - .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) - .baseUrl("https://tcgbusfs.blob.core.windows.net/blobtcmsv/") - .client(client) - .build() - - apiInterface = retrofit.create(ApiInterface::class.java) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt deleted file mode 100644 index 99f9398..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = TABLE_FAVORITE) -data class Favorite( - @PrimaryKey - val id: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt deleted file mode 100644 index 67324f7..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single - -@Dao -interface FavoriteDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(item: Favorite): Completable - - @Query("SELECT * FROM TABLE_FAVORITE WHERE id LIKE :id") - fun getByID(id: String): Single - - @Query("SELECT * FROM TABLE_FAVORITE INNER JOIN TABLE_PARKING_LOT ON TABLE_FAVORITE.id = TABLE_PARKING_LOT.id") - fun getLoveList(): Single> - - @Query("DELETE FROM TABLE_FAVORITE WHERE id LIKE :id") - fun deleteByID(id: String): Completable -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt deleted file mode 100644 index 4b0e9b0..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = TABLE_HISTORY) -data class History( - @PrimaryKey - val id: String, - var hashTag: Long = System.currentTimeMillis() -) \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt deleted file mode 100644 index 7a03277..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single - -@Dao -interface HistoryDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(item: History): Completable - - @Query("SELECT * FROM TABLE_HISTORY INNER JOIN TABLE_PARKING_LOT ON TABLE_HISTORY.id = TABLE_PARKING_LOT.id ORDER BY hashTag DESC") - fun getHistoryList(): Single> - - @Query("DELETE FROM TABLE_HISTORY WHERE id LIKE :id") - fun deleteByID(id: String): Completable -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt deleted file mode 100644 index 5c99833..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.recyclerview.widget.DiffUtil -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = TABLE_PARKING_LOT) -data class ParkingLot( - @PrimaryKey - val id: String, - var area: String?, - var name: String?, - var summary: String?, - var address: String?, - var tel: String?, - var payex: String?, - var totalcar: Int = 0, - var totalmotor: Int = 0, - var totalbike: Int = 0, - var totalbus: Int = 0, - var lat: Double = 0.0, - var lng: Double = 0.0, -) - -class ParkingLotCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ParkingLot, newItem: ParkingLot): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: ParkingLot, newItem: ParkingLot): Boolean { - return oldItem == newItem - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt deleted file mode 100644 index 41b5a06..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import androidx.room.* -import androidx.sqlite.db.SupportSQLiteQuery -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single - -@Dao -interface ParkingLotDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(item: ParkingLot): Completable - - @Query("SELECT * FROM TABLE_PARKING_LOT WHERE id LIKE :id") - fun getByID(id: String): Single - - @Query("DELETE FROM TABLE_PARKING_LOT WHERE id LIKE :id") - fun deleteByID(id: String): Completable - - @Transaction - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(parkingLots: List): Single> - - @Query("SELECT * FROM TABLE_PARKING_LOT") - fun getAll(): Single> - - @Query("SELECT * FROM TABLE_PARKING_LOT WHERE name LIKE '%' || :name || '%'") - fun getAllByName(name: String): Single> - - @RawQuery - fun getAllByQuery(query: SupportSQLiteQuery): Single> - - @Query("DELETE FROM TABLE_PARKING_LOT") - fun deleteAll(): Completable -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt deleted file mode 100644 index 591ac1b..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import android.content.Context -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder -import androidx.datastore.rxjava3.RxDataStore - -private const val DS_NAME = "settings" - -val UPDATE_TIME = longPreferencesKey("update_time") - -object ParkingLotDataStore { - private lateinit var _ds: RxDataStore - - val ds: RxDataStore - get() = _ds - - fun build(context: Context) { - _ds = RxPreferenceDataStoreBuilder(context, DS_NAME).build() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt deleted file mode 100644 index 2173582..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.a1573595.parkinglotdemo.database - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import net.sqlcipher.database.SupportFactory - -private const val DB_NAME = "PK.db" -private const val DB_VERSION = 1 - -const val TABLE_PARKING_LOT = "Table_Parking_Lot" -const val TABLE_FAVORITE = "Table_Favorite" -const val TABLE_HISTORY = "Table_History" - -@Database( - entities = [ParkingLot::class, Favorite::class, History::class], - version = DB_VERSION, - exportSchema = false -) -abstract class ParkingLotDatabase : RoomDatabase() { - companion object { - lateinit var instance: ParkingLotDatabase - - @Synchronized - fun build(context: Context, password: String) { - instance = Room.databaseBuilder( - context, - ParkingLotDatabase::class.java, - DB_NAME - ) - .openHelperFactory(SupportFactory(password.toByteArray())) - .fallbackToDestructiveMigration() - .build() - } - } - - abstract fun getParkingDao(): ParkingLotDao - - abstract fun getFavoriteDao(): FavoriteDao - - abstract fun getHistoryDao(): HistoryDao -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt b/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt deleted file mode 100644 index 2a8c1ad..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.a1573595.parkinglotdemo.model - -import com.google.gson.annotations.SerializedName - -class TaipeiParkingLotInfo { - @SerializedName("data") - val data: Data = Data() - - class Data { - @SerializedName("park") - val park: Array = emptyArray() - - class Park { - @SerializedName("id") - var id: String? = null - @SerializedName("area") - var area : String? = null - @SerializedName("name") - var name : String? = null - @SerializedName("summary") - var summary : String? = null - @SerializedName("address") - var address : String? = null - @SerializedName("tel") - var tel : String? = null - @SerializedName("payex") - var payex : String? = null - @SerializedName("tw97x") - var tw97x = 0.0 - @SerializedName("tw97y") - var tw97y = 0.0 - @SerializedName("totalcar") - var totalcar = 0 - @SerializedName("totalmotor") - var totalmotor = 0 - @SerializedName("totalbike") - var totalbike = 0 - @SerializedName("totalbus") - var totalbus = 0 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt deleted file mode 100644 index 5858e76..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.a1573595.parkinglotdemo.page.detail - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import android.view.animation.Animation -import android.view.animation.ScaleAnimation -import androidx.activity.viewModels -import com.a1573595.parkinglotdemo.BaseActivity -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.databinding.ActivityDetailBinding - -class DetailActivity : BaseActivity() { - companion object { - private const val KEY_ID = "id" - - fun startActivity(context: Context, id: String) { - val intent = Intent(context, DetailActivity::class.java) - val bundle = Bundle() - bundle.putString(KEY_ID, id) - intent.putExtras(bundle) - context.startActivity(intent) - } - } - - private val viewModel: DetailViewModel by viewModels() - - private lateinit var binding: ActivityDetailBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityDetailBinding.inflate(layoutInflater) - setContentView(binding.root) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - subscriptViewModel() - viewModel.loadData(intent.extras?.getString(KEY_ID)) - } - - private fun subscriptViewModel() { - viewModel.parkingLotEvent.observe(this) { - val parkingLot = it.peekContent() - supportActionBar?.title = parkingLot.name - - binding.tvName.text = parkingLot.name - binding.tvAddress.text = parkingLot.address - binding.tvArea.text = parkingLot.area - binding.tvPhone.text = parkingLot.tel - binding.tvInfo.text = parkingLot.summary - binding.tvRate.text = parkingLot.payex - binding.tvBus.text = java.lang.String.valueOf(parkingLot.totalbus) - binding.tvCar.text = java.lang.String.valueOf(parkingLot.totalcar) - binding.tvMoto.text = java.lang.String.valueOf(parkingLot.totalmotor) - binding.tvBike.text = java.lang.String.valueOf(parkingLot.totalbike) - - binding.imgFavorite.setOnClickListener { - viewModel.addFavorite(parkingLot.id) - } - } - - viewModel.isFavoriteEvent.observeForever { - binding.imgFavorite.setImageResource(if (it.peekContent()) R.drawable.ic_favorite else R.drawable.ic_unfavorite) - - val scaleAnimation = ScaleAnimation( - 1.0f, 1.2f, 1.0f, 1.2f, - Animation.RELATIVE_TO_SELF, .5f, - Animation.RELATIVE_TO_SELF, .5f - ) - scaleAnimation.duration = 300 - - binding.imgFavorite.startAnimation(scaleAnimation) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressedDispatcher.onBackPressed() - } - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt deleted file mode 100644 index 6f73215..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.a1573595.parkinglotdemo.page.detail - -import android.app.Application -import androidx.lifecycle.MutableLiveData -import com.a1573595.parkinglotdemo.BaseViewModel -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.repository.ParkingLotRepository -import com.a1573595.parkinglotdemo.tool.Event - -class DetailViewModel : BaseViewModel { - constructor(application: Application) : super(application) { - repository = ParkingLotRepository() - } - - constructor(application: Application, repository: ParkingLotRepository) : super(application) { - this.repository = repository - } - - private val repository: ParkingLotRepository - - private var id: String = "" - - val parkingLotEvent: MutableLiveData> = MutableLiveData() - val isFavoriteEvent: MutableLiveData> = MutableLiveData() - - fun loadData(id: String?) { - id?.let { - this.id = it - } - - addDisposable(repository.getFavorites(this.id).subscribe({ - isFavoriteEvent.postValue(Event(true)) - }, { - isFavoriteEvent.postValue(Event(false)) - })) - - addDisposable(repository.getParkingLot(this.id) - .subscribe { list -> - parkingLotEvent.postValue(Event(list)) - } - ) - } - - fun addFavorite(id: String) { - if (isFavoriteEvent.value?.peekContent() == true) { - addDisposable(repository.deleteFavorite(id).subscribe { - isFavoriteEvent.postValue(Event(false)) - }) - } else { - addDisposable(repository.addFavorite(id).subscribe { - isFavoriteEvent.postValue(Event(true)) - }) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt deleted file mode 100644 index 5740dfb..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.a1573595.parkinglotdemo.page.fuzzySearch - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.activity.viewModels -import androidx.core.widget.addTextChangedListener -import com.a1573595.parkinglotdemo.BaseActivity -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.databinding.ActivityFuzzySearchBinding - -class FuzzySearchActivity : BaseActivity() { - companion object { - fun startActivity(context: Context) { - context.startActivity(Intent(context, FuzzySearchActivity::class.java)) - } - } - - private val viewModel: FuzzySearchViewModel by viewModels() - - private lateinit var binding: ActivityFuzzySearchBinding - - private val adapter: FuzzySearchAdapter = FuzzySearchAdapter() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityFuzzySearchBinding.inflate(layoutInflater) - setContentView(binding.root) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - binding.recyclerView.adapter = adapter - - binding.edSearch.addTextChangedListener { - viewModel.setKeyword(binding.edSearch.text.toString()) - } - - binding.groupTransportation.setOnCheckedStateChangeListener { _, checkedIds -> - val mode = when (checkedIds.firstOrNull()) { - R.id.chip_bus -> 0 - R.id.chip_car -> 1 - R.id.chip_moto -> 2 - R.id.chip_bike -> 3 - else -> 1 - } - - viewModel.setMode(mode) - } - - subscriptViewModel() - viewModel.loadDataSet() - } - - private fun subscriptViewModel() { - viewModel.dataSetEvent.observe(this) { - adapter.submitList(it.peekContent()) - binding.recyclerView.postDelayed({ - binding.recyclerView.layoutManager?.scrollToPosition(0) - }, 200) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressedDispatcher.onBackPressed() - } - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt deleted file mode 100644 index 3738237..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.a1573595.parkinglotdemo.page.fuzzySearch - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.database.ParkingLotCallback -import com.a1573595.parkinglotdemo.databinding.ItemParkingLotBinding -import com.a1573595.parkinglotdemo.page.detail.DetailActivity - -internal class FuzzySearchAdapter : - ListAdapter(ParkingLotCallback()) { - internal inner class Holder(private val binding: ItemParkingLotBinding) : - RecyclerView.ViewHolder(binding.root) { - init { - itemView.setOnClickListener { - DetailActivity.startActivity(it.context, getItem(adapterPosition).id) - } - } - - fun bind(parkingLot: ParkingLot) { - binding.tvName.text = parkingLot.name - binding.tvAddress.text = parkingLot.address - binding.tvTotal.text = parkingLot.address - - binding.tvTotal.text = binding.tvTotal.context.getString( - R.string.transportation, - parkingLot.totalbus, - parkingLot.totalcar, - parkingLot.totalmotor, - parkingLot.totalbike - ) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder( - ItemParkingLotBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder(holder: Holder, position: Int) { - holder.bind(getItem(position)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt deleted file mode 100644 index c8e79d8..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.a1573595.parkinglotdemo.page.fuzzySearch - -import android.app.Application -import androidx.lifecycle.MutableLiveData -import com.a1573595.parkinglotdemo.BaseViewModel -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.repository.ParkingLotRepository -import com.a1573595.parkinglotdemo.tool.Event - -class FuzzySearchViewModel : BaseViewModel { - constructor(application: Application) : super(application) { - repository = ParkingLotRepository() - } - - constructor(application: Application, repository: ParkingLotRepository) : super(application) { - this.repository = repository - } - - private val repository: ParkingLotRepository - - private var mode = 1 - private var keyword = "" - - val dataSetEvent: MutableLiveData>> = MutableLiveData() - - fun setKeyword(keyword: String) { - this.keyword = keyword - - loadDataSet() - } - - fun setMode(mode: Int) { - this.mode = mode - - loadDataSet() - } - - fun loadDataSet() { - var query = "SELECT * FROM TABLE_PARKING_LOT WHERE " - - query += when (mode) { - 0 -> "totalbus > 0" - 1 -> "totalcar > 0" - 2 -> "totalmotor > 0" - else -> "totalbike > 0" - } - - if (keyword.isNotEmpty()) { - query += String.format( - " AND (name LIKE '%%%s%%' OR address LIKE '%%%s%%')", - keyword, - keyword - ) - } - - addDisposable(repository.searchParkingLots(query).subscribe { list -> - dataSetEvent.postValue(Event(list)) - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt deleted file mode 100644 index 7a60fcc..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.a1573595.parkinglotdemo.page.history - -import android.content.Context -import android.content.Intent -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.RectF -import android.os.Bundle -import android.view.MenuItem -import androidx.activity.viewModels -import androidx.core.content.ContextCompat -import androidx.core.graphics.BlendModeColorFilterCompat -import androidx.core.graphics.BlendModeCompat -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import com.a1573595.parkinglotdemo.BaseActivity -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.databinding.ActivityHistoryBinding -import com.google.android.material.snackbar.Snackbar - -class HistoryActivity : BaseActivity() { - companion object { - private const val KET_MODE = "mode" - - const val MODE_FAVORITE = 1 - const val MODE_HISTORY = 2 - - fun startActivity(context: Context, mode: Int) { - val intent = Intent(context, HistoryActivity::class.java) - val bundle = Bundle() - bundle.putInt(KET_MODE, mode) - intent.putExtras(bundle) - context.startActivity(intent) - } - } - - private val viewModel: HistoryModel by viewModels() - - private lateinit var binding: ActivityHistoryBinding - - private val adapter: HistoryAdapter = HistoryAdapter() - private val textPaint = Paint() - private val backgroundPaint = Paint() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityHistoryBinding.inflate(layoutInflater) - setContentView(binding.root) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - textPaint.color = ContextCompat.getColor( - this@HistoryActivity, - android.R.color.white - ) - textPaint.textSize = 72f - textPaint.style = Paint.Style.FILL - textPaint.textAlign = Paint.Align.CENTER - backgroundPaint.color = ContextCompat.getColor( - this@HistoryActivity, - android.R.color.holo_red_light - ) - - val itemTouchHelper = ItemTouchHelper(callback) - itemTouchHelper.attachToRecyclerView(binding.recyclerView) - binding.recyclerView.adapter = adapter - - subscriptViewModel() - intent.extras?.getInt(KET_MODE)?.let { - viewModel.setMode(it) - } - } - - private fun subscriptViewModel() { - viewModel.dataSetEvent.observe(this) { - adapter.submitList(it.peekContent()) - } - } - - override fun onResume() { - super.onResume() - - viewModel.loadDataSet() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressedDispatcher.onBackPressed() - } - return super.onOptionsItemSelected(item) - } - - private val callback: ItemTouchHelper.Callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT - ) { - override fun getMovementFlags( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - val dragFlags = 0 - val swipeFlags = ItemTouchHelper.LEFT - return makeMovementFlags(dragFlags, swipeFlags) - } - - override fun onChildDraw( - c: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - dY: Float, - actionState: Int, - isCurrentlyActive: Boolean - ) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) - val icon = ContextCompat.getDrawable(this@HistoryActivity, R.drawable.ic_delete)!! - icon.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - Color.WHITE, - BlendModeCompat.SRC_ATOP - ) - - val itemView = viewHolder.itemView - - val rectF: RectF - - val multiple = 1f - val iconMargin = (itemView.height - icon.intrinsicHeight) / 2 - val iconTop = itemView.top + (iconMargin * multiple).toInt() - val iconBottom = iconTop + (icon.intrinsicHeight / multiple).toInt() - if (dX > 0) { // left - rectF = RectF( - itemView.left.toFloat(), - itemView.top.toFloat(), - itemView.left + dX, - itemView.bottom.toFloat() - ) - - val iconLeft = itemView.left + iconMargin - val iconRight = iconLeft + (icon.intrinsicWidth / multiple).toInt() - icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) - } else { // right - rectF = RectF( - itemView.right + dX, - itemView.top.toFloat(), - itemView.right.toFloat(), - itemView.bottom.toFloat() - ) - - val iconRight = itemView.right - iconMargin - val iconLeft = iconRight - (icon.intrinsicWidth / multiple).toInt() - icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) - } - - c.drawRect(rectF, backgroundPaint) - icon.draw(c) -// c.drawText( -// "Delete", -// rectf.right - (textPaint.textSize * 3), // rectf.centerX() -// rectf.centerY() + (textPaint.textSize / 2), -// textPaint -// ) - } - - override fun onMove( - recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - return false - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val position: Int = viewHolder.adapterPosition - viewModel.delete(position) - - Snackbar.make(binding.root, R.string.delete, Snackbar.LENGTH_LONG) - .setAction(R.string.recover) { viewModel.undoDelete() } - .show() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt deleted file mode 100644 index 9cd873a..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.a1573595.parkinglotdemo.page.history - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.database.ParkingLotCallback -import com.a1573595.parkinglotdemo.databinding.ItemParkingLotBinding -import com.a1573595.parkinglotdemo.page.detail.DetailActivity - -internal class HistoryAdapter : ListAdapter( - ParkingLotCallback() -) { - internal inner class Holder(private val binding: ItemParkingLotBinding) : - RecyclerView.ViewHolder(binding.root) { - init { - itemView.setOnClickListener { - DetailActivity.startActivity(it.context, getItem(adapterPosition).id) - } - } - - fun bind(parkingLot: ParkingLot) { - binding.tvName.text = parkingLot.name - binding.tvAddress.text = parkingLot.address - binding.tvTotal.text = binding.tvTotal.context.getString( - R.string.transportation, - parkingLot.totalbus, - parkingLot.totalcar, - parkingLot.totalmotor, - parkingLot.totalbike - ) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder( - ItemParkingLotBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder(holder: Holder, position: Int) { - holder.bind(getItem(position)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt deleted file mode 100644 index 3db5572..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.a1573595.parkinglotdemo.page.history - -import android.app.Application -import androidx.lifecycle.MutableLiveData -import com.a1573595.parkinglotdemo.BaseViewModel -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.repository.ParkingLotRepository -import com.a1573595.parkinglotdemo.tool.Event - -class HistoryModel : BaseViewModel { - constructor(application: Application) : super(application) { - repository = ParkingLotRepository() - } - - constructor(application: Application, repository: ParkingLotRepository) : super(application) { - this.repository = repository - } - - private val repository: ParkingLotRepository - - private var mode: Int = 0 - - val dataSetEvent: MutableLiveData>> = MutableLiveData() - - private var lastDeleteID: String? = null - - fun setMode(mode: Int) { - this.mode = mode - } - - fun loadDataSet() { - val completable = if (mode == HistoryActivity.MODE_FAVORITE) { - repository.getFavorites() - } else { - repository.getHistory() - } - - addDisposable(completable.subscribe { list -> dataSetEvent.postValue(Event(list)) }) - } - - fun delete(position: Int) { - dataSetEvent.value?.peekContent()?.let { - lastDeleteID = it[position].id - - val completable = if (mode == HistoryActivity.MODE_FAVORITE) { - repository.deleteFavorite(it[position].id) - } else { - repository.deleteHistory(it[position].id) - } - - addDisposable(completable.subscribe { loadDataSet() }) - } - } - - fun undoDelete() { - lastDeleteID?.let { - val completable = if (mode == HistoryActivity.MODE_FAVORITE) { - repository.addFavorite(it) - } else { - repository.addHistory(it) - } - - addDisposable(completable.subscribe { loadDataSet() }) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt deleted file mode 100644 index f5397b1..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.a1573595.parkinglotdemo.page.main - -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.Menu -import android.view.MenuItem -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback -import androidx.activity.viewModels -import com.a1573595.parkinglotdemo.BaseActivity -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.databinding.ActivityMainBinding -import com.a1573595.parkinglotdemo.page.history.HistoryActivity -import com.a1573595.parkinglotdemo.page.fuzzySearch.FuzzySearchActivity -import com.a1573595.parkinglotdemo.page.map.MapActivity - -class MainActivity : BaseActivity() { - private val viewModel: MainViewModel by viewModels() - - private lateinit var binding: ActivityMainBinding - - private val backHandler = Handler(Looper.getMainLooper()) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - registerOnBackPress() - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - setSupportActionBar(binding.toolbar) - - subscriptViewModel() - viewModel.loadDataSet() - } - - private fun subscriptViewModel() { - viewModel.dataSetEvent.observe(this) { - binding.tvDataset.text = getString(R.string.total_of_data_set, it.peekContent().size) - - setListen() - } - - viewModel.updateTimeEvent.observe(this) { - binding.tvUpdateTime.text = getString(R.string.download_at, it.peekContent()) - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.menu_update, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_update -> { - binding.tvDataset.text = getString(R.string.downloading) - viewModel.updateDataSet() - } - } - return super.onOptionsItemSelected(item) - } - - private fun registerOnBackPress() { - onBackPressedDispatcher.addCallback { - if (backHandler.hasMessages(0)) { - finish() - } else { - Toast.makeText(this@MainActivity, getString(R.string.press_again_to_exit), Toast.LENGTH_SHORT) - .show() - backHandler.removeCallbacksAndMessages(null) - backHandler.postDelayed({}, 2000) - } - } - } - - private fun setListen() { - binding.cardMap.setOnClickListener { - MapActivity.startActivity(this) - } - - binding.cardList.setOnClickListener { - FuzzySearchActivity.startActivity(this) - } - - binding.cardFavorite.setOnClickListener { - HistoryActivity.startActivity(this, HistoryActivity.MODE_FAVORITE) - } - - binding.cardHistory.setOnClickListener { - HistoryActivity.startActivity(this, HistoryActivity.MODE_HISTORY) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt deleted file mode 100644 index 1047788..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.a1573595.parkinglotdemo.page.main - -import android.app.Application -import androidx.lifecycle.MutableLiveData -import com.a1573595.parkinglotdemo.BaseViewModel -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.repository.ParkingLotRepository -import com.a1573595.parkinglotdemo.tool.Event -import java.text.SimpleDateFormat -import java.util.* - -class MainViewModel : BaseViewModel { - constructor(application: Application) : super(application) { - repository = ParkingLotRepository() - } - - constructor(application: Application, repository: ParkingLotRepository) : super(application) { - this.repository = repository - } - - private val repository: ParkingLotRepository - val updateTimeEvent: MutableLiveData> = MutableLiveData() - val dataSetEvent: MutableLiveData>> = MutableLiveData() - - fun loadDataSet() { - addDisposable( - repository.getUpdateTime() - .subscribe({ - getParkingLots(it) - }, { - updateDataSet() - }) - ) - } - - fun updateDataSet() { - addDisposable(repository.downloadDataSet() - .subscribe { _ -> - loadDataSet() - } - ) - } - - private fun getParkingLots(updateTime: Long) { - addDisposable(repository.getParkingLots() - .subscribe { list -> - dataSetEvent.postValue(Event(list)) - updateTimeEvent.postValue(Event(calTimeMilliToTime(updateTime))) - } - ) - } - - private fun calTimeMilliToTime(time: Long): String { - val date = Date(time) - val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()) - return format.format(date) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt deleted file mode 100644 index 07aba1b..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.a1573595.parkinglotdemo.page.map - -import android.content.Context -import android.content.Intent -import android.database.Cursor -import android.database.MatrixCursor -import android.location.Address -import android.location.Geocoder -import android.location.Geocoder.GeocodeListener -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.provider.BaseColumns -import android.view.* -import androidx.activity.viewModels -import androidx.appcompat.widget.SearchView -import androidx.cursoradapter.widget.CursorAdapter -import androidx.cursoradapter.widget.SimpleCursorAdapter -import com.a1573595.parkinglotdemo.BaseActivity -import com.a1573595.parkinglotdemo.R -import com.a1573595.parkinglotdemo.databinding.ActivityMapBinding -import com.a1573595.parkinglotdemo.databinding.DialogParkingLotBinding -import com.a1573595.parkinglotdemo.page.detail.DetailActivity -import com.a1573595.parkinglotdemo.tool.ParkingCluster -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener -import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener -import com.google.android.gms.maps.OnMapReadyCallback -import com.google.android.gms.maps.SupportMapFragment -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds -import com.google.android.gms.maps.model.Marker -import com.google.android.gms.maps.model.MarkerOptions -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.maps.android.clustering.Cluster -import com.google.maps.android.clustering.ClusterManager -import com.google.maps.android.clustering.ClusterManager.OnClusterClickListener -import com.google.maps.android.clustering.ClusterManager.OnClusterItemClickListener -import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm -import com.google.maps.android.clustering.view.DefaultClusterRenderer -import com.google.maps.android.collections.MarkerManager -import java.lang.Exception -import java.util.* - -class MapActivity : BaseActivity(), OnMapReadyCallback, - OnCameraIdleListener, OnMarkerClickListener, - OnClusterClickListener, - OnClusterItemClickListener { - companion object { - private const val Max_Clustering_Room_Level = 17f - - fun startActivity(context: Context) { - context.startActivity(Intent(context, MapActivity::class.java)) - } - } - - private val viewModel: MapViewModel by viewModels() - - private lateinit var binding: ActivityMapBinding - - private lateinit var simpleCursorAdapter: SimpleCursorAdapter - - private lateinit var mMap: GoogleMap - private lateinit var mClusterManager: ClusterManager - private lateinit var normalMarkerManager: MarkerManager.Collection - - private var zoomLevel = 0f - - private lateinit var geocoder: Geocoder - private val searchHandler = Handler(Looper.getMainLooper()) - - private inner class ParkingRender : - DefaultClusterRenderer(applicationContext, mMap, mClusterManager) { - override fun shouldRenderAsCluster(cluster: Cluster): Boolean { - return zoomLevel < Max_Clustering_Room_Level && super.shouldRenderAsCluster( - cluster - ) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityMapBinding.inflate(layoutInflater) - setContentView(binding.root) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.insetsController?.hide(WindowInsets.Type.statusBars()) - } else { - window.setFlags( - WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN - ) - } - - val mapFragment = - supportFragmentManager.findFragmentById(binding.map.id) as SupportMapFragment - mapFragment.getMapAsync(this) - - subscriptViewModel() - } - - private fun subscriptViewModel() { - viewModel.dataSetEvent.observe(this) { - it.peekContent().forEach { parkingLot -> - mClusterManager.addItem( - ParkingCluster( - LatLng(parkingLot.lat, parkingLot.lng), parkingLot.id, - parkingLot.name, parkingLot.area, parkingLot.totalcar, - parkingLot.totalmotor, parkingLot.totalbike, parkingLot.totalbus - ) - ) - } - - mClusterManager.cluster() - mMap.uiSettings.setAllGesturesEnabled(true) - } - } - - override fun onMapReady(googleMap: GoogleMap) { - mMap = googleMap - - mMap.uiSettings.setAllGesturesEnabled(false) - initClusterManager() - initSearchView() - - mMap.moveCamera( - CameraUpdateFactory.newLatLngZoom( - LatLng( - 25.0329694, - 121.56541770000001 - ), 15f - ) - ) - - mMap.setOnCameraIdleListener(this) - mMap.setOnMapLoadedCallback { viewModel.loadDataSet() } - } - - override fun onCameraIdle() { - zoomLevel = mMap.cameraPosition.zoom - mClusterManager.onCameraIdle() - } - - override fun onMarkerClick(marker: Marker): Boolean { - marker.showInfoWindow() - mMap.moveCamera(CameraUpdateFactory.newLatLng(marker.position)) - - return true - } - - override fun onClusterClick(cluster: Cluster): Boolean { - val builder = LatLngBounds.builder() - for (item in cluster.items) { - builder.include(item.position) - } - - try { - val bounds = builder.build() - mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100)) - } catch (e: Exception) { - e.printStackTrace() - } - return true - } - - override fun onClusterItemClick(item: ParkingCluster): Boolean { - mMap.moveCamera(CameraUpdateFactory.newLatLng(item.position)) - showDialog(item) - return true - } - - private fun initClusterManager() { - mClusterManager = ClusterManager(this, mMap) - val dm = resources.displayMetrics - mClusterManager.setAlgorithm( - NonHierarchicalViewBasedAlgorithm( - dm.widthPixels, - dm.heightPixels - ) - ) - mClusterManager.renderer = ParkingRender() - mClusterManager.setOnClusterClickListener(this) - mClusterManager.setOnClusterItemClickListener(this) - - normalMarkerManager = mClusterManager.markerManager.newCollection() - normalMarkerManager.setOnMarkerClickListener(this) - } - - private fun initSearchView() { - geocoder = Geocoder(this, Locale.getDefault()) - - val from = arrayOf("address", "lat", "lng") - val to = intArrayOf(android.R.id.text1) - simpleCursorAdapter = SimpleCursorAdapter( - this, android.R.layout.simple_list_item_1, - null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER - ) - binding.searchView.suggestionsAdapter = simpleCursorAdapter - - binding.searchView.setOnClickListener { binding.searchView.isIconified = false } - - binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - return false - } - - override fun onQueryTextChange(newText: String): Boolean { - simpleCursorAdapter.changeCursor(null) - searchHandler.removeCallbacksAndMessages(null) - searchHandler.postDelayed({ - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - geocoder.getFromLocationName( - binding.searchView.query.toString(), - 5, - object : GeocodeListener { - override fun onGeocode(list: MutableList
) { - handleAddress(list) - } - - override fun onError(errorMessage: String?) { - handleAddress(emptyList()) - } - }) - } else { - val geocodeAddress = - geocoder.getFromLocationName(binding.searchView.query.toString(), 5) - ?: emptyList() - handleAddress(geocodeAddress) - } - } catch (e: Exception) { - e.printStackTrace() - } - }, 750) - return false - } - }) - - binding.searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { - override fun onSuggestionSelect(position: Int): Boolean { - return false - } - - override fun onSuggestionClick(position: Int): Boolean { - val cursor = simpleCursorAdapter.getItem(position) as Cursor - val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) - val lat = cursor.getDouble(cursor.getColumnIndexOrThrow("lat")) - val lng = cursor.getDouble(cursor.getColumnIndexOrThrow("lng")) - normalMarkerManager.clear() - val marker = normalMarkerManager.addMarker( - MarkerOptions().position(LatLng(lat, lng)).title(address) - ) - marker.showInfoWindow() - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(lat, lng), 15f)) - return false - } - }) - - binding.searchView.queryHint = getString(R.string.query_hint) - binding.searchView.visibility = View.VISIBLE - } - - private fun showDialog(cluster: ParkingCluster) { - val binding = DialogParkingLotBinding.inflate(LayoutInflater.from(this)).apply { - tvName.text = cluster.name - tvAddress.text = cluster.area - tvTotal.text = getString( - R.string.transportation, cluster.totalBus, cluster.totalCar, - cluster.totalMotor, cluster.totalBike - ) - - root.setOnClickListener { - DetailActivity.startActivity(this@MapActivity, cluster.id) - } - } - - MaterialAlertDialogBuilder(this) - .setView(binding.root) - .show() - } - - private fun handleAddress(geocodeAddress: List
) { - val c = MatrixCursor( - arrayOf( - BaseColumns._ID, - "address", - "lat", - "lng" - ) - ) - for (i in geocodeAddress.indices) { - c.addRow( - arrayOf( - i, - geocodeAddress[i].getAddressLine(0), - geocodeAddress[i].latitude, - geocodeAddress[i].longitude, - ) - ) - } - simpleCursorAdapter.changeCursor(c) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt deleted file mode 100644 index 8c5cd6b..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.a1573595.parkinglotdemo.page.map - -import android.app.Application -import androidx.lifecycle.MutableLiveData -import com.a1573595.parkinglotdemo.BaseViewModel -import com.a1573595.parkinglotdemo.database.ParkingLot -import com.a1573595.parkinglotdemo.repository.ParkingLotRepository -import com.a1573595.parkinglotdemo.tool.Event - -class MapViewModel : BaseViewModel { - constructor(application: Application) : super(application) { - repository = ParkingLotRepository() - } - - constructor(application: Application, repository: ParkingLotRepository) : super(application) { - this.repository = repository - } - - private val repository: ParkingLotRepository - - val dataSetEvent: MutableLiveData>> = MutableLiveData() - - fun loadDataSet() { - addDisposable(repository.getParkingLots().subscribe { list -> - dataSetEvent.postValue(Event(list)) - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt b/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt deleted file mode 100644 index a3bd64d..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.a1573595.parkinglotdemo.repository - -import androidx.datastore.preferences.core.MutablePreferences -import androidx.sqlite.db.SimpleSQLiteQuery -import com.a1573595.parkinglotdemo.api.NetWorkService -import com.a1573595.parkinglotdemo.database.* -import com.a1573595.parkinglotdemo.model.TaipeiParkingLotInfo -import com.a1573595.parkinglotdemo.tool.LatLngCoding -import com.google.gson.Gson -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import java.io.ByteArrayOutputStream -import java.util.zip.GZIPInputStream - -class ParkingLotRepository { - fun getUpdateTime(): Flowable = - ParkingLotDataStore.ds.data().map { it[UPDATE_TIME]!! } - .subscribeOn(Schedulers.io()) - - fun downloadDataSet(): Single> = - NetWorkService.apiInterface.downloadFileWithDynamicUrlSync("TCMSV_alldesc.gz") - .map { body -> - val inputStream = body.byteStream() - val unGzip = GZIPInputStream(inputStream) - - val buffer = ByteArray(256) - val out = ByteArrayOutputStream() - - var length: Int - while (unGzip.read(buffer).also { length = it } >= 0) { - out.write(buffer, 0, length) - } - val info: TaipeiParkingLotInfo = - Gson().fromJson(out.toString("UTF-8"), TaipeiParkingLotInfo::class.java) - - val list: MutableList = mutableListOf() - var latLng: List - - info.data.park.filterNot { it.id.isNullOrEmpty() }.forEach { - latLng = LatLngCoding.calTWD97ToLonLat(it.tw97x, it.tw97y).split(",") - list.add( - ParkingLot( - it.id!!, it.area, it.name, it.summary, it.address, - it.tel, it.payex, it.totalcar, - it.totalmotor, it.totalbike, it.totalbus, - latLng[0].toDouble(), latLng[1].toDouble() - ) - ) - } - list - } - .flatMap { deleteDataSet().toSingle { it } } - .flatMap { writeDataSet(it) } - .doOnSuccess { - ParkingLotDataStore.ds.updateDataAsync { prefsIn -> - val mutablePreferences: MutablePreferences = - prefsIn.toMutablePreferences() - mutablePreferences[UPDATE_TIME] = System.currentTimeMillis() - Single.just(mutablePreferences) - }.subscribe() - } - .subscribeOn(Schedulers.io()) - - fun deleteDataSet(): Completable = - ParkingLotDatabase.instance.getParkingDao() - .deleteAll() - .subscribeOn(Schedulers.io()) - - fun writeDataSet(list: List): Single> = - ParkingLotDatabase.instance.getParkingDao() - .insertAll(list) - .subscribeOn(Schedulers.io()) - - fun getParkingLots(): Single> = - ParkingLotDatabase.instance.getParkingDao() - .getAll() - .subscribeOn(Schedulers.io()) - - fun getParkingLot(id: String): Single = - ParkingLotDatabase.instance.getParkingDao() - .getByID(id) - .flatMap { addHistory(it.id).toSingle { it } } - .subscribeOn(Schedulers.io()) - - fun searchParkingLots(query: String): Single> = - ParkingLotDatabase.instance.getParkingDao() - .getAllByQuery(SimpleSQLiteQuery(query)) - .subscribeOn(Schedulers.io()) - - fun getHistory(): Single> = - ParkingLotDatabase.instance.getHistoryDao() - .getHistoryList() - .subscribeOn(Schedulers.io()) - - fun addHistory(id: String): Completable = - ParkingLotDatabase.instance.getHistoryDao() - .insert(History(id)) - .subscribeOn(Schedulers.io()) - - fun deleteHistory(id: String): Completable = - ParkingLotDatabase.instance.getHistoryDao() - .deleteByID(id) - .subscribeOn(Schedulers.io()) - - fun getFavorites(): Single> = - ParkingLotDatabase.instance.getFavoriteDao() - .getLoveList() - .subscribeOn(Schedulers.io()) - - fun getFavorites(id: String): Single = - ParkingLotDatabase.instance.getFavoriteDao() - .getByID(id) - .subscribeOn(Schedulers.io()) - - fun addFavorite(id: String): Completable = - ParkingLotDatabase.instance.getFavoriteDao() - .insert(Favorite(id)) - .subscribeOn(Schedulers.io()) - - fun deleteFavorite(id: String): Completable = - ParkingLotDatabase.instance.getFavoriteDao() - .deleteByID(id) - .subscribeOn(Schedulers.io()) -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt deleted file mode 100644 index ddd29b6..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.a1573595.parkinglotdemo.tool - -import androidx.lifecycle.Observer - -/** - * Used as a wrapper for data that is exposed via a LiveData that represents an event. - */ -open class Event(private val content: T) { - - @Suppress("MemberVisibilityCanBePrivate") - var hasBeenHandled = false - private set // Allow external read but not write - - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } - - /** - * Returns the content, even if it's already been handled. - */ - fun peekContent(): T = content -} - -/** - * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has - * already been handled. - * - * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. - */ -class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { - override fun onChanged(value: Event) { - value.getContentIfNotHandled()?.let { - onEventUnhandledContent(it) - } - } -} diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt deleted file mode 100644 index 509a90a..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.a1573595.parkinglotdemo.tool - -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.sin -import kotlin.math.tan - -class LatLngCoding { - companion object { - private const val a = 6378137.0 - private const val b = 6356752.314245 - private const val lon0 = 121 * Math.PI / 180 - private const val k0 = 0.9999 - private const val dx = 250000 - - fun calTWD97ToLonLat(x: Double, y: Double): String { - var x = x - var y = y - val dy = 0.0 - val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5) - x -= dx.toDouble() - y -= dy - - // Calculate the Meridional Arc - val M = y / k0 - - // Calculate Footprint Latitude - val mu = - M / (a * (1.0 - e.pow(2.0) / 4.0 - 3 * e.pow(4.0) / 64.0 - 5 * e.pow(6.0) / 256.0)) - val e1 = (1.0 - (1.0 - e.pow(2.0)).pow(0.5)) / (1.0 + (1.0 - e.pow(2.0)).pow(0.5)) - val j1 = 3 * e1 / 2 - 27 * e1.pow(3.0) / 32.0 - val j2 = 21 * e1.pow(2.0) / 16 - 55 * e1.pow(4.0) / 32.0 - val j3 = 151 * e1.pow(3.0) / 96.0 - val j4 = 1097 * e1.pow(4.0) / 512.0 - val fp = - mu + j1 * sin(2 * mu) + j2 * sin(4 * mu) + j3 * sin(6 * mu) + j4 * sin( - 8 * mu - ) - - // Calculate Latitude and Longitude - val e2 = (e * a / b).pow(2.0) - val c1 = (e2 * cos(fp)).pow(2.0) - val t1 = tan(fp).pow(2.0) - val r1 = a * (1 - e.pow(2.0)) / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(3.0 / 2.0) - val n1 = a / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(0.5) - val D = x / (n1 * k0) - - // 計算緯度 - val q1 = n1 * tan(fp) / r1 - val q2 = D.pow(2.0) / 2.0 - val q3 = - (5 + 3 * t1 + 10 * c1 - 4 * c1.pow(2.0) - 9 * e2) * D.pow(4.0) / 24.0 - val q4 = - (61 + 90 * t1 + 298 * c1 + 45 * t1.pow(2.0) - 3 * c1.pow(2.0) - 252 * e2) * D.pow( - 6.0 - ) / 720.0 - var lat = fp - q1 * (q2 - q3 + q4) - - // 計算經度 - val q6 = (1 + 2 * t1 + c1) * D.pow(3.0) / 6 - val q7 = - (5 - 2 * c1 + 28 * t1 - 3 * c1.pow(2.0) + 8 * e2 + 24 * t1.pow(2.0)) * D.pow(5.0) / 120.0 - var lon = lon0 + (D - q6 + q7) / cos(fp) - lat = lat * 180 / Math.PI //緯 - lon = lon * 180 / Math.PI //經 - return "$lat,$lon" - } - } - - // 給WGS84經緯度度分秒轉成TWD97坐標 - fun lonLatToTWD97(lonD: Int, lonM: Int, lonS: Int, latD: Int, latM: Int, latS: Int): String { - val radianLon = lonD.toDouble() + lonM.toDouble() / 60 + lonS.toDouble() / 3600 - val radianLat = latD.toDouble() + latM.toDouble() / 60 + latS.toDouble() / 3600 - return calLonLatToTWD97(radianLon, radianLat) - } - - // 給WGS84經緯度弧度轉成TWD97坐標 - fun lonLatToWED97(radianLon: Double, radianLat: Double): String { - return calLonLatToTWD97(radianLon, radianLat) - } - - // 給TWD97坐標 轉成 WGS84 度分秒字串 (type1傳度分秒 2傳弧度) - fun twd97ToLonLat(x: Double, y: Double, type: Int): String { - var lonLat = "" - if (type == 1) { - val answer = calTWD97ToLonLat(x, y).split(",".toRegex()).toTypedArray() - val lonDValue = answer[0].toInt() - val lonMValue = ((answer[0].toDouble() - lonDValue) * 60).toInt() - val lonSValue = (((answer[0].toDouble() - lonDValue) * 60 - lonMValue) * 60).toInt() - val latDValue = answer[1].toInt() - val latMValue = ((answer[1].toDouble() - latDValue) * 60).toInt() - val latSValue = (((answer[1].toDouble() - latDValue) * 60 - latMValue) * 60).toInt() - lonLat = - lonDValue.toString() + "度" + lonMValue + "分" + lonSValue + "秒," + latDValue + "度" + latMValue + "分" + latSValue + "秒," - } else if (type == 2) { - lonLat = calTWD97ToLonLat(x, y) - } - return lonLat - } - - private fun calLonLatToTWD97(lon: Double, lat: Double): String { - val lon = lon / 180 * Math.PI - val lat = lat / 180 * Math.PI - - //--------------------------------------------------------- - val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5) - val e2 = e.pow(2.0) / (1 - e.pow(2.0)) - val n = (a - b) / (a + b) - val nu = a / (1 - e.pow(2.0) * sin(lat).pow(2.0)).pow(0.5) - val p = lon - lon0 - val A = - a * (1 - n + 5 / 4 * (n.pow(2.0) - n.pow(3.0)) + 81 / 64 * (n.pow(4.0) - n.pow(5.0))) - val B = - 3 * a * n / 2.0 * (1 - n + 7 / 8.0 * (n.pow(2.0) - n.pow(3.0)) + 55 / 64.0 * (n.pow(4.0) - n.pow( - 5.0 - ))) - val C = 15 * a * n.pow(2.0) / 16.0 * (1 - n + 3 / 4.0 * (n.pow(2.0) - n.pow(3.0))) - val D = - 35 * a * n.pow(3.0) / 48.0 * (1 - n + 11 / 16.0 * (n.pow(2.0) - n.pow(3.0))) - val E = 315 * a * n.pow(4.0) / 51.0 * (1 - n) - val S = - A * lat - B * sin(2 * lat) + C * sin(4 * lat) - D * sin(6 * lat) + E * sin( - 8 * lat - ) - - //計算Y值 - val k1 = S * k0 - val k2 = k0 * nu * sin(2 * lat) / 4.0 - val k3 = - k0 * nu * sin(lat) * cos(lat).pow(3.0) / 24.0 * (5 - tan(lat).pow(2.0) + 9 * e2 * cos( - lat - ).pow(2.0) + 4 * e2.pow(2.0) * cos(lat).pow(4.0)) - val y = k1 + k2 * p.pow(2.0) + k3 * p.pow(4.0) - - //計算X值 - val k4 = k0 * nu * cos(lat) - val k5 = - k0 * nu * cos(lat).pow(3.0) / 6.0 * (1 - tan(lat).pow(2.0) + e2 * cos(lat).pow(2.0)) - val x = k4 * p + k5 * p.pow(3.0) + dx - return "$x,$y" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt deleted file mode 100644 index c71a821..0000000 --- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.a1573595.parkinglotdemo.tool - -import com.google.android.gms.maps.model.LatLng -import com.google.maps.android.clustering.ClusterItem - -class ParkingCluster( - private val mPosition: LatLng, - val id: String, - val name: String?, - val area: String?, - val totalCar: Int, - val totalMotor: Int, - val totalBike: Int, - val totalBus: Int -) : ClusterItem { - override fun getPosition(): LatLng = mPosition - - override fun getTitle(): String? = null - - override fun getSnippet(): String? = null - - override fun getZIndex(): Float? = null -} \ No newline at end of file diff --git a/app/src/main/res/anim/grow_fade_in_from_bottom.xml b/app/src/main/res/anim/grow_fade_in_from_bottom.xml deleted file mode 100644 index 0fe9963..0000000 --- a/app/src/main/res/anim/grow_fade_in_from_bottom.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bike.xml b/app/src/main/res/drawable/ic_bike.xml deleted file mode 100644 index ec9c249..0000000 --- a/app/src/main/res/drawable/ic_bike.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_bus.xml b/app/src/main/res/drawable/ic_bus.xml deleted file mode 100644 index 74edece..0000000 --- a/app/src/main/res/drawable/ic_bus.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_car.xml b/app/src/main/res/drawable/ic_car.xml deleted file mode 100644 index dbeb88a..0000000 --- a/app/src/main/res/drawable/ic_car.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_coin.xml b/app/src/main/res/drawable/ic_coin.xml deleted file mode 100644 index ce7b47a..0000000 --- a/app/src/main/res/drawable/ic_coin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml deleted file mode 100644 index 5148ba5..0000000 --- a/app/src/main/res/drawable/ic_delete.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml deleted file mode 100644 index 389bb28..0000000 --- a/app/src/main/res/drawable/ic_info.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_motor.xml b/app/src/main/res/drawable/ic_motor.xml deleted file mode 100644 index 6fb0e64..0000000 --- a/app/src/main/res/drawable/ic_motor.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml deleted file mode 100644 index c75fe2b..0000000 --- a/app/src/main/res/drawable/ic_phone.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml deleted file mode 100644 index 627a8c6..0000000 --- a/app/src/main/res/drawable/ic_pin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_unfavorite.xml b/app/src/main/res/drawable/ic_unfavorite.xml deleted file mode 100644 index 8a5a2fd..0000000 --- a/app/src/main/res/drawable/ic_unfavorite.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/rectangle_black.xml b/app/src/main/res/drawable/rectangle_black.xml deleted file mode 100644 index 60ba4d1..0000000 --- a/app/src/main/res/drawable/rectangle_black.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_white.xml b/app/src/main/res/drawable/rectangle_white.xml deleted file mode 100644 index 5df2dba..0000000 --- a/app/src/main/res/drawable/rectangle_white.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml deleted file mode 100644 index a0e4bfe..0000000 --- a/app/src/main/res/layout/activity_detail.xml +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_fuzzy_search.xml b/app/src/main/res/layout/activity_fuzzy_search.xml deleted file mode 100644 index b5ace8d..0000000 --- a/app/src/main/res/layout/activity_fuzzy_search.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml deleted file mode 100644 index 0bc7737..0000000 --- a/app/src/main/res/layout/activity_history.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 1580b9e..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml deleted file mode 100644 index 18dc9de..0000000 --- a/app/src/main/res/layout/activity_map.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_parking_lot.xml b/app/src/main/res/layout/dialog_parking_lot.xml deleted file mode 100644 index 89306f8..0000000 --- a/app/src/main/res/layout/dialog_parking_lot.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_parking_lot.xml b/app/src/main/res/layout/item_parking_lot.xml deleted file mode 100644 index 51ef74d..0000000 --- a/app/src/main/res/layout/item_parking_lot.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_update.xml b/app/src/main/res/menu/menu_update.xml deleted file mode 100644 index 2bc5d61..0000000 --- a/app/src/main/res/menu/menu_update.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxxhdpi/park.png b/app/src/main/res/mipmap-xxxhdpi/park.png deleted file mode 100644 index 77559ea..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/park.png and /dev/null differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index d5f4242..0000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml deleted file mode 100644 index 933b92e..0000000 --- a/app/src/main/res/values-zh/strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - 地圖模式 - 列表模式 - 最愛模式 - 歷史模式 - - 資料下載中\u002e\u002e\u002e - 資料處理中\u002e\u002e\u002e - - 總收錄%d筆資料 - 下載於: %s - - 公車:%d\t\t轎車:%d\t\t機車:%d\t\t自行車:%d - - 公車 - 轎車 - 機車 - 自行車 - 停車場名稱或地址 - - 復原 - 刪除 - - 再按一次退出 - - 請輸入地址 - \ No newline at end of file diff --git a/app/src/main/res/values/diemns.xml b/app/src/main/res/values/diemns.xml deleted file mode 100644 index 45361ef..0000000 --- a/app/src/main/res/values/diemns.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 14dp - 18dp - 22dp - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 251f7f7..b018a09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,29 +1,20 @@ - ParkingLotDemo - - Map mode - List mode - Favorite mode - History mode - - Downloading\u002e\u002e\u002e - Processing\u002e\u002e\u002e - - Total of %d data sets - download at: %s - - Bus:%d\t\tCar:%d\t\tMoto:%d\t\tBike:%d - + ParkingLot + Press again to exit + ParkingLot + No More + Favorite + History + Search + Map + Total of %1$s parking lots,\n download at: %2$s. + Keyword Bus Car - Moto + Motor Bike - Parking name or address - - Recover - Delete - - Press again to exit - - Enter address + Bus: %1$s + Car: %1$s + Motor: %1$s + Bike: %1$s \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index fffbff5..7ac90f6 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,5 @@ + - - -