diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..ff29e76dea2e4ed3ca65fec4f9a3968441372d26 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,44 @@ +@file:Suppress("UnstableApiUsage") + +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Mareike Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +plugins { + id("justjanne.kotlin") + alias(libs.plugins.kotlin.serialization) +} + +dependencies { + implementation(libs.kotlin.stdlib) + + implementation(libs.kotlinx.coroutines.core) + testImplementation(libs.kotlinx.coroutines.test) + + testImplementation(libs.kotlin.test) + testImplementation(libs.junit.api) + testImplementation(libs.junit.params) + testRuntimeOnly(libs.junit.engine) + + implementation(libs.kotlinx.datetime) + + implementation(libs.okhttp) + implementation(libs.retrofit.core) + implementation(libs.retrofit.converter.kotlinx) + implementation(libs.kotlinx.serialization.json) +} diff --git a/api/proguard-rules.pro b/api/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..690a4be68db285dea8841626be81fa215a589a84 --- /dev/null +++ b/api/proguard-rules.pro @@ -0,0 +1,54 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# The project is open source anyway, obfuscation is useless. +-dontobfuscate + +# remove unnecessary warnings +# Android HTTP Libs +-dontnote android.net.http.** +-dontnote org.apache.http.** +# Kotlin stuff +-dontnote kotlin.** +# Gson +-dontnote com.google.gson.** +# Dagger +-dontwarn com.google.errorprone.annotations.* +# Retrofit +-dontwarn retrofit2.** +# Annotation used by Retrofit on Java 8 VMs +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault +-dontwarn javax.annotation.concurrent.GuardedBy +# Retain generic type information for use by reflection by converters and adapters. +-keepattributes Signature +# Retain declared checked exceptions for use by a Proxy instance. +-keepattributes Exceptions +# Okio +-dontwarn okio.** +-dontwarn org.conscrypt.** +# OkHttp3 +-dontwarn okhttp3.** diff --git a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt new file mode 100644 index 0000000000000000000000000000000000000000..61245654db6684cb8eaf6456a705f464cfe75d37 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete + +import kotlinx.serialization.Serializable + +@Serializable +@JvmInline +value class BarcodeId(val value: Long) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7ce209dc7fb65fc629fa0ff8d3879134f861b90 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete + +import kotlinx.serialization.Serializable + +@Serializable +@JvmInline +value class DrinkId(val value: Long) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt b/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt new file mode 100644 index 0000000000000000000000000000000000000000..7734a53237cc1ac1025219e912cfc7047ef4cc47 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt @@ -0,0 +1,34 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete + +import kotlinx.serialization.Serializable + +@Serializable +data class PwaIcon( + val src: String?, + val sizes: String?, + val type: String? +) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt b/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2732335e5c80baef808284b78648b854f3f1a789 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt @@ -0,0 +1,40 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PwaManifest( + val name: String?, + @SerialName("short_name") + val shortName: String?, + val icons: List<PwaIcon>, + val display: String?, + @SerialName("start_url") + val startUrl: String?, + val scope: String? +) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f72b510a21243674af3e290c5aee0be9fef9541 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete + +import kotlinx.serialization.Serializable + +@Serializable +@JvmInline +value class UserId(val value: Long) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..8ba703392c092f96a4e70d126a34507e513b8905 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.UserId +import kotlinx.datetime.Instant + +data class AuditEntryModelV1( + val difference: Double, + val drink: DrinkId, + val user: UserId, + val createdAt: Instant +) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..93f8461efe0f3e6e8e93d758ea22ed98cda0e4b3 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt @@ -0,0 +1,35 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.BarcodeId +import de.chaosdorf.mete.DrinkId +import kotlinx.serialization.Serializable + +@Serializable +data class BarcodeModelV1( + val id: BarcodeId, + val drink: DrinkId, +) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..bba984b3d95d0622a87dd7b238c5ab85b028db72 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt @@ -0,0 +1,55 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.DrinkId +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DrinkModelV1( + val id: DrinkId, + val name: String, + @SerialName("bottle_size") + val bottleSize: Double, + val caffeine: Int?, + val price: Double, + @SerialName("logo_file_name") + val logoFileName: String, + @SerialName("created_at") + val createdAt: Instant, + @SerialName("updated_at") + val updatedAt: Instant, + @SerialName("logo_content_type") + val logoContentType: String, + @SerialName("logo_file_size") + val logoFileSize: Long, + @SerialName("logo_updated_at") + val logoUpdatedAt: Instant, + @SerialName("logo_url") + val logoUrl: String, + val active: Boolean +) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..a120e4eeecd292d1a1e4ee8b843ca4c1b797049a --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt @@ -0,0 +1,29 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +interface MeteApiFactory<T> { + fun newInstance(baseUrl: String): T +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a1b1ee34a1c7568fcf9c1c2546aac1cce7eeae8 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt @@ -0,0 +1,72 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.PwaManifest +import de.chaosdorf.mete.UserId +import retrofit2.http.GET +import retrofit2.http.Path + +interface MeteApiV1 { + @GET("manifest.json") + suspend fun getManifest(): PwaManifest? + + @GET("api/v1/audits.json") + suspend fun getAudits(): List<AuditEntryModelV1> + + @GET("api/v1/barcodes.json") + suspend fun listBarcodes(): List<BarcodeModelV1> + + @GET("api/v1/barcodes/{id}.json") + suspend fun getBarcode(): BarcodeModelV1? + + @GET("api/v1/drinks.json") + suspend fun listDrinks(): List<DrinkModelV1> + + @GET("api/v1/drinks/{id}.json") + suspend fun getDrink(@Path("id") id: DrinkId): DrinkModelV1? + + @GET("api/v1/users.json") + suspend fun listUsers(): List<UserModelV1> + + @GET("api/v1/users/{id}.json") + suspend fun getUser(@Path("id") id: UserId): UserModelV1? + + @GET("api/v1/users/{id}/deposit.json") + suspend fun deposit(@Path("id") id: UserId) + + @GET("api/v1/users/{id}/payment.json") + suspend fun payment(@Path("id") id: UserId) + + @GET("api/v1/users/{id}/buy.json") + suspend fun buy(@Path("id") id: UserId) + + @GET("api/v1/users/{id}/buy_barcode.json") + suspend fun buyWithBarcode(@Path("id") id: UserId) + + @GET("api/v1/users/stats.json") + suspend fun getStats() +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt similarity index 59% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt rename to api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt index 1563705d5d29fdc2215887481e9f7fe7dd09fd86..2e51fec5877d10c5a8885ea4b1103471d25d1e55 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt @@ -22,33 +22,25 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui +package de.chaosdorf.mete.v1 -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material.icons.twotone.Person -import androidx.compose.ui.graphics.vector.ImageVector -import de.chaosdorf.meteroid.icons.MeteroidIcons -import de.chaosdorf.meteroid.icons.outlined.WaterFull -import de.chaosdorf.meteroid.icons.twotone.WaterFull +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Retrofit +import retrofit2.create -sealed class Screen( - val label: String, - val route: String, - val icon: ImageVector, - val selectedIcon: ImageVector = icon -) { - object Drinks : Screen( - "Drinks", - "drinks", - MeteroidIcons.Outlined.WaterFull, - MeteroidIcons.TwoTone.WaterFull - ) +object MeteApiV1Factory : MeteApiFactory<MeteApiV1> { + private val json = Json { + ignoreUnknownKeys = true + } - object Users : Screen( - "Users", - "users", - Icons.Outlined.Person, - Icons.TwoTone.Person - ) + override fun newInstance(baseUrl: String): MeteApiV1 { + val contentType = "application/json".toMediaType() + val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(json.asConverterFactory(contentType)) + .build() + return retrofit.create<MeteApiV1>() + } } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7feac18f7201d54eaf062963573813dee5f99bf --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt @@ -0,0 +1,42 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.UserId +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +@Serializable +data class UserModelV1( + val id: UserId, + val name: String, + val email: String, + val createdAt: Instant, + val updatedAt: Instant, + val balance: Double, + val active: Boolean, + val audit: Boolean, + val redirect: Boolean +) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18766b3e1bb6a0a59472ababca79ebd2992fb872..b64fb07f37feb6cce7896abcc69f783ccbe04dd6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,8 +76,18 @@ dependencies { implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.paging) + implementation(libs.androidx.navigation.compose) + implementation(libs.okhttp) + implementation(libs.kotlinx.serialization.json) + implementation(libs.coil.compose) + implementation(project(":api")) + implementation(project(":persistence")) + debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.preview) testImplementation(libs.androidx.compose.ui.test) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efe8d657816b3b46d6ad728f9cffb60bde054343..3ae67d5ae61dbb0dfc50a8c2ffce2a3cb7f2a18d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="de.chaosdorf.meteroid"> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET" /> @@ -14,7 +13,6 @@ <activity android:name=".MainActivity" android:exported="true" - android:label="@string/application_name" android:theme="@style/Theme.Meteroid"> <intent-filter> <action android:name="android.intent.action.MAIN" /> diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 3a62e2bdfc769330ccb6228cb4ab802a9e4bec12..875e72b70e622265bed12b57871ffb7f9a8206b5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -27,32 +27,37 @@ package de.chaosdorf.meteroid import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.navigation.compose.rememberNavController -import de.chaosdorf.meteroid.ui.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.MeteroidRouter +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import de.chaosdorf.meteroid.di.RootViewModel +import de.chaosdorf.meteroid.di.RootViewModelFactory +import de.chaosdorf.meteroid.routes.RootRouter import de.chaosdorf.meteroid.ui.theme.MeteroidTheme +import kotlinx.coroutines.MainScope class MainActivity : ComponentActivity() { + private val rootViewModelFactory = object : RootViewModelFactory {} + private lateinit var rootViewModel: RootViewModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { - val navController = rememberNavController() + val scope = MainScope() + rootViewModel = + rootViewModelFactory.newInstance(scope, applicationContext) + setContent { MeteroidTheme { - Scaffold( - bottomBar = { MeteroidBottomBar(navController) } - ) { padding: PaddingValues -> - MeteroidRouter( - navController, - modifier = Modifier.padding(padding) - ) - } + App(rootViewModel) } } } } + +@Composable +fun App(viewModel: RootViewModel) { + val route by viewModel.route.collectAsState() + + RootRouter(route) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b07b4ca5c84946e991b55ed97d67d63de8b9ecb --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt @@ -0,0 +1,116 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import android.util.Log +import de.chaosdorf.mete.PwaManifest +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.util.await +import de.chaosdorf.meteroid.util.findBestIcon +import de.chaosdorf.meteroid.util.resolve +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request + + +interface AddServerViewModelFactory { + fun newInstance( + scope: CoroutineScope, + isFirstServer: Boolean, + onSubmit: (url: String, manifest: PwaManifest?) -> Unit, + onCancel: () -> Unit + ) = AddServerViewModelImpl(scope, isFirstServer, onSubmit, onCancel) +} + +interface AddServerViewModel { + val url: MutableStateFlow<String> + val server: StateFlow<Server?> + val loading: StateFlow<Boolean> + val isFirstServer: Boolean + fun submit() + fun cancel() +} + +class AddServerViewModelImpl( + scope: CoroutineScope, + override val isFirstServer: Boolean, + private val onSubmit: (url: String, manifest: PwaManifest?) -> Unit, + private val onCancel: () -> Unit +) : AddServerViewModel { + private val json = Json { + ignoreUnknownKeys = true + } + private val httpClient = OkHttpClient() + override val url = MutableStateFlow("") + private val _loading = MutableStateFlow(false) + override val loading = _loading + + @OptIn(ExperimentalSerializationApi::class) + private val manifest = url.debounce(300).mapNotNull { address -> + _loading.value = true + try { + val url = address.toHttpUrl().resolve("manifest.json") + val call = httpClient.newCall(Request.Builder().url(url!!).build()) + val body = call.await() + val manifest = json.decodeFromStream<PwaManifest>(body.byteStream()) + Pair(address, manifest) + } catch (_: Exception) { + null + } finally { + _loading.value = false + } + }.stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val server = manifest.mapLatest { pair -> + pair?.let { (url, manifest) -> + Server( + id = ServerId(-1), + url = url, + name = manifest.name, + logoUrl = manifest.findBestIcon()?.resolve(url) + ) + } + }.stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override fun submit() { + onSubmit(url.value, manifest.value?.second) + } + + override fun cancel() { + onCancel() + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8a46446dc672b9eab61615d4813fd591c476d6e --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt @@ -0,0 +1,52 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import de.chaosdorf.mete.DrinkId +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.Repository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +interface DrinkViewModelFactory { + fun newInstance( + scope: CoroutineScope, + repository: Repository<DrinkId, Drink> + ) = DrinkListViewModelImpl(scope, repository) +} + +interface DrinkListViewModel { + val drinks: StateFlow<List<Drink>> +} + +class DrinkListViewModelImpl( + scope: CoroutineScope, + repository: Repository<DrinkId, Drink> +) : DrinkListViewModel { + override val drinks: StateFlow<List<Drink>> = repository.getAllFlow() + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e732b34426176d28a2defd007306990cb982ccb --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import androidx.room.withTransaction +import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.meteroid.MeteroidDatabase +import de.chaosdorf.meteroid.RepositorySyncHandler +import de.chaosdorf.meteroid.SyncHandler +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.Server +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + + +interface MainLayoutViewModelFactory { + fun newInstance( + scope: CoroutineScope, db: MeteroidDatabase, server: Server, onOpenServerSelection: () -> Unit + ): MainLayoutViewModel = MainLayoutViewModelImpl(scope, db, server, onOpenServerSelection) +} + +interface MainLayoutViewModel { + val server: Server + val syncState: StateFlow<SyncHandler.State> + val drinkListViewModel: DrinkListViewModel + + fun openServerSelection() +} + +class MainLayoutViewModelImpl( + scope: CoroutineScope, + db: MeteroidDatabase, + override val server: Server, + private val onOpenServerSelection: () -> Unit +) : MainLayoutViewModel { + private val api = MeteApiV1Factory.newInstance(server.url) + override val drinkListViewModel = DrinkListViewModelImpl(scope, db.drinks()) + private val syncHandler = RepositorySyncHandler(db::withTransaction, db.drinks()) { + api.listDrinks().map { Drink.fromModelV1(it) } + } + override val syncState: StateFlow<SyncHandler.State> = syncHandler.state + + init { + scope.launch { + syncHandler.doSync() + } + } + + override fun openServerSelection() { + onOpenServerSelection() + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..4e9ced5b09c9b4b36aa26961f910122adc080a00 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt @@ -0,0 +1,142 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import android.content.Context +import androidx.room.Room +import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.meteroid.MeteroidDatabase +import de.chaosdorf.meteroid.Repository +import de.chaosdorf.meteroid.SyncHandler +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.util.findBestIcon +import de.chaosdorf.meteroid.util.resolve +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +interface RootViewModelFactory { + fun newInstance( + scope: CoroutineScope, context: Context + ): RootViewModel = RootViewModelImpl(scope, context) +} + +interface RootViewModel { + val route: StateFlow<RootRoute> +} + +class ServerListSyncHandler( + private val repository: Repository<ServerId, Server>, +) : SyncHandler { + private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle) + override val state = _state + + override suspend fun doSync() { + _state.value = SyncHandler.State.Syncing + for (server in repository.getAll()) { + val api = MeteApiV1Factory.newInstance(server.url) + val manifest = api.getManifest() + val updated = server.copy( + name = manifest?.name, + logoUrl = manifest?.findBestIcon()?.resolve(server.url) + ) + repository.save(updated) + } + _state.value = SyncHandler.State.Idle + } +} + +class RootViewModelImpl( + scope: CoroutineScope, context: Context +) : RootViewModel { + private val db = Room.databaseBuilder(context, MeteroidDatabase::class.java, "mete").build() + + private val syncHandler = ServerListSyncHandler(db.server()) + + private val setupViewModelFactory: SetupViewModelFactory = object : SetupViewModelFactory {} + + private val mainLayoutViewModelFactory: MainLayoutViewModelFactory = + object : MainLayoutViewModelFactory {} + + private val _serverSelectionOpen = MutableStateFlow(false) + private val _serverId: MutableStateFlow<ServerId?> = MutableStateFlow(null) + private val _server: StateFlow<Server?> = _serverId.flatMapLatest { + it?.let { db.server().getFlow(it) } ?: flowOf(null) + }.stateIn(scope, SharingStarted.WhileSubscribed(), null) + override val route: StateFlow<RootRoute> = + combine(_server, _serverSelectionOpen) { server, serverSelectionOpen -> + if (server == null || serverSelectionOpen) { + RootRoute.Setup( + setupViewModelFactory.newInstance( + scope, + db, + db.server(), + server != null, + ::onSelect, + ::onCloseServerSelection + ) + ) + } else { + RootRoute.MainLayout( + mainLayoutViewModelFactory.newInstance( + scope, + db, + server, + ::onOpenServerSelection + ) + ) + } + }.stateIn(scope, SharingStarted.WhileSubscribed(), RootRoute.Init) + + init { + scope.launch { syncHandler.doSync() } + } + + private fun onSelect(server: ServerId) { + _serverId.value = server + _serverSelectionOpen.value = false + } + + private fun onOpenServerSelection() { + _serverSelectionOpen.value = true + } + + private fun onCloseServerSelection() { + _serverSelectionOpen.value = true + } +} + +sealed class RootRoute { + data object Init : RootRoute() + data class Setup(val viewModel: SetupViewModel) : RootRoute() + data class MainLayout(val viewModel: MainLayoutViewModel) : RootRoute() +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..639df3ddad8375ee41dbf2344ade0235cda39eac --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt @@ -0,0 +1,92 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import de.chaosdorf.meteroid.Repository +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + + +interface ServerSelectionViewModelFactory { + fun newInstance( + scope: CoroutineScope, + repository: Repository<ServerId, Server>, + hasSelectedServer: Boolean, + onAddServer: () -> Unit, + onSelect: (server: ServerId) -> Unit, + onClose: () -> Unit + ) = ServerSelectionViewModelImpl( + scope, + repository, + hasSelectedServer, + onAddServer, + onSelect, + onClose + ) +} + +interface ServerSelectionViewModel { + val servers: StateFlow<List<Server>> + val hasSelectedServer: Boolean + fun addServer() + fun select(server: ServerId) + fun remove(server: ServerId) + fun close() +} + +class ServerSelectionViewModelImpl( + private val scope: CoroutineScope, + private val repository: Repository<ServerId, Server>, + override val hasSelectedServer: Boolean, + private val onAddServer: () -> Unit, + private val onSelect: (server: ServerId) -> Unit, + private val onClose: () -> Unit +) : ServerSelectionViewModel { + override val servers: StateFlow<List<Server>> = + repository.getAllFlow().stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + + override fun addServer() { + onAddServer() + } + + override fun select(server: ServerId) { + onSelect(server) + } + + override fun remove(server: ServerId) { + scope.launch { + repository.delete(server) + } + } + + override fun close() { + onClose() + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..16bbde1943a8f2f1259ddeb19f1dbd9c30b6fa35 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt @@ -0,0 +1,126 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.di + +import androidx.room.withTransaction +import de.chaosdorf.mete.PwaManifest +import de.chaosdorf.meteroid.MeteroidDatabase +import de.chaosdorf.meteroid.Repository +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.util.findBestIcon +import de.chaosdorf.meteroid.util.resolve +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + + +interface SetupViewModelFactory { + fun newInstance( + scope: CoroutineScope, + db: MeteroidDatabase, + repository: Repository<ServerId, Server>, + hasSelectedServer: Boolean, + onSelect: (server: ServerId) -> Unit, + onClose: () -> Unit + ): SetupViewModel = SetupViewModelImpl( + scope, db, repository, hasSelectedServer, onSelect, onClose + ) +} + +interface SetupViewModel { + val route: StateFlow<SetupRoute> +} + +class SetupViewModelImpl( + private val scope: CoroutineScope, + private val db: MeteroidDatabase, + private val repository: Repository<ServerId, Server>, + private val hasSelectedServer: Boolean, + private val onSelect: (server: ServerId) -> Unit, + private val onClose: () -> Unit +) : SetupViewModel { + private val addServerViewModelFactory = object : AddServerViewModelFactory {} + private val serverSelectionViewModelFactory = object : ServerSelectionViewModelFactory {} + private val isFirstServer = repository.getAllFlow().map(List<Server>::isEmpty) + private val isAddingServer = MutableStateFlow(false) + override val route: StateFlow<SetupRoute> = + combine(isFirstServer, isAddingServer) { isFirstServer, addingServer -> + if (addingServer || isFirstServer) { + SetupRoute.AddServer( + addServerViewModelFactory.newInstance( + scope, isFirstServer, ::onSubmit, ::onCancel + ) + ) + } else { + SetupRoute.ServerSelection( + serverSelectionViewModelFactory.newInstance( + scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose + ) + ) + } + }.stateIn( + scope, SharingStarted.WhileSubscribed(), SetupRoute.ServerSelection( + serverSelectionViewModelFactory.newInstance( + scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose + ) + ) + ) + + private fun onAddServer() { + isAddingServer.value = true + } + + private fun onSubmit(url: String, manifest: PwaManifest?) { + isAddingServer.value = false + scope.launch { + db.withTransaction { + val lastId = repository.getAll().map(Server::id).maxByOrNull(ServerId::value)?.value ?: 0 + repository.save( + Server( + id = ServerId(lastId + 1), + url = url, + name = manifest?.name, + logoUrl = manifest?.findBestIcon()?.resolve(url) + ) + ) + } + } + } + + private fun onCancel() { + isAddingServer.value = false + } +} + +sealed class SetupRoute { + data class ServerSelection(val viewModel: ServerSelectionViewModel) : SetupRoute() + data class AddServer(val viewModel: AddServerViewModel) : SetupRoute() +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..a31fbd8a5c163524c6a2b9a3b42ffb0386628449 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt @@ -0,0 +1,33 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun InitRoute() { + Text("Loading…") +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt new file mode 100644 index 0000000000000000000000000000000000000000..c58ef4258d426e887d87cc3a29edd8fffe870897 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt @@ -0,0 +1,39 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes + +import androidx.compose.runtime.Composable +import de.chaosdorf.meteroid.di.RootRoute +import de.chaosdorf.meteroid.routes.main.MainLayoutRoute +import de.chaosdorf.meteroid.routes.setup.SetupView + +@Composable +fun RootRouter(route: RootRoute) { + when (route) { + RootRoute.Init -> InitRoute() + is RootRoute.MainLayout -> MainLayoutRoute(viewModel = route.viewModel) + is RootRoute.Setup -> SetupView(viewModel = route.viewModel) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f7b05e93612881a62df5e0ae802fb75d5500327 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt @@ -0,0 +1,38 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes + +import androidx.compose.runtime.Composable +import de.chaosdorf.meteroid.di.SetupRoute +import de.chaosdorf.meteroid.routes.setup.AddServerRoute +import de.chaosdorf.meteroid.routes.setup.ServerSelectionRoute + +@Composable +fun SetupRouter(route: SetupRoute) { + when (route) { + is SetupRoute.AddServer -> AddServerRoute(viewModel = route.viewModel) + is SetupRoute.ServerSelection -> ServerSelectionRoute(viewModel = route.viewModel) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt similarity index 70% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt index b69b7ec8608fbbecfb9685ce3508b54d3a66588c..67907a294fb2a28b0365e25ccebd42eeb5c940f0 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt @@ -22,26 +22,22 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui +package de.chaosdorf.meteroid.routes.main +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import de.chaosdorf.meteroid.di.DrinkListViewModel @Composable -fun MeteroidRouter( - navController: NavHostController, - modifier: Modifier = Modifier -) { - NavHost( - navController, - modifier = modifier, - startDestination = Screen.Drinks.route, - ) { - composable(Screen.Users.route) { Text("Userlist") } - composable(Screen.Drinks.route) { Text("Drinklist") } +fun DrinkList(viewModel: DrinkListViewModel) { + val drinks by viewModel.drinks.collectAsState() + LazyColumn { + items(drinks) { drink -> + Text(drink.name) + } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..e009649fcb75d3fd38f8bbf5f1c57da0f837a624 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt @@ -0,0 +1,74 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes.main + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import de.chaosdorf.meteroid.RepositorySyncHandler +import de.chaosdorf.meteroid.SyncHandler +import de.chaosdorf.meteroid.di.MainLayoutViewModel + +@Composable +fun MainLayoutRoute(viewModel: MainLayoutViewModel) { + val syncState by viewModel.syncState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text("Meteroid") + }, + navigationIcon = { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + modifier = Modifier.clickable { + viewModel.openServerSelection() + }, + contentDescription = "Back" + ) + } + ) + } + ) { paddingValues -> + Column(Modifier.padding(paddingValues)) { + if (syncState == SyncHandler.State.Syncing) { + LinearProgressIndicator() + } + DrinkList(viewModel.drinkListViewModel) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..0866e2653f418c21727aaeb3b3268b23d21cc9a1 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt @@ -0,0 +1,95 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes.setup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import coil.compose.AsyncImage +import de.chaosdorf.meteroid.di.AddServerViewModel + +@Composable +fun AddServerRoute(viewModel: AddServerViewModel) { + val url by viewModel.url.collectAsState() + val server by viewModel.server.collectAsState() + val loading by viewModel.loading.collectAsState() + + Scaffold( + topBar = { + TopAppBar(title = { + Text("Add Server") + }) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + if (loading) { + LinearProgressIndicator() + } + + TextField( + label = { + Text("Server URL") + }, + value = url, + onValueChange = { value -> + viewModel.url.value = value + } + ) + + Button( + onClick = viewModel::submit + ) { + Text("Save") + } + + if (!viewModel.isFirstServer) { + Button( + onClick = viewModel::cancel + ) { + Text("Cancel") + } + } + + server?.let { + ListItem( + headlineContent = { Text(it.name ?: it.url) }, + leadingContent = { + AsyncImage(model = it.logoUrl, contentDescription = null) + } + ) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..bee5ecc82e7e2eee8b17d2b62425d61ece0415c1 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt @@ -0,0 +1,91 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes.setup + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import coil.compose.AsyncImage +import de.chaosdorf.meteroid.di.ServerSelectionViewModel + +@Composable +fun ServerSelectionRoute(viewModel: ServerSelectionViewModel) { + val servers by viewModel.servers.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Select Server") }, + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { viewModel.addServer() } + ) { + Icon( + Icons.Default.Add, + contentDescription = "Select Server" + ) + } + } + ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { + items(servers) { server -> + ListItem( + modifier = Modifier.clickable { + viewModel.select(server.id) + }, + headlineContent = { Text(server.name ?: server.url) }, + leadingContent = { + AsyncImage(model = server.logoUrl, contentDescription = null) + }, + trailingContent = { + Icon( + Icons.Default.Delete, + modifier = Modifier.clickable { + viewModel.remove(server.id) + }, + contentDescription = "Delete" + ) + } + ) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt new file mode 100644 index 0000000000000000000000000000000000000000..63de80b72a282a02d4da65c2c0dd48638eb71eee --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt @@ -0,0 +1,38 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.routes.setup + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import de.chaosdorf.meteroid.routes.SetupRouter +import de.chaosdorf.meteroid.di.SetupViewModel + +@Composable +fun SetupView(viewModel: SetupViewModel) { + val route by viewModel.route.collectAsState() + + SetupRouter(route) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt deleted file mode 100644 index 146dd7f75bc512ac2f6bfd83a899dec7d52a94e2..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2013-2023 Chaosdorf e.V. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package de.chaosdorf.meteroid.ui - -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.currentBackStackEntryAsState - -@Composable -fun MeteroidBottomBar(navController: NavController) { - val screens = listOf(Screen.Drinks, Screen.Users) - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - - NavigationBar { - for (screen in screens) { - val selected = currentDestination?.hierarchy?.any { - it.route == screen.route - } == true - - NavigationBarItem( - label = { Text(screen.label) }, - icon = { - Icon( - if (selected) screen.selectedIcon - else screen.icon, - screen.label - ) - }, - selected = selected, - onClick = { - navController.navigate(screen.route) { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true - } - } - ) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..606c83c23406ec4b7e29443d2f8cad10d2556570 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt @@ -0,0 +1,58 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.util + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + + +suspend fun Call.await() = suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + cancel() + } + enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val body = response.body + if (body == null) { + continuation.resumeWithException(KotlinNullPointerException("Response from ${call.request().method}.${call.request().url} was null but response body type was declared as non-null")) + } else { + continuation.resume(body) + } + } else { + continuation.resumeWithException(IOException("Response from ${call.request().method}.${call.request().url} returned a non-successful response code: ${response.code}")) + } + } + }) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd7ecd4dc8fddc4a5adc7f90df3ed638fea08372 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.util + +import de.chaosdorf.mete.PwaIcon +import de.chaosdorf.mete.PwaManifest +import okhttp3.HttpUrl.Companion.toHttpUrl + +fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull { + it.sizes?.split("x")?.firstOrNull()?.toIntOrNull() ?: 0 +} + +fun PwaIcon.resolve(baseUrl: String): String? = + this.src?.let { baseUrl.toHttpUrl().resolve(it) }?.toString() diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt index 683e704d3b3a3e93193cb727732272a40aaf0756..629dd50abcdc62ac44805f8b32c58f58cf89e311 100644 --- a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt +++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt @@ -16,11 +16,11 @@ class AndroidApplicationConvention : Plugin<Project> { } extensions.configure<ApplicationExtension> { - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 applicationId = "${rootProject.group}.${rootProject.name.lowercase(Locale.ROOT)}" versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1 diff --git a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt index feafcc4950807c8f5d8fe30d5239fbae86020caa..c048de7841d78191de848ec5808a009d99f0fa52 100644 --- a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt +++ b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt @@ -13,7 +13,7 @@ class AndroidLibraryConvention : Plugin<Project> { } extensions.configure<LibraryExtension> { - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 diff --git a/gradle/convention/src/main/kotlin/KotlinConvention.kt b/gradle/convention/src/main/kotlin/KotlinConvention.kt index 9b364add5f645e899a1c890959a00d2475fbbdfa..d99f88171db54cdf468239e7c182cf8221b3d722 100644 --- a/gradle/convention/src/main/kotlin/KotlinConvention.kt +++ b/gradle/convention/src/main/kotlin/KotlinConvention.kt @@ -5,10 +5,11 @@ import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.tasks.testing.Test import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.kotlin import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension class KotlinConvention : Plugin<Project> { override fun apply(target: Project) { @@ -17,8 +18,8 @@ class KotlinConvention : Plugin<Project> { apply("org.jetbrains.kotlin.jvm") } - extensions.configure<KotlinJvmOptions> { - freeCompilerArgs = freeCompilerArgs + listOf( + extensions.configure<KotlinJvmProjectExtension> { + compilerOptions.freeCompilerArgs.addAll( "-opt-in=kotlin.RequiresOptIn", // Enable experimental coroutines APIs, including Flow "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", @@ -27,7 +28,7 @@ class KotlinConvention : Plugin<Project> { "-opt-in=kotlin.ExperimentalUnsignedTypes", ) - jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget.set(JvmTarget.JVM_17) } tasks.withType<Test> { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77c021b6017d8087d59a5c892a7b8d7710fb053e..b1ef11609752905ed6396ae66043312ab8ff1367 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,22 @@ [versions] -androidGradlePlugin = "8.0.1" -androidx-activity = "1.7.1" +androidGradlePlugin = "8.1.2" +androidx-activity = "1.8.0" androidx-appcompat = "1.6.1" -androidx-compose-bom = "2023.05.01" -androidx-compose-compiler = "1.4.7" +androidx-compose-bom = "2023.10.01" +androidx-compose-compiler = "1.5.3" androidx-compose-material = "1.5.0-alpha04" -androidx-compose-material3 = "1.2.0-alpha01" -androidx-compose-runtimetracing = "1.0.0-alpha03" -androidx-compose-tooling = "1.5.0-alpha04" -androidx-navigation = "2.6.0-rc01" -kotlin = "1.8.21" +androidx-compose-material3 = "1.2.0-alpha10" +androidx-compose-runtimetracing = "1.0.0-alpha04" +androidx-compose-tooling = "1.6.0-alpha08" +androidx-navigation = "2.7.4" +androidx-room = "2.6.0" +coil = "2.4.0" +kotlin = "1.9.10" +kotlin-ksp = "1.9.10-1.0.13" kotlinxCoroutines = "1.7.1" kotlinxDatetime = "0.4.0" kotlinxSerializationJson = "1.5.1" -junit = "5.9.3" +junit = "5.10.0" [libraries] androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } @@ -27,7 +30,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } -androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material3" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.6.0-alpha08" } androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } androidx-compose-material-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } @@ -39,6 +42,17 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose-tooling" } androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } +androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" } + +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } + +okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" } +retrofit-core = { module = "com.squareup.retrofit2:retrofit", version = "2.9.0" } +retrofit-converter-kotlinx = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" } + androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } @@ -47,6 +61,7 @@ junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "kotlin" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } @@ -62,3 +77,4 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin.ksp" } diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..56a70fe39af3e93fc9daca1c463a792a8f130cc5 --- /dev/null +++ b/persistence/build.gradle.kts @@ -0,0 +1,56 @@ +@file:Suppress("UnstableApiUsage") + +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Mareike Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +plugins { + id("justjanne.android.library") + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.ksp) +} + +android { + namespace = "de.chaosdorf.meteroid.persistence" +} + +ksp { + arg("room.generateKotlin", "true") + arg("room.schemaLocation", "$projectDir/room/schemas") +} + +dependencies { + implementation(libs.kotlin.stdlib) + + implementation(libs.kotlinx.coroutines.core) + testImplementation(libs.kotlinx.coroutines.test) + + testImplementation(libs.kotlin.test) + testImplementation(libs.junit.api) + testImplementation(libs.junit.params) + testRuntimeOnly(libs.junit.engine) + + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.paging) + + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(project(":api")) +} diff --git a/persistence/proguard-rules.pro b/persistence/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..690a4be68db285dea8841626be81fa215a589a84 --- /dev/null +++ b/persistence/proguard-rules.pro @@ -0,0 +1,54 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# The project is open source anyway, obfuscation is useless. +-dontobfuscate + +# remove unnecessary warnings +# Android HTTP Libs +-dontnote android.net.http.** +-dontnote org.apache.http.** +# Kotlin stuff +-dontnote kotlin.** +# Gson +-dontnote com.google.gson.** +# Dagger +-dontwarn com.google.errorprone.annotations.* +# Retrofit +-dontwarn retrofit2.** +# Annotation used by Retrofit on Java 8 VMs +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault +-dontwarn javax.annotation.concurrent.GuardedBy +# Retain generic type information for use by reflection by converters and adapters. +-keepattributes Signature +# Retain declared checked exceptions for use by a Proxy instance. +-keepattributes Exceptions +# Okio +-dontwarn okio.** +-dontwarn org.conscrypt.** +# OkHttp3 +-dontwarn okhttp3.** diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json new file mode 100644 index 0000000000000000000000000000000000000000..ab7f5e79cd26de6948213a5e361d2783ae728c9f --- /dev/null +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -0,0 +1,144 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", + "entities": [ + { + "tableName": "Drink", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "caffeine", + "columnName": "caffeine", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileName", + "columnName": "logoFileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoContentType", + "columnName": "logoContentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileSize", + "columnName": "logoFileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUpdatedAt", + "columnName": "logoUpdatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')" + ] + } +} \ No newline at end of file diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json new file mode 100644 index 0000000000000000000000000000000000000000..846eb8cc9382c7fc6b285df977e24458b3cf8531 --- /dev/null +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json @@ -0,0 +1,144 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", + "entities": [ + { + "tableName": "Drink", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "caffeine", + "columnName": "caffeine", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileName", + "columnName": "logoFileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoContentType", + "columnName": "logoContentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileSize", + "columnName": "logoFileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUpdatedAt", + "columnName": "logoUpdatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')" + ] + } +} \ No newline at end of file diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json new file mode 100644 index 0000000000000000000000000000000000000000..67da8743f1fcb087a306db3aacf2121138c34b1b --- /dev/null +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json @@ -0,0 +1,144 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", + "entities": [ + { + "tableName": "Drink", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "caffeine", + "columnName": "caffeine", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileName", + "columnName": "logoFileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoContentType", + "columnName": "logoContentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoFileSize", + "columnName": "logoFileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUpdatedAt", + "columnName": "logoUpdatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')" + ] + } +} \ No newline at end of file diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..20c64ac94d5e3668f2ad6801b84d6ca553a7e29b --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid + +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.DrinkDao +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerDao +import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter + +@Database( + version = 1, + entities = [ + Drink::class, + Server::class + ], + autoMigrations = [], +) +@TypeConverters(value = [KotlinDatetimeTypeConverter::class]) +abstract class MeteroidDatabase : RoomDatabase() { + abstract fun drinks(): DrinkDao + abstract fun server(): ServerDao +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt new file mode 100644 index 0000000000000000000000000000000000000000..26dfb12059dd4e75446ee523d8ebe940d4568641 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt @@ -0,0 +1,38 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid + +import kotlinx.coroutines.flow.Flow + +interface Repository<K, V> { + fun getKey(value: V): K + suspend fun get(key: K): V? + fun getFlow(key: K): Flow<V?> + suspend fun getAll(): List<V> + fun getAllFlow(): Flow<List<V>> + suspend fun save(value: V) + suspend fun delete(key: K) + suspend fun deleteAll() +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..752839909edf666bc1d461fa159b7255a07ff3e1 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt @@ -0,0 +1,59 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class RepositorySyncHandler<K, V>( + private val withTransaction: suspend (block: suspend () -> Any?) -> Any?, + private val repository: Repository<K, V>, + private val loader: suspend () -> List<V>, +) : SyncHandler { + private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle) + override val state: StateFlow<SyncHandler.State> = _state + + override suspend fun doSync() { + _state.value = SyncHandler.State.Syncing + val values = + try { + loader() + } catch (e: Exception) { + _state.value = SyncHandler.State.Error("Error while syncing: $e") + return + } + withTransaction { + val existing = repository.getAll().map(repository::getKey).toSet() + val deletedEntries = existing - values.map(repository::getKey).toSet() + for (deletedEntry in deletedEntries) { + repository.delete(deletedEntry) + } + for (entry in values) { + repository.save(entry) + } + } + _state.value = SyncHandler.State.Idle + } +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a671a841fb5889abe36eee2b7cd60be68d008c9 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt @@ -0,0 +1,39 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid + +import kotlinx.coroutines.flow.StateFlow + +interface SyncHandler { + val state: StateFlow<State> + + suspend fun doSync() + + sealed class State { + data object Idle : State() + data object Syncing : State() + data class Error(val message: String) : State() + } +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bf745e8f3f0e7d4edaaf68d092b475dc65c047a --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -0,0 +1,99 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.model + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.v1.DrinkModelV1 +import de.chaosdorf.meteroid.Repository +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + +@Entity +data class Drink( + @PrimaryKey + val id: DrinkId, + val name: String, + val volume: Double, + val caffeine: Int?, + val price: Double, + val active: Boolean, + val createdAt: Instant, + val updatedAt: Instant, + val logoUrl: String, + val logoFileName: String, + val logoContentType: String, + val logoFileSize: Long, + val logoUpdatedAt: Instant +) { + companion object { + fun fromModelV1(value: DrinkModelV1) = Drink( + value.id, + value.name, + value.bottleSize, + value.caffeine, + value.price, + value.active, + value.createdAt, + value.updatedAt, + value.logoUrl, + value.logoFileName, + value.logoContentType, + value.logoFileSize, + value.logoUpdatedAt + ) + } +} + +@Dao +interface DrinkDao : Repository<DrinkId, Drink> { + override fun getKey(value: Drink): DrinkId = value.id + + @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1") + override suspend fun get(id: DrinkId): Drink? + + @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1") + override fun getFlow(id: DrinkId): Flow<Drink?> + + @Query("SELECT * FROM Drink") + override suspend fun getAll(): List<Drink> + + @Query("SELECT * FROM Drink") + override fun getAllFlow(): Flow<List<Drink>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + override suspend fun save(drink: Drink) + + @Query("DELETE FROM Drink WHERE id = :id") + override suspend fun delete(id: DrinkId) + + @Query("DELETE FROM Drink") + override suspend fun deleteAll() +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt new file mode 100644 index 0000000000000000000000000000000000000000..c735ca8efa45f1d81bd8145ae82158a23afbd8e8 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt @@ -0,0 +1,73 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.model + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import de.chaosdorf.mete.PwaManifest +import de.chaosdorf.meteroid.Repository +import kotlinx.coroutines.flow.Flow + +@JvmInline +value class ServerId(val value: Long) + +@Entity +data class Server( + @PrimaryKey + val id: ServerId, + val name: String?, + val url: String, + val logoUrl: String? +) + +@Dao +interface ServerDao : Repository<ServerId, Server> { + override fun getKey(value: Server): ServerId = value.id + + @Query("SELECT * FROM Server WHERE id = :id LIMIT 1") + override suspend fun get(id: ServerId): Server? + + @Query("SELECT * FROM Server WHERE id = :id LIMIT 1") + override fun getFlow(id: ServerId): Flow<Server?> + + @Query("SELECT * FROM Server") + override suspend fun getAll(): List<Server> + + @Query("SELECT * FROM Server") + override fun getAllFlow(): Flow<List<Server>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + override suspend fun save(drink: Server) + + @Query("DELETE FROM Server WHERE id = :id") + override suspend fun delete(id: ServerId) + + @Query("DELETE FROM Server") + override suspend fun deleteAll() +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt new file mode 100644 index 0000000000000000000000000000000000000000..289ea21afd72e5fce225fbf92445cf4930f55241 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt @@ -0,0 +1,35 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.util + +import androidx.room.TypeConverter +import kotlinx.datetime.Instant + +class KotlinDatetimeTypeConverter { + @TypeConverter + fun load(value: Long): Instant = Instant.fromEpochMilliseconds(value) + @TypeConverter + fun store(value: Instant): Long = value.toEpochMilliseconds() +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 564449ee08aa15ab093d2a24a4d4714864439411..4ff6281f603d29e099c6f78f0bcc6a6b530732c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,4 +40,4 @@ dependencyResolutionManagement { } } -include(":app") +include(":app", ":api", ":persistence")