diff --git a/android/app/build.gradle b/android/app/build.gradle index ca473a3..b1c16fe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation "androidx.glance:glance-appwidget:1.1.1" implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'io.coil-kt.coil3:coil-compose:3.0.4' implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4' } @@ -50,8 +51,7 @@ android { buildTypes { debug { - minifyEnabled true - shrinkResources true + debuggable true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarCommon.kt b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarCommon.kt new file mode 100644 index 0000000..762553f --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarCommon.kt @@ -0,0 +1,6 @@ +package dev.solsynth.solian.data + +import androidx.annotation.Keep + +@Keep +data class SolarPagination(val count: Int, val data: List) \ No newline at end of file diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt index d3b325b..78805d5 100644 --- a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt +++ b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt @@ -1,16 +1,22 @@ import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.glance.GlanceId import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent import androidx.glance.background -import androidx.glance.currentState import androidx.glance.layout.Alignment import androidx.glance.layout.Column +import androidx.glance.layout.ContentScale import androidx.glance.layout.Row import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxHeight @@ -24,11 +30,27 @@ import androidx.glance.text.FontFamily import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle +import coil3.Image +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import coil3.imageLoader +import coil3.request.ErrorResult +import coil3.request.ImageRequest +import coil3.request.SuccessResult +import coil3.request.crossfade +import coil3.toBitmap import com.google.gson.FieldNamingPolicy import com.google.gson.GsonBuilder -import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken import dev.solsynth.solian.data.InstantAdapter +import dev.solsynth.solian.data.SolarPagination import dev.solsynth.solian.data.SolarPost +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okio.IOException import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId @@ -38,23 +60,83 @@ class FeaturedPostWidget : GlanceAppWidget() { override val stateDefinition: GlanceStateDefinition<*>? get() = HomeWidgetGlanceStateDefinition() + private val defaultUrl = "https://api.sn.solsynth.dev" + override suspend fun provideGlance(context: Context, id: GlanceId) { + // TODO: Fix this +// val state = currentState() +// val prefs = state.preferences +// var baseUrl = prefs.getString("nex_server_url", null) ?: defaultUrl +// if (baseUrl.startsWith("\"") && baseUrl.endsWith("\"")) { +// baseUrl = baseUrl.substring(1, baseUrl.length - 1) +// } + + val postData = withContext(Dispatchers.IO) { fetchPostRandomly(defaultUrl) } + val avatarImage = withContext(Dispatchers.IO) { + postData?.publisher?.avatar?.let { + loadImageFromUrl(it) + } + } + provideContent { - GlanceContent(context, currentState()) + GlanceTheme { + GlanceContent(context, postData, avatarImage) + } } } - @Composable - private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { + private val client = OkHttpClient() + + private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap { + val aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat() + val newWidth = if (bitmap.width > maxWidth) maxWidth else bitmap.width + val newHeight = (newWidth / aspectRatio).toInt() + val resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + return resizedBitmap + } + + private fun loadImageFromUrl(url: String): Bitmap? { + val request = Request.Builder().url(url).build() + + return try { + val response: Response = client.newCall(request).execute() + val inputStream = response.body?.byteStream() + val bitmap = BitmapFactory.decodeStream(inputStream) + resizeBitmap(bitmap, 120, 120) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + private fun fetchPostRandomly(baseUrl: String): SolarPost? { val gson = GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(Instant::class.java, InstantAdapter()) .create() + val type = object : TypeToken>() {}.type - val prefs = currentState.preferences - val postFeaturedRaw = prefs.getString("post_featured", null) + val request = Request.Builder() + .url("$baseUrl/cgi/co/recommendations/shuffle?take=1") + .build() + return try { + val response: Response = client.newCall(request).execute() + if (response.isSuccessful) { + val body = response.body?.string() + val resp = gson.fromJson>(body, type) + resp.data.firstOrNull() + } else { + null + } + } catch (e: IOException) { + null + } + } + + @Composable + private fun GlanceContent(context: Context, data: SolarPost?, avatar: Bitmap?) { Column( modifier = GlanceModifier .fillMaxWidth() @@ -62,55 +144,63 @@ class FeaturedPostWidget : GlanceAppWidget() { .background(Color.White) .padding(16.dp) ) { - if (postFeaturedRaw != null) { - val postFeatured = gson.fromJson(postFeaturedRaw, SolarPost::class.java) + if (data != null) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (avatar != null) { + Image( + provider = ImageProvider(bitmap = avatar), + contentDescription = null, + modifier = GlanceModifier.width(36.dp).height(36.dp) + .cornerRadius(18.dp), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = GlanceModifier.width(8.dp)) - Row { Text( - text = postFeatured?.publisher?.nick ?: "Unknown", + text = data.publisher.nick, style = TextStyle(fontSize = 15.sp) ) Spacer(modifier = GlanceModifier.width(8.dp)) Text( - text = "@${postFeatured?.publisher?.name}", + text = "@${data.publisher.name}", style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace) ) } Spacer(modifier = GlanceModifier.height(8.dp)) - if (postFeatured?.body?.title != null) { + if (data.body.title != null) { Text( - text = postFeatured.body.title, + text = data.body.title, style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif) ) } - if (postFeatured?.body?.description != null) { + if (data.body.description != null) { Text( - text = postFeatured.body.description, + text = data.body.description, style = TextStyle(fontSize = 19.sp, fontFamily = FontFamily.Serif) ) } - if (postFeatured?.body?.title != null || postFeatured?.body?.description != null) { + if (data.body.title != null || data.body.description != null) { Spacer(modifier = GlanceModifier.height(8.dp)) } Text( - text = postFeatured?.body?.content ?: "No content", + text = data.body.content ?: "No content", style = TextStyle(fontSize = 15.sp), ) Spacer(modifier = GlanceModifier.height(8.dp)) - if (postFeatured?.createdAt != null) { - Text( - LocalDateTime.ofInstant(postFeatured.createdAt, ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), - style = TextStyle(fontSize = 13.sp), - ) - } + Text( + LocalDateTime.ofInstant(data.createdAt, ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + style = TextStyle(fontSize = 13.sp), + ) Text( "Solar Network Featured Post", @@ -126,11 +216,11 @@ class FeaturedPostWidget : GlanceAppWidget() { horizontalAlignment = Alignment.Horizontal.CenterHorizontally ) { Text( - text = "No featured posts", + text = "Unable to fetch post", style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold) ) Text( - text = "Open the app to load recommendations", + text = "Check your internet connection", style = TextStyle(fontSize = 15.sp) ) } diff --git a/android/app/src/main/res/xml/featured_post_widget.xml b/android/app/src/main/res/xml/featured_post_widget.xml index c67faba..5043a84 100644 --- a/android/app/src/main/res/xml/featured_post_widget.xml +++ b/android/app/src/main/res/xml/featured_post_widget.xml @@ -1,6 +1,6 @@ diff --git a/pubspec.yaml b/pubspec.yaml index 8bbcd5f..790d488 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.1.1+33 +version: 2.1.1+34 environment: sdk: ^3.5.4