From b3250e10022e44af09e3a46a038f696e38827799 Mon Sep 17 00:00:00 2001 From: Alishen Date: Fri, 26 Sep 2025 22:00:15 +0200 Subject: [PATCH 1/2] Add PokePatch feature with Retrofit and Coil integration --- app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 1 + .../de/berlindroid/zepatch/MainActivity.kt | 11 + .../zepatch/patchable/PokePatch.kt | 192 ++++++++++++++++++ converter/build.gradle.kts | 11 +- gradle/libs.versions.toml | 6 + 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc67530..488c0b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,6 +60,10 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.composableSheep) + implementation(libs.retro) + implementation(libs.retro.scalar) + implementation(libs.coil) + implementation(libs.coil.network) // Patch annotations + processor implementation(project(":patch-annotations")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 71df8a0..54335ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + + ImageLoader.Builder(context) + .components { + add(OkHttpNetworkFetcherFactory()) + } + .build() + } + setContent { ZePatchTheme { val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() diff --git a/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt b/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt new file mode 100644 index 0000000..45b0c1c --- /dev/null +++ b/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt @@ -0,0 +1,192 @@ +package de.berlindroid.zepatch.patchable + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import de.berlindroid.zepatch.annotations.Patch +import de.berlindroid.zepatch.ui.SafeArea +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.create +import retrofit2.http.GET + +data class Mon( + val name: String, + val url: String, +) + +interface PokeApi { + @GET("pokemon?limit=20&offset=0") + suspend fun mons(): Response +} + +@Patch("PokePatch") +@Preview +@Composable +fun PokePatch( + shouldCapture: Boolean = false, + onBitmap: (ImageBitmap) -> Unit = {}, +) { + var mons by remember { mutableStateOf(listOf()) } + var mon by remember { mutableStateOf(null) } + var error by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch(Dispatchers.IO) { + isLoading = true + try { + val api = Retrofit + .Builder() + .baseUrl("https://pokeapi.co/api/v2/") + .addConverterFactory(ScalarsConverterFactory.create()) + .build() + .create() + + val response = api.mons() + if (response.isSuccessful && response.body() != null) { + val body = response.body()!! + mons = JSONObject(body).getJSONArray("results").toMons() + Log.d("PokePatch", "Successfully loaded ${mons.size} Pokemon") + } else { + error = "API response error: ${response.code()}" + Log.e("PokePatch", "API response error: ${response.code()}") + } + } catch (th: Throwable) { + Log.e("PokePatch", "Error loading Pokemon data: ${th.message}", th) + error = "Failed to load Pokemon: ${th.message}" + } finally { + isLoading = false + } + } + } + + Column { + SafeArea( + shouldCapture = shouldCapture, + onBitmap = onBitmap, + ) { + when { + isLoading -> { + Text( + modifier = Modifier.rotate(45f), + text = "Loading Pokemon..." + ) + } + + error != null -> { + Text( + modifier = Modifier.rotate(45f), + text = error!!, + color = Color.Red + ) + } + + mon != null -> { + AsyncImage( + modifier = Modifier + .size(150.dp, 150.dp) + .background(Color.White), + model = mon!!.url, + contentScale = ContentScale.Fit, + contentDescription = mon!!.name, + onError = { error -> + Log.e( + "PokePatch", + "Failed to load image for ${mon!!.name}: ${mon!!.url}, error: $error" + ) + } + ) + Text( + text = mon!!.name.replaceFirstChar { it.uppercaseChar() }, + color = Color.Black + ) + } + + else -> { + Text( + modifier = Modifier.rotate(45f), + text = "Click a Pokemon to see it!" + ) + } + } + } + + if (mons.isNotEmpty()) { + LazyRow { + items(mons) { pokemon -> + Button( + modifier = Modifier.padding(4.dp), + onClick = { + mon = pokemon + Log.d( + "PokePatch", + "Selected Pokemon: ${pokemon.name}, URL: ${pokemon.url}" + ) + } + ) { + Text(text = pokemon.name.replaceFirstChar { it.uppercaseChar() }) + } + } + } + } + } +} + +fun JSONArray.toMons(): List = List(length()) { i -> + try { + val monData = getJSONObject(i) + val name = monData.getString("name") + val pokemonUrl = monData.getString("url") + + // Extract Pokemon ID from URL more safely + // URL format: https://pokeapi.co/api/v2/pokemon/1/ + val urlParts = pokemonUrl.trimEnd('/').split("/") + val pokemonId = urlParts.lastOrNull() ?: return@List null + + // Validate that pokemonId is actually a number + if (pokemonId.toIntOrNull() == null) { + Log.w("PokePatch", "Invalid Pokemon ID: $pokemonId from URL: $pokemonUrl") + return@List null + } + + // Use the correct PokeAPI sprites URL format + val spriteUrl = + "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/$pokemonId.png" + + Mon( + name = name, + url = spriteUrl + ) + } catch (e: Exception) { + Log.e("PokePatch", "Error parsing Pokemon data at index $i: ${e.message}") + null + } +}.filterNotNull() \ No newline at end of file diff --git a/converter/build.gradle.kts b/converter/build.gradle.kts index 7a6be91..7920e8e 100644 --- a/converter/build.gradle.kts +++ b/converter/build.gradle.kts @@ -33,8 +33,17 @@ kotlin { chaquopy { defaultConfig { val pythonPath = System.getenv("ZEPATCH_PYTHON_PATH") - if(pythonPath != null) { + if (!pythonPath.isNullOrBlank()) { buildPython(pythonPath) + } else { + val osName = System.getProperty("os.name").lowercase() + val defaultPythonPath = when { + osName.contains("windows") -> "C:\\Python313\\python.exe" + osName.contains("mac") -> "/usr/local/bin/python3" + else -> "python3" // Linux or other Unix-like systems + } + + buildPython(defaultPythonPath) } pip { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e9e664..66054f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,8 @@ appcompat = "1.7.1" material = "1.13.0" jts = "1.20.0" composableSheep = "1.1.0" +retro = "3.0.0" +coil = "3.3.0" [libraries] kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinpoet" } @@ -49,6 +51,10 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version material = { group = "com.google.android.material", name = "material", version.ref = "material" } jts = { group = "org.locationtech.jts", name = "jts-core", version.ref = "jts" } composableSheep = { module = "dev.nstv:composablesheep", version.ref = "composableSheep" } +retro = { module = "com.squareup.retrofit2:retrofit", version.ref = "retro" } +retro-scalar = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retro" } +coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 070f756e7156afa570ba98ca6c86cbd9a0c1a408 Mon Sep 17 00:00:00 2001 From: Alishen Date: Sat, 27 Sep 2025 12:46:30 +0200 Subject: [PATCH 2/2] update build script to remove default python path, clean up main activity initialization, and increase pokemon fetch limit to 1400 --- .../main/java/de/berlindroid/zepatch/MainActivity.kt | 12 ------------ .../de/berlindroid/zepatch/patchable/PokePatch.kt | 2 +- converter/build.gradle.kts | 9 --------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/app/src/main/java/de/berlindroid/zepatch/MainActivity.kt b/app/src/main/java/de/berlindroid/zepatch/MainActivity.kt index 917a12d..b353a57 100644 --- a/app/src/main/java/de/berlindroid/zepatch/MainActivity.kt +++ b/app/src/main/java/de/berlindroid/zepatch/MainActivity.kt @@ -24,9 +24,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp -import coil3.ImageLoader -import coil3.SingletonImageLoader -import coil3.network.okhttp.OkHttpNetworkFetcherFactory import de.berlindroid.zepatch.ui.PatchableDetail import de.berlindroid.zepatch.ui.PatchableList import de.berlindroid.zepatch.ui.theme.ZePatchTheme @@ -38,14 +35,6 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - SingletonImageLoader.setSafe { context -> - ImageLoader.Builder(context) - .components { - add(OkHttpNetworkFetcherFactory()) - } - .build() - } - setContent { ZePatchTheme { val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() @@ -106,4 +95,3 @@ class MainActivity : ComponentActivity() { } } } - diff --git a/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt b/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt index 45b0c1c..257157a 100644 --- a/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt +++ b/app/src/main/java/de/berlindroid/zepatch/patchable/PokePatch.kt @@ -42,7 +42,7 @@ data class Mon( ) interface PokeApi { - @GET("pokemon?limit=20&offset=0") + @GET("pokemon?limit=1400&offset=0") suspend fun mons(): Response } diff --git a/converter/build.gradle.kts b/converter/build.gradle.kts index 7920e8e..8817bc1 100644 --- a/converter/build.gradle.kts +++ b/converter/build.gradle.kts @@ -35,15 +35,6 @@ chaquopy { val pythonPath = System.getenv("ZEPATCH_PYTHON_PATH") if (!pythonPath.isNullOrBlank()) { buildPython(pythonPath) - } else { - val osName = System.getProperty("os.name").lowercase() - val defaultPythonPath = when { - osName.contains("windows") -> "C:\\Python313\\python.exe" - osName.contains("mac") -> "/usr/local/bin/python3" - else -> "python3" // Linux or other Unix-like systems - } - - buildPython(defaultPythonPath) } pip {