Compare commits
	
		
			29 Commits
		
	
	
		
			4daff41b3e
			...
			2.1.1+36
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6427ec1f82 | |||
| 35dc7f4392 | |||
| b50191970e | |||
| 1b69e6dd42 | |||
| 39fb4d474f | |||
| 392aebcad7 | |||
| e9e3a4c474 | |||
| 7182336a0d | |||
| be98fe133d | |||
| e458943f56 | |||
| eb125fc436 | |||
| dc78f39969 | |||
| f5c06bc89c | |||
| d6d60e60a9 | |||
| 435b730f3b | |||
| 73468c5c6d | |||
| 8db6513eef | |||
| 65a8f1e6c3 | |||
| 2671ffad4b | |||
| 8a628823e0 | |||
| 94d19a1524 | |||
| d98f6c8d18 | |||
| 6d0f62016a | |||
| 7e0faba5db | |||
| 7508a54907 | |||
| 2eb1f4b52b | |||
| 00678c0ac8 | |||
| abc21f858b | |||
| d67e33a41d | 
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,16 +0,0 @@ | ||||
| # surface | ||||
|  | ||||
| A new Flutter project. | ||||
|  | ||||
| ## Getting Started | ||||
|  | ||||
| This project is a starting point for a Flutter application. | ||||
|  | ||||
| A few resources to get you started if this is your first Flutter project: | ||||
|  | ||||
| - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) | ||||
| - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) | ||||
|  | ||||
| For help getting started with Flutter development, view the | ||||
| [online documentation](https://docs.flutter.dev/), which offers tutorials, | ||||
| samples, guidance on mobile development, and a full API reference. | ||||
| @@ -9,14 +9,32 @@ plugins { | ||||
|     id "dev.flutter.flutter-gradle-plugin" | ||||
| } | ||||
|  | ||||
| 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' | ||||
|     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' | ||||
| } | ||||
|  | ||||
| android { | ||||
|     buildFeatures { | ||||
|         compose true | ||||
|     } | ||||
|  | ||||
|     namespace = "dev.solsynth.solian" | ||||
|     compileSdk = flutter.compileSdkVersion | ||||
|     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 { | ||||
| @@ -24,21 +42,25 @@ android { | ||||
|     } | ||||
|  | ||||
|     defaultConfig { | ||||
|         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | ||||
|         applicationId = "dev.solsynth.solian" | ||||
|         // You can update the following values to match your application needs. | ||||
|         // For more information, see: https://flutter.dev/to/review-gradle-config. | ||||
|         minSdk = flutter.minSdkVersion | ||||
|         minSdk = 26 | ||||
|         targetSdk = flutter.targetSdkVersion | ||||
|         versionCode = flutter.versionCode | ||||
|         versionName = flutter.versionName | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             debuggable true | ||||
|  | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|         release { | ||||
|             // TODO: Add your own signing config for the release build. | ||||
|             // Signing with the debug keys for now, so `flutter run --release` works. | ||||
|             signingConfig = signingConfigs.debug | ||||
|  | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -27,23 +27,12 @@ | ||||
|             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" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|                 <data | ||||
|                     android:scheme="https" | ||||
|                     android:host="sn.solsynth.dev" | ||||
|                     android:pathPrefix="/invite"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data | ||||
|                     android:mimeType="*/*" | ||||
|                     android:scheme="content" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
| @@ -52,12 +41,22 @@ | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <data android:mimeType="image/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <data android:mimeType="image/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="video/*" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="video/*" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Specifies an Android theme to apply to this Activity as soon as | ||||
| @@ -78,7 +77,30 @@ | ||||
|         <meta-data | ||||
|             android:name="flutterEmbedding" | ||||
|             android:value="2" /> | ||||
|  | ||||
|         <!-- Widgets --> | ||||
|         <receiver android:name=".widgets.CheckInWidgetReceiver" | ||||
|             android:label="Check In" | ||||
|             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/check_in_widget" /> | ||||
|         </receiver> | ||||
|         <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/random_post_widget" /> | ||||
|         </receiver> | ||||
|     </application> | ||||
|  | ||||
|     <!-- Required to query activities that can process text, see: | ||||
|          https://developer.android.com/training/package-visibility and | ||||
|          https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. | ||||
|   | ||||
| @@ -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>) | ||||
| @@ -0,0 +1,35 @@ | ||||
| package dev.solsynth.solian.data | ||||
|  | ||||
| import androidx.annotation.Keep | ||||
| import java.time.Instant | ||||
|  | ||||
| @Keep | ||||
| data class SolarPost( | ||||
|     val id: Int, | ||||
|     val body: SolarPostBody, | ||||
|     val publisher: SolarPublisher, | ||||
|     val publisherId: Int, | ||||
|     val createdAt: Instant, | ||||
|     val updatedAt: Instant, | ||||
|     val editedAt: Instant?, | ||||
|     val publishedAt: Instant? | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| data class SolarPostBody( | ||||
|     val content: String?, | ||||
|     val title: String?, | ||||
|     val description: String?, | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| data class SolarPublisher( | ||||
|     val id: Int, | ||||
|     val name: String, | ||||
|     val nick: String, | ||||
|     val description: String?, | ||||
|     val avatar: String?, | ||||
|     val banner: String?, | ||||
|     val createdAt: Instant, | ||||
|     val updatedAt: Instant | ||||
| ) | ||||
| @@ -0,0 +1,38 @@ | ||||
| package dev.solsynth.solian.data | ||||
|  | ||||
| import androidx.annotation.Keep | ||||
| 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 | ||||
|  | ||||
| @Keep | ||||
| 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,19 @@ | ||||
| package dev.solsynth.solian.data | ||||
|  | ||||
| import androidx.annotation.Keep | ||||
| import java.time.Instant | ||||
|  | ||||
| @Keep | ||||
| data class SolarUser( | ||||
|     val id: Int, | ||||
|     val name: String, | ||||
|     val nick: String | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| data class SolarCheckInRecord( | ||||
|     val id: Int, | ||||
|     val resultTier: Int, | ||||
|     val resultExperience: Int, | ||||
|     val createdAt: Instant | ||||
| ) | ||||
| @@ -0,0 +1,128 @@ | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import androidx.compose.runtime.Composable | ||||
| 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.action.clickable | ||||
| 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.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.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.SolarCheckInRecord | ||||
| import es.antonborri.home_widget.actionStartActivity | ||||
| 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() | ||||
|  | ||||
|     override suspend fun provideGlance(context: Context, id: GlanceId) { | ||||
|         provideContent { | ||||
|             GlanceTheme { | ||||
|                 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: String? = prefs.getString("pas_check_in_record", null) | ||||
|  | ||||
|         val checkIn: SolarCheckInRecord? = | ||||
|             checkInRaw?.let { checkInRaw -> | ||||
|                 gson.fromJson(checkInRaw, SolarCheckInRecord::class.java) | ||||
|             } ?: null; | ||||
|  | ||||
|         Column( | ||||
|             modifier = GlanceModifier | ||||
|                 .fillMaxWidth() | ||||
|                 .fillMaxHeight() | ||||
|                 .background(GlanceTheme.colors.widgetBackground) | ||||
|                 .padding(16.dp) | ||||
|                 .clickable( | ||||
|                     onClick = actionStartActivity<MainActivity>( | ||||
|                         context, | ||||
|                         Uri.parse("https://sn.solsynth.dev") | ||||
|                     ) | ||||
|                 ) | ||||
|         ) { | ||||
|             if (checkIn != null) { | ||||
|                 val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd") | ||||
|  | ||||
|                 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 = 17.sp, | ||||
|                                 color = GlanceTheme.colors.onSurface | ||||
|                             ) | ||||
|                         ) | ||||
|                         Text( | ||||
|                             text = "+${checkIn.resultExperience} EXP", | ||||
|                             style = TextStyle( | ||||
|                                 fontSize = 13.sp, | ||||
|                                 fontFamily = FontFamily.Monospace, | ||||
|                                 color = GlanceTheme.colors.onSurface | ||||
|                             ) | ||||
|                         ) | ||||
|                     } | ||||
|                     Spacer(modifier = GlanceModifier.height(8.dp)) | ||||
|                     Row(horizontalAlignment = Alignment.CenterHorizontally) { | ||||
|                         Text( | ||||
|                             text = OffsetDateTime.ofInstant( | ||||
|                                 checkIn.createdAt, | ||||
|                                 ZoneId.systemDefault() | ||||
|                             ) | ||||
|                                 .format(dateFormatter), | ||||
|                             style = TextStyle( | ||||
|                                 fontSize = 11.sp, | ||||
|                                 color = GlanceTheme.colors.onSurface | ||||
|                             ) | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                     return@Column; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Text( | ||||
|                 text = "You haven't checked in today", | ||||
|                 style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| package dev.solsynth.solian.widgets | ||||
|  | ||||
| import CheckInWidget | ||||
| import HomeWidgetGlanceWidgetReceiver | ||||
|  | ||||
| class CheckInWidgetReceiver : HomeWidgetGlanceWidgetReceiver<CheckInWidget>() { | ||||
|     override val glanceAppWidget = CheckInWidget() | ||||
| } | ||||
| @@ -0,0 +1,168 @@ | ||||
| import HomeWidgetGlanceState | ||||
| import HomeWidgetGlanceStateDefinition | ||||
| 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.GlanceTheme | ||||
| import androidx.glance.action.clickable | ||||
| 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 dev.solsynth.solian.MainActivity | ||||
| import dev.solsynth.solian.data.InstantAdapter | ||||
| import dev.solsynth.solian.data.SolarPost | ||||
| import es.antonborri.home_widget.actionStartActivity | ||||
| 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() | ||||
|  | ||||
|     override suspend fun provideGlance(context: Context, id: GlanceId) { | ||||
|         provideContent { | ||||
|             GlanceTheme { | ||||
|                 GlanceContent(context, currentState()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun GlanceContent( | ||||
|         context: Context, | ||||
|         currentState: HomeWidgetGlanceState, | ||||
|     ) { | ||||
|         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(GlanceTheme.colors.widgetBackground) | ||||
|                 .padding(16.dp) | ||||
|                 .clickable( | ||||
|                     onClick = actionStartActivity<MainActivity>( | ||||
|                         context, | ||||
|                         Uri.parse("https://sn.solsynth.dev/posts/${data!!.id}") | ||||
|                     ) | ||||
|                 ) | ||||
|         ) { | ||||
|             if (data != null) { | ||||
|                 Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                     Text( | ||||
|                         text = data.publisher.nick, | ||||
|                         style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface) | ||||
|                     ) | ||||
|                     Spacer(modifier = GlanceModifier.width(8.dp)) | ||||
|                     Text( | ||||
|                         text = "@${data.publisher.name}", | ||||
|                         style = TextStyle( | ||||
|                             fontSize = 13.sp, | ||||
|                             fontFamily = FontFamily.Monospace, | ||||
|                             color = GlanceTheme.colors.onSurface | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 Spacer(modifier = GlanceModifier.height(8.dp)) | ||||
|  | ||||
|                 if (data.body.title != null) { | ||||
|                     Text( | ||||
|                         text = data.body.title, | ||||
|                         style = TextStyle(fontSize = 19.sp, color = GlanceTheme.colors.onSurface) | ||||
|                     ) | ||||
|                 } | ||||
|                 if (data.body.description != null) { | ||||
|                     Text( | ||||
|                         text = data.body.description, | ||||
|                         style = TextStyle(fontSize = 17.sp, color = GlanceTheme.colors.onSurface) | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 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, color = GlanceTheme.colors.onSurface), | ||||
|                 ) | ||||
|  | ||||
|                 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, color = GlanceTheme.colors.onSurface), | ||||
|                 ) | ||||
|  | ||||
|                 Text( | ||||
|                     "#${data.id}", | ||||
|                     style = TextStyle( | ||||
|                         fontSize = 11.sp, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         color = GlanceTheme.colors.onSurface | ||||
|                     ), | ||||
|                 ) | ||||
|  | ||||
|                 return@Column; | ||||
|             } | ||||
|  | ||||
|             Column( | ||||
|                 modifier = GlanceModifier.fillMaxSize(), | ||||
|                 verticalAlignment = Alignment.Vertical.CenterVertically, | ||||
|                 horizontalAlignment = Alignment.Horizontal.CenterHorizontally | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = "No Recommendations", | ||||
|                     style = TextStyle( | ||||
|                         fontSize = 17.sp, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         color = GlanceTheme.colors.onSurface | ||||
|                     ) | ||||
|                 ) | ||||
|                 Text( | ||||
|                     text = "Open app to load some posts", | ||||
|                     style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| package dev.solsynth.solian.widgets | ||||
|  | ||||
| import RandomPostWidget | ||||
| import HomeWidgetGlanceWidgetReceiver | ||||
|  | ||||
| class RandomPostWidgetReceiver : HomeWidgetGlanceWidgetReceiver<RandomPostWidget>() { | ||||
|     override val glanceAppWidget = RandomPostWidget() | ||||
| } | ||||
							
								
								
									
										7
									
								
								android/app/src/main/res/xml/check_in_widget.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								android/app/src/main/res/xml/check_in_widget.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										7
									
								
								android/app/src/main/res/xml/random_post_widget.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								android/app/src/main/res/xml/random_post_widget.xml
									
									
									
									
									
										Normal 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="240dp" | ||||
|     android:minHeight="40dp" | ||||
|     android:resizeMode="horizontal|vertical" | ||||
|     android:updatePeriodMillis="10000"> | ||||
| </appwidget-provider> | ||||
							
								
								
									
										14
									
								
								android/app/src/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								android/app/src/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| -keepclassmembers class kotlin.Metadata { *; } | ||||
| -keep class dev.solsynth.solian.** { *; } | ||||
| -keep public class dev.solsynth.solian.data.** { public *; } | ||||
| -keepclassmembers class dev.solsynth.solian.data.** { *; } | ||||
|  | ||||
| -keepattributes *Annotation* | ||||
| -keepattributes Signature | ||||
| -keepattributes EnclosingMethod | ||||
|  | ||||
| -keep class com.google.gson.** { *; } | ||||
|  | ||||
| -keepclassmembers class * { | ||||
|     @com.google.gson.annotations.SerializedName <fields>; | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -190,6 +190,13 @@ | ||||
|   "settingsNetworkServerPreset": "Present HyperNet Server", | ||||
|   "settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.", | ||||
|   "settingsNetworkServerSaved": "Server address saved.", | ||||
|   "settingsPerformance": "Performance", | ||||
|   "settingsImageQuality": "Image Quality", | ||||
|   "settingsImageQualityDescription": "Set the image quality, it will affect the decoding speed of the image.", | ||||
|   "settingsImageQualityLowest": "Lowest", | ||||
|   "settingsImageQualityLow": "Low", | ||||
|   "settingsImageQualityMedium": "Medium", | ||||
|   "settingsImageQualityHigh": "High", | ||||
|   "settingsMisc": "Misc", | ||||
|   "settingsMiscAbout": "About", | ||||
|   "settingsMiscAboutDescription": "View the version information of Solian.", | ||||
| @@ -363,6 +370,8 @@ | ||||
|   "dailyCheckNegativeHint6": "Going out", | ||||
|   "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", | ||||
|   "happyBirthday": "Happy birthday, {}!", | ||||
|   "celebrateMerryXmas": "Merry christmas, {}!", | ||||
|   "celebrateNewYear": "Happy new year, {}!", | ||||
|   "friendNew": "Add Friend", | ||||
|   "friendRequests": "Friend Requests", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -448,5 +457,7 @@ | ||||
|   "poweredBy": "Powered by {}", | ||||
|   "shareIntent": "Share", | ||||
|   "shareIntentDescription":  "What do you want to do with the content you are sharing?", | ||||
|   "shareIntentPostStory": "Post a Story" | ||||
|   "shareIntentPostStory": "Post a Story", | ||||
|   "updateAvailable": "Update Available", | ||||
|   "updateOngoing": "正在更新,请稍后..." | ||||
| } | ||||
|   | ||||
| @@ -188,6 +188,13 @@ | ||||
|   "settingsNetworkServerPreset": "预设的 HyperNet 服务器", | ||||
|   "settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。", | ||||
|   "settingsNetworkServerSaved": "服务器地址已保存。", | ||||
|   "settingsPerformance": "性能", | ||||
|   "settingsImageQuality": "图片预览质量", | ||||
|   "settingsImageQualityDescription": "设置图片预览质量,会影响图片解码速度。", | ||||
|   "settingsImageQualityLowest": "极低", | ||||
|   "settingsImageQualityLow": "低", | ||||
|   "settingsImageQualityMedium": "中", | ||||
|   "settingsImageQualityHigh": "高", | ||||
|   "settingsMisc": "杂项", | ||||
|   "settingsMiscAbout": "关于", | ||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||
| @@ -361,6 +368,8 @@ | ||||
|   "dailyCheckNegativeHint6": "出门", | ||||
|   "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", | ||||
|   "happyBirthday": "生日快乐,{}!", | ||||
|   "celebrateMerryXmas": "圣诞快乐,{}!", | ||||
|   "celebrateNewYear": "新年快乐,{}!", | ||||
|   "friendNew": "添加好友", | ||||
|   "friendRequests": "好友请求", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -446,5 +455,7 @@ | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想对您分享的内容做些什么?", | ||||
|   "shareIntentPostStory": "发布动态" | ||||
|   "shareIntentPostStory": "发布动态", | ||||
|   "updateAvailable": "检测到更新可用", | ||||
|   "updateOngoing": "正在更新,请稍后……" | ||||
| } | ||||
|   | ||||
| @@ -188,6 +188,13 @@ | ||||
|   "settingsNetworkServerPreset": "預設的 HyperNet 服務器", | ||||
|   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。", | ||||
|   "settingsNetworkServerSaved": "服務器地址已保存。", | ||||
|   "settingsPerformance": "性能", | ||||
|   "settingsImageQuality": "圖片預覽質量", | ||||
|   "settingsImageQualityDescription": "設置圖片預覽質量,會影響圖片解碼速度。", | ||||
|   "settingsImageQualityLowest": "極低", | ||||
|   "settingsImageQualityLow": "低", | ||||
|   "settingsImageQualityMedium": "中", | ||||
|   "settingsImageQualityHigh": "高", | ||||
|   "settingsMisc": "雜項", | ||||
|   "settingsMiscAbout": "關於", | ||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||
| @@ -441,5 +448,10 @@ | ||||
|   "postImageShareReadMore": "掃描右側 QRCode 查看全文", | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖" | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||
|   "shareIntentPostStory": "發佈動態" | ||||
| } | ||||
|   | ||||
| @@ -188,6 +188,13 @@ | ||||
|   "settingsNetworkServerPreset": "預設的 HyperNet 伺服器", | ||||
|   "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。", | ||||
|   "settingsNetworkServerSaved": "伺服器地址已儲存。", | ||||
|   "settingsPerformance": "效能", | ||||
|   "settingsImageQuality": "圖片預覽質量", | ||||
|   "settingsImageQualityDescription": "設定圖片預覽質量,會影響圖片解碼速度。", | ||||
|   "settingsImageQualityLowest": "極低", | ||||
|   "settingsImageQualityLow": "低", | ||||
|   "settingsImageQualityMedium": "中", | ||||
|   "settingsImageQualityHigh": "高", | ||||
|   "settingsMisc": "雜項", | ||||
|   "settingsMiscAbout": "關於", | ||||
|   "settingsMiscAboutDescription": "檢視 Solian 的版本資訊。", | ||||
| @@ -441,5 +448,10 @@ | ||||
|   "postImageShareReadMore": "掃描右側 QRCode 檢視全文", | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖" | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支援", | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||
|   "shareIntentPostStory": "釋出動態" | ||||
| } | ||||
|   | ||||
							
								
								
									
										18
									
								
								ios/Podfile
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								ios/Podfile
									
									
									
									
									
								
							| @@ -36,6 +36,24 @@ target 'Runner' do | ||||
|     inherit! :search_paths | ||||
|   end | ||||
|  | ||||
|   target 'SolarNotifyService' do | ||||
|     inherit! :search_paths | ||||
|     pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios' | ||||
|  | ||||
|     pod 'Kingfisher', '~> 8.0' | ||||
|     pod 'Alamofire' | ||||
|   end | ||||
|  | ||||
|   target 'SolarWidgetExtension' do | ||||
|     inherit! :search_paths | ||||
|     use_frameworks! | ||||
|     use_modular_headers! | ||||
|  | ||||
|     pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios' | ||||
|  | ||||
|     pod 'Kingfisher', '~> 8.0' | ||||
|   end | ||||
|  | ||||
|   target 'SolarShare' do | ||||
|     inherit! :search_paths | ||||
|   end | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| PODS: | ||||
|   - Alamofire (5.10.2) | ||||
|   - connectivity_plus (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -56,7 +57,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): | ||||
| @@ -102,6 +103,8 @@ PODS: | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_app_update (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_native_splash (2.4.3): | ||||
|     - Flutter | ||||
|   - flutter_udid (0.0.1): | ||||
| @@ -167,6 +170,9 @@ PODS: | ||||
|     - Flutter | ||||
|   - image_picker_ios (0.0.1): | ||||
|     - Flutter | ||||
|   - in_app_review (2.0.0): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.1.3) | ||||
|   - livekit_client (2.3.2): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
| @@ -216,8 +222,11 @@ PODS: | ||||
|   - wakelock_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - WebRTC-SDK (125.6422.06) | ||||
|   - workmanager (0.0.1): | ||||
|     - Flutter | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - Alamofire | ||||
|   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) | ||||
|   - croppy (from `.symlinks/plugins/croppy/ios`) | ||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||
| @@ -227,12 +236,15 @@ DEPENDENCIES: | ||||
|   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) | ||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||
|   - gal (from `.symlinks/plugins/gal/darwin`) | ||||
|   - home_widget (from `.symlinks/plugins/home_widget/ios`) | ||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||
|   - in_app_review (from `.symlinks/plugins/in_app_review/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,9 +261,11 @@ 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: | ||||
|     - Alamofire | ||||
|     - DKImagePickerController | ||||
|     - DKPhotoGallery | ||||
|     - Firebase | ||||
| @@ -263,6 +277,7 @@ SPEC REPOS: | ||||
|     - GoogleAppMeasurement | ||||
|     - GoogleDataTransport | ||||
|     - GoogleUtilities | ||||
|     - Kingfisher | ||||
|     - nanopb | ||||
|     - PromisesObjC | ||||
|     - SAMKeychain | ||||
| @@ -289,6 +304,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/firebase_messaging/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_app_update: | ||||
|     :path: ".symlinks/plugins/flutter_app_update/ios" | ||||
|   flutter_native_splash: | ||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||
|   flutter_udid: | ||||
| @@ -301,6 +318,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/home_widget/ios" | ||||
|   image_picker_ios: | ||||
|     :path: ".symlinks/plugins/image_picker_ios/ios" | ||||
|   in_app_review: | ||||
|     :path: ".symlinks/plugins/in_app_review/ios" | ||||
|   livekit_client: | ||||
|     :path: ".symlinks/plugins/livekit_client/ios" | ||||
|   media_kit_libs_ios_video: | ||||
| @@ -333,8 +352,11 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/volume_controller/ios" | ||||
|   wakelock_plus: | ||||
|     :path: ".symlinks/plugins/wakelock_plus/ios" | ||||
|   workmanager: | ||||
|     :path: ".symlinks/plugins/workmanager/ios" | ||||
|  | ||||
| SPEC CHECKSUMS: | ||||
|   Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 | ||||
|   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 | ||||
|   croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 | ||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||
| @@ -344,7 +366,7 @@ SPEC CHECKSUMS: | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||
|   firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 | ||||
|   firebase_core: 418aed674e9a0b8b6088aec16cde82a811f6261f | ||||
|   firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10 | ||||
|   firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 | ||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||
| @@ -352,15 +374,18 @@ SPEC CHECKSUMS: | ||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc | ||||
|   flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a | ||||
|   flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 | ||||
|   flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab | ||||
|   flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 | ||||
|   gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||
|   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||
|   livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd | ||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||
| @@ -381,9 +406,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: 9b244e02f87527430136c8d21cbdcf1cd586b6bc | ||||
|  | ||||
| COCOAPODS: 1.16.2 | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| 		738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */; }; | ||||
| 		738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */; }; | ||||
| 		738C1EB82D0D76A500A215F3 /* SolarWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		7396A3522D16BD890095F4A8 /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */; }; | ||||
| 		73B7746E2D0E869200A789CE /* SolarShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73B774642D0E869200A789CE /* SolarShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		73DA8A012D05C7620024A03E /* SolarNotifyService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; | ||||
| @@ -22,6 +23,8 @@ | ||||
| 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; | ||||
| 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | ||||
| 		CED170BFB6A72CDDAC285637 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */; }; | ||||
| 		D5125CF12F159F0B8BC7641D /* Pods_SolarNotifyService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */; }; | ||||
| 		D962B51F682FBDEC00AC7281 /* Pods_SolarWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */; }; | ||||
| 		F51C4E3C8FA95426C91FC0A4 /* Pods_SolarShare.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */; }; | ||||
| /* End PBXBuildFile section */ | ||||
|  | ||||
| @@ -83,30 +86,40 @@ | ||||
| /* End PBXCopyFilesBuildPhase section */ | ||||
|  | ||||
| /* Begin PBXFileReference section */ | ||||
| 		02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarNotifyService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		1077EFD9ACF793E9DA5D5B63 /* Pods-Runner-SolarNotifyService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.release.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		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>"; }; | ||||
| 		331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; | ||||
| 		40B53769EB464E54DACA7CE4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		430F31F96B82659CBEAD4326 /* Pods-Runner-SolarWidgetExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.profile.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.profile.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		48AE73F9950AF4FB02B5E9F4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		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>"; }; | ||||
| 		4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.profile.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.profile.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; }; | ||||
| 		731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; | ||||
| 		738C1EAB2D0D76A400A215F3 /* SolarWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		738C1F132D0D7DDC00A215F3 /* SolarWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SolarWidgetExtension.entitlements; sourceTree = "<group>"; }; | ||||
| 		7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		73B774642D0E869200A789CE /* SolarShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		73DA89FA2D05C7620024A03E /* SolarNotifyService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolarNotifyService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; | ||||
| 		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; | ||||
| 		7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolarWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		8E44A071621D5CAF864FB2F1 /* Pods-Runner-SolarNotifyService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.debug.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		931FBE9EDB99B3AD8B1FFB00 /* Pods-Runner-SolarWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; | ||||
| 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; | ||||
| @@ -117,6 +130,11 @@ | ||||
| 		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>"; }; | ||||
| 		B4550C68292419CDC580808B /* Pods-Runner-SolarNotifyService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarNotifyService.profile.xcconfig"; path = "Target Support Files/Pods-Runner-SolarNotifyService/Pods-Runner-SolarNotifyService.profile.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>"; }; | ||||
| 		BFF3B436D74FA8CBFFE34A27 /* Pods-Runner-SolarWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-SolarWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-Runner-SolarWidgetExtension/Pods-Runner-SolarWidgetExtension.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.release.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolarNotifyService.debug.xcconfig"; path = "Target Support Files/Pods-SolarNotifyService/Pods-SolarNotifyService.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		EDF483E994343CDFBF9BA347 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| /* End PBXFileReference section */ | ||||
|  | ||||
| @@ -217,6 +235,7 @@ | ||||
| 			files = ( | ||||
| 				738C1EAD2D0D76A400A215F3 /* SwiftUI.framework in Frameworks */, | ||||
| 				738C1EAC2D0D76A400A215F3 /* WidgetKit.framework in Frameworks */, | ||||
| 				D962B51F682FBDEC00AC7281 /* Pods_SolarWidgetExtension.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| @@ -232,6 +251,7 @@ | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				D5125CF12F159F0B8BC7641D /* Pods_SolarNotifyService.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| @@ -262,6 +282,8 @@ | ||||
| 				731B7B6B2D0D6CE000CEB9B7 /* WidgetKit.framework */, | ||||
| 				731B7B6D2D0D6CE000CEB9B7 /* SwiftUI.framework */, | ||||
| 				16F41E029731EA30268EDE2A /* Pods_SolarShare.framework */, | ||||
| 				02469D286F48D84300484B1E /* Pods_SolarNotifyService.framework */, | ||||
| 				7B1A159F5551E280D0EFC129 /* Pods_SolarWidgetExtension.framework */, | ||||
| 			); | ||||
| 			name = Frameworks; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -328,6 +350,7 @@ | ||||
| 				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, | ||||
| 				74858FAE1ED2DC5600515810 /* AppDelegate.swift */, | ||||
| 				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, | ||||
| 				7396A3512D16BD890095F4A8 /* NotifyDelegate.swift */, | ||||
| 			); | ||||
| 			path = Runner; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -344,6 +367,18 @@ | ||||
| 				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 */, | ||||
| 				D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */, | ||||
| 				D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */, | ||||
| 				4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */, | ||||
| 				8E44A071621D5CAF864FB2F1 /* Pods-Runner-SolarNotifyService.debug.xcconfig */, | ||||
| 				1077EFD9ACF793E9DA5D5B63 /* Pods-Runner-SolarNotifyService.release.xcconfig */, | ||||
| 				B4550C68292419CDC580808B /* Pods-Runner-SolarNotifyService.profile.xcconfig */, | ||||
| 				BFF3B436D74FA8CBFFE34A27 /* Pods-Runner-SolarWidgetExtension.debug.xcconfig */, | ||||
| 				931FBE9EDB99B3AD8B1FFB00 /* Pods-Runner-SolarWidgetExtension.release.xcconfig */, | ||||
| 				430F31F96B82659CBEAD4326 /* Pods-Runner-SolarWidgetExtension.profile.xcconfig */, | ||||
| 			); | ||||
| 			path = Pods; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -374,6 +409,7 @@ | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 738C1EBA2D0D76A500A215F3 /* Build configuration list for PBXNativeTarget "SolarWidgetExtension" */; | ||||
| 			buildPhases = ( | ||||
| 				F2FCDA0E1BD434BF4883AFFD /* [CP] Check Pods Manifest.lock */, | ||||
| 				738C1EA72D0D76A400A215F3 /* Sources */, | ||||
| 				738C1EA82D0D76A400A215F3 /* Frameworks */, | ||||
| 				738C1EA92D0D76A400A215F3 /* Resources */, | ||||
| @@ -416,6 +452,7 @@ | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 73DA8A072D05C7620024A03E /* Build configuration list for PBXNativeTarget "SolarNotifyService" */; | ||||
| 			buildPhases = ( | ||||
| 				50F5704AB2E7309C916CA2E7 /* [CP] Check Pods Manifest.lock */, | ||||
| 				73DA89F62D05C7620024A03E /* Sources */, | ||||
| 				73DA89F72D05C7620024A03E /* Frameworks */, | ||||
| 				73DA89F82D05C7620024A03E /* Resources */, | ||||
| @@ -611,6 +648,28 @@ | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| 		50F5704AB2E7309C916CA2E7 /* [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-SolarNotifyService-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; | ||||
| 		}; | ||||
| 		738C1EBE2D0D76C500A215F3 /* Copy Bundle Version */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -710,6 +769,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; | ||||
| @@ -765,6 +846,7 @@ | ||||
| 			files = ( | ||||
| 				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, | ||||
| 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, | ||||
| 				7396A3522D16BD890095F4A8 /* NotifyDelegate.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| @@ -879,7 +961,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 +1029,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 +1073,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 +1114,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; | ||||
| @@ -1193,6 +1278,7 @@ | ||||
| 		}; | ||||
| 		73DA8A032D05C7620024A03E /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = D96D1DB4ED46A2640C1B9D34 /* Pods-SolarNotifyService.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| @@ -1234,6 +1320,7 @@ | ||||
| 		}; | ||||
| 		73DA8A042D05C7620024A03E /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = D7E1FA77FDA53439DB2C0E75 /* Pods-SolarNotifyService.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| @@ -1272,6 +1359,7 @@ | ||||
| 		}; | ||||
| 		73DA8A052D05C7620024A03E /* Profile */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 4CBF45ABD292EE527D0A4D1E /* Pods-SolarNotifyService.profile.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| @@ -1433,7 +1521,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 +1549,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", | ||||
|   | ||||
| @@ -1,14 +1,26 @@ | ||||
| import Flutter | ||||
| import UIKit | ||||
|  | ||||
| import workmanager | ||||
|  | ||||
| @main | ||||
| @objc class AppDelegate: FlutterAppDelegate { | ||||
|   override func application( | ||||
|     _ application: UIApplication, | ||||
|     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | ||||
|   ) -> Bool { | ||||
|     GeneratedPluginRegistrant.register(with: self) | ||||
|        | ||||
|     return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||
|   } | ||||
|     let notifyDelegate = NotifyDelegate() | ||||
|      | ||||
|     override func application( | ||||
|         _ application: UIApplication, | ||||
|         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | ||||
|     ) -> Bool { | ||||
|         GeneratedPluginRegistrant.register(with: self) | ||||
|         WorkmanagerPlugin.setPluginRegistrantCallback { registry in | ||||
|             GeneratedPluginRegistrant.register(with: registry) | ||||
|         } | ||||
|          | ||||
|         UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*5)) | ||||
|          | ||||
|         UNUserNotificationCenter.current().delegate = notifyDelegate | ||||
|          | ||||
|         return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										39
									
								
								ios/Runner/AppIntent.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								ios/Runner/AppIntent.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| // | ||||
| //  AppIntent.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2024/12/21. | ||||
| // | ||||
|  | ||||
| import AppIntents | ||||
| import Flutter | ||||
| import Foundation | ||||
| import home_widget | ||||
|  | ||||
| @available(iOS 17, *) | ||||
| public struct AppBackgroundIntent: AppIntent { | ||||
|    static public var title: LocalizedStringResource = "Solar Network Background Intent" | ||||
|  | ||||
|    @Parameter(title: "Widget URI") | ||||
|    var url: URL? | ||||
|  | ||||
|    @Parameter(title: "AppGroup") | ||||
|    var appGroup: String? | ||||
|  | ||||
|    public init() {} | ||||
|  | ||||
|    public init(url: URL?, appGroup: String?) { | ||||
|       self.url = url | ||||
|       self.appGroup = appGroup | ||||
|    } | ||||
|  | ||||
|    public func perform() async throws -> some IntentResult { | ||||
|       await HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!) | ||||
|  | ||||
|       return .result() | ||||
|    } | ||||
| } | ||||
|  | ||||
| @available(iOS 17, *) | ||||
| @available(iOSApplicationExtension, unavailable) | ||||
| extension AppBackgroundIntent: ForegroundContinuableIntent {} | ||||
| @@ -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> | ||||
| @@ -34,9 +47,9 @@ | ||||
| 	<key>LSRequiresIPhoneOS</key> | ||||
| 	<true/> | ||||
| 	<key>NSCameraUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian take photo or video for your post.</string> | ||||
| 	<string>Grant access to Camera will allow Solian take photo or video for your post.</string> | ||||
| 	<key>NSMicrophoneUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian record audio for your post.</string> | ||||
| 	<string>Grant access to Microphone will allow Solian record audio for your post.</string> | ||||
| 	<key>NSPhotoLibraryAddUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> | ||||
| 	<key>NSPhotoLibraryUsageDescription</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> | ||||
|   | ||||
							
								
								
									
										55
									
								
								ios/Runner/NotifyDelegate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								ios/Runner/NotifyDelegate.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| // | ||||
| //  NotifyDelegate.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2024/12/21. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import home_widget | ||||
| import Alamofire | ||||
|  | ||||
| class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { | ||||
|     func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { | ||||
|         if let textResponse = response as? UNTextInputNotificationResponse { | ||||
|             let content = response.notification.request.content | ||||
|             guard let metadata = content.userInfo["metadata"] as? [AnyHashable: Any] else { | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             let channelId = metadata["channel_id"] as? Int | ||||
|             let eventId = metadata["event_id"] as? Int | ||||
|              | ||||
|             let replyToken = metadata["reply_token"] as? String | ||||
|              | ||||
|             if (channelId == nil || eventId == nil || replyToken == nil) { | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             let serverUrl = "https://api.sn.solsynth.dev" | ||||
|             let url = "\(serverUrl)/cgi/im/quick/\(channelId!)/reply/\(eventId!)?replyToken=\(replyToken!)" | ||||
|              | ||||
|             let parameters: [String: Any] = [ | ||||
|                 "type": "messages.new", | ||||
|                 "body": [ | ||||
|                     "text": textResponse.userText, | ||||
|                     "algorithm": "plain" | ||||
|                 ] | ||||
|             ] | ||||
|              | ||||
|             AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default) | ||||
|                 .validate() | ||||
|                 .responseString { response in | ||||
|                     switch response.result { | ||||
|                     case .success(_): | ||||
|                         break | ||||
|                     case .failure(let error): | ||||
|                         print("Failed to send chat reply message: \(error)") | ||||
|                         break | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|          | ||||
|         completionHandler() | ||||
|     } | ||||
| } | ||||
| @@ -7,6 +7,8 @@ | ||||
|  | ||||
| import UserNotifications | ||||
| import Intents | ||||
| import Kingfisher | ||||
| import UniformTypeIdentifiers | ||||
|  | ||||
| enum ParseNotificationPayloadError: Error { | ||||
|     case missingMetadata(String) | ||||
| @@ -18,58 +20,6 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|     private var contentHandler: ((UNNotificationContent) -> Void)? | ||||
|     private var bestAttemptContent: UNMutableNotificationContent? | ||||
|      | ||||
|     private func fetchAvatarImage(from url: String, completion: @escaping (INImage?) -> Void) { | ||||
|         guard let imageURL = URL(string: url) else { | ||||
|             completion(nil) | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         // Define a cache location based on the URL hash | ||||
|         let cacheFileName = imageURL.lastPathComponent | ||||
|         let tempDirectory = FileManager.default.temporaryDirectory | ||||
|         let cachedFileUrl = tempDirectory.appendingPathComponent(cacheFileName) | ||||
|          | ||||
|         // Check if the image is already cached | ||||
|         if FileManager.default.fileExists(atPath: cachedFileUrl.path) { | ||||
|             do { | ||||
|                 let data = try Data(contentsOf: cachedFileUrl) | ||||
|                 let cachedImage = INImage(imageData: data) // No optional binding here | ||||
|                 completion(cachedImage) | ||||
|                 return | ||||
|             } catch { | ||||
|                 print("Failed to load cached avatar image: \(error.localizedDescription)") | ||||
|                 try? FileManager.default.removeItem(at: cachedFileUrl) // Clear corrupted cache | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Download the image if not cached | ||||
|         let session = URLSession(configuration: .default) | ||||
|         session.downloadTask(with: imageURL) { localUrl, response, error in | ||||
|             if let error = error { | ||||
|                 print("Failed to fetch avatar image: \(error.localizedDescription)") | ||||
|                 completion(nil) | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             guard let localUrl = localUrl, let data = try? Data(contentsOf: localUrl) else { | ||||
|                 print("Failed to fetch data for avatar image.") | ||||
|                 completion(nil) | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             do { | ||||
|                 // Cache the downloaded file | ||||
|                 try FileManager.default.moveItem(at: localUrl, to: cachedFileUrl) | ||||
|             } catch { | ||||
|                 print("Failed to cache avatar image: \(error.localizedDescription)") | ||||
|             } | ||||
|              | ||||
|             // Create INImage from the downloaded data | ||||
|             let inImage = INImage(imageData: data) // Create directly | ||||
|             completion(inImage) | ||||
|         }.resume() | ||||
|     } | ||||
|      | ||||
|     override func didReceive( | ||||
|         _ request: UNNotificationRequest, | ||||
|         withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void | ||||
| @@ -112,16 +62,39 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|             throw ParseNotificationPayloadError.missingAvatarUrl("The notification has no avatar.") | ||||
|         } | ||||
|          | ||||
|         let replyableMessageCategory = UNNotificationCategory( | ||||
|             identifier: content.categoryIdentifier, | ||||
|             actions: [ | ||||
|                 UNTextInputNotificationAction( | ||||
|                     identifier: "reply_action", | ||||
|                     title: "Reply", | ||||
|                     options: [] | ||||
|                 ), | ||||
|             ], | ||||
|             intentIdentifiers: [], | ||||
|             options: [] | ||||
|         ) | ||||
|          | ||||
|         UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) | ||||
|         content.categoryIdentifier = replyableMessageCategory.identifier | ||||
|          | ||||
|         let metadataCopy = metadata as? [String: String] ?? [:] | ||||
|         let avatarUrl = getAttachmentUrl(for: avatarIdentifier) | ||||
|         fetchAvatarImage(from: avatarUrl) { [weak self] inImage in | ||||
|             guard let self = self else { return } | ||||
|         KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, completionHandler: { result in | ||||
|             var image: Data? | ||||
|             switch result { | ||||
|             case .success(let value): | ||||
|                 image = value.image.pngData() | ||||
|             case .failure(let error): | ||||
|                 print("Unable to get avatar url: \(error)") | ||||
|             } | ||||
|              | ||||
|             let handle = INPersonHandle(value: "\(metadata["user_id"] ?? "")", type: .unknown) | ||||
|             let handle = INPersonHandle(value: "\(metadataCopy["user_id"] ?? "")", type: .unknown) | ||||
|             let sender = INPerson( | ||||
|                 personHandle: handle, | ||||
|                 nameComponents: nil, | ||||
|                 displayName: content.title, | ||||
|                 image: inImage, | ||||
|                 image: image == nil ? nil : INImage(imageData: image!), | ||||
|                 contactIdentifier: nil, | ||||
|                 customIdentifier: nil | ||||
|             ) | ||||
| @@ -132,12 +105,12 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|                 let updatedContent = try? request.content.updating(from: intent) | ||||
|                 self.contentHandler?(updatedContent ?? content) | ||||
|             } else { | ||||
|                 let intent = self.createMessageIntent(with: sender, metadata: metadata, body: content.body) | ||||
|                 let intent = self.createMessageIntent(with: sender, metadata: metadataCopy, body: content.body) | ||||
|                 self.donateInteraction(for: intent) | ||||
|                 let updatedContent = try? request.content.updating(from: intent) | ||||
|                 self.contentHandler?(updatedContent ?? content) | ||||
|             } | ||||
|         } | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     private func handleDefaultNotification(content: UNMutableNotificationContent) throws { | ||||
| @@ -146,15 +119,15 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|         } | ||||
|          | ||||
|         if let imageIdentifier = metadata["image"] as? String { | ||||
|             attachMedia(to: content, withIdentifier: imageIdentifier) | ||||
|             attachMedia(to: content, withIdentifier: imageIdentifier, fileType: UTType.jpeg, doScaleDown: true) | ||||
|         } else if let avatarIdentifier = metadata["avatar"] as? String { | ||||
|             attachMedia(to: content, withIdentifier: avatarIdentifier) | ||||
|             attachMedia(to: content, withIdentifier: avatarIdentifier, fileType: UTType.jpeg, doScaleDown: true) | ||||
|         } else { | ||||
|             contentHandler?(content) | ||||
|         } | ||||
|          | ||||
|         contentHandler?(content) | ||||
|     } | ||||
|      | ||||
|     private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String) { | ||||
|     private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) { | ||||
|         let attachmentUrl = getAttachmentUrl(for: identifier) | ||||
|          | ||||
|         guard let remoteUrl = URL(string: attachmentUrl) else { | ||||
| @@ -162,49 +135,62 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         // Define a cache location based on the identifier | ||||
|         let tempDirectory = FileManager.default.temporaryDirectory | ||||
|         let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) | ||||
|         let targetSize = 800 | ||||
|         let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) | ||||
|          | ||||
|         if FileManager.default.fileExists(atPath: cachedFileUrl.path) { | ||||
|             // Use cached file | ||||
|             attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) | ||||
|         } else { | ||||
|             // Download and cache the file | ||||
|             let session = URLSession(configuration: .default) | ||||
|             session.downloadTask(with: remoteUrl) { [weak content] localUrl, response, error in | ||||
|                 guard let content = content else { return } | ||||
|                  | ||||
|                 if let error = error { | ||||
|                     print("Failed to download media: \(error.localizedDescription)") | ||||
|                     self.contentHandler?(content) | ||||
|                     return | ||||
|                 } | ||||
|                  | ||||
|                 guard let localUrl = localUrl else { | ||||
|                     print("No local file URL after download") | ||||
|                     self.contentHandler?(content) | ||||
|                     return | ||||
|                 } | ||||
|         KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [ | ||||
|             .processor(scaleProcessor) | ||||
|         ] : nil) { [weak self] result in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch result { | ||||
|             case .success(let retrievalResult): | ||||
|                 // The image is either retrieved from cache or downloaded | ||||
|                 let tempDirectory = FileManager.default.temporaryDirectory | ||||
|                 let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) | ||||
|                  | ||||
|                 do { | ||||
|                     // Move the downloaded file to the cache | ||||
|                     try FileManager.default.moveItem(at: localUrl, to: cachedFileUrl) | ||||
|                     self.attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) | ||||
|                     // Write the image data to a temporary file for UNNotificationAttachment | ||||
|                     try retrievalResult.image.pngData()?.write(to: cachedFileUrl) | ||||
|                     self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier) | ||||
|                 } catch { | ||||
|                     print("Failed to cache media file: \(error.localizedDescription)") | ||||
|                     print("Failed to write media to temporary file: \(error.localizedDescription)") | ||||
|                     self.contentHandler?(content) | ||||
|                 } | ||||
|             }.resume() | ||||
|                  | ||||
|             case .failure(let error): | ||||
|                 print("Failed to retrieve image: \(error.localizedDescription)") | ||||
|                 self.contentHandler?(content) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func attachLocalMedia(to content: UNMutableNotificationContent, from localUrl: URL, withIdentifier identifier: String) { | ||||
|         if let attachment = try? UNNotificationAttachment(identifier: identifier, url: localUrl) { | ||||
|      | ||||
|     private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) { | ||||
|         do { | ||||
|             let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [ | ||||
|                 UNNotificationAttachmentOptionsTypeHintKey: type as Any, | ||||
|                 UNNotificationAttachmentOptionsThumbnailHiddenKey: 0, | ||||
|             ]) | ||||
|             content.attachments = [attachment] | ||||
|         } else { | ||||
|             print("Failed to create attachment from cached file: \(localUrl.path)") | ||||
|         } catch let error as NSError { | ||||
|             // Log detailed error information | ||||
|             print("Failed to create attachment from file at \(localUrl.path)") | ||||
|             print("Error: \(error.localizedDescription)") | ||||
|              | ||||
|             // Check specific error codes if needed | ||||
|             if error.domain == NSCocoaErrorDomain { | ||||
|                 switch error.code { | ||||
|                 case NSFileReadNoSuchFileError: | ||||
|                     print("File does not exist at \(localUrl.path)") | ||||
|                 case NSFileReadNoPermissionError: | ||||
|                     print("No permission to read file at \(localUrl.path)") | ||||
|                 default: | ||||
|                     print("Unhandled file error: \(error.code)") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Call content handler regardless of success or failure | ||||
|         self.contentHandler?(content) | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import SwiftUI | ||||
|  | ||||
| struct CheckInProvider: TimelineProvider { | ||||
|     func placeholder(in context: Context) -> CheckInEntry { | ||||
|         CheckInEntry(date: Date(), user: nil, checkIn: nil) | ||||
|         CheckInEntry(date: Date(), checkIn: nil) | ||||
|     } | ||||
|  | ||||
|     func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) { | ||||
| @@ -23,21 +23,17 @@ struct CheckInProvider: TimelineProvider { | ||||
|         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 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( | ||||
|             date: Date(), | ||||
|             user: user, | ||||
|             checkIn: checkIn | ||||
|         ) | ||||
|         completion(entry) | ||||
| @@ -53,14 +49,13 @@ struct CheckInProvider: TimelineProvider { | ||||
|  | ||||
| struct CheckInEntry: TimelineEntry { | ||||
|     let date: Date | ||||
|     let user: SolarUser? | ||||
|     let checkIn: SolarCheckInRecord? | ||||
| } | ||||
|  | ||||
| struct CheckInWidgetEntryView : View { | ||||
|     var entry: CheckInProvider.Entry | ||||
|      | ||||
|     private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "大吉", "吉"] | ||||
|     private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "吉", "大吉"] | ||||
|      | ||||
|     func checkIn() -> Void {} | ||||
|      | ||||
| @@ -105,7 +100,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")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -132,10 +127,9 @@ struct CheckInWidget: Widget { | ||||
| #Preview(as: .systemSmall) { | ||||
|     CheckInWidget() | ||||
| } timeline: { | ||||
|     CheckInEntry(date: .now, user: nil, checkIn: nil) | ||||
|     CheckInEntry(date: .now, checkIn: nil) | ||||
|     CheckInEntry( | ||||
|         date: .now, | ||||
|         user: SolarUser(id: 1, name: "demo", nick: "Deemo"), | ||||
|         checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now) | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										246
									
								
								ios/SolarWidget/RandomPostWidget.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								ios/SolarWidget/RandomPostWidget.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | ||||
| // | ||||
| //  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 | ||||
|                             let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size), mode: .aspectFill) | ||||
|                              | ||||
|                             KFImage.url(URL(string: avatarUrl)) | ||||
|                                 .resizable() | ||||
|                                 .setProcessor(scaleProcessor) | ||||
|                                 .fade(duration: 0.25) | ||||
|                                 .placeholder{ | ||||
|                                     ProgressView() | ||||
|                                         .progressViewStyle(CircularProgressViewStyle()) | ||||
|                                 } | ||||
|                                 .aspectRatio(contentMode: .fill) | ||||
|                                 .frame(width: size, height: size) | ||||
|                                 .cornerRadius(size / 2) | ||||
|                              | ||||
|                                 .frame(width: size, height: size, alignment: .center) | ||||
|                         } | ||||
|                          | ||||
|                         Text(randomPost.publisher.nick) | ||||
|                             .font(.system(size: 15)) | ||||
|                             .opacity(0.9) | ||||
|                          | ||||
|                         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 | ||||
|     ) | ||||
| } | ||||
| @@ -12,6 +12,6 @@ import SwiftUI | ||||
| struct SolarWidgetBundle: WidgetBundle { | ||||
|     var body: some Widget { | ||||
|         CheckInWidget() | ||||
|         FeaturedPostWidget() | ||||
|         RandomPostWidget() | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										118
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,8 +1,10 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:croppy/croppy.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:easy_localization_loader/easy_localization_loader.dart'; | ||||
| import 'package:firebase_core/firebase_core.dart'; | ||||
| @@ -11,13 +13,16 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/firebase_options.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/link_preview.dart'; | ||||
| import 'package:surface/providers/navigation.dart'; | ||||
| import 'package:surface/providers/notification.dart'; | ||||
| @@ -36,7 +41,26 @@ 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:version/version.dart'; | ||||
| import 'package:workmanager/workmanager.dart'; | ||||
| import 'package:in_app_review/in_app_review.dart'; | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| void appBackgroundDispatcher() { | ||||
|   Workmanager().executeTask((task, inputData) async { | ||||
|     log("[WorkManager] 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 +88,22 @@ void main() async { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||
|     Workmanager().initialize( | ||||
|       appBackgroundDispatcher, | ||||
|       isInDebugMode: kDebugMode, | ||||
|     ); | ||||
|     if (Platform.isAndroid) { | ||||
|       Workmanager().registerPeriodicTask( | ||||
|         "widget-update-random-post", | ||||
|         "WidgetUpdateRandomPost", | ||||
|         frequency: Duration(minutes: 1), | ||||
|         constraints: Constraints(networkType: NetworkType.connected), | ||||
|         tag: "widget-update", | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   runApp(const SolianApp()); | ||||
| } | ||||
|  | ||||
| @@ -86,15 +126,18 @@ class SolianApp extends StatelessWidget { | ||||
|         assetLoader: JsonAssetLoader(), | ||||
|         child: MultiProvider( | ||||
|           providers: [ | ||||
|             // System extensions layer | ||||
|             Provider(create: (ctx) => HomeWidgetProvider(ctx)), | ||||
|  | ||||
|             // Preferences layer | ||||
|             ChangeNotifierProvider(create: (ctx) => ConfigProvider(ctx)), | ||||
|  | ||||
|             // Display layer | ||||
|             ChangeNotifierProvider(create: (_) => ThemeProvider()), | ||||
|             ChangeNotifierProvider(create: (ctx) => NavigationProvider()), | ||||
|  | ||||
|             // System extensions layer | ||||
|             Provider(create: (ctx) => HomeWidgetProvider(ctx)), | ||||
|  | ||||
|             // Data layer | ||||
|             Provider(create: (_) => SnNetworkProvider()), | ||||
|             Provider(create: (ctx) => SnNetworkProvider(ctx)), | ||||
|             Provider(create: (ctx) => UserDirectoryProvider(ctx)), | ||||
|             Provider(create: (ctx) => SnAttachmentProvider(ctx)), | ||||
|             Provider(create: (ctx) => SnPostContentProvider(ctx)), | ||||
| @@ -163,13 +206,63 @@ class _AppSplashScreen extends StatefulWidget { | ||||
| class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|   bool _isReady = false; | ||||
|  | ||||
|   late StreamSubscription _shareIntentSubscription; | ||||
|   void _tryRequestRating() async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     if (prefs.containsKey('first_boot_time')) { | ||||
|       final rawTime = prefs.getString('first_boot_time'); | ||||
|       final time = DateTime.tryParse(rawTime ?? ''); | ||||
|       if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) { | ||||
|         final inAppReview = InAppReview.instance; | ||||
|         if (prefs.getBool('rating_requested') == true) return; | ||||
|         if (await inAppReview.isAvailable()) { | ||||
|           await inAppReview.requestReview(); | ||||
|           prefs.setBool('rating_requested', true); | ||||
|         } else { | ||||
|           log('Unable request app review, unavailable'); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       prefs.setString('first_boot_time', DateTime.now().toIso8601String()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _checkForUpdate() async { | ||||
|     if (kIsWeb) return; | ||||
|     try { | ||||
|       final info = await PackageInfo.fromPlatform(); | ||||
|       final localVersionString = '${info.version}+${info.buildNumber}'; | ||||
|       final resp = await Dio( | ||||
|         BaseOptions( | ||||
|           sendTimeout: const Duration(seconds: 60), | ||||
|           receiveTimeout: const Duration(seconds: 60), | ||||
|         ), | ||||
|       ).get( | ||||
|         'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1', | ||||
|       ); | ||||
|       final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0'; | ||||
|       final remoteVersion = Version.parse(remoteVersionString.split('+').first); | ||||
|       final localVersion = Version.parse(localVersionString.split('+').first); | ||||
|       final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0; | ||||
|       final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0; | ||||
|       log("[Update] Local: $localVersionString, Remote: $remoteVersionString"); | ||||
|       if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) { | ||||
|         final config = context.read<ConfigProvider>(); | ||||
|         config.setUpdate(remoteVersionString); | ||||
|         log("[Update] Update available: $remoteVersionString"); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       if (mounted) context.showErrorDialog('Unable to check update: $e'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _initialize() async { | ||||
|     try { | ||||
|       final home = context.read<HomeWidgetProvider>(); | ||||
|       await home.initialize(); | ||||
|       if (!mounted) return; | ||||
|       // The Network initialization must be done after the HomeWidget initialization | ||||
|       // The Network initialization will save the server url to the HomeWidget | ||||
|       // The Network initialization will also save initialize the Config, so it not need to be initialized again | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.initializeUserAgent(); | ||||
|       if (!mounted) return; | ||||
| @@ -189,10 +282,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _postInitialization() async { | ||||
|     await widgetUpdateRandomPost(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _initialize(); | ||||
|     _initialize().then((_) { | ||||
|       _postInitialization(); | ||||
|       _tryRequestRating(); | ||||
|       _checkForUpdate(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -206,7 +307,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               Image.asset("assets/icon/icon.png", width: 64, height: 64), | ||||
|               if (MediaQuery.of(context).platformBrightness == Brightness.dark) | ||||
|                 Image.asset("assets/icon/icon-dark.png", width: 64, height: 64) | ||||
|               else | ||||
|                 Image.asset("assets/icon/icon.png", width: 64, height: 64), | ||||
|               const Gap(6), | ||||
|               LinearProgressIndicator( | ||||
|                 backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|   | ||||
| @@ -125,10 +125,8 @@ class ChatChannelProvider extends ChangeNotifier { | ||||
|       final channelBox = await Hive.openBox<SnChatMessage>( | ||||
|         '${ChatMessageController.kChatMessageBoxPrefix}${channel.id}', | ||||
|       ); | ||||
|       final lastMessage = channelBox.isNotEmpty | ||||
|           ? channelBox.values | ||||
|               .reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) | ||||
|           : null; | ||||
|       final lastMessage = | ||||
|           channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null; | ||||
|       if (lastMessage != null) result.add(lastMessage); | ||||
|       channelBox.close(); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										51
									
								
								lib/providers/config.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/providers/config.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
|  | ||||
| const kAtkStoreKey = 'nex_user_atk'; | ||||
| const kRtkStoreKey = 'nex_user_rtk'; | ||||
|  | ||||
| const kNetworkServerDefault = 'https://api.sn.solsynth.dev'; | ||||
| const kNetworkServerStoreKey = 'app_server_url'; | ||||
|  | ||||
| const Map<String, FilterQuality> kImageQualityLevel = { | ||||
|   'settingsImageQualityLowest': FilterQuality.none, | ||||
|   'settingsImageQualityLow': FilterQuality.low, | ||||
|   'settingsImageQualityMedium': FilterQuality.medium, | ||||
|   'settingsImageQualityHigh': FilterQuality.high, | ||||
| }; | ||||
|  | ||||
| class ConfigProvider extends ChangeNotifier { | ||||
|   late final SharedPreferences prefs; | ||||
|  | ||||
|   late final HomeWidgetProvider _home; | ||||
|  | ||||
|   ConfigProvider(BuildContext context) { | ||||
|     _home = context.read<HomeWidgetProvider>(); | ||||
|   } | ||||
|  | ||||
|   Future<void> initialize() async { | ||||
|     prefs = await SharedPreferences.getInstance(); | ||||
|   } | ||||
|  | ||||
|   FilterQuality get imageQuality { | ||||
|     return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; | ||||
|   } | ||||
|  | ||||
|   String get serverUrl { | ||||
|     return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; | ||||
|   } | ||||
|  | ||||
|   set serverUrl(String url) { | ||||
|     prefs.setString(kNetworkServerStoreKey, url); | ||||
|     _home.saveWidgetData("nex_server_url", url); | ||||
|   } | ||||
|  | ||||
|   String? updatableVersion; | ||||
|  | ||||
|   void setUpdate(String newVersion) { | ||||
|     updatableVersion = newVersion; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
| @@ -6,30 +6,34 @@ import 'dart:io'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:dio_smart_retry/dio_smart_retry.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
|  | ||||
| const kAtkStoreKey = 'nex_user_atk'; | ||||
| const kRtkStoreKey = 'nex_user_rtk'; | ||||
|  | ||||
| const kNetworkServerDefault = 'https://api.sn.solsynth.dev'; | ||||
| const kNetworkServerStoreKey = 'app_server_url'; | ||||
|  | ||||
| const kNetworkServerDirectory = [ | ||||
|   ('Solar Network', 'https://api.sn.solsynth.dev'), | ||||
|   ('Local', 'http://localhost:8001'), | ||||
| ]; | ||||
|  | ||||
| Completer<String?>? _refreshCompleter; | ||||
|  | ||||
| class SnNetworkProvider { | ||||
|   late final Dio client; | ||||
|  | ||||
|   late final SharedPreferences _prefs; | ||||
|   late final ConfigProvider _config; | ||||
|   late final HomeWidgetProvider _home; | ||||
|  | ||||
|   String? _userAgent; | ||||
|  | ||||
|   SnNetworkProvider() { | ||||
|   SnNetworkProvider(BuildContext context) { | ||||
|     _home = context.read<HomeWidgetProvider>(); | ||||
|  | ||||
|     client = Dio(); | ||||
|  | ||||
|     client.interceptors.add(RetryInterceptor( | ||||
| @@ -60,13 +64,52 @@ class SnNetworkProvider { | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       _prefs = prefs; | ||||
|       client.options.baseUrl = _prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; | ||||
|     _config = context.read<ConfigProvider>(); | ||||
|     _config.initialize().then((_) { | ||||
|       _prefs = _config.prefs; | ||||
|       client.options.baseUrl = _config.serverUrl; | ||||
|       if (!context.mounted) return; | ||||
|       _home.saveWidgetData("nex_server_url", client.options.baseUrl); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   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 { | ||||
|           final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) { | ||||
|             prefs.setString(kAtkStoreKey, atk); | ||||
|             prefs.setString(kRtkStoreKey, rtk); | ||||
|           }); | ||||
|           if (atk != null) { | ||||
|             options.headers['Authorization'] = 'Bearer $atk'; | ||||
|           } | ||||
|           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; | ||||
| @@ -92,14 +135,22 @@ 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(); | ||||
|  | ||||
|   Completer<String?>? _refreshCompleter; | ||||
|  | ||||
|   Future<String?> getFreshAtk() async { | ||||
|     return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) { | ||||
|       setTokenPair(atk, rtk); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async { | ||||
|     if (_refreshCompleter != null) { | ||||
|       return await _refreshCompleter!.future; | ||||
|     } else { | ||||
| @@ -107,7 +158,6 @@ class SnNetworkProvider { | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       var atk = _prefs.getString(kAtkStoreKey); | ||||
|       if (atk != null) { | ||||
|         final atkParts = atk.split('.'); | ||||
|         if (atkParts.length != 3) { | ||||
| @@ -133,7 +183,13 @@ class SnNetworkProvider { | ||||
|         final exp = jsonDecode(payload)['exp']; | ||||
|         if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { | ||||
|           log('Access token need refresh, doing it at ${DateTime.now()}'); | ||||
|           atk = await refreshToken(); | ||||
|           final result = await _refreshToken(client.options.baseUrl, rtk); | ||||
|           if (result == null) { | ||||
|             atk = null; | ||||
|           } else { | ||||
|             atk = result.$1; | ||||
|             onRefresh?.call(atk, result.$2); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (atk != null) { | ||||
| @@ -171,24 +227,32 @@ class SnNetworkProvider { | ||||
|  | ||||
|   Future<String?> refreshToken() async { | ||||
|     final rtk = _prefs.getString(kRtkStoreKey); | ||||
|     final result = await _refreshToken(client.options.baseUrl, rtk); | ||||
|     if (result == null) return null; | ||||
|     _prefs.setString(kAtkStoreKey, result.$1); | ||||
|     _prefs.setString(kRtkStoreKey, result.$2); | ||||
|     return result.$1; | ||||
|   } | ||||
|  | ||||
|   static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async { | ||||
|     if (rtk == null) return null; | ||||
|  | ||||
|     final dio = Dio(); | ||||
|     dio.options.baseUrl = client.options.baseUrl; | ||||
|     dio.options.baseUrl = baseUrl; | ||||
|  | ||||
|     final resp = await dio.post('/cgi/id/auth/token', data: { | ||||
|       'grant_type': 'refresh_token', | ||||
|       'refresh_token': rtk, | ||||
|     }); | ||||
|  | ||||
|     final atk = resp.data['access_token']; | ||||
|     final nRtk = resp.data['refresh_token']; | ||||
|     setTokenPair(atk, nRtk); | ||||
|     final String atk = resp.data['access_token']; | ||||
|     final String nRtk = resp.data['refresh_token']; | ||||
|  | ||||
|     return atk; | ||||
|     return (atk, nRtk); | ||||
|   } | ||||
|  | ||||
|   void setBaseUrl(String url) { | ||||
|     _config.serverUrl = url; | ||||
|     client.options.baseUrl = url; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:home_widget/home_widget.dart'; | ||||
| import 'package:path/path.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| @@ -14,10 +15,12 @@ class UserProvider extends ChangeNotifier { | ||||
|  | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final HomeWidgetProvider _home; | ||||
|   late final ConfigProvider _config; | ||||
|  | ||||
|   UserProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _home = context.read<HomeWidgetProvider>(); | ||||
|     _config = context.read<ConfigProvider>(); | ||||
|   } | ||||
|  | ||||
|   Future<String?> get atk async { | ||||
| @@ -26,8 +29,7 @@ class UserProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   Future<void> initialize() async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     final value = prefs.getString(kAtkStoreKey); | ||||
|     final value = _config.prefs.getString(kAtkStoreKey); | ||||
|     isAuthorized = value != null; | ||||
|     notifyListeners(); | ||||
|     refreshUser().then((value) { | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| 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); | ||||
| @@ -24,15 +27,34 @@ 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, | ||||
|           iOSName: widget, | ||||
|           androidName: "com.solsynth.solian.$widget", | ||||
|           qualifiedAndroidName: "group.solsynth.solian.$widget", | ||||
|         ); | ||||
|       } | ||||
|     } else if (Platform.isAndroid) { | ||||
|       const widgets = ["RandomPostWidget", "CheckInWidget"]; | ||||
|       for (final widget in widgets) { | ||||
|         await HomeWidget.updateWidget( | ||||
|           androidName: "${widget}Receiver", | ||||
|           qualifiedAndroidName: "dev.solsynth.solian.widgets.${widget}Receiver", | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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", | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -158,7 +158,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     GoRouter.of(context).pushNamed( | ||||
|       'chatCallRoom', | ||||
|       pathParameters: { | ||||
|         'scope': _channel!.realm!.alias, | ||||
|         'scope': _channel!.realm?.alias ?? 'global', | ||||
|         'alias': _channel!.alias, | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter_app_update/flutter_app_update.dart'; | ||||
| import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| @@ -10,6 +13,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| @@ -76,7 +80,8 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     _HomeDashSpecialDayWidget().padding(top: 8, horizontal: 8), | ||||
|                     _HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8), | ||||
|                     _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), | ||||
|                     StaggeredGrid.extent( | ||||
|                       maxCrossAxisExtent: 280, | ||||
|                       mainAxisSpacing: 8, | ||||
| @@ -100,6 +105,52 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _HomeDashUpdateWidget extends StatelessWidget { | ||||
|   final EdgeInsets? padding; | ||||
|  | ||||
|   const _HomeDashUpdateWidget({super.key, this.padding}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final config = context.watch<ConfigProvider>(); | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|       listenable: config, | ||||
|       builder: (context, _) { | ||||
|         if (config.updatableVersion != null) { | ||||
|           return Container( | ||||
|             padding: padding, | ||||
|             child: Card( | ||||
|               child: ListTile( | ||||
|                 leading: Icon(Symbols.update), | ||||
|                 title: Text('updateAvailable').tr(), | ||||
|                 subtitle: Text(config.updatableVersion!), | ||||
|                 trailing: (kIsWeb || Platform.isWindows || Platform.isLinux) | ||||
|                     ? null | ||||
|                     : IconButton( | ||||
|                         icon: const Icon(Symbols.arrow_right_alt), | ||||
|                         onPressed: () { | ||||
|                           final model = UpdateModel( | ||||
|                             'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk', | ||||
|                             'solian-app-release-${config.updatableVersion!}.apk', | ||||
|                             'ic_notification', | ||||
|                             'https://apps.apple.com/us/app/solian/id6499032345', | ||||
|                           ); | ||||
|                           AzhonAppUpdate.update(model); | ||||
|                           context.showSnackbar('updateOngoing'.tr()); | ||||
|                         }, | ||||
|                       ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return SizedBox.shrink(); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _HomeDashSpecialDayWidget extends StatelessWidget { | ||||
|   const _HomeDashSpecialDayWidget({super.key}); | ||||
|  | ||||
| @@ -109,7 +160,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { | ||||
|     final today = DateTime.now(); | ||||
|     final birthday = ua.user?.profile?.birthday?.toLocal(); | ||||
|     final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month; | ||||
|  | ||||
|     return Column( | ||||
|       spacing: 8, | ||||
|       children: [ | ||||
|         if (isBirthday) | ||||
|           Card( | ||||
| @@ -118,6 +171,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { | ||||
|               title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|           ).padding(bottom: 8), | ||||
|         if (today.month == 12 && today.day == 25) | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: Text('🎄').fontSize(24), | ||||
|               title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|           ), | ||||
|         if (today.month == 1 && today.day == 1) | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: Text('🎉').fontSize(24), | ||||
|               title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| @@ -145,7 +212,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); | ||||
|     } | ||||
| @@ -158,7 +225,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); | ||||
| @@ -429,7 +496,7 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati | ||||
|       final pt = context.read<SnPostContentProvider>(); | ||||
|       final home = context.read<HomeWidgetProvider>(); | ||||
|       _posts = await pt.listRecommendations(); | ||||
|       home.saveWidgetData('post_featured', _posts!.map((e) => e.toJson()).toList()); | ||||
|       home.saveWidgetData('post_featured', _posts!.first.toJson()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
| @@ -25,7 +26,7 @@ class SettingsScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   SharedPreferences? _prefs; | ||||
|   late final SharedPreferences _prefs; | ||||
|   String _docBasepath = '/'; | ||||
|  | ||||
|   final TextEditingController _serverUrlController = TextEditingController(); | ||||
| @@ -39,12 +40,9 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|         setState(() {}); | ||||
|       } | ||||
|     }); | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       setState(() { | ||||
|         _prefs = prefs; | ||||
|         _serverUrlController.text = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; | ||||
|       }); | ||||
|     }); | ||||
|     final config = context.read<ConfigProvider>(); | ||||
|     _prefs = config.prefs; | ||||
|     _serverUrlController.text = config.serverUrl; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -60,6 +58,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|     return Scaffold( | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           spacing: 16, | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Column( | ||||
| @@ -78,7 +77,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                       if (image == null) return; | ||||
|  | ||||
|                       await File(image.path).copy('$_docBasepath/app_background_image'); | ||||
|                       _prefs?.setBool('has_background_image', true); | ||||
|                       _prefs.setBool('has_background_image', true); | ||||
|  | ||||
|                       setState(() {}); | ||||
|                     }, | ||||
| @@ -99,29 +98,28 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                           trailing: const Icon(Symbols.chevron_right), | ||||
|                           onTap: () { | ||||
|                             File('$_docBasepath/app_background_image').deleteSync(); | ||||
|                             _prefs?.remove('has_background_image'); | ||||
|                             _prefs.remove('has_background_image'); | ||||
|                             setState(() {}); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }), | ||||
|                 if (_prefs != null) | ||||
|                   CheckboxListTile( | ||||
|                     title: Text('settingsThemeMaterial3').tr(), | ||||
|                     subtitle: Text('settingsThemeMaterial3Description').tr(), | ||||
|                     contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                     secondary: const Icon(Symbols.new_releases), | ||||
|                     value: _prefs!.getBool(kMaterialYouToggleStoreKey) ?? false, | ||||
|                     onChanged: (value) { | ||||
|                       setState(() { | ||||
|                         _prefs!.setBool( | ||||
|                           kMaterialYouToggleStoreKey, | ||||
|                           value ?? false, | ||||
|                         ); | ||||
|                       }); | ||||
|                       final th = context.watch<ThemeProvider>(); | ||||
|                       th.reloadTheme(useMaterial3: value ?? false); | ||||
|                     }, | ||||
|                   ), | ||||
|                 CheckboxListTile( | ||||
|                   title: Text('settingsThemeMaterial3').tr(), | ||||
|                   subtitle: Text('settingsThemeMaterial3Description').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   secondary: const Icon(Symbols.new_releases), | ||||
|                   value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool( | ||||
|                         kMaterialYouToggleStoreKey, | ||||
|                         value ?? false, | ||||
|                       ); | ||||
|                     }); | ||||
|                     final th = context.watch<ThemeProvider>(); | ||||
|                     th.reloadTheme(useMaterial3: value ?? false); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
| @@ -139,7 +137,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                       icon: const Icon(Symbols.save), | ||||
|                       onPressed: () { | ||||
|                         sn.setBaseUrl(_serverUrlController.text); | ||||
|                         _prefs?.setString( | ||||
|                         _prefs.setString( | ||||
|                           kNetworkServerStoreKey, | ||||
|                           _serverUrlController.text, | ||||
|                         ); | ||||
| @@ -182,7 +180,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                       onChanged: (String? value) { | ||||
|                         if (value == null) return; | ||||
|                         _serverUrlController.text = value; | ||||
|                         _prefs?.setString(kNetworkServerStoreKey, value); | ||||
|                         _prefs.setString(kNetworkServerStoreKey, value); | ||||
|                         context.showSnackbar('settingsNetworkServerSaved'.tr()); | ||||
|                         setState(() {}); | ||||
|                       }, | ||||
| @@ -208,13 +206,56 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                   trailing: const Icon(Symbols.chevron_right), | ||||
|                   onTap: () { | ||||
|                     _serverUrlController.text = kNetworkServerDefault; | ||||
|                     _prefs?.remove(kNetworkServerStoreKey); | ||||
|                     _prefs.remove(kNetworkServerStoreKey); | ||||
|                     context.showSnackbar('settingsNetworkServerSaved'.tr()); | ||||
|                     setState(() {}); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsPerformance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 ListTile( | ||||
|                   title: Text('settingsImageQuality').tr(), | ||||
|                   subtitle: Text('settingsImageQualityDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   leading: const Icon(Symbols.image), | ||||
|                   trailing: DropdownButtonHideUnderline( | ||||
|                     child: DropdownButton2<FilterQuality>( | ||||
|                       value: kImageQualityLevel.values.elementAtOrNull(_prefs.getInt('app_image_quality') ?? 3) ?? | ||||
|                           FilterQuality.high, | ||||
|                       isExpanded: true, | ||||
|                       items: kImageQualityLevel.entries | ||||
|                           .map( | ||||
|                             (item) => DropdownMenuItem<FilterQuality>( | ||||
|                               value: item.value, | ||||
|                               child: Text(item.key).tr().fontSize(14), | ||||
|                             ), | ||||
|                           ) | ||||
|                           .toList(), | ||||
|                       onChanged: (FilterQuality? value) { | ||||
|                         if (value == null) return; | ||||
|                         _prefs.setInt('app_image_quality', kImageQualityLevel.values.toList().indexOf(value)); | ||||
|                         setState(() {}); | ||||
|                       }, | ||||
|                       buttonStyleData: const ButtonStyleData( | ||||
|                         padding: EdgeInsets.symmetric( | ||||
|                           horizontal: 16, | ||||
|                           vertical: 5, | ||||
|                         ), | ||||
|                         height: 40, | ||||
|                         width: 160, | ||||
|                       ), | ||||
|                       menuItemStyleData: const MenuItemStyleData( | ||||
|                         height: 60, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
| @@ -231,7 +272,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ].expand((ele) => [ele, const Gap(16)]).toList(), | ||||
|           ], | ||||
|         ).padding(vertical: 20), | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:cross_file/cross_file.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/scheduler.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/screens/post/post_editor.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
|  | ||||
| class AppSharingListener extends StatefulWidget { | ||||
|   final Widget child; | ||||
| @@ -103,8 +103,10 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _initialize(); | ||||
|     _initialHandle(); | ||||
|     if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||
|       _initialize(); | ||||
|       _initialHandle(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
|     import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| @@ -130,7 +130,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> { | ||||
|                     Text('callCamera').tr(), | ||||
|                     Switch( | ||||
|                       value: call.enableVideo, | ||||
|                       onChanged: (value) => call.setEnableAudio(value), | ||||
|                       onChanged: call.setEnableVideo, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).padding(bottom: 5), | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|   | ||||
| @@ -458,6 +458,7 @@ class _PostBottomAction extends StatelessWidget { | ||||
|       children: [ | ||||
|         if (showReactions || showComments) | ||||
|           Row( | ||||
|             spacing: 8, | ||||
|             children: [ | ||||
|               if (showReactions) | ||||
|                 InkWell( | ||||
| @@ -523,8 +524,7 @@ class _PostBottomAction extends StatelessWidget { | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|             ].expand((ele) => [ele, const Gap(8)]).toList() | ||||
|               ..removeLast(), | ||||
|             ], | ||||
|           ), | ||||
|         InkWell( | ||||
|           onTap: onShare, | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
|  | ||||
| // Keep this import to make the web image render work | ||||
| import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
|  | ||||
| class UniversalImage extends StatelessWidget { | ||||
|   final String url; | ||||
|   final double? width, height; | ||||
| @@ -14,6 +17,7 @@ class UniversalImage extends StatelessWidget { | ||||
|   final bool noProgressIndicator; | ||||
|   final bool noErrorWidget; | ||||
|   final double? cacheWidth, cacheHeight; | ||||
|   final FilterQuality? filterQuality; | ||||
|  | ||||
|   const UniversalImage( | ||||
|     this.url, { | ||||
| @@ -25,45 +29,43 @@ class UniversalImage extends StatelessWidget { | ||||
|     this.noErrorWidget = false, | ||||
|     this.cacheWidth, | ||||
|     this.cacheHeight, | ||||
|     this.filterQuality, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|     final double? resizeHeight = | ||||
|         cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; | ||||
|     final double? resizeWidth = | ||||
|         cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; | ||||
|     final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; | ||||
|     final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; | ||||
|  | ||||
|     return Image( | ||||
|       image: ResizeImage( | ||||
|         UniversalImage.provider(url), | ||||
|         width: resizeWidth?.round(), | ||||
|         height: resizeHeight?.round(), | ||||
|         policy: ResizeImagePolicy.fit, | ||||
|       ), | ||||
|       filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality, | ||||
|       image: kIsWeb | ||||
|           ? UniversalImage.provider(url) | ||||
|           : ResizeImage( | ||||
|               UniversalImage.provider(url), | ||||
|               width: resizeWidth?.round(), | ||||
|               height: resizeHeight?.round(), | ||||
|               policy: ResizeImagePolicy.fit, | ||||
|             ), | ||||
|       width: width, | ||||
|       height: height, | ||||
|       fit: fit, | ||||
|       loadingBuilder: noProgressIndicator | ||||
|           ? null | ||||
|           : (BuildContext context, Widget child, | ||||
|               ImageChunkEvent? loadingProgress) { | ||||
|           : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { | ||||
|               if (loadingProgress == null) return child; | ||||
|               return Center( | ||||
|                 child: TweenAnimationBuilder( | ||||
|                   tween: Tween( | ||||
|                     begin: 0, | ||||
|                     end: loadingProgress.expectedTotalBytes != null | ||||
|                         ? loadingProgress.cumulativeBytesLoaded / | ||||
|                             loadingProgress.expectedTotalBytes! | ||||
|                         ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! | ||||
|                         : 0, | ||||
|                   ), | ||||
|                   duration: const Duration(milliseconds: 300), | ||||
|                   builder: (context, value, _) => CircularProgressIndicator( | ||||
|                     value: loadingProgress.expectedTotalBytes != null | ||||
|                         ? value.toDouble() | ||||
|                         : null, | ||||
|                     value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
| @@ -94,10 +96,13 @@ class UniversalImage extends StatelessWidget { | ||||
|   } | ||||
|  | ||||
|   static ImageProvider provider(String url) { | ||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { | ||||
|       return CachedNetworkImageProvider(url); | ||||
|     } | ||||
|     return NetworkImage(url); | ||||
|     // This place used to use network image or cached network image depending on the platform. | ||||
|     // But now the cached network image is working on every platform. | ||||
|     // So we just use it now. | ||||
|     return CachedNetworkImageProvider( | ||||
|       url, | ||||
|       imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import firebase_messaging | ||||
| import flutter_udid | ||||
| import flutter_webrtc | ||||
| import gal | ||||
| import in_app_review | ||||
| import livekit_client | ||||
| import media_kit_libs_macos_video | ||||
| import media_kit_video | ||||
| @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) | ||||
|   FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) | ||||
|   GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) | ||||
|   InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) | ||||
|   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) | ||||
|   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) | ||||
|   MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) | ||||
|   | ||||
| @@ -26,7 +26,7 @@ PODS: | ||||
|     - Firebase/Analytics (= 11.4.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - firebase_core (3.8.1): | ||||
|   - firebase_core (3.9.0): | ||||
|     - Firebase/CoreOnly (~> 11.4.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.1.6): | ||||
| @@ -132,6 +132,8 @@ PODS: | ||||
|   - GoogleUtilities/UserDefaults (8.0.2): | ||||
|     - GoogleUtilities/Logger | ||||
|     - GoogleUtilities/Privacy | ||||
|   - in_app_review (2.0.0): | ||||
|     - FlutterMacOS | ||||
|   - livekit_client (2.3.2): | ||||
|     - flutter_webrtc | ||||
|     - FlutterMacOS | ||||
| @@ -186,6 +188,7 @@ DEPENDENCIES: | ||||
|   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) | ||||
|   - FlutterMacOS (from `Flutter/ephemeral`) | ||||
|   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) | ||||
|   - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) | ||||
|   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) | ||||
|   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) | ||||
|   - media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`) | ||||
| @@ -243,6 +246,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter/ephemeral | ||||
|   gal: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin | ||||
|   in_app_review: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos | ||||
|   livekit_client: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos | ||||
|   media_kit_libs_macos_video: | ||||
| @@ -279,20 +284,21 @@ SPEC CHECKSUMS: | ||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||
|   firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef | ||||
|   firebase_core: e4a35c426636a2cce00a5163df7ba69bfd0cca57 | ||||
|   firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f | ||||
|   firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf | ||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||
|   flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07 | ||||
|   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 | ||||
|   flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a | ||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||
|   gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 | ||||
|   livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a | ||||
|   media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 | ||||
|   media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 | ||||
|   | ||||
| @@ -31,16 +31,18 @@ | ||||
| 	<key>NSPrincipalClass</key> | ||||
| 	<string>NSApplication</string> | ||||
| 	<key>NSCameraUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian take photo or video for your post.</string> | ||||
| 	<string>Grant access to Camera will allow Solian use your camera during a call.</string> | ||||
| 	<key>NSMicrophoneUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian record audio for your post.</string> | ||||
| 	<string>Grant access to Microphone will allow Solian use your microphone during a call.</string> | ||||
| 	<key>NSPhotoLibraryAddUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> | ||||
| 	<key>NSPhotoLibraryUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string> | ||||
| 	<key>CFBundleDisplayName</key> | ||||
| 	<string>$(PRODUCT_NAME)</string> | ||||
| 	<key>NSCameraUseContinuityCameraDeviceType</key> | ||||
| 	<string></string> | ||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | ||||
| 	<false/> | ||||
| 	<key>NSPhotoLibraryAddUsageDescription</key> | ||||
| 	<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> | ||||
| </dict> | ||||
| </plist> | ||||
|   | ||||
							
								
								
									
										160
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -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: | ||||
| @@ -122,50 +122,50 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: build | ||||
|       sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" | ||||
|       sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|     version: "2.4.2" | ||||
|   build_config: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: build_config | ||||
|       sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 | ||||
|       sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.1" | ||||
|     version: "1.1.2" | ||||
|   build_daemon: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: build_daemon | ||||
|       sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" | ||||
|       sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.0.2" | ||||
|     version: "4.0.3" | ||||
|   build_resolvers: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: build_resolvers | ||||
|       sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" | ||||
|       sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.2" | ||||
|     version: "2.4.3" | ||||
|   build_runner: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: build_runner | ||||
|       sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" | ||||
|       sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.13" | ||||
|     version: "2.4.14" | ||||
|   build_runner_core: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: build_runner_core | ||||
|       sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 | ||||
|       sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.3.2" | ||||
|     version: "8.0.0" | ||||
|   built_collection: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -178,10 +178,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: built_value | ||||
|       sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb | ||||
|       sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.9.2" | ||||
|     version: "8.9.3" | ||||
|   cached_network_image: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -343,13 +343,13 @@ packages: | ||||
|     source: hosted | ||||
|     version: "2.3.7" | ||||
|   dart_webrtc: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dart_webrtc | ||||
|       sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6 | ||||
|       sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.4.9" | ||||
|     version: "1.4.10" | ||||
|   dbus: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -490,10 +490,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c" | ||||
|       sha256: c2376a6aae82358a9f9ccdd7d1f4006d08faa39a2767cce01031d9f593a8bd3b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.4" | ||||
|     version: "8.1.6" | ||||
|   file_saver: | ||||
|     dependency: "direct main" | ||||
|     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: | ||||
| @@ -627,6 +627,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.5.2" | ||||
|   flutter_app_update: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_app_update | ||||
|       sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.2" | ||||
|   flutter_cache_manager: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -692,10 +700,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" | ||||
|       sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.23" | ||||
|     version: "2.0.24" | ||||
|   flutter_shaders: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -729,10 +737,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_udid | ||||
|       sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" | ||||
|       sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.0" | ||||
|     version: "3.0.1" | ||||
|   flutter_web_plugins: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -742,10 +750,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "4838217405c42cce422698eacc9c2e17089b9c05322be899c0a725107dcddbdc" | ||||
|       sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.3" | ||||
|     version: "0.12.4" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -774,10 +782,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: gal | ||||
|       sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977" | ||||
|       sha256: "2771519c8b29f784d5e27f4efc2667667eef51c6c47cccaa0435a8fe8aa208e4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|     version: "2.3.1" | ||||
|   gap: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -870,10 +878,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: http_multi_server | ||||
|       sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" | ||||
|       sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.1" | ||||
|     version: "3.2.2" | ||||
|   http_parser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -894,10 +902,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image | ||||
|       sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" | ||||
|       sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.4.0" | ||||
|     version: "4.5.2" | ||||
|   image_picker: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -962,6 +970,22 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.1+1" | ||||
|   in_app_review: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: in_app_review | ||||
|       sha256: "36a06771b88fb0e79985b15e7f2ac0f1142e903fe72517f3c055d78bc3bc1819" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.10" | ||||
|   in_app_review_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: in_app_review_platform_interface | ||||
|       sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.5" | ||||
|   intl: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1038,10 +1062,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 +1478,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: | ||||
| @@ -1598,10 +1622,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" | ||||
|       sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.3" | ||||
|     version: "2.3.4" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1691,10 +1715,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: source_helper | ||||
|       sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" | ||||
|       sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.4" | ||||
|     version: "1.3.5" | ||||
|   source_span: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1771,10 +1795,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: stream_transform | ||||
|       sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" | ||||
|       sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|     version: "2.1.1" | ||||
|   string_scanner: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1975,6 +1999,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.4" | ||||
|   version: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: version | ||||
|       sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|   very_good_infinite_list: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -2003,26 +2035,26 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: wakelock_plus | ||||
|       sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 | ||||
|       sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.8" | ||||
|     version: "1.2.10" | ||||
|   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: | ||||
|       name: watcher | ||||
|       sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" | ||||
|       sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.0" | ||||
|     version: "1.1.1" | ||||
|   web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2071,6 +2103,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 +2131,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-0 <4.0.0" | ||||
|   dart: ">=3.6.0 <4.0.0" | ||||
|   flutter: ">=3.24.0" | ||||
|   | ||||
							
								
								
									
										12
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| name: surface | ||||
| description: "A new Flutter project." | ||||
| description: "The application for Solar Network" | ||||
| # The following line prevents the package from being accidentally published to | ||||
| # pub.dev using `flutter pub publish`. This is preferred for private packages. | ||||
| publish_to: "none" # Remove this line if you wish to publish to pub.dev | ||||
| @@ -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.0.1+29 | ||||
| version: 2.1.1+36 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
| @@ -61,7 +61,7 @@ dependencies: | ||||
|   relative_time: ^5.0.0 | ||||
|   image_picker: ^1.1.2 | ||||
|   cross_file: ^0.3.4+2 | ||||
|   file_picker: 8.1.4 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643 | ||||
|   file_picker: ^8.1.6 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643 | ||||
|   croppy: ^1.3.1 | ||||
|   flutter_expandable_fab: ^2.3.0 | ||||
|   dropdown_button2: ^2.3.9 | ||||
| @@ -86,6 +86,7 @@ dependencies: | ||||
|   media_kit_libs_video: ^1.0.5 | ||||
|   pasteboard: ^0.3.0 | ||||
|   synchronized: ^3.3.0+3 | ||||
|   dart_webrtc: ^1.4.10 | ||||
|   livekit_client: ^2.3.1+hotfix.1 | ||||
|   wakelock_plus: ^1.2.8 | ||||
|   permission_handler: ^11.3.1 | ||||
| @@ -106,6 +107,10 @@ dependencies: | ||||
|   flutter_svg: ^2.0.16 | ||||
|   home_widget: ^0.7.0 | ||||
|   receive_sharing_intent: ^1.8.1 | ||||
|   workmanager: ^0.5.2 | ||||
|   flutter_app_update: ^3.2.2 | ||||
|   in_app_review: ^2.0.10 | ||||
|   version: ^3.0.2 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
| @@ -138,6 +143,7 @@ flutter: | ||||
|   # To add assets to your application, add an assets section, like this: | ||||
|   assets: | ||||
|     - assets/icon/icon.png | ||||
|     - assets/icon/icon-dark.png | ||||
|     - assets/icon/icon-light-radius.png | ||||
|     - assets/translations/ | ||||
|  | ||||
|   | ||||
							
								
								
									
										25
									
								
								web/.well-known/apple-app-site-association
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/.well-known/apple-app-site-association
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| { | ||||
|   "applinks": { | ||||
|     "apps": [], | ||||
|     "details": [ | ||||
|       { | ||||
|         "appIDs": [ | ||||
|           "W7HPZ53V6B.dev.solsynth.solian" | ||||
|         ], | ||||
|         "paths": [ | ||||
|           "*" | ||||
|         ], | ||||
|         "components": [ | ||||
|           { | ||||
|             "/": "/*" | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "webcredentials": { | ||||
|     "apps": [ | ||||
|       "W7HPZ53V6B.dev.solsynth.solian" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user