Compare commits

..

4 Commits

Author SHA1 Message Date
d6d60e60a9 🐛 Fix ios widget image 2024-12-21 13:21:44 +08:00
435b730f3b ♻️ Android use background info too 2024-12-21 13:03:07 +08:00
73468c5c6d iOS background widget fetching 2024-12-21 11:56:18 +08:00
8db6513eef Show random post instead of featured post 2024-12-21 04:12:52 +08:00
25 changed files with 718 additions and 475 deletions

View File

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

View File

@ -27,6 +27,11 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Widgets Indents -->
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
<!-- Sharing Intents -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -100,15 +105,15 @@
android:name="android.appwidget.provider"
android:resource="@xml/check_in_widget" />
</receiver>
<receiver android:name=".widgets.FeaturedPostWidgetReceiver"
android:label="Featured Post"
<receiver android:name=".widgets.RandomPostWidgetReceiver"
android:label="Random Post"
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" />
android:resource="@xml/random_post_widget" />
</receiver>
</application>

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,10 +1,12 @@
import android.content.Context
import android.net.Uri
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.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.background
@ -26,11 +28,11 @@ import com.google.gson.GsonBuilder
import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarCheckInRecord
import java.time.Instant
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class CheckInWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
@ -51,7 +53,7 @@ class CheckInWidget : GlanceAppWidget() {
val resultTierSymbols = listOf("大凶", "", "中平", "", "大吉")
val prefs = currentState.preferences
val checkInRaw = prefs.getString("today_check_in", null)
val checkInRaw = prefs.getString("pas_check_in_record", null)
Column(
modifier = GlanceModifier
@ -61,33 +63,43 @@ class CheckInWidget : GlanceAppWidget() {
.padding(16.dp)
) {
if (checkInRaw != null) {
val checkIn = gson.fromJson(checkInRaw, SolarCheckInRecord::class.java)
val checkIn: SolarCheckInRecord =
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)
)
val checkDate = checkIn.createdAt.atZone(ZoneId.of("UTC")).toLocalDate()
val currentDate = LocalDate.now()
if (checkDate.isEqual(currentDate)) {
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)
)
}
return@Column;
}
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)
)
}
}
Text(
text = "You haven't checked in today",
style = TextStyle(fontSize = 15.sp)
)
}
}

View File

@ -1,139 +0,0 @@
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.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.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
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 com.google.gson.TypeAdapterFactory
import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarPost
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class FeaturedPostWidget : 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 prefs = currentState.preferences
val postFeaturedRaw = prefs.getString("post_featured", null)
Column(
modifier = GlanceModifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.White)
.padding(16.dp)
) {
if (postFeaturedRaw != null) {
val postFeatured = gson.fromJson(postFeaturedRaw, SolarPost::class.java)
Row {
Text(
text = postFeatured?.publisher?.nick ?: "Unknown",
style = TextStyle(fontSize = 15.sp)
)
Spacer(modifier = GlanceModifier.width(8.dp))
Text(
text = "@${postFeatured?.publisher?.name}",
style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace)
)
}
Spacer(modifier = GlanceModifier.height(8.dp))
if (postFeatured?.body?.title != null) {
Text(
text = postFeatured.body.title,
style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif)
)
}
if (postFeatured?.body?.description != null) {
Text(
text = postFeatured.body.description,
style = TextStyle(fontSize = 19.sp, fontFamily = FontFamily.Serif)
)
}
if (postFeatured?.body?.title != null || postFeatured?.body?.description != null) {
Spacer(modifier = GlanceModifier.height(8.dp))
}
Text(
text = postFeatured?.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(
"Solar Network Featured Post",
style = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Bold),
)
return@Column;
}
Column(
modifier = GlanceModifier.fillMaxSize(),
verticalAlignment = Alignment.Vertical.CenterVertically,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
) {
Text(
text = "No featured posts",
style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold)
)
Text(
text = "Open the app to load recommendations",
style = TextStyle(fontSize = 15.sp)
)
}
}
}
}

View File

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

View File

@ -0,0 +1,202 @@
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
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.action.clickable
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
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
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.MainActivity
import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarPost
import es.antonborri.home_widget.actionStartActivity
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class RandomPostWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
private val defaultUrl = "https://api.sn.solsynth.dev"
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
GlanceContent(context, currentState(), null)
}
}
}
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
}
}
@Composable
private fun GlanceContent(
context: Context,
currentState: HomeWidgetGlanceState,
avatar: Bitmap?
) {
val prefs = currentState.preferences
val postRaw = prefs.getString("int_random_post", null)
val gson =
GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant::class.java, InstantAdapter())
.create()
val data: SolarPost? = postRaw?.let { postRaw ->
gson.fromJson(postRaw, SolarPost::class.java)
} ?: null;
Column(
modifier = GlanceModifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.White)
.padding(16.dp)
.clickable(
onClick = actionStartActivity<MainActivity>(
context,
Uri.parse("https://sn.solsynth.dev/posts/${data!!.id}")
)
)
) {
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))
}
Text(
text = data.publisher.nick,
style = TextStyle(fontSize = 15.sp)
)
Spacer(modifier = GlanceModifier.width(8.dp))
Text(
text = "@${data.publisher.name}",
style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace)
)
}
Spacer(modifier = GlanceModifier.height(8.dp))
if (data.body.title != null) {
Text(
text = data.body.title,
style = TextStyle(fontSize = 25.sp)
)
}
if (data.body.description != null) {
Text(
text = data.body.description,
style = TextStyle(fontSize = 19.sp)
)
}
if (data.body.title != null || data.body.description != null) {
Spacer(modifier = GlanceModifier.height(8.dp))
}
Text(
text = data.body.content ?: "No content",
style = TextStyle(fontSize = 15.sp),
)
Spacer(modifier = GlanceModifier.height(8.dp))
Text(
LocalDateTime.ofInstant(data.createdAt, ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
style = TextStyle(fontSize = 13.sp),
)
Text(
"#${data.id}",
style = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Bold),
)
return@Column;
}
Column(
modifier = GlanceModifier.fillMaxSize(),
verticalAlignment = Alignment.Vertical.CenterVertically,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
) {
Text(
text = "Unable to fetch post",
style = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold)
)
Text(
text = "Check your internet connection",
style = TextStyle(fontSize = 15.sp)
)
}
}
}
}

View File

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

View File

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

View File

@ -3,6 +3,15 @@ allprojects {
google()
mavenCentral()
}
configurations.all {
resolutionStrategy {
eachDependency {
if ((requested.group == "androidx.work") && (requested.name.startsWith("work-runtime"))) {
useVersion("2.9.1")
}
}
}
}
}
rootProject.buildDir = "../build"

View File

@ -36,6 +36,11 @@ target 'Runner' do
inherit! :search_paths
end
target 'SolarWidgetExtension' do
inherit! :search_paths
pod 'Kingfisher', '~> 8.0'
end
target 'SolarShare' do
inherit! :search_paths
end

View File

@ -56,7 +56,7 @@ PODS:
- Firebase/Analytics (= 11.4.0)
- firebase_core
- Flutter
- firebase_core (3.8.1):
- firebase_core (3.9.0):
- Firebase/CoreOnly (= 11.4.0)
- Flutter
- firebase_messaging (15.1.6):
@ -167,6 +167,7 @@ PODS:
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- Kingfisher (8.1.3)
- livekit_client (2.3.2):
- Flutter
- flutter_webrtc
@ -216,6 +217,8 @@ PODS:
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.06)
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
@ -233,6 +236,7 @@ DEPENDENCIES:
- gal (from `.symlinks/plugins/gal/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -249,6 +253,7 @@ DEPENDENCIES:
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
trunk:
@ -263,6 +268,7 @@ SPEC REPOS:
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- Kingfisher
- nanopb
- PromisesObjC
- SAMKeychain
@ -333,6 +339,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
workmanager:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
@ -344,7 +352,7 @@ SPEC CHECKSUMS:
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
@ -361,6 +369,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
@ -381,9 +390,10 @@ SPEC CHECKSUMS:
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: 23d35ad686cacf9103d1e85035ee4f3e9750630d
PODFILE CHECKSUM: f36978bb00ec01cd27f69faaf9a821024de98fcc
COCOAPODS: 1.16.2

View File

@ -3,12 +3,13 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
0B21A2B78F1AE403D3BE143E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26CC8DE2338798EAB472B62D /* Pods_RunnerTests.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2630F2992106E991467A6FC4 /* Pods_SolarWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F357CFDA89A0D9E5692846D4 /* Pods_SolarWidgetExtension.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */; };
@ -86,6 +87,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarShare.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.debug.xcconfig"; sourceTree = "<group>"; };
26CC8DE2338798EAB472B62D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.profile.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -96,6 +98,7 @@
4A2F84B6033057E3BD2C7CB8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.debug.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.debug.xcconfig"; sourceTree = "<group>"; };
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
72E9279EFA6DAC00BBAC493C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
73111C212CEE3D5E004CF4B3 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
@ -117,7 +120,9 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A2C24C5238FAC44EA2CCF738 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarShare.release.xcconfig"; path = "Target Support Files/Pods-SolarShare/Pods-SolarShare.release.xcconfig"; sourceTree = "<group>"; };
BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarWidgetExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolarWidgetExtension/Pods-SolarWidgetExtension.profile.xcconfig"; sourceTree = "<group>"; };
EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F357CFDA89A0D9E5692846D4 /* Pods_SolarWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -217,6 +222,7 @@
files = (
738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */,
738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */,
2630F2992106E991467A6FC4 /* Pods_SolarWidgetExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -262,6 +268,7 @@
731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */,
731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */,
16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */,
F357CFDA89A0D9E5692846D4 /* Pods_SolarWidgetExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -344,6 +351,9 @@
5922A50B1231B06B92E31F20 /* Pods-SolarShare.debug.xcconfig */,
B1763F1D7318A2745CA7EDFE /* Pods-SolarShare.release.xcconfig */,
2DA1B873D39B9FD33298BBCE /* Pods-SolarShare.profile.xcconfig */,
2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */,
6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */,
BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -374,6 +384,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */;
buildPhases = (
F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */,
738C1EA72D0D76A400A215F3 /* Sources */,
738C1EA82D0D76A400A215F3 /* Frameworks */,
738C1EA92D0D76A400A215F3 /* Resources */,
@ -710,6 +721,28 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-SolarWidgetExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
FC4815D44D909666EB1FA614 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -879,7 +912,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -947,6 +980,7 @@
};
738C1EBB2D0D76A500A215F3 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2134F3903A0E8EB8CC2670BE /* Pods-SolarWidgetExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -990,6 +1024,7 @@
};
738C1EBC2D0D76A500A215F3 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6618E2E3015264643175B43D /* Pods-SolarWidgetExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -1030,6 +1065,7 @@
};
738C1EBD2D0D76A500A215F3 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BCE0C4086B776A27B202B373 /* Pods-SolarWidgetExtension.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -1433,7 +1469,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1461,7 +1497,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -1,6 +1,8 @@
import Flutter
import UIKit
import workmanager
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
@ -9,6 +11,12 @@ import UIKit
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*5))
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>group.solsynth.solian</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -27,6 +29,17 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
@ -66,8 +79,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>AppGroupId</key>
<string>group.solsynth.solian</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@ -75,16 +86,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -29,10 +29,13 @@ struct CheckInProvider: TimelineProvider {
user = try! jsonDecoder.decode(SolarUser.self, from: userRaw.data(using: .utf8)!)
}
let checkInRaw = prefs?.string(forKey: "today_check_in")
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
var checkIn: SolarCheckInRecord?
if let checkInRaw = checkInRaw {
checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!)
if checkIn != nil && Calendar.current.isDate(checkIn!.createdAt, inSameDayAs: Date()) {
checkIn = nil
}
}
let entry = CheckInEntry(
@ -105,7 +108,7 @@ struct CheckInWidgetEntryView : View {
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
}
}
}.padding(8)
}.padding(8).widgetURL(URL(string: "https://sn.solsynth.dev"))
}
}

View File

@ -1,241 +0,0 @@
//
// FeaturedPostWidget.swift
// Runner
//
// Created by LittleSheep on 2024/12/14.
//
import SwiftUI
import WidgetKit
struct FeaturedPostProvider: TimelineProvider {
func placeholder(in context: Context) -> FeaturedPostEntry {
FeaturedPostEntry(date: Date(), user: nil, featuredPost: nil, family: .systemMedium)
}
func getSnapshot(in context: Context, completion: @escaping (FeaturedPostEntry) -> ()) {
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let userRaw = prefs?.string(forKey: "user")
var user: SolarUser?
if let userRaw = userRaw {
user = try! jsonDecoder.decode(SolarUser.self, from: userRaw.data(using: .utf8)!)
}
let featuredPostRaw = prefs?.string(forKey: "post_featured")
var featuredPosts: [SolarPost]?
if let featuredPostRaw = featuredPostRaw {
featuredPosts = try! jsonDecoder.decode([SolarPost].self, from: featuredPostRaw.data(using: .utf8)!)
}
let entry = FeaturedPostEntry(
date: Date(),
user: user,
featuredPost: featuredPosts?.first,
family: context.family
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
getSnapshot(in: context) { (entry) in
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
struct FeaturedPostEntry: TimelineEntry {
let date: Date
let user: SolarUser?
let featuredPost: SolarPost?
let family: WidgetFamily
}
struct FeaturedPostWidgetEntryView : View {
var entry: FeaturedPostProvider.Entry
private let resultTierSymbols: [String] = ["大凶", "", "中平", "大吉", ""]
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let featuredPost = entry.featuredPost {
HStack(alignment: .center) {
if let avatar = featuredPost.publisher.avatar {
let avatarUrl = getAttachmentUrl(for: avatar)
let size: CGFloat = 24
AsyncImage(url: URL(string: avatarUrl)) { image in
image.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.cornerRadius(size / 2)
.overlay(
Circle()
.stroke(Color.white, lineWidth: 4)
.frame(width: size, height: size)
)
.shadow(radius: 10)
.frame(width: 24, height: 24, alignment: .center)
} placeholder: {
ProgressView().frame(width: 24, height: 24, alignment: .center)
}
}
Text("@\(featuredPost.publisher.name)")
.font(.system(size: 13, design: .monospaced))
.opacity(0.9)
Spacer()
}.frame(maxWidth: .infinity).padding(.bottom, 12)
if featuredPost.body.title != nil || featuredPost.body.description != nil {
VStack(alignment: .leading) {
if let title = featuredPost.body.title {
Text(title)
.font(.system(size: 17))
}
if let description = featuredPost.body.description {
Text(description)
.font(.system(size: 15))
}
}.padding(.bottom, 8)
}
if let content = featuredPost.body.content {
if (featuredPost.body.title == nil && featuredPost.body.description == nil) || entry.family == .systemLarge || entry.family == .systemExtraLarge {
Text(
(entry.family == .systemLarge || entry.family == .systemExtraLarge) ? content : content.replacingOccurrences(of: "\n", with: " ")
)
.font(.system(size: 15))
} else {
Text("\(Image(systemName: "plus")) total \(content.count) characters")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 1)
}
}
if let attachment = featuredPost.body.attachments {
if attachment.count == 1 {
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachment")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 1)
} else if attachment.count > 1 {
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachments")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 1)
}
}
Spacer()
Text(featuredPost.publishedAt!, format: .dateTime)
.font(.system(size: 11))
Text("Solar Network Featured Posts")
.font(.system(size: 9))
} else {
VStack(alignment: .center) {
Text("No Recommendations").font(.system(size: 19, weight: .bold))
Text("Click the widget to open the app to load featured posts")
.font(.system(size: 15))
.multilineTextAlignment(.center)
}.frame(alignment: .center)
}
}.padding(8).frame(maxWidth: .infinity)
}
}
struct FeaturedPostWidget: Widget {
let kind: String = "SolarFeaturedPostWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: FeaturedPostProvider()) { entry in
if #available(iOS 17.0, *) {
FeaturedPostWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
FeaturedPostWidgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("Featured Posts")
.description("View the featured posts on the Solar Network")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemSmall) {
FeaturedPostWidget()
} timeline: {
FeaturedPostEntry(date: Date.now, user: nil, featuredPost: nil, family: .systemLarge)
FeaturedPostEntry(
date: .now,
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
featuredPost: SolarPost(
id: 1,
body: SolarPostBody(
content: "Hello, World",
title: nil,
description: nil,
attachments: ["zb2hiUEmYcnpHfVN"]
),
publisher: SolarPublisher(
id: 1,
name: "demo",
nick: "Deemo",
description: nil,
avatar: "IZxCFkJUPKRijFCx",
banner: nil,
createdAt: .now,
updatedAt: .now
),
publisherId: 1,
createdAt: .now,
updatedAt: .now,
editedAt: nil,
publishedAt: .now
),
family: .systemSmall
)
FeaturedPostEntry(
date: .now,
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
featuredPost: SolarPost(
id: 1,
body: SolarPostBody(
content: "Hello, World\nOh wow",
title: "Title",
description: "Description",
attachments: ["zb2hiUEmYcnpHfVN"]
),
publisher: SolarPublisher(
id: 1,
name: "demo",
nick: "Deemo",
description: nil,
avatar: "IZxCFkJUPKRijFCx",
banner: nil,
createdAt: .now,
updatedAt: .now
),
publisherId: 1,
createdAt: .now,
updatedAt: .now,
editedAt: nil,
publishedAt: .now
),
family: .systemLarge
)
}

View File

@ -0,0 +1,235 @@
//
// RandomPostWidget.swift
// Runner
//
// Created by LittleSheep on 2024/12/14.
//
import SwiftUI
import WidgetKit
import Kingfisher
struct RandomPostProvider: TimelineProvider {
func placeholder(in context: Context) -> RandomPostEntry {
RandomPostEntry(date: Date(), user: nil, randomPost: nil, family: .systemMedium)
}
func getSnapshot(in context: Context, completion: @escaping (RandomPostEntry) -> ()) {
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let userRaw = prefs?.string(forKey: "user")
var user: SolarUser?
if let userRaw = userRaw {
user = try! jsonDecoder.decode(SolarUser.self, from: userRaw.data(using: .utf8)!)
}
let randomPostRaw = prefs?.string(forKey: "int_random_post")
var randomPost: SolarPost?
if let randomPostRaw = randomPostRaw {
randomPost = try! jsonDecoder.decode(SolarPost.self, from: randomPostRaw.data(using: .utf8)!)
}
let entry = RandomPostEntry(
date: Date(),
user: user,
randomPost: randomPost,
family: context.family
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
getSnapshot(in: context) { (entry) in
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
struct RandomPostEntry: TimelineEntry {
let date: Date
let user: SolarUser?
let randomPost: SolarPost?
let family: WidgetFamily
}
struct RandomPostWidgetEntryView : View {
var entry: RandomPostProvider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let randomPost = entry.randomPost {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .center) {
if let avatar = randomPost.publisher.avatar {
let avatarUrl = getAttachmentUrl(for: avatar)
let size: CGFloat = 28
KFImage.url(URL(string: avatarUrl))
.resizable()
.setProcessor(ResizingImageProcessor(referenceSize: CGSize(width: size, height: size), mode: .aspectFit))
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.cornerRadius(size / 2)
.frame(width: size, height: size, alignment: .center)
}
Text("@\(randomPost.publisher.name)")
.font(.system(size: 13, design: .monospaced))
.opacity(0.9)
Spacer()
}.frame(maxWidth: .infinity).padding(.bottom, 12)
if randomPost.body.title != nil || randomPost.body.description != nil {
VStack(alignment: .leading) {
if let title = randomPost.body.title {
Text(title)
.font(.system(size: 17))
}
if let description = randomPost.body.description {
Text(description)
.font(.system(size: 15))
}
}.padding(.bottom, 8)
}
if let content = randomPost.body.content {
if (randomPost.body.title == nil && randomPost.body.description == nil) || entry.family == .systemLarge || entry.family == .systemExtraLarge {
Text(
(entry.family == .systemLarge || entry.family == .systemExtraLarge) ? content : content.replacingOccurrences(of: "\n", with: " ")
)
.font(.system(size: 15))
} else {
Text("\(Image(systemName: "plus")) total \(content.count) characters")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 1)
}
}
if let attachment = randomPost.body.attachments {
if attachment.count == 1 {
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachment")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 2)
} else if attachment.count > 1 {
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachments")
.font(.system(size: 11, design: .monospaced))
.opacity(0.75)
.padding(.top, 2)
}
}
Spacer()
Text(randomPost.publishedAt!, format: .dateTime)
.font(.system(size: 11))
Text("#\(randomPost.id)")
.font(.system(size: 9))
}.widgetURL(URL(string: "https://sn.solsynth.dev/posts/\(randomPost.id)"))
} else {
VStack(alignment: .center) {
Text("No Recommendations").font(.system(size: 19, weight: .bold))
Text("Open the app to load some random post")
.font(.system(size: 15))
.multilineTextAlignment(.center)
}.frame(alignment: .center)
}
}.padding(8).frame(maxWidth: .infinity)
}
}
struct RandomPostWidget: Widget {
let kind: String = "SolarRandomPostWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: RandomPostProvider()) { entry in
if #available(iOS 17.0, *) {
RandomPostWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
RandomPostWidgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("Random Post")
.description("View the random post on the Solar Network")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemSmall) {
RandomPostWidget()
} timeline: {
RandomPostEntry(date: Date.now, user: nil, randomPost: nil, family: .systemLarge)
RandomPostEntry(
date: .now,
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
randomPost: SolarPost(
id: 1,
body: SolarPostBody(
content: "Hello, World",
title: nil,
description: nil,
attachments: ["zb2hiUEmYcnpHfVN"]
),
publisher: SolarPublisher(
id: 1,
name: "demo",
nick: "Deemo",
description: nil,
avatar: "IZxCFkJUPKRijFCx",
banner: nil,
createdAt: .now,
updatedAt: .now
),
publisherId: 1,
createdAt: .now,
updatedAt: .now,
editedAt: nil,
publishedAt: .now
),
family: .systemSmall
)
RandomPostEntry(
date: .now,
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
randomPost: SolarPost(
id: 1,
body: SolarPostBody(
content: "Hello, World\nOh wow",
title: "Title",
description: "Description",
attachments: ["zb2hiUEmYcnpHfVN"]
),
publisher: SolarPublisher(
id: 1,
name: "demo",
nick: "Deemo",
description: nil,
avatar: "IZxCFkJUPKRijFCx",
banner: nil,
createdAt: .now,
updatedAt: .now
),
publisherId: 1,
createdAt: .now,
updatedAt: .now,
editedAt: nil,
publishedAt: .now
),
family: .systemLarge
)
}

View File

@ -12,6 +12,6 @@ import SwiftUI
struct SolarWidgetBundle: WidgetBundle {
var body: some Widget {
CheckInWidget()
FeaturedPostWidget()
RandomPostWidget()
}
}

View File

@ -37,6 +37,24 @@ import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/version_label.dart';
import 'package:workmanager/workmanager.dart';
@pragma('vm:entry-point')
void appBackgroundDispatcher() {
Workmanager().executeTask((task, inputData) async {
print("Native called background task: $task");
switch (task) {
case Workmanager.iOSBackgroundTask:
await Future.wait([widgetUpdateRandomPost()]);
return true;
case "WidgetUpdateRandomPost":
await widgetUpdateRandomPost();
return true;
default:
return true;
}
});
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -64,6 +82,20 @@ void main() async {
});
}
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize(
appBackgroundDispatcher,
isInDebugMode: kDebugMode,
);
Workmanager().registerPeriodicTask(
"widget-update-random-post",
"WidgetUpdateRandomPost",
frequency: Duration(minutes: 1),
constraints: Constraints(networkType: NetworkType.connected),
tag: "widget-update",
);
}
runApp(const SolianApp());
}
@ -193,10 +225,14 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
}
}
Future<void> _postInitialization() async {
await widgetUpdateRandomPost();
}
@override
void initState() {
super.initState();
_initialize();
_initialize().then((_) => _postInitialization());
}
@override

View File

@ -71,7 +71,36 @@ class SnNetworkProvider {
});
}
Future<void> initializeUserAgent() async {
static Future<Dio> createOffContextClient() async {
final prefs = await SharedPreferences.getInstance();
final client = Dio();
client.interceptors.add(RetryInterceptor(
dio: client,
retries: 3,
retryDelays: const [
Duration(milliseconds: 300),
Duration(milliseconds: 1000),
Duration(milliseconds: 3000),
],
));
final ua = await _getUserAgent();
client.interceptors.add(
InterceptorsWrapper(
onRequest: (
RequestOptions options,
RequestInterceptorHandler handler,
) async {
options.headers['User-Agent'] = ua;
return handler.next(options);
},
),
);
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
return client;
}
static Future<String> _getUserAgent() async {
final String platformInfo;
if (kIsWeb) {
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
@ -97,7 +126,11 @@ class SnNetworkProvider {
final packageInfo = await PackageInfo.fromPlatform();
_userAgent = 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)';
return 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)';
}
Future<void> initializeUserAgent() async {
_userAgent = await _getUserAgent();
}
final tkLock = Lock();

View File

@ -4,6 +4,8 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart';
class HomeWidgetProvider {
HomeWidgetProvider(BuildContext context);
@ -15,8 +17,7 @@ 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();
@ -25,7 +26,7 @@ class HomeWidgetProvider {
Future<void> updateWidget() async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
if (Platform.isIOS) {
const widgets = ["SolarFeaturedPostWidget", "SolarCheckInWidget"];
const widgets = ["SolarRandomPostWidget", "SolarCheckInWidget"];
for (final widget in widgets) {
await HomeWidget.updateWidget(
name: widget,
@ -33,7 +34,7 @@ class HomeWidgetProvider {
);
}
} else if (Platform.isAndroid) {
const widgets = ["FeaturedPostWidget", "CheckInWidget"];
const widgets = ["RandomPostWidget", "CheckInWidget"];
for (final widget in widgets) {
await HomeWidget.updateWidget(
androidName: "${widget}Receiver",
@ -43,3 +44,16 @@ class HomeWidgetProvider {
}
}
}
Future<void> widgetUpdateRandomPost() async {
final snc = await SnNetworkProvider.createOffContextClient();
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
final post = SnPost.fromJson(resp.data['data'][0]);
await HomeWidget.saveWidgetData("int_random_post", jsonEncode(post.toJson()));
await HomeWidget.updateWidget(
name: "SolarRandomPostWidget",
iOSName: "SolarRandomPostWidget",
androidName: "RandomPostWidgetReceiver",
qualifiedAndroidName: "dev.solsynth.solian.widgets.RandomPostWidgetReceiver",
);
}

View File

@ -151,7 +151,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
final home = context.read<HomeWidgetProvider>();
final resp = await sn.client.get('/cgi/id/check-in/today');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
home.saveWidgetData('today_check_in', _todayRecord!.toJson());
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
} finally {
setState(() => _isBusy = false);
}
@ -164,7 +164,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
final home = context.read<HomeWidgetProvider>();
final resp = await sn.client.post('/cgi/id/check-in');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
home.saveWidgetData('today_check_in', _todayRecord!.toJson());
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: eae3133cbb06de9205899b822e3897fc6a8bc278ad4c944b4ce612689369694b
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
url: "https://pub.dev"
source: hosted
version: "1.3.47"
version: "1.3.48"
_macros:
dependency: transitive
description: dart
@ -50,10 +50,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8"
sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
version: "4.0.2"
args:
dependency: transitive
description:
@ -562,26 +562,26 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: fef81a53ba1ca618def1f8bef4361df07968434e62cb204c1fb90bb880a03da2
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
url: "https://pub.dev"
source: hosted
version: "3.8.1"
version: "3.9.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: b94b217e3ad745e784960603d33d99471621ecca151c99c670869b76e50ad2a6
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
url: "https://pub.dev"
source: hosted
version: "5.3.1"
version: "5.4.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "9e69806bb3d905aeec3c1242e0e1475de6ea6d48f456af29d598fb229a2b4e5e"
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
url: "https://pub.dev"
source: hosted
version: "2.18.2"
version: "2.19.0"
firebase_messaging:
dependency: "direct main"
description:
@ -894,10 +894,10 @@ packages:
dependency: transitive
description:
name: image
sha256: b50b415345578583de0f1cf4c7bd389f164de0b316d890c707b41133047dbc2a
sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6"
url: "https://pub.dev"
source: hosted
version: "4.5.1"
version: "4.5.2"
image_picker:
dependency: "direct main"
description:
@ -1038,10 +1038,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.0"
version: "5.1.1"
livekit_client:
dependency: "direct main"
description:
@ -1454,10 +1454,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
qr:
dependency: transitive
description:
@ -2003,18 +2003,18 @@ packages:
dependency: "direct main"
description:
name: wakelock_plus
sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484
sha256: "1aeab49f24aec1e5ab417d7cdfc47c7bbcb815353f1840667ffe68c89a0cd2e6"
url: "https://pub.dev"
source: hosted
version: "1.2.8"
version: "1.2.9"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16"
sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
watcher:
dependency: transitive
description:
@ -2071,6 +2071,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.5"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
version: "0.5.2"
xdg_directories:
dependency: transitive
description:
@ -2091,10 +2099,10 @@ packages:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0"

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
# 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
@ -107,6 +107,7 @@ dependencies:
flutter_svg: ^2.0.16
home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1
workmanager: ^0.5.2
dev_dependencies:
flutter_test: