Show random post instead of featured post

This commit is contained in:
LittleSheep 2024-12-21 04:12:52 +08:00
parent 65a8f1e6c3
commit 8db6513eef
5 changed files with 127 additions and 31 deletions

View File

@ -14,6 +14,7 @@ dependencies {
implementation "androidx.glance:glance-appwidget:1.1.1" implementation "androidx.glance:glance-appwidget:1.1.1"
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6' implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
implementation 'com.google.code.gson:gson:2.10.1' 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-compose:3.0.4'
implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4' implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.4'
} }
@ -50,8 +51,7 @@ android {
buildTypes { buildTypes {
debug { debug {
minifyEnabled true debuggable true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }

View File

@ -0,0 +1,6 @@
package dev.solsynth.solian.data
import androidx.annotation.Keep
@Keep
data class SolarPagination<T>(val count: Int, val data: List<T>)

View File

@ -1,16 +1,22 @@
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId import androidx.glance.GlanceId
import androidx.glance.GlanceModifier 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.GlanceAppWidget
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.provideContent
import androidx.glance.background import androidx.glance.background
import androidx.glance.currentState
import androidx.glance.layout.Alignment import androidx.glance.layout.Alignment
import androidx.glance.layout.Column import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
import androidx.glance.layout.Row import androidx.glance.layout.Row
import androidx.glance.layout.Spacer import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxHeight
@ -24,11 +30,27 @@ import androidx.glance.text.FontFamily
import androidx.glance.text.FontWeight import androidx.glance.text.FontWeight
import androidx.glance.text.Text import androidx.glance.text.Text
import androidx.glance.text.TextStyle 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.FieldNamingPolicy
import com.google.gson.GsonBuilder 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.InstantAdapter
import dev.solsynth.solian.data.SolarPagination
import dev.solsynth.solian.data.SolarPost 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.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@ -38,23 +60,83 @@ class FeaturedPostWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>? override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition() get() = HomeWidgetGlanceStateDefinition()
private val defaultUrl = "https://api.sn.solsynth.dev"
override suspend fun provideGlance(context: Context, id: GlanceId) { override suspend fun provideGlance(context: Context, id: GlanceId) {
// TODO: Fix this
// val state = currentState<HomeWidgetGlanceState>()
// 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 { provideContent {
GlanceContent(context, currentState()) GlanceTheme {
GlanceContent(context, postData, avatarImage)
}
} }
} }
@Composable private val client = OkHttpClient()
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
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 = val gson =
GsonBuilder() GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant::class.java, InstantAdapter()) .registerTypeAdapter(Instant::class.java, InstantAdapter())
.create() .create()
val type = object : TypeToken<SolarPagination<SolarPost>>() {}.type
val prefs = currentState.preferences val request = Request.Builder()
val postFeaturedRaw = prefs.getString("post_featured", null) .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<SolarPagination<SolarPost>>(body, type)
resp.data.firstOrNull()
} else {
null
}
} catch (e: IOException) {
null
}
}
@Composable
private fun GlanceContent(context: Context, data: SolarPost?, avatar: Bitmap?) {
Column( Column(
modifier = GlanceModifier modifier = GlanceModifier
.fillMaxWidth() .fillMaxWidth()
@ -62,55 +144,63 @@ class FeaturedPostWidget : GlanceAppWidget() {
.background(Color.White) .background(Color.White)
.padding(16.dp) .padding(16.dp)
) { ) {
if (postFeaturedRaw != null) { if (data != null) {
val postFeatured = gson.fromJson(postFeaturedRaw, SolarPost::class.java) 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(
text = postFeatured?.publisher?.nick ?: "Unknown", text = data.publisher.nick,
style = TextStyle(fontSize = 15.sp) style = TextStyle(fontSize = 15.sp)
) )
Spacer(modifier = GlanceModifier.width(8.dp)) Spacer(modifier = GlanceModifier.width(8.dp))
Text( Text(
text = "@${postFeatured?.publisher?.name}", text = "@${data.publisher.name}",
style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace) style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace)
) )
} }
Spacer(modifier = GlanceModifier.height(8.dp)) Spacer(modifier = GlanceModifier.height(8.dp))
if (postFeatured?.body?.title != null) { if (data.body.title != null) {
Text( Text(
text = postFeatured.body.title, text = data.body.title,
style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif) style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif)
) )
} }
if (postFeatured?.body?.description != null) { if (data.body.description != null) {
Text( Text(
text = postFeatured.body.description, text = data.body.description,
style = TextStyle(fontSize = 19.sp, fontFamily = FontFamily.Serif) 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)) Spacer(modifier = GlanceModifier.height(8.dp))
} }
Text( Text(
text = postFeatured?.body?.content ?: "No content", text = data.body.content ?: "No content",
style = TextStyle(fontSize = 15.sp), style = TextStyle(fontSize = 15.sp),
) )
Spacer(modifier = GlanceModifier.height(8.dp)) Spacer(modifier = GlanceModifier.height(8.dp))
if (postFeatured?.createdAt != null) { Text(
Text( LocalDateTime.ofInstant(data.createdAt, ZoneId.systemDefault())
LocalDateTime.ofInstant(postFeatured.createdAt, ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), style = TextStyle(fontSize = 13.sp),
style = TextStyle(fontSize = 13.sp), )
)
}
Text( Text(
"Solar Network Featured Post", "Solar Network Featured Post",
@ -126,11 +216,11 @@ class FeaturedPostWidget : GlanceAppWidget() {
horizontalAlignment = Alignment.Horizontal.CenterHorizontally horizontalAlignment = Alignment.Horizontal.CenterHorizontally
) { ) {
Text( Text(
text = "No featured posts", text = "Unable to fetch post",
style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold) style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold)
) )
Text( Text(
text = "Open the app to load recommendations", text = "Check your internet connection",
style = TextStyle(fontSize = 15.sp) style = TextStyle(fontSize = 15.sp)
) )
} }

View File

@ -1,6 +1,6 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout" android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="320dp" android:minWidth="280dp"
android:minHeight="40dp" android:minHeight="40dp"
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="10000"> android:updatePeriodMillis="10000">

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ^3.5.4 sdk: ^3.5.4