Android check in widget

This commit is contained in:
2024-12-15 18:23:12 +08:00
parent d67e33a41d
commit abc21f858b
13 changed files with 212 additions and 14 deletions

@ -13,6 +13,7 @@ dependencies {
implementation "androidx.glance:glance:1.1.1"
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'
}
android {
@ -29,13 +30,17 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "dev.solsynth.solian"
minSdk = flutter.minSdkVersion
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

@ -80,14 +80,23 @@
android:value="2" />
<!-- Widgets -->
<receiver android:name=".glance.HomeWidgetReceiver"
<receiver android:name=".widgets.CheckInWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/home_widget" />
android:resource="@xml/check_in_widget" />
</receiver>
<receiver android:name=".widgets.FeaturedPostWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/featured_post_widget" />
</receiver>
</application>

@ -0,0 +1,37 @@
package dev.solsynth.solian.data
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
import java.time.Instant
import java.time.format.DateTimeFormatter
class InstantAdapter : JsonSerializer<Instant?>,
JsonDeserializer<Instant?> {
override fun serialize(
src: Instant?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonPrimitive(formatter.format(src))
}
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext?
): Instant {
return Instant.parse(json.asString)
}
companion object {
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
}
}

@ -0,0 +1,16 @@
package dev.solsynth.solian.data
import java.time.Instant
data class SolarUser(
val id: Int,
val name: String,
val nick: String
)
data class SolarCheckInRecord(
val id: Int,
val resultTier: Int,
val resultExperience: Int,
val createdAt: Instant
)

@ -0,0 +1,98 @@
import android.content.Context
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.Button
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.FontFamily
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder
import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarCheckInRecord
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class CheckInWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceContent(context, currentState())
}
}
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
val gson =
GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant::class.java, InstantAdapter())
.create()
val resultTierSymbols = listOf("大凶", "", "中平", "大吉", "")
val prefs = currentState.preferences
val checkInRaw = prefs.getString("today_check_in", null)
Column(
modifier = GlanceModifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
) {
if (checkInRaw != null) {
val checkIn = gson.fromJson(checkInRaw, SolarCheckInRecord::class.java)
val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd")
Column {
Text(
text = resultTierSymbols[checkIn.resultTier],
style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif)
)
Text(
text = "+${checkIn.resultExperience} EXP",
style = TextStyle(fontSize = 15.sp, fontFamily = FontFamily.Monospace)
)
}
Spacer(modifier = GlanceModifier.height(8.dp))
Row(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = OffsetDateTime.ofInstant(checkIn.createdAt, ZoneId.systemDefault())
.format(dateFormatter),
style = TextStyle(fontSize = 13.sp)
)
}
} else {
Text(
text = "You haven't checked in today",
style = TextStyle(fontSize = 15.sp)
)
Spacer(modifier = GlanceModifier.height(8.dp))
Button(
text = "Check In",
onClick = {}
)
}
}
}
}

@ -0,0 +1,8 @@
package dev.solsynth.solian.widgets
import CheckInWidget
import HomeWidgetGlanceWidgetReceiver
class CheckInWidgetReceiver : HomeWidgetGlanceWidgetReceiver<CheckInWidget>() {
override val glanceAppWidget = CheckInWidget()
}

@ -13,7 +13,7 @@ import androidx.glance.layout.padding
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.Text
class AppWidget : GlanceAppWidget() {
class FeaturedPostWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
@ -26,11 +26,13 @@ class AppWidget : GlanceAppWidget() {
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
val prefs = currentState.preferences
val counter = prefs.getInt("counter", 0)
val checkIn = prefs.getString("post_featured", null)
Box(modifier = GlanceModifier.background(Color.White).padding(16.dp)) {
Text(
counter.toString()
)
checkIn?.let {
Text(it)
} ?: run {
Text("No featured posts")
}
}
}
}

@ -0,0 +1,8 @@
package dev.solsynth.solian.widgets
import FeaturedPostWidget
import HomeWidgetGlanceWidgetReceiver
class FeaturedPostWidgetReceiver : HomeWidgetGlanceWidgetReceiver<FeaturedPostWidget>() {
override val glanceAppWidget = FeaturedPostWidget()
}

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

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
kotlin.suppressKotlinVersionCompatibilityCheck=true

@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.2' apply false
id "com.android.application" version '8.7.3' apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false

@ -15,7 +15,8 @@ class HomeWidgetProvider {
}
}
Future<void> saveWidgetData(String id, dynamic data, {bool update = true}) async {
Future<void> saveWidgetData(String id, dynamic data,
{bool update = true}) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
await HomeWidget.saveWidgetData(id, jsonEncode(data));
if (update) await updateWidget();
@ -29,8 +30,14 @@ class HomeWidgetProvider {
await HomeWidget.updateWidget(
name: widget,
iOSName: widget,
androidName: "com.solsynth.solian.$widget",
qualifiedAndroidName: "group.solsynth.solian.$widget",
);
}
} else if (Platform.isAndroid) {
const widgets = ["FeaturedPostWidget", "CheckInWidget"];
for (final widget in widgets) {
await HomeWidget.updateWidget(
androidName: "${widget}Receiver",
qualifiedAndroidName: "dev.solsynth.solian.widgets.${widget}Receiver",
);
}
}