From abc21f858bc4ae448a7446b4549f3280f36d541f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 15 Dec 2024 18:23:12 +0800 Subject: [PATCH] :sparkles: Android check in widget --- android/app/build.gradle | 11 ++- android/app/src/main/AndroidManifest.xml | 13 ++- .../dev/solsynth/solian/data/SolarTime.kt | 37 +++++++ .../dev/solsynth/solian/data/SolarUser.kt | 16 +++ .../solsynth/solian/widgets/CheckInWidget.kt | 98 +++++++++++++++++++ .../solian/widgets/CheckInWidgetReceiver.kt | 8 ++ .../{AppWidget.kt => FeaturedPostWidget.kt} | 12 ++- .../widgets/FeaturedPostWidgetReceiver.kt | 8 ++ .../{home_widget.xml => check_in_widget.xml} | 0 .../src/main/res/xml/featured_post_widget.xml | 7 ++ android/gradle.properties | 1 + android/settings.gradle | 2 +- lib/providers/widget.dart | 13 ++- 13 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 android/app/src/main/kotlin/dev/solsynth/solian/data/SolarTime.kt create mode 100644 android/app/src/main/kotlin/dev/solsynth/solian/data/SolarUser.kt create mode 100644 android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidget.kt create mode 100644 android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidgetReceiver.kt rename android/app/src/main/kotlin/dev/solsynth/solian/widgets/{AppWidget.kt => FeaturedPostWidget.kt} (81%) create mode 100644 android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidgetReceiver.kt rename android/app/src/main/res/xml/{home_widget.xml => check_in_widget.xml} (100%) create mode 100644 android/app/src/main/res/xml/featured_post_widget.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index e3b3bf3..0dee8d9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c1a347f..8373f81 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -80,14 +80,23 @@ android:value="2" /> - + android:resource="@xml/check_in_widget" /> + + + + + + diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarTime.kt b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarTime.kt new file mode 100644 index 0000000..121eb68 --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarTime.kt @@ -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, + JsonDeserializer { + 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 + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarUser.kt b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarUser.kt new file mode 100644 index 0000000..64f21fa --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/data/SolarUser.kt @@ -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 +) \ No newline at end of file diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidget.kt b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidget.kt new file mode 100644 index 0000000..cd4b2dd --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidget.kt @@ -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 = {} + ) + } + } + } +} diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidgetReceiver.kt b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidgetReceiver.kt new file mode 100644 index 0000000..c617065 --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/CheckInWidgetReceiver.kt @@ -0,0 +1,8 @@ +package dev.solsynth.solian.widgets + +import CheckInWidget +import HomeWidgetGlanceWidgetReceiver + +class CheckInWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = CheckInWidget() +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/AppWidget.kt b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt similarity index 81% rename from android/app/src/main/kotlin/dev/solsynth/solian/widgets/AppWidget.kt rename to android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt index 8ebf8ab..b6f535f 100644 --- a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/AppWidget.kt +++ b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidget.kt @@ -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") + } } } } diff --git a/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidgetReceiver.kt b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidgetReceiver.kt new file mode 100644 index 0000000..03dab9d --- /dev/null +++ b/android/app/src/main/kotlin/dev/solsynth/solian/widgets/FeaturedPostWidgetReceiver.kt @@ -0,0 +1,8 @@ +package dev.solsynth.solian.widgets + +import FeaturedPostWidget +import HomeWidgetGlanceWidgetReceiver + +class FeaturedPostWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = FeaturedPostWidget() +} \ No newline at end of file diff --git a/android/app/src/main/res/xml/home_widget.xml b/android/app/src/main/res/xml/check_in_widget.xml similarity index 100% rename from android/app/src/main/res/xml/home_widget.xml rename to android/app/src/main/res/xml/check_in_widget.xml diff --git a/android/app/src/main/res/xml/featured_post_widget.xml b/android/app/src/main/res/xml/featured_post_widget.xml new file mode 100644 index 0000000..0b33ead --- /dev/null +++ b/android/app/src/main/res/xml/featured_post_widget.xml @@ -0,0 +1,7 @@ + + diff --git a/android/gradle.properties b/android/gradle.properties index 2597170..768b3b2 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +kotlin.suppressKotlinVersionCompatibilityCheck=true diff --git a/android/settings.gradle b/android/settings.gradle index 7e7f058..4c34d90 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -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 diff --git a/lib/providers/widget.dart b/lib/providers/widget.dart index 77324e0..96cc551 100644 --- a/lib/providers/widget.dart +++ b/lib/providers/widget.dart @@ -15,7 +15,8 @@ class HomeWidgetProvider { } } - Future saveWidgetData(String id, dynamic data, {bool update = true}) async { + Future 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", ); } }