Android check in widget

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

View File

@ -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 {
@ -25,8 +26,12 @@ android {
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
kotlinOptions {
@ -35,7 +40,7 @@ android {
defaultConfig {
applicationId = "dev.solsynth.solian"
minSdk = flutter.minSdkVersion
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@ -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>

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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 = {}
)
}
}
}
}

View File

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

View File

@ -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")
}
}
}
}

View File

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

View File

@ -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>

View File

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

View File

@ -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

View File

@ -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",
);
}
}